feat: 增强 LegacyContext 和 WorkerSession 的消息处理,支持更灵活的参数传递和连接关闭管理

This commit is contained in:
whatevertogo
2026-03-13 00:07:34 +08:00
parent 8922e1409c
commit b2d87df223
4 changed files with 100 additions and 8 deletions

View File

@@ -117,7 +117,15 @@ class LegacyContext:
async def send_message(self, session: str, message_chain: Any) -> None:
_warn_once("context.send_message()", "ctx.platform.send(session, text)")
ctx = self.require_runtime_context()
await ctx.platform.send(session, str(message_chain))
# 正确序列化 MessageChain 对象
# 优先使用 get_plain_text() 方法(旧版 MessageChain
if hasattr(message_chain, "get_plain_text") and callable(message_chain.get_plain_text):
text = message_chain.get_plain_text()
elif hasattr(message_chain, "to_text") and callable(message_chain.to_text):
text = message_chain.to_text()
else:
text = str(message_chain)
await ctx.platform.send(session, text)
async def add_llm_tools(self, *tools: Any) -> None:
_warn_once("context.add_llm_tools()", "ctx.llm.chat_raw(..., tools=...)")

View File

@@ -5,6 +5,7 @@ import inspect
import os
import signal
import sys
from collections.abc import Callable
from pathlib import Path
from typing import IO, Any
@@ -71,11 +72,13 @@ class WorkerSession:
repo_root: Path,
env_manager: PluginEnvironmentManager,
capability_router: CapabilityRouter,
on_closed: Callable[[], None] | None = None,
) -> None:
self.plugin = plugin
self.repo_root = repo_root.resolve()
self.env_manager = env_manager
self.capability_router = capability_router
self.on_closed = on_closed
self.peer: Peer | None = None
self.handlers = []
@@ -110,12 +113,42 @@ class WorkerSession:
self.peer.set_invoke_handler(self._handle_capability_invoke)
try:
await self.peer.start()
await self.peer.wait_until_remote_initialized()
# 同时监听初始化完成和连接关闭,避免 worker 崩溃时等满超时
init_task = asyncio.create_task(self.peer.wait_until_remote_initialized(timeout=None))
closed_task = asyncio.create_task(self.peer.wait_closed())
done, pending = await asyncio.wait(
{init_task, closed_task},
return_when=asyncio.FIRST_COMPLETED,
)
for task in pending:
task.cancel()
try:
await task
except asyncio.CancelledError:
pass
if closed_task in done:
raise RuntimeError(f"插件 {self.plugin.name} worker 进程在初始化阶段退出")
self.handlers = list(self.peer.remote_handlers)
# 启动后台任务监听连接关闭
if self.on_closed is not None:
asyncio.create_task(self._watch_connection())
except Exception:
await self.stop()
raise
async def _watch_connection(self) -> None:
"""监听 Worker 连接关闭,触发清理回调"""
if self.peer is not None:
await self.peer.wait_closed()
if self.on_closed is not None:
try:
self.on_closed()
except Exception:
logger.exception("on_closed callback failed for plugin {}", self.plugin.name)
async def stop(self) -> None:
if self.peer is not None:
await self.peer.stop()
@@ -220,6 +253,7 @@ class SupervisorRuntime:
repo_root=self.repo_root,
env_manager=self.env_manager,
capability_router=self.capability_router,
on_closed=lambda name=plugin.name: self._handle_worker_closed(name),
)
try:
await session.start()
@@ -248,6 +282,19 @@ class SupervisorRuntime:
await self.stop()
raise
def _handle_worker_closed(self, plugin_name: str) -> None:
"""Worker 连接关闭时的清理回调"""
session = self.worker_sessions.pop(plugin_name, None)
if session is None:
return
# 从 handler_to_worker 中移除该 worker 的所有 handlers
for handler in session.handlers:
self.handler_to_worker.pop(handler.id, None)
# 从 loaded_plugins 中移除
if plugin_name in self.loaded_plugins:
self.loaded_plugins.remove(plugin_name)
logger.warning("插件 {} worker 连接已关闭,已清理相关 handlers", plugin_name)
async def stop(self) -> None:
for session in list(self.worker_sessions.values()):
await session.stop()
@@ -312,7 +359,12 @@ class PluginWorkerRuntime:
[item.descriptor for item in self.loaded_plugin.handlers],
metadata={"plugin_id": self.plugin.name},
)
await self._run_lifecycle("on_start")
try:
await self._run_lifecycle("on_start")
except Exception:
# on_start 失败时,通知 Supervisor 并退出
await self.peer.stop()
raise
async def stop(self) -> None:
try:

View File

@@ -29,7 +29,10 @@ class HandlerDispatcher:
if loaded.legacy_context is not None:
loaded.legacy_context.bind_runtime_context(ctx)
task = asyncio.create_task(self._run_handler(loaded, event, ctx))
# 提取 legacy args 用于兼容旧版 handler 签名
legacy_args = message.input.get("args") or {}
task = asyncio.create_task(self._run_handler(loaded, event, ctx, legacy_args))
self._active[message.id] = (task, cancel_token)
try:
await task
@@ -50,9 +53,10 @@ class HandlerDispatcher:
loaded: LoadedHandler,
event: MessageEvent,
ctx: Context,
legacy_args: dict[str, Any] | None = None,
) -> None:
try:
result = loaded.callable(*self._build_args(loaded.callable, event, ctx))
result = loaded.callable(*self._build_args(loaded.callable, event, ctx, legacy_args))
if inspect.isasyncgen(result):
async for item in result:
await self._consume_legacy_result(item, event)
@@ -65,9 +69,16 @@ class HandlerDispatcher:
await self._handle_error(loaded.owner, exc, event, ctx)
raise
def _build_args(self, handler, event: MessageEvent, ctx: Context) -> list[Any]:
def _build_args(
self,
handler,
event: MessageEvent,
ctx: Context,
legacy_args: dict[str, Any] | None = None,
) -> list[Any]:
signature = inspect.signature(handler)
args: list[Any] = []
legacy_args = legacy_args or {}
for parameter in signature.parameters.values():
if parameter.kind not in (
inspect.Parameter.POSITIONAL_ONLY,
@@ -78,6 +89,9 @@ class HandlerDispatcher:
args.append(event)
elif parameter.name in {"ctx", "context"}:
args.append(ctx)
elif parameter.name in legacy_args:
# 支持从 legacy args 中注入参数如命令参数、regex 捕获组等)
args.append(legacy_args[parameter.name])
return args
async def _consume_legacy_result(self, item: Any, event: MessageEvent) -> None:

View File

@@ -67,6 +67,22 @@ class Peer:
async def stop(self) -> None:
self._closed = 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 in list(self._inbound_tasks.values()):
token.cancel()
task.cancel()
self._inbound_tasks.clear()
await self.transport.stop()
async def wait_closed(self) -> None:
@@ -296,13 +312,15 @@ class Peer:
if queue is not None:
await queue.put(AstrBotError.protocol_error("stream=true 调用不应收到 result"))
return
future.set_result(message)
# 检查 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:
if future is not None and not future.done():
future.set_exception(AstrBotError.protocol_error("stream=false 调用不应收到 event"))
return
await queue.put(message)