feat: DeepSeek Web Adapter - Array platform integration with browser control panel

This commit is contained in:
DeepSeek-Web Adapter
2026-04-17 16:11:31 +08:00
parent 9a349dae8b
commit a6820b2cbb
12 changed files with 2527 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
"""列阵协作系统 - Worker Python SDK"""
from .client import ArrayClient
from .adapter import BaseAdapter
from .models import Worker, Task, TaskParticipant, Message, Notification, Event, Organization, OrgMember, OrgInvite
__all__ = ["ArrayClient", "BaseAdapter", "Worker", "Task", "TaskParticipant", "Message", "Notification", "Event", "Organization", "OrgMember", "OrgInvite"]
+186
View File
@@ -0,0 +1,186 @@
"""BaseAdapter - 适配器基类,封装 Worker 的完整生命周期"""
import asyncio
from .client import ArrayClient
from .models import Task, Event
class BaseAdapter:
"""适配器基类,封装 Worker 的完整生命周期:连接 -> 监听 -> 处理
关于 Worker 描述(description)的推荐格式:
在注册 Worker 或更新简历时,description 字段建议按以下结构编写,
方便其他 Worker 和人类快速理解你的能力。
推荐格式:
[一句话功能概述]
能力:
- 能力1
- 能力2
使用方式:
[说明怎么跟这个 Worker 交互]
限制:(可选)
- 限制1
示例:
"多语言实时翻译工具,支持中英日韩互译。\n\n"
"能力:\n"
"- 文本翻译(中/英/日/韩)\n"
"- 自动检测源语言\n\n"
"使用方式:\n"
"@提及并发送需要翻译的文本,默认翻译为中文。\n\n"
"限制:\n"
"- 单次最多翻译 5000 字"
实际使用时,可通过 client.update_profile(description="...") 或注册时传入。
关于组织(Organization)的说明:
Worker 可以属于多个组织,组织提供了资源隔离和协作边界。如果你的 Worker
属于多个组织,建议在适配器初始化时通过参数指定当前工作组织上下文:
class MyAdapter(BaseAdapter):
def __init__(self, server_url: str, api_key: str, org_id: str | None = None):
super().__init__(server_url, api_key)
self.org_id = org_id # 当前工作组织
创建任务时,可以通过 org_id 参数指定任务所属组织:
await self.client.create_task(
title="任务标题",
org_id=self.org_id # 指定组织
)
通过邀请码加入组织:
await self.client.join_organization(invite_code="xxx")
"""
def __init__(self, server_url: str, api_key: str):
self.server_url = server_url
self.api_key = api_key
self.client: ArrayClient | None = None
async def on_task_invited(self, task: Task):
"""被邀请加入新任务时的处理逻辑。子类必须实现。"""
raise NotImplementedError
async def on_task_assigned(self, task: Task):
"""兼容别名,推荐使用 on_task_invited"""
await self.on_task_invited(task)
async def on_task_message(self, task_id: str, message: dict):
"""收到任务消息时的处理逻辑。子类可选覆盖。"""
pass
async def on_task_updated(self, task_id: str, changes: dict):
"""任务状态/内容变更时的处理逻辑。子类可选覆盖。"""
pass
async def on_pending_recovery(self, tasks: list[dict]):
"""WebSocket 连上后收到待恢复任务列表时调用。子类可覆盖。
tasks 是 [{"id": "...", "title": "...", "status": "..."}] 形式的列表。
适配器可在此恢复之前中断的任务处理。
"""
pass
async def on_connected(self):
"""连接成功后的回调。子类可选覆盖。"""
pass
async def on_disconnected(self):
"""WebSocket 断开时的回调,子类可覆盖"""
pass
async def run(self):
"""启动 Worker 完整生命周期。
外层重连:当 HTTP 连接或整个 WebSocket 会话断开时(如服务端宕机),
在这里做指数退避重连(5s 起步,最大 120s),每次重连会重新认证、
处理离线通知、再启动 WebSocket 监听。
内层重连由 client.listen() 负责:当 WebSocket 连接断开但 HTTP
会话仍然有效时,listen() 内部自动重连(2s 起步,最大 60s)。
两层的区别:
- 内层(listen):仅 WebSocket 断线重连,不涉及重新认证
- 外层(run):完整的会话重建,包括重新认证和离线通知恢复
"""
import logging
logger = logging.getLogger(__name__)
self._running = True
delay = 5.0
while self._running:
try:
async with ArrayClient(self.server_url, self.api_key) as client:
self.client = client
delay = 5.0 # 重置退避
print(f"Worker {client.worker_id} 已连接到平台")
# 处理离线期间的通知
await self._process_offline_notifications()
# 启动 WebSocket 监听(在后台运行)
print("开始监听实时事件...")
listen_task = asyncio.create_task(self._listen_loop(client))
# 等待 WebSocket 连接建立
await client.wait_for_ws_connection()
await self.on_connected()
# 等待监听任务完成(通常不会完成,除非连接断开)
await listen_task
# 正常断开(非异常)
await self.on_disconnected()
except asyncio.CancelledError:
break
except Exception as e:
logger.warning(f"连接失败: {e},{delay:.0f}s 后重试")
await self.on_disconnected()
await asyncio.sleep(delay)
delay = min(delay * 2, 120.0)
async def _listen_loop(self, client: ArrayClient):
"""WebSocket 监听循环"""
async for event in client.listen():
await self._handle_event(event)
async def _process_offline_notifications(self):
"""处理离线期间积累的通知"""
notifications, total = await self.client.get_notifications(is_read=False)
if notifications:
print(f"处理 {len(notifications)} 条离线通知...")
for n in notifications:
event = Event(event_type=n.event_type, payload=n.payload or {}, timestamp=str(n.created_at))
await self._handle_event(event)
await self.client.mark_notification_read(n.id)
async def _handle_event(self, event: Event):
"""分发事件到对应的处理方法"""
if event.event_type == "task.assigned":
task_id = event.payload.get("task_id")
if task_id:
task = await self.client.get_task(task_id)
await self.on_task_invited(task)
elif event.event_type == "participant.joined":
task_id = event.payload.get("task_id")
if task_id:
task = await self.client.get_task(task_id)
await self.on_task_invited(task)
elif event.event_type == "tasks.pending_recovery":
tasks = event.payload.get("tasks", [])
await self.on_pending_recovery(tasks)
elif event.event_type == "task.message":
task_id = event.payload.get("task_id")
if task_id:
await self.on_task_message(task_id, event.payload)
elif event.event_type == "task.updated":
task_id = event.payload.get("task_id")
changes = event.payload.get("changes", {})
if task_id:
await self.on_task_updated(task_id, changes)
+641
View File
@@ -0,0 +1,641 @@
"""ArrayClient - 列阵平台客户端,封装所有 REST API 和 WebSocket 通信"""
import asyncio
import base64
import json
from typing import AsyncGenerator
import httpx
import websockets
from .models import Worker, Task, TaskParticipant, Message, Notification, Event, Organization, OrgMember, OrgInvite
class ArrayClient:
"""列阵平台客户端,封装所有 REST API 和 WebSocket 通信"""
PROTOCOL_VERSION = 5 # 协议版本,与 server/app/protocol.py 对应
def __init__(self, server_url: str, api_key: str):
self.server_url = server_url.rstrip("/")
self.api_key = api_key
self.worker_id: str | None = None
self._access_token: str | None = None
self._refresh_token: str | None = None
self._http: httpx.AsyncClient | None = None
self._ws_connected_event = asyncio.Event()
async def __aenter__(self):
self._http = httpx.AsyncClient(base_url=self.server_url, timeout=30.0, follow_redirects=True)
await self._authenticate()
return self
async def __aexit__(self, *args):
if self._http:
await self._http.aclose()
async def _authenticate(self):
"""用 api_key 获取 JWT token"""
resp = await self._http.post("/api/v1/workers/token", json={"api_key": self.api_key})
resp.raise_for_status()
data = resp.json()
self._access_token = data["access_token"]
self._refresh_token = data["refresh_token"]
# 从 token 中解析 worker_id(解码JWT payload的sub字段)
payload_part = self._access_token.split(".")[1]
# 补齐 base64 padding
padding = 4 - len(payload_part) % 4
if padding != 4:
payload_part += "=" * padding
payload = json.loads(base64.urlsafe_b64decode(payload_part))
self.worker_id = payload["sub"]
async def _refresh_access_token(self):
"""用 refresh_token 刷新 JWT"""
resp = await self._http.post("/api/v1/workers/token/refresh", json={"refresh_token": self._refresh_token})
resp.raise_for_status()
data = resp.json()
self._access_token = data["access_token"]
self._refresh_token = data["refresh_token"]
def _headers(self) -> dict:
return {
"Authorization": f"Bearer {self._access_token}",
"X-Protocol-Version": str(self.PROTOCOL_VERSION)
}
async def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
"""统一请求方法,自动处理 token 刷新"""
resp = await self._http.request(method, path, headers=self._headers(), **kwargs)
if resp.status_code == 401:
# token 可能过期,尝试刷新
await self._refresh_access_token()
resp = await self._http.request(method, path, headers=self._headers(), **kwargs)
resp.raise_for_status()
return resp
# === Worker 相关 ===
async def get_worker(self, worker_id: str) -> Worker:
"""获取 Worker 详情"""
resp = await self._request("GET", f"/api/v1/workers/{worker_id}")
return Worker(**resp.json())
async def list_workers(self, type: str | None = None, status: str | None = None, org_id: str | None = None, page: int = 1, limit: int = 20) -> tuple[list[Worker], int]:
"""获取 Worker 列表
参数:
type: 按类型筛选
status: 按状态筛选
org_id: 按所属组织筛选
page: 页码,默认 1
limit: 每页数量,默认 20
"""
params = {"page": page, "limit": limit}
if type:
params["type"] = type
if status:
params["status"] = status
if org_id:
params["org_id"] = org_id
resp = await self._request("GET", "/api/v1/workers", params=params)
data = resp.json()
return [Worker(**w) for w in data["workers"]], data["total"]
async def update_worker(self, **kwargs) -> Worker:
"""更新自己的信息"""
resp = await self._request("PATCH", f"/api/v1/workers/{self.worker_id}", json=kwargs)
return Worker(**resp.json())
async def update_profile(self, **kwargs) -> Worker:
"""更新自己的简历信息。支持的字段:name, description, organization, location, timezone, tags, capabilities, max_concurrent_tasks, avatar_url"""
if self.worker_id is None:
raise ValueError("worker_id is not available, please authenticate first")
resp = await self._request("PATCH", f"/api/v1/workers/{self.worker_id}", json=kwargs)
return Worker(**resp.json())
async def get_worker_profile(self, worker_id: str) -> Worker:
"""获取任意 worker 的完整简历"""
resp = await self._request("GET", f"/api/v1/workers/{worker_id}")
return Worker(**resp.json())
async def get_worker_workload(self, worker_id: str) -> dict:
"""查询 worker 的工作负载"""
resp = await self._request("GET", f"/api/v1/workers/{worker_id}/workload")
return resp.json()
async def discover_workers(self, skill=None, domain=None, tag=None, keyword=None, available_only=False, status=None, type=None, page=1, limit=20) -> dict:
"""多条件能力发现
参数:
skill: 技能名称
domain: 领域名称
tag: 标签
keyword: 关键词搜索
available_only: 是否只返回可接受的 worker
status: 按状态筛选
type: 按类型筛选
page: 页码,默认 1
limit: 每页数量,默认 20
"""
params = {"page": page, "limit": limit}
if skill:
params["skill"] = skill
if domain:
params["domain"] = domain
if tag:
params["tag"] = tag
if keyword:
params["keyword"] = keyword
if available_only:
params["available_only"] = "true"
if status:
params["status"] = status
if type:
params["type"] = type
resp = await self._request("GET", "/api/v1/workers/discover", params=params)
return resp.json()
# === Task 相关 ===
async def create_task(self, title: str, description: str | None = None,
priority: str = "normal", parent_id: str | None = None,
tags: list[str] | None = None, deadline: str | None = None,
metadata: dict | None = None,
participant_ids: list[str] | None = None,
org_id: str | None = None) -> Task:
"""创建任务
参数:
title: 任务标题
description: 任务描述
priority: 优先级,默认 normal
parent_id: 父任务 ID
tags: 标签列表
deadline: 截止日期
metadata: 元数据字典
participant_ids: 参与者 ID 列表
org_id: 所属组织 ID
异常:
可能返回 409 状态码,表示创建者或某个参与者已达到最大并发任务数限制,
detail 字段会包含具体是谁超了(名字和 ID)
"""
payload = {"title": title, "priority": priority}
if description:
payload["description"] = description
if parent_id:
payload["parent_id"] = parent_id
if tags:
payload["tags"] = tags
if deadline:
payload["deadline"] = deadline
if metadata:
payload["metadata"] = metadata
if participant_ids:
payload["participant_ids"] = participant_ids
if org_id:
payload["org_id"] = org_id
resp = await self._request("POST", "/api/v1/tasks", json=payload)
return Task(**resp.json())
async def get_task(self, task_id: str) -> Task:
"""获取任务详情"""
resp = await self._request("GET", f"/api/v1/tasks/{task_id}")
return Task(**resp.json())
async def list_tasks(self, status: str | None = None, creator_id: str | None = None,
tag: str | None = None, org_id: str | None = None,
sort: str = "-created_at", page: int = 1, limit: int = 20) -> tuple[list[Task], int]:
"""获取任务列表
参数:
status: 按状态筛选
creator_id: 按创建者筛选
tag: 按标签筛选
org_id: 按所属组织筛选
sort: 排序方式,默认 -created_at
page: 页码,默认 1
limit: 每页数量,默认 20
"""
params = {"sort": sort, "page": page, "limit": limit}
if status:
params["status"] = status
if creator_id:
params["creator_id"] = creator_id
if tag:
params["tag"] = tag
if org_id:
params["org_id"] = org_id
resp = await self._request("GET", "/api/v1/tasks", params=params)
data = resp.json()
return [Task(**t) for t in data["tasks"]], data["total"]
async def update_task(self, task_id: str, **kwargs) -> Task:
"""更新任务(权限由服务端校验)"""
resp = await self._request("PATCH", f"/api/v1/tasks/{task_id}", json=kwargs)
return Task(**resp.json())
async def get_children(self, task_id: str) -> list[Task]:
"""获取子任务列表"""
resp = await self._request("GET", f"/api/v1/tasks/{task_id}/children")
return [Task(**t) for t in resp.json()]
# === 参与者管理 ===
async def add_participants(self, task_id: str, worker_ids: list[str]) -> list[TaskParticipant]:
"""添加参与者到任务
异常:
可能返回 409 状态码,表示某个参与者已达到最大并发任务数限制,
detail 字段会包含具体是谁超了(名字和 ID)
"""
payload = {"worker_ids": worker_ids}
resp = await self._request("POST", f"/api/v1/tasks/{task_id}/participants", json=payload)
return [TaskParticipant(**p) for p in resp.json()]
async def list_participants(self, task_id: str) -> list[TaskParticipant]:
"""获取任务的参与者列表"""
resp = await self._request("GET", f"/api/v1/tasks/{task_id}/participants")
return [TaskParticipant(**p) for p in resp.json()]
async def remove_participant(self, task_id: str, worker_id: str) -> None:
"""从任务中移除参与者"""
await self._request("DELETE", f"/api/v1/tasks/{task_id}/participants/{worker_id}")
# === Message 相关 ===
async def send_message(self, task_id: str, content: str, type: str = "comment",
reply_to_id: str | None = None,
mention_type: str = "none",
mention_ids: list[str] | None = None,
scope: str = "chat",
private_to_id: str | None = None) -> Message:
"""在任务上发消息
scope 参数:
- "chat"(默认):正常对话消息
- "trace":适配器响应/状态记录,不参与正常聊天流
"""
payload = {"content": content, "type": type, "scope": scope}
if reply_to_id:
payload["reply_to_id"] = reply_to_id
if mention_type:
payload["mention_type"] = mention_type
if mention_ids:
payload["mention_ids"] = mention_ids
if private_to_id:
payload["private_to_id"] = private_to_id
resp = await self._request("POST", f"/api/v1/tasks/{task_id}/messages", json=payload)
return Message(**resp.json())
async def list_messages(self, task_id: str, page: int = 1, limit: int = 20,
sort: str = "-created_at",
scope: str = "chat",
include_private: bool = False,
include_system: bool = False,
type_filter: str | None = None) -> tuple[list[Message], int]:
"""获取任务消息列表
scope 参数:
- "chat"(默认):只返回对话消息
- "trace":只返回适配器响应/状态记录
- "all":返回全部消息
"""
params = {"page": page, "limit": limit, "sort": sort, "scope": scope}
if include_private:
params["include_private"] = "true"
if include_system:
params["include_system"] = "true"
if type_filter:
params["type_filter"] = type_filter
resp = await self._request("GET", f"/api/v1/tasks/{task_id}/messages", params=params)
data = resp.json()
return [Message(**m) for m in data["messages"]], data["total"]
# === Notification 相关 ===
async def get_notifications(self, is_read: bool | None = None, page: int = 1,
limit: int = 50) -> tuple[list[Notification], int]:
"""获取通知列表"""
params = {"page": page, "limit": limit}
if is_read is not None:
params["is_read"] = str(is_read).lower()
resp = await self._request("GET", "/api/v1/notifications", params=params)
data = resp.json()
return [Notification(**n) for n in data["notifications"]], data["total"]
async def mark_notification_read(self, notification_id: str) -> None:
"""标记单条通知已读"""
await self._request("PATCH", f"/api/v1/notifications/{notification_id}/read")
async def mark_all_notifications_read(self) -> None:
"""标记所有通知已读"""
await self._request("POST", "/api/v1/notifications/read-all")
# === Review 相关 ===
async def create_review(self, worker_id: str, task_id: str, rating: int,
capability_tags: list[str] | None = None,
comment: str | None = None,
suggestion: str | None = None) -> dict:
"""为某个 worker 在某个任务中创建评价(调用者必须是任务创建者)"""
data = {"task_id": task_id, "rating": rating}
if capability_tags is not None:
data["capability_tags"] = capability_tags
if comment is not None:
data["comment"] = comment
if suggestion is not None:
data["suggestion"] = suggestion
resp = await self._request("POST", f"/api/v1/workers/{worker_id}/reviews", json=data)
return resp.json()
async def get_worker_reviews(self, worker_id: str, page: int = 1, limit: int = 20) -> dict:
"""获取某个 worker 收到的评价列表"""
resp = await self._request("GET", f"/api/v1/workers/{worker_id}/reviews",
params={"page": page, "limit": limit})
return resp.json()
async def get_review_summary(self, worker_id: str) -> dict:
"""获取某个 worker 的评价汇总"""
resp = await self._request("GET", f"/api/v1/workers/{worker_id}/reviews/summary")
return resp.json()
# === 组织管理 ===
async def create_organization(self, name: str, description: str | None = None) -> Organization:
"""创建新组织
参数:
name: 组织名称
description: 组织描述
返回:
创建的组织信息
"""
payload = {"name": name}
if description:
payload["description"] = description
resp = await self._request("POST", "/api/v1/organizations", json=payload)
return Organization(**resp.json())
async def list_my_organizations(self) -> list[Organization]:
"""获取我所属的所有组织
返回:
组织列表
"""
resp = await self._request("GET", "/api/v1/organizations")
return [Organization(**o) for o in resp.json()]
async def get_organization(self, org_id: str) -> Organization:
"""获取组织详情
参数:
org_id: 组织 ID
返回:
组织详细信息
"""
resp = await self._request("GET", f"/api/v1/organizations/{org_id}")
return Organization(**resp.json())
async def update_organization(self, org_id: str, name: str | None = None, description: str | None = None) -> Organization:
"""更新组织信息
参数:
org_id: 组织 ID
name: 新名称(可选)
description: 新描述(可选)
返回:
更新后的组织信息
注意:
需要组织管理员权限
"""
payload = {}
if name is not None:
payload["name"] = name
if description is not None:
payload["description"] = description
resp = await self._request("PATCH", f"/api/v1/organizations/{org_id}", json=payload)
return Organization(**resp.json())
# === 邀请码管理 ===
async def create_invite(self, org_id: str, expires_at: str | None = None, max_uses: int | None = None) -> OrgInvite:
"""创建组织邀请码
参数:
org_id: 组织 ID
expires_at: 过期时间(ISO 格式字符串,可选)
max_uses: 最大使用次数(可选)
返回:
创建的邀请码信息
注意:
需要组织管理员权限
"""
payload = {}
if expires_at:
payload["expires_at"] = expires_at
if max_uses is not None:
payload["max_uses"] = max_uses
resp = await self._request("POST", f"/api/v1/organizations/{org_id}/invites", json=payload)
return OrgInvite(**resp.json())
async def list_invites(self, org_id: str) -> list[OrgInvite]:
"""获取组织的邀请码列表
参数:
org_id: 组织 ID
返回:
邀请码列表
注意:
需要组织管理员权限
"""
resp = await self._request("GET", f"/api/v1/organizations/{org_id}/invites")
return [OrgInvite(**i) for i in resp.json()]
async def join_organization(self, invite_code: str) -> dict:
"""通过邀请码加入组织
参数:
invite_code: 邀请码
返回:
加入结果,包含 org_id、worker_id、role、joined_at
"""
payload = {"invite_code": invite_code}
resp = await self._request("POST", "/api/v1/organizations/join", json=payload)
return resp.json()
# === 组织成员管理 ===
async def list_org_members(self, org_id: str) -> list[OrgMember]:
"""获取组织成员列表
参数:
org_id: 组织 ID
返回:
成员列表
"""
resp = await self._request("GET", f"/api/v1/organizations/{org_id}/members")
return [OrgMember(**m) for m in resp.json()]
async def remove_org_member(self, org_id: str, worker_id: str) -> None:
"""移除组织成员
参数:
org_id: 组织 ID
worker_id: 要移除的成员 Worker ID
注意:
需要组织管理员权限,不能移除自己
"""
await self._request("DELETE", f"/api/v1/organizations/{org_id}/members/{worker_id}")
async def update_member_role(self, org_id: str, worker_id: str, role: str) -> OrgMember:
"""修改成员角色
参数:
org_id: 组织 ID
worker_id: 成员 Worker ID
role: 新角色("admin" 或 "member")
返回:
更新后的成员信息
注意:
需要组织管理员权限
"""
payload = {"role": role}
resp = await self._request("PATCH", f"/api/v1/organizations/{org_id}/members/{worker_id}/role", json=payload)
# 服务端返回的是简化格式,构造 OrgMember
data = resp.json()
return OrgMember(
worker_id=data["worker_id"],
role=data["role"],
worker_name="",
worker_type="",
joined_at=None
)
# === Admin 管理 ===
async def admin_rename_worker(self, worker_id: str, name: str, **kwargs):
"""Admin 修改 Worker 信息
仅 type="admin" 的 Worker 可调用。
Args:
worker_id: 目标 Worker ID
name: 新名称
**kwargs: 其他可选字段 (description, capabilities, organization, location, timezone, tags, avatar_url)
Returns:
修改后的 Worker 信息
Raises:
403: 调用者不是 admin
404: Worker 不存在
"""
data = {"name": name, **kwargs}
return await self._request("PATCH", f"/admin/workers/{worker_id}", json=data)
async def admin_delete_worker(self, worker_id: str):
"""Admin 软删除 Worker
将 Worker 状态设为 deleted,保留历史数据。被删除的 Worker 无法登录和出现在列表中。
仅 type="admin" 的 Worker 可调用。
Args:
worker_id: 目标 Worker ID
Returns:
删除确认信息
Raises:
403: 调用者不是 admin
404: Worker 不存在
"""
return await self._request("DELETE", f"/admin/workers/{worker_id}")
async def admin_create_worker(self, name: str, type: str, **kwargs):
"""Admin 代注册 Worker
Admin 代替他人注册 Worker,返回包含 API Key 的注册信息。
仅 type="admin" 的 Worker 可调用。
Args:
name: Worker 名称
type: Worker 类型 (human/ai/tool/admin)
**kwargs: 其他可选字段 (description, capabilities, organization, location, timezone, tags, avatar_url)
Returns:
注册信息,包含 id 和 api_key
Raises:
403: 调用者不是 admin
"""
data = {"name": name, "type": type, **kwargs}
return await self._request("POST", f"/admin/workers", json=data)
# === WebSocket 实时监听 ===
async def _ensure_authenticated(self):
"""确保已认证,如果未认证则进行认证"""
if self._access_token is None:
await self._authenticate()
def _build_ws_url(self) -> str:
"""构建 WebSocket URL"""
ws_url = self.server_url.replace("http://", "ws://").replace("https://", "wss://")
return f"{ws_url}/api/v1/ws?token={self._access_token}&protocol={self.PROTOCOL_VERSION}"
async def listen(self) -> AsyncGenerator[Event, None]:
"""连接 WebSocket 并 yield 实时事件。自动心跳保活,带断线重连。"""
import logging
logger = logging.getLogger(__name__)
delay = 2.0
while True:
try:
await self._ensure_authenticated()
ws_url = self._build_ws_url()
async with websockets.connect(ws_url) as ws:
delay = 2.0 # 重置退避
# 启动心跳任务
async def heartbeat():
while True:
try:
await asyncio.sleep(30)
await ws.send("ping")
except Exception:
break
heartbeat_task = asyncio.create_task(heartbeat())
# 通知 WebSocket 已连接
self._ws_connected_event.set()
try:
async for raw_message in ws:
if raw_message == "pong":
continue
data = json.loads(raw_message)
yield Event(**data)
finally:
heartbeat_task.cancel()
except asyncio.CancelledError:
return
except Exception as e:
logger.warning(f"WebSocket 断开: {e},{delay:.0f}s 后重连")
await asyncio.sleep(delay)
delay = min(delay * 2, 60.0)
async def wait_for_ws_connection(self, timeout: float = 10.0):
"""等待 WebSocket 连接建立"""
await asyncio.wait_for(self._ws_connected_event.wait(), timeout=timeout)
+131
View File
@@ -0,0 +1,131 @@
"""SDK 数据模型 - 与服务端响应对应的 Pydantic 模型"""
from datetime import datetime
from pydantic import BaseModel, ConfigDict
class Worker(BaseModel):
"""Worker 模型"""
id: str
name: str
type: str
description: str | None = None
capabilities: dict | None = None
status: str
created_at: datetime
last_seen_at: datetime | None = None
organization: str | None = None
location: str | None = None
timezone: str | None = None
avatar_url: str | None = None
tags: list | None = None
max_concurrent_tasks: int | None = None
model_config = ConfigDict(from_attributes=True)
class TaskParticipant(BaseModel):
"""任务参与者模型"""
id: str
task_id: str
worker_id: str
is_creator: bool
joined_at: datetime
model_config = ConfigDict(from_attributes=True)
class Task(BaseModel):
"""任务模型"""
id: str
title: str
description: str | None = None
status: str
priority: str
creator_id: str
parent_id: str | None = None
tags: list[str] | None = None
deadline: datetime | None = None
result: str | None = None
metadata: dict | None = None
created_at: datetime
updated_at: datetime
last_message_at: datetime | None = None
participants: list[TaskParticipant] | None = None
model_config = ConfigDict(from_attributes=True)
class Message(BaseModel):
"""消息模型"""
id: str
task_id: str
sender_id: str
content: str
type: str
reply_to_id: str | None = None
created_at: datetime
mention_type: str = "none"
mention_ids: list[str] | None = None
scope: str = "chat"
private_to_id: str | None = None
model_config = ConfigDict(from_attributes=True)
class Notification(BaseModel):
"""通知模型"""
id: str
worker_id: str
event_type: str
payload: dict | None = None
is_read: bool
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class Event(BaseModel):
"""WebSocket 推送的实时事件"""
event_type: str
payload: dict
timestamp: str
model_config = ConfigDict(from_attributes=True)
class Organization(BaseModel):
"""组织模型"""
id: str
name: str
description: str | None
created_by: str
created_at: datetime
member_count: int = 0
model_config = ConfigDict(from_attributes=True)
class OrgMember(BaseModel):
"""组织成员模型"""
worker_id: str
worker_name: str
worker_type: str
role: str # "admin" | "member"
joined_at: datetime
model_config = ConfigDict(from_attributes=True)
class OrgInvite(BaseModel):
"""组织邀请码模型"""
id: str
org_id: str
code: str
created_by: str
expires_at: datetime | None
max_uses: int | None
used_count: int
created_at: datetime
model_config = ConfigDict(from_attributes=True)