Files

187 lines
7.2 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)