"""协议对等端模块。 定义 Peer 类,封装双向传输通道上的消息收发、初始化握手、能力调用、 流式事件转发与取消处理。这里的 peer 指"通信对端/本端"这一网络协议概念, 而不是业务上的用户、群聊或会话对象。 核心职责: - 消息序列化/反序列化 - 初始化握手协议 - 能力调用(同步/流式) - 取消处理 - 连接生命周期管理 消息处理: 入站: ResultMessage -> 唤醒等待的 Future EventMessage -> 投递到流式队列 InitializeMessage -> 调用 initialize_handler InvokeMessage -> 创建任务调用 invoke_handler CancelMessage -> 取消对应的任务 出站: initialize() -> InitializeMessage invoke() -> InvokeMessage(stream=False) invoke_stream() -> InvokeMessage(stream=True) cancel() -> CancelMessage 与旧版对比: 旧版 JSON-RPC: - 分离的 JSONRPCClient 和 JSONRPCServer - 通过 method 字段区分操作类型 - 使用 JSONRPCRequest/Response 消息类型 - 流式通过独立的 notification 实现 - 无统一的取消机制 新版 Peer: - 统一的 Peer 抽象,既是客户端也是服务端 - 通过 type 字段区分消息类型 - 使用 InitializeMessage/InvokeMessage/EventMessage 等 - 流式通过 EventMessage(phase=delta) 实现 - 统一的 CancelMessage 取消机制 使用示例: # 作为客户端发起调用 peer = Peer(transport=transport, peer_info=PeerInfo(...)) await peer.start() output = await peer.initialize(handlers) result = await peer.invoke("llm.chat", {"prompt": "hello"}) # 作为服务端处理调用 peer.set_invoke_handler(my_handler) await peer.start() 消息处理流程: 入站消息: ResultMessage -> 唤醒等待的 Future EventMessage -> 投递到流式队列 InitializeMessage -> 调用 _initialize_handler InvokeMessage -> 创建任务调用 _invoke_handler CancelMessage -> 取消对应的任务 出站消息: initialize() -> InitializeMessage invoke() -> InvokeMessage(stream=False) invoke_stream() -> InvokeMessage(stream=True) cancel() -> CancelMessage 取消机制: - CancelToken 用于检查取消状态 - 入站任务在收到 CancelMessage 时被取消 - 早到取消:在任务执行前检查 cancel_token,避免竞态条件 `Peer` 把 `Transport` 和 v4 协议消息模型接起来,负责: - 握手与远端元数据缓存 - 请求 ID 关联 - 非流式 / 流式调用分发 - 取消传播 - 连接异常时的统一收口 它本身不做业务路由,真正的执行逻辑交给 `CapabilityRouter` 或 `HandlerDispatcher`。 """ from __future__ import annotations import asyncio import inspect from collections.abc import AsyncIterator, Awaitable, Callable from typing import Any from ..context import CancelToken from ..errors import AstrBotError, ErrorCodes from ..protocol.messages import ( CancelMessage, ErrorPayload, EventMessage, InitializeMessage, InitializeOutput, InvokeMessage, PeerInfo, ResultMessage, parse_message, ) from .capability_router import StreamExecution InitializeHandler = Callable[[InitializeMessage], Awaitable[InitializeOutput]] InvokeHandler = Callable[ [InvokeMessage, CancelToken], Awaitable[dict[str, Any] | StreamExecution] ] CancelHandler = Callable[[str], Awaitable[None]] class Peer: """表示协议连接中的一个对等端。 `Peer` 封装一条双向传输通道上的消息收发、初始化握手、能力调用、 流式事件转发与取消处理。这里的 `peer` 指“通信对端/本端”这一网络 协议概念,而不是业务上的用户、群聊或会话对象。 """ def __init__( self, *, transport, peer_info: PeerInfo, protocol_version: str = "1.0", ) -> None: """创建一个协议对等端实例。 Args: transport: 底层传输实现,负责发送字符串消息并回调入站消息。 peer_info: 当前端点对外声明的身份信息。 protocol_version: 当前端点支持的协议版本,用于初始化握手校验。 """ self.transport = transport self.peer_info = peer_info self.protocol_version = protocol_version self.remote_peer: PeerInfo | None = None self.remote_handlers = [] self.remote_provided_capabilities = [] self.remote_capabilities = [] self.remote_capability_map: dict[str, Any] = {} self.remote_provided_capability_map: dict[str, Any] = {} self.remote_metadata: dict[str, Any] = {} self._initialize_handler: InitializeHandler | None = None self._invoke_handler: InvokeHandler | None = None self._cancel_handler: CancelHandler | None = None self._counter = 0 self._closed = asyncio.Event() self._unusable = False self._stopping = False self._pending_results: dict[str, asyncio.Future[ResultMessage]] = {} self._pending_streams: dict[str, asyncio.Queue[Any]] = {} self._inbound_tasks: dict[ str, tuple[asyncio.Task[None], CancelToken, asyncio.Event] ] = {} self._remote_initialized = asyncio.Event() self._transport_watch_task: asyncio.Task[None] | None = None def set_initialize_handler(self, handler: InitializeHandler) -> None: """注册处理远端 `initialize` 请求的握手处理器。""" self._initialize_handler = handler def set_invoke_handler(self, handler: InvokeHandler) -> None: """注册处理远端 `invoke` 请求的能力调用处理器。""" self._invoke_handler = handler def set_cancel_handler(self, handler: CancelHandler) -> None: """注册处理远端 `cancel` 请求的取消回调。""" self._cancel_handler = handler async def start(self) -> None: """启动传输层并将原始入站消息绑定到当前 `Peer`。""" self._closed.clear() self._unusable = False self._stopping = False self._remote_initialized.clear() self.transport.set_message_handler(self._handle_raw_message) await self.transport.start() self._transport_watch_task = asyncio.create_task(self._watch_transport_closed()) async def stop(self) -> None: """关闭 `Peer` 并清理所有挂起中的请求、流和入站任务。""" if self._closed.is_set(): return self._stopping = True # 终止所有挂起的 RPC,避免调用方永久挂起 for future in list(self._pending_results.values()): if not future.done(): future.set_exception(AstrBotError.internal_error("连接已关闭")) self._pending_results.clear() for queue in list(self._pending_streams.values()): await queue.put(AstrBotError.internal_error("连接已关闭")) self._pending_streams.clear() # 取消所有入站任务 for task, token, _started in list(self._inbound_tasks.values()): token.cancel() task.cancel() self._inbound_tasks.clear() await self.transport.stop() self._closed.set() async def wait_closed(self) -> None: """等待底层传输彻底关闭。""" await self.transport.wait_closed() async def _watch_transport_closed(self) -> None: """监视底层传输的意外关闭,并主动失败挂起调用。""" try: await self.transport.wait_closed() if self._closed.is_set() or self._stopping: return await self._fail_connection( AstrBotError( code=ErrorCodes.NETWORK_ERROR, message="连接已关闭", hint="请检查对端进程或传输连接", retryable=True, ) ) finally: current_task = asyncio.current_task() if self._transport_watch_task is current_task: self._transport_watch_task = None async def wait_until_remote_initialized(self, timeout: float | None = 30.0) -> None: """等待远端完成初始化握手。 Args: timeout: 等待秒数。传入 `None` 表示无限等待。 """ init_waiter = asyncio.create_task(self._remote_initialized.wait()) closed_waiter = asyncio.create_task(self.wait_closed()) try: done, pending = await asyncio.wait( {init_waiter, closed_waiter}, timeout=timeout, return_when=asyncio.FIRST_COMPLETED, ) if not done: raise TimeoutError() if init_waiter in done: return raise AstrBotError.protocol_error("连接在初始化完成前关闭") finally: for task in (init_waiter, closed_waiter): if not task.done(): task.cancel() try: await task except asyncio.CancelledError: pass async def initialize( self, handlers, *, provided_capabilities=None, metadata: dict[str, Any] | None = None, ) -> InitializeOutput: """向远端发送初始化请求并缓存远端声明的能力信息。 Args: handlers: 当前端点声明可接收的处理器列表。 metadata: 附带给远端的握手元数据。 Returns: 远端返回的初始化结果。 """ self._ensure_usable() request_id = self._next_id() future: asyncio.Future[ResultMessage] = ( asyncio.get_running_loop().create_future() ) self._pending_results[request_id] = future await self._send( InitializeMessage( id=request_id, protocol_version=self.protocol_version, peer=self.peer_info, handlers=list(handlers), provided_capabilities=list(provided_capabilities or []), metadata=metadata or {}, ) ) result = await future if result.kind != "initialize_result": raise AstrBotError.protocol_error("initialize 必须收到 initialize_result") if not result.success: self._unusable = True await self.stop() raise AstrBotError.from_payload( result.error.model_dump() if result.error else {} ) output = InitializeOutput.model_validate(result.output) self.remote_peer = output.peer self.remote_capabilities = output.capabilities self.remote_capability_map = {item.name: item for item in output.capabilities} self.remote_metadata = output.metadata return output async def invoke( self, capability: str, payload: dict[str, Any], *, stream: bool = False, request_id: str | None = None, ) -> dict[str, Any]: """发起一次非流式能力调用并等待最终结果。 Args: capability: 远端能力名。 payload: 调用输入。 stream: 必须为 `False`;流式场景应改用 `invoke_stream()`。 request_id: 可选的请求 ID;未提供时自动生成。 """ self._ensure_usable() if stream: raise ValueError("stream=True 请使用 invoke_stream()") request_id = request_id or self._next_id() future: asyncio.Future[ResultMessage] = ( asyncio.get_running_loop().create_future() ) self._pending_results[request_id] = future await self._send( InvokeMessage( id=request_id, capability=capability, input=payload, stream=False, ) ) result = await future if not result.success: raise AstrBotError.from_payload( result.error.model_dump() if result.error else {} ) return result.output async def invoke_stream( self, capability: str, payload: dict[str, Any], *, request_id: str | None = None, ) -> AsyncIterator[EventMessage]: """发起一次流式能力调用并返回事件迭代器。 调用方会收到 `delta` 事件,`started` 会被内部吞掉, `completed` 用于结束迭代,`failed` 会转换为异常抛出。 Args: capability: 远端能力名。 payload: 调用输入。 request_id: 可选的请求 ID;未提供时自动生成。 """ self._ensure_usable() request_id = request_id or self._next_id() queue: asyncio.Queue[Any] = asyncio.Queue() self._pending_streams[request_id] = queue await self._send( InvokeMessage( id=request_id, capability=capability, input=payload, stream=True, ) ) async def iterator() -> AsyncIterator[EventMessage]: try: while True: item = await queue.get() if isinstance(item, Exception): raise item if not isinstance(item, EventMessage): raise AstrBotError.protocol_error("流式调用收到非法事件") if item.phase == "started": continue if item.phase == "delta": yield item continue if item.phase == "completed": break if item.phase == "failed": raise AstrBotError.from_payload( item.error.model_dump() if item.error else {} ) finally: self._pending_streams.pop(request_id, None) return iterator() async def cancel(self, request_id: str, reason: str = "user_cancelled") -> None: """向远端发送取消请求,尝试中止指定 ID 的在途调用。""" await self._send(CancelMessage(id=request_id, reason=reason)) def _next_id(self) -> str: """生成当前连接内递增的消息 ID。""" self._counter += 1 return f"msg_{self._counter:04d}" def _ensure_usable(self) -> None: """确保连接仍处于可用状态,否则立即抛出协议错误。""" if self._unusable: raise AstrBotError.protocol_error("连接已进入不可用状态") async def _handle_raw_message(self, payload: str) -> None: """解析原始消息并分发到对应的消息处理分支。""" try: message = parse_message(payload) if isinstance(message, ResultMessage): await self._handle_result(message) return if isinstance(message, EventMessage): await self._handle_event(message) return if isinstance(message, InitializeMessage): await self._handle_initialize(message) return if isinstance(message, InvokeMessage): token = CancelToken() started = asyncio.Event() task = asyncio.create_task(self._handle_invoke(message, token, started)) self._inbound_tasks[message.id] = (task, token, started) task.add_done_callback( lambda _task, request_id=message.id: self._inbound_tasks.pop( request_id, None ) ) return if isinstance(message, CancelMessage): await self._handle_cancel(message) return except Exception as exc: if isinstance(exc, AstrBotError): error = exc else: error = AstrBotError.protocol_error(f"无法解析协议消息: {exc}") await self._fail_connection(error) raise error from exc async def _handle_initialize(self, message: InitializeMessage) -> None: """处理远端发起的初始化握手并返回握手结果。""" self.remote_peer = message.peer self.remote_handlers = message.handlers self.remote_provided_capabilities = message.provided_capabilities self.remote_provided_capability_map = { item.name: item for item in message.provided_capabilities } self.remote_metadata = message.metadata if self._initialize_handler is None: await self._reject_initialize( message, AstrBotError.protocol_error("对端不接受 initialize"), ) return if message.protocol_version != self.protocol_version: await self._reject_initialize( message, AstrBotError.protocol_version_mismatch( f"服务端支持协议版本 {self.protocol_version},客户端请求版本 {message.protocol_version}" ), ) return output = await self._initialize_handler(message) await self._send( ResultMessage( id=message.id, kind="initialize_result", success=True, output=output.model_dump(), ) ) self._remote_initialized.set() async def _handle_invoke( self, message: InvokeMessage, token: CancelToken, started: asyncio.Event, ) -> None: """处理远端发起的能力调用,并按流式或非流式协议返回结果。""" try: started.set() token.raise_if_cancelled() if self._invoke_handler is None: raise AstrBotError.capability_not_found(message.capability) execution = await self._invoke_handler(message, token) if inspect.isawaitable(execution): execution = await execution if message.stream: if not isinstance(execution, StreamExecution): raise AstrBotError.protocol_error( "stream=true 必须返回 StreamExecution" ) await self._send(EventMessage(id=message.id, phase="started")) chunks: list[dict[str, Any]] = [] async for chunk in execution.iterator: chunks.append(chunk) await self._send( EventMessage(id=message.id, phase="delta", data=chunk) ) await self._send( EventMessage( id=message.id, phase="completed", output=execution.finalize(chunks), ) ) return if isinstance(execution, StreamExecution): raise AstrBotError.protocol_error("stream=false 不能返回流式执行对象") await self._send( ResultMessage(id=message.id, success=True, output=execution) ) except asyncio.CancelledError: await self._send_cancelled_termination(message) except LookupError as exc: error = AstrBotError.invalid_input(str(exc)) await self._send_error_result(message, error) except AstrBotError as exc: await self._send_error_result(message, exc) except Exception as exc: await self._send_error_result( message, AstrBotError.internal_error(str(exc)) ) async def _handle_cancel(self, message: CancelMessage) -> None: """处理远端取消请求并终止对应的入站任务。""" inbound = self._inbound_tasks.get(message.id) if inbound is None: return task, token, started = inbound token.cancel() if self._cancel_handler is not None: await self._cancel_handler(message.id) if started.is_set(): task.cancel() async def _handle_result(self, message: ResultMessage) -> None: """处理非流式结果消息并唤醒等待中的调用方。""" future = self._pending_results.pop(message.id, None) if future is None: queue = self._pending_streams.get(message.id) if queue is not None: await queue.put( AstrBotError.protocol_error("stream=true 调用不应收到 result") ) return # 检查 future 是否已完成(可能被调用方取消) if not future.done(): future.set_result(message) async def _handle_event(self, message: EventMessage) -> None: """处理流式事件消息并投递到对应请求的事件队列。""" queue = self._pending_streams.get(message.id) if queue is None: future = self._pending_results.get(message.id) if future is not None and not future.done(): future.set_exception( AstrBotError.protocol_error("stream=false 调用不应收到 event") ) return await queue.put(message) async def _send_error_result( self, message: InvokeMessage, error: AstrBotError ) -> None: """根据调用模式,将错误编码为 `result` 或失败事件发回远端。""" if message.stream: await self._send( EventMessage( id=message.id, phase="failed", error=ErrorPayload.model_validate(error.to_payload()), ) ) return await self._send( ResultMessage( id=message.id, success=False, error=ErrorPayload.model_validate(error.to_payload()), ) ) async def _reject_initialize( self, message: InitializeMessage, error: AstrBotError ) -> None: """拒绝一次初始化握手,并把连接标记为不可继续使用。""" await self._send( ResultMessage( id=message.id, kind="initialize_result", success=False, error=ErrorPayload.model_validate(error.to_payload()), ) ) self._unusable = True self._remote_initialized.set() await self.stop() async def _send_cancelled_termination(self, message: InvokeMessage) -> None: """把本端取消执行转换为标准化的取消错误响应。""" error = AstrBotError.cancelled() await self._send_error_result(message, error) async def _fail_connection(self, error: AstrBotError) -> None: """把连接标记为不可用,并让所有等待中的调用尽快失败。""" if self._unusable: return self._unusable = True self._remote_initialized.set() for future in list(self._pending_results.values()): if not future.done(): future.set_exception(error) self._pending_results.clear() for queue in list(self._pending_streams.values()): await queue.put(error) self._pending_streams.clear() for task, token, _started in list(self._inbound_tasks.values()): token.cancel() task.cancel() self._inbound_tasks.clear() asyncio.create_task(self.stop()) async def _send(self, message) -> None: """序列化协议消息并通过底层传输发送出去。""" await self.transport.send(message.model_dump_json(exclude_none=True))