feat: DeepSeek Web Adapter - Array platform integration with browser control panel
This commit is contained in:
@@ -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"]
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user