From 4b6e20468bde67820f27f6a12e5019066d590a7f Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Fri, 13 Mar 2026 06:23:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=92=8C=E4=BB=A3=E7=A0=81=EF=BC=8C=E4=BC=98=E5=8C=96=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E6=A8=A1=E5=9D=97=E7=9A=84=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=EF=BC=8C=E7=A1=AE=E4=BF=9D=E9=AB=98=E7=BA=A7=E5=8E=9F=E8=AF=AD?= =?UTF-8?q?=E7=9A=84=E7=A8=B3=E5=AE=9A=E6=80=A7=E5=92=8C=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + CLAUDE.md | 1 + src-new/astrbot_sdk/runtime/__init__.py | 170 +-------------------- test_plugin/commands/hello.py | 12 +- tests_v4/test_legacy_plugin_integration.py | 48 ++++-- tests_v4/test_top_level_modules.py | 47 ++++++ 6 files changed, 94 insertions(+), 185 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 66f7f43ce..dc1be4bf1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ - 2026-03-13: Old Star docs under `docs/zh/dev/star/` describe end-to-end legacy behavior, not just import surfaces. Current compat layer can import `AstrMessageEvent`, `MessageChain`, and some `filter.*` helpers, but handler result consumption is still effectively plain-text only and many documented legacy features remain absent or partial, including command groups, lifecycle/LLM hooks, session waiters, rich media helper constructors, config schema loading, and persona/provider management. Do not treat "type exists" as "old plugin behavior is compatible"; verify the runtime path end to end before declaring parity. - 2026-03-13: Old Star docs and examples frequently import message components via `astrbot.api.message_components`, not only `astrbot.api.message`. When checking message compatibility, verify the dedicated `api.message_components` import path, legacy constructor aliases like `At(qq=...)` / `Node(uin=..., name=...)`, and helper factories such as `Image.fromURL()` before declaring the message compat surface complete. - 2026-03-13: `api.message.components.BaseMessageComponent.to_dict()` must emit JSON-ready primitive values, not raw `Enum` members. Leaving `ComponentType` objects in payloads only looks harmless when a later JSON serializer fixes them; it breaks direct mock assertions, in-process capability routing, and any non-JSON send path. +- 2026-03-13: Keep `astrbot_sdk.runtime` root exports narrow. `Peer` / `Transport` / `CapabilityRouter` / `HandlerDispatcher` are reasonable advanced runtime primitives, but loader/bootstrap data structures and orchestration helpers (`LoadedPlugin`, `PluginEnvironmentManager`, `WorkerSession`, `run_supervisor`, etc.) should stay in their submodules instead of becoming accidental root-level stable API. # 开发命令 diff --git a/CLAUDE.md b/CLAUDE.md index 66f7f43ce..dc1be4bf1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,6 +11,7 @@ - 2026-03-13: Old Star docs under `docs/zh/dev/star/` describe end-to-end legacy behavior, not just import surfaces. Current compat layer can import `AstrMessageEvent`, `MessageChain`, and some `filter.*` helpers, but handler result consumption is still effectively plain-text only and many documented legacy features remain absent or partial, including command groups, lifecycle/LLM hooks, session waiters, rich media helper constructors, config schema loading, and persona/provider management. Do not treat "type exists" as "old plugin behavior is compatible"; verify the runtime path end to end before declaring parity. - 2026-03-13: Old Star docs and examples frequently import message components via `astrbot.api.message_components`, not only `astrbot.api.message`. When checking message compatibility, verify the dedicated `api.message_components` import path, legacy constructor aliases like `At(qq=...)` / `Node(uin=..., name=...)`, and helper factories such as `Image.fromURL()` before declaring the message compat surface complete. - 2026-03-13: `api.message.components.BaseMessageComponent.to_dict()` must emit JSON-ready primitive values, not raw `Enum` members. Leaving `ComponentType` objects in payloads only looks harmless when a later JSON serializer fixes them; it breaks direct mock assertions, in-process capability routing, and any non-JSON send path. +- 2026-03-13: Keep `astrbot_sdk.runtime` root exports narrow. `Peer` / `Transport` / `CapabilityRouter` / `HandlerDispatcher` are reasonable advanced runtime primitives, but loader/bootstrap data structures and orchestration helpers (`LoadedPlugin`, `PluginEnvironmentManager`, `WorkerSession`, `run_supervisor`, etc.) should stay in their submodules instead of becoming accidental root-level stable API. # 开发命令 diff --git a/src-new/astrbot_sdk/runtime/__init__.py b/src-new/astrbot_sdk/runtime/__init__.py index 475ad034c..f50dff207 100644 --- a/src-new/astrbot_sdk/runtime/__init__.py +++ b/src-new/astrbot_sdk/runtime/__init__.py @@ -1,155 +1,13 @@ -"""运行时模块。 +"""AstrBot SDK 的高级运行时原语。 -定义 AstrBot SDK 的运行时架构,包括插件加载、能力路由、处理器分发和通信抽象。 - -架构说明: - 旧版: - - 目录结构复杂:api/, rpc/, stars/ 等多个子目录 - - 使用 JSON-RPC 2.0 协议进行通信 - - StarManager 负责插件发现和加载 - - StarRunner 负责处理器执行 - - Galaxy 负责虚拟星层管理 - - 传输层分离为 client/server 两套实现 - - 新版: - - 目录结构精简:仅 6 个核心文件 - - 使用自描述协议进行通信 - - Peer 统一处理协议层消息收发 - - Transport 抽象传输层,支持多种实现 - - CapabilityRouter 注册和路由能力调用 - - HandlerDispatcher 分发处理器调用 - - SupervisorRuntime 管理多 Worker 会话 - -核心概念对比: - 旧版概念: - - StarManager: 插件发现和加载 - - StarRunner: 处理器执行 - - Galaxy: 虚拟星层管理 - - JSONRPCServer/Client: JSON-RPC 通信 - - HandshakeHandler: 握手处理 - - HandlerExecutor: 处理器执行 - - 新版概念: - - Peer: 协议对等端,统一处理消息 - - Transport: 传输层抽象 - - CapabilityRouter: 能力路由 - - HandlerDispatcher: 处理器分发 - - SupervisorRuntime: 多 Worker 管理 - - WorkerSession: 单个 Worker 会话 - - PluginWorkerRuntime: 插件 Worker 运行时 - -通信流程对比: - 旧版 JSON-RPC 流程: - 1. Core -> Plugin: {"method": "handshake", ...} - 2. Plugin -> Core: {"result": {"handlers": [...]}} - 3. Core -> Plugin: {"method": "call_handler", "params": {...}} - 4. Plugin -> Core: {"method": "handler_stream_start", ...} - 5. Plugin -> Core: {"method": "handler_stream_update", ...} - 6. Plugin -> Core: {"method": "handler_stream_end", ...} - - 新版协议流程: - 1. Plugin -> Core: {"type": "initialize", "handlers": [...], "provided_capabilities": [...]} - 2. Core -> Plugin: {"type": "result", "kind": "initialize_result", ...} - 3. Core -> Plugin: {"type": "invoke", "capability": "handler.invoke", ...} - 4. Plugin -> Core: {"type": "event", "phase": "started"} - 5. Plugin -> Core: {"type": "event", "phase": "delta", "data": {...}} - 6. Plugin -> Core: {"type": "event", "phase": "completed", "output": {...}} - -插件加载对比: - 旧版 StarManager: - - 通过 plugin.yaml 发现插件 - - 动态导入组件类并实例化 - - 注册到 star_handlers_registry - - 使用 functools.partial 绑定实例 - - 新版 loader.py: - - PluginSpec 描述插件规范 - - PluginEnvironmentManager 管理虚拟环境 - - load_plugin() 加载并解析组件 - - LoadedHandler 封装处理器和描述符 - - 支持新旧 Star 组件兼容 - -传输层对比: - 旧版传输层: - - 分离的 client/ 和 server/ 目录 - - JSONRPCClient 基类 + StdioClient/WebSocketClient - - JSONRPCServer 基类 + StdioServer/WebSocketServer - - 通过 set_message_handler 设置回调 - - 新版传输层: - - 统一的 Transport 抽象基类 - - StdioTransport: 支持进程模式和文件模式 - - WebSocketServerTransport: WebSocket 服务端 - - WebSocketClientTransport: WebSocket 客户端 - - 通过 set_message_handler 设置回调 - -处理器执行对比: - 旧版 HandlerExecutor: - - 从 star_handlers_registry 获取处理器 - - 调用 handler(event, **args) - - 通过 JSON-RPC notification 发送流式结果 - - 无参数注入支持 - - 新版 HandlerDispatcher: - - 从 LoadedHandler 映射获取处理器 - - 支持类型注解注入 (MessageEvent, Context) - - 支持参数名注入 (event, ctx, context) - - 支持 legacy_args 注入 (命令参数等) - - 支持 Optional[Type] 类型 - - 统一的错误处理和生命周期回调 - -能力系统对比: - 旧版: - - 无显式的能力声明系统 - - 通过 call_context_function 调用核心功能 - - 上下文函数硬编码在核心侧 - - 新版 CapabilityRouter: - - CapabilityDescriptor 声明能力 - - JSON Schema 验证输入输出 - - 支持流式能力 (stream_handler) - - 内置能力:llm.chat, memory.*, db.*, platform.* - - 支持 Supervisor 聚合并转发插件自定义 capability - -`runtime` 负责把协议、传输、插件加载和生命周期管理拼成一条完整执行链: - -- `Transport`: 只负责字符串级别收发 -- `Peer`: 负责协议消息、请求关联、流式事件和取消 -- `CapabilityRouter`: 核心侧能力注册与路由 -- `HandlerDispatcher`: 插件侧 handler 调用适配 -- `loader` / `bootstrap`: 插件发现、Worker 启动和 Supervisor 编排 - -设计上,legacy 兼容只出现在加载与分发边界;`Transport` 和 `Peer` 不直接携带 -旧版业务语义。 +这里仅暴露相对稳定的运行时构件:协议 `Peer`、传输抽象以及能力/处理器分发器。 +大多数插件作者应优先使用顶层 `astrbot_sdk` 或 `astrbot_sdk.api`。 +`loader` / `bootstrap` 等编排细节保留在各自子模块中,不作为根级稳定契约。 """ -from .bootstrap import ( - PluginWorkerRuntime, - SupervisorRuntime, - WorkerSession, - run_plugin_worker, - run_supervisor, - run_websocket_server, -) from .capability_router import CapabilityRouter, StreamExecution from .handler_dispatcher import HandlerDispatcher -from .loader import ( - LoadedCapability, - LoadedHandler, - LoadedPlugin, - PluginDiscoveryResult, - PluginEnvironmentManager, - PluginSpec, - discover_plugins, - load_plugin, - load_plugin_spec, -) -from .peer import ( - CancelHandler, - InitializeHandler, - InvokeHandler, - Peer, -) +from .peer import Peer from .transport import ( MessageHandler, StdioTransport, @@ -159,31 +17,13 @@ from .transport import ( ) __all__ = [ - "CancelHandler", "CapabilityRouter", "HandlerDispatcher", - "InitializeHandler", - "InvokeHandler", - "LoadedCapability", - "LoadedHandler", - "LoadedPlugin", "MessageHandler", "Peer", - "PluginDiscoveryResult", - "PluginEnvironmentManager", - "PluginSpec", - "PluginWorkerRuntime", "StdioTransport", "StreamExecution", - "SupervisorRuntime", "Transport", "WebSocketClientTransport", "WebSocketServerTransport", - "WorkerSession", - "discover_plugins", - "load_plugin", - "load_plugin_spec", - "run_plugin_worker", - "run_supervisor", - "run_websocket_server", ] diff --git a/test_plugin/commands/hello.py b/test_plugin/commands/hello.py index 7ad9ab4d7..6f717c7c7 100644 --- a/test_plugin/commands/hello.py +++ b/test_plugin/commands/hello.py @@ -10,6 +10,7 @@ from astrbot_sdk.api.components.command import CommandComponent from astrbot_sdk.api.event import AstrMessageEvent, filter +from astrbot_sdk.api.event.filter import EventMessageType, PlatformAdapterType from astrbot_sdk.api.star.context import Context from astrbot_sdk.api.message import MessageChain from astrbot_sdk.api.message_components import Plain, At, Image @@ -77,15 +78,16 @@ class HelloCommand(CommandComponent): at = At(user_id="123", user_name="test_user") # 测试 Image - img = Image.fromURL("https://example.com/img.png") + image = Image.fromURL("https://example.com/img.png") # 测试 to_dict plain_dict = plain.to_dict() at_dict = at.to_dict() + image_dict = image.to_dict() - logger.info(f"Plain: {plain_dict}, At: {at_dict}") + logger.info(f"Plain: {plain_dict}, At: {at_dict}, Image: {image_dict}") - yield event.plain_result(f"Components created: Plain, At, Image") + yield event.plain_result("Components created: Plain, At, Image") @filter.regex(r"^ping.*") async def ping_regex(self, event: AstrMessageEvent): @@ -97,12 +99,12 @@ class HelloCommand(CommandComponent): """测试权限过滤 (应该被跳过,因为没有 require_admin)。""" yield event.plain_result("Admin command executed") - @filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE) + @filter.event_message_type(EventMessageType.GROUP_MESSAGE) async def group_only(self, event: AstrMessageEvent): """测试消息类型过滤。""" yield event.plain_result("Group message received") - @filter.platform_adapter_type("aiocqhttp") + @filter.platform_adapter_type(PlatformAdapterType.AIOCQHTTP) async def cqhttp_only(self, event: AstrMessageEvent): """测试平台过滤。""" yield event.plain_result("CQHttp platform detected") diff --git a/tests_v4/test_legacy_plugin_integration.py b/tests_v4/test_legacy_plugin_integration.py index 0e9d74270..977013c58 100644 --- a/tests_v4/test_legacy_plugin_integration.py +++ b/tests_v4/test_legacy_plugin_integration.py @@ -9,14 +9,12 @@ import sys import tempfile import textwrap from pathlib import Path -from unittest.mock import AsyncMock, MagicMock, patch import pytest import yaml from astrbot_sdk.protocol.descriptors import CommandTrigger, MessageTrigger from astrbot_sdk.runtime.loader import ( - LoadedPlugin, load_plugin, load_plugin_spec, ) @@ -186,8 +184,9 @@ class TestFilterDecorators: def test_event_message_type_decorator(self): """测试 event_message_type 装饰器。""" from astrbot_sdk.api.event import filter + from astrbot_sdk.api.event.filter import EventMessageType - @filter.event_message_type(filter.EventMessageType.GROUP_MESSAGE) + @filter.event_message_type(EventMessageType.GROUP_MESSAGE) @filter.command("group_cmd") async def handler(event): pass @@ -323,11 +322,12 @@ class TestLoadLegacyStylePlugin: manifest_path = plugin_dir / "plugin.yaml" requirements_path = plugin_dir / "requirements.txt" - commands_dir = plugin_dir / "commands" - commands_dir.mkdir() - (commands_dir / "__init__.py").write_text("", encoding="utf-8") + # 使用唯一的模块名避免与其他测试冲突 + handlers_dir = plugin_dir / "chain_handlers" + handlers_dir.mkdir() + (handlers_dir / "__init__.py").write_text("", encoding="utf-8") - (commands_dir / "chain.py").write_text( + (handlers_dir / "chain_cmd.py").write_text( textwrap.dedent(""" from astrbot_sdk.api.components.command import CommandComponent from astrbot_sdk.api.event import AstrMessageEvent, filter @@ -355,7 +355,7 @@ class TestLoadLegacyStylePlugin: "runtime": {"python": "3.12"}, "components": [ { - "class": "commands.chain:ChainCommand", + "class": "chain_handlers.chain_cmd:ChainCommand", "type": "command", "name": "chain", } @@ -368,8 +368,10 @@ class TestLoadLegacyStylePlugin: spec = load_plugin_spec(plugin_dir) + path_added = False if str(plugin_dir) not in sys.path: sys.path.insert(0, str(plugin_dir)) + path_added = True try: loaded = load_plugin(spec) @@ -377,22 +379,30 @@ class TestLoadLegacyStylePlugin: assert len(loaded.instances) == 1 assert len(loaded.handlers) >= 1 finally: - if str(plugin_dir) in sys.path: + # 清理导入的模块 + modules_to_remove = [ + k for k in list(sys.modules.keys()) if k.startswith("chain_handlers") + ] + for mod in modules_to_remove: + del sys.modules[mod] + if path_added and str(plugin_dir) in sys.path: sys.path.remove(str(plugin_dir)) def test_load_plugin_with_regex_handler(self): """测试加载使用正则处理器的插件。""" + with tempfile.TemporaryDirectory() as temp_dir: plugin_dir = Path(temp_dir) / "regex_plugin" plugin_dir.mkdir() manifest_path = plugin_dir / "plugin.yaml" requirements_path = plugin_dir / "requirements.txt" - commands_dir = plugin_dir / "commands" - commands_dir.mkdir() - (commands_dir / "__init__.py").write_text("", encoding="utf-8") + # 使用唯一的模块名避免与其他测试冲突 + regex_handlers_dir = plugin_dir / "regex_handlers" + regex_handlers_dir.mkdir() + (regex_handlers_dir / "__init__.py").write_text("", encoding="utf-8") - (commands_dir / "matchers.py").write_text( + (regex_handlers_dir / "matcher.py").write_text( textwrap.dedent(""" from astrbot_sdk.api.components.command import CommandComponent from astrbot_sdk.api.event import AstrMessageEvent, filter @@ -416,7 +426,7 @@ class TestLoadLegacyStylePlugin: "runtime": {"python": "3.12"}, "components": [ { - "class": "commands.matchers:RegexCommand", + "class": "regex_handlers.matcher:RegexCommand", "type": "command", "name": "regex", } @@ -429,8 +439,10 @@ class TestLoadLegacyStylePlugin: spec = load_plugin_spec(plugin_dir) + path_added = False if str(plugin_dir) not in sys.path: sys.path.insert(0, str(plugin_dir)) + path_added = True try: loaded = load_plugin(spec) @@ -440,7 +452,13 @@ class TestLoadLegacyStylePlugin: assert isinstance(handler.descriptor.trigger, MessageTrigger) assert handler.descriptor.trigger.regex == r"^ping.*" finally: - if str(plugin_dir) in sys.path: + # 清理导入的模块 + modules_to_remove = [ + k for k in sys.modules if k.startswith("regex_handlers") + ] + for mod in modules_to_remove: + del sys.modules[mod] + if path_added and str(plugin_dir) in sys.path: sys.path.remove(str(plugin_dir)) diff --git a/tests_v4/test_top_level_modules.py b/tests_v4/test_top_level_modules.py index 5581027b5..77479a0d7 100644 --- a/tests_v4/test_top_level_modules.py +++ b/tests_v4/test_top_level_modules.py @@ -12,6 +12,7 @@ from unittest.mock import AsyncMock, Mock, patch import astrbot_sdk import astrbot_sdk.compat as compat_module +import astrbot_sdk.runtime as runtime_module import pytest from click.testing import CliRunner @@ -25,6 +26,16 @@ from astrbot_sdk.context import Context from astrbot_sdk.decorators import on_command from astrbot_sdk.errors import AstrBotError, ErrorCodes from astrbot_sdk.events import MessageEvent +from astrbot_sdk.runtime.capability_router import CapabilityRouter, StreamExecution +from astrbot_sdk.runtime.handler_dispatcher import HandlerDispatcher +from astrbot_sdk.runtime.peer import Peer +from astrbot_sdk.runtime.transport import ( + MessageHandler, + StdioTransport, + Transport, + WebSocketClientTransport, + WebSocketServerTransport, +) from astrbot_sdk.star import Star TOP_LEVEL_MODULES = [ @@ -36,6 +47,7 @@ TOP_LEVEL_MODULES = [ "astrbot_sdk.decorators", "astrbot_sdk.errors", "astrbot_sdk.events", + "astrbot_sdk.runtime", "astrbot_sdk.star", ] @@ -89,6 +101,41 @@ class TestTopLevelImports: "LegacyConversationManager", ] + def test_runtime_module_reexports_advanced_runtime_primitives(self): + """runtime module should expose only the small advanced runtime surface.""" + assert runtime_module.Peer is Peer + assert runtime_module.CapabilityRouter is CapabilityRouter + assert runtime_module.HandlerDispatcher is HandlerDispatcher + assert runtime_module.Transport is Transport + assert runtime_module.MessageHandler is MessageHandler + assert runtime_module.StdioTransport is StdioTransport + assert runtime_module.WebSocketClientTransport is WebSocketClientTransport + assert runtime_module.WebSocketServerTransport is WebSocketServerTransport + assert runtime_module.StreamExecution is StreamExecution + + def test_runtime_module_does_not_reexport_loader_or_bootstrap_details(self): + """runtime root should not expose loader/bootstrap internals as stable API.""" + assert not hasattr(runtime_module, "PluginEnvironmentManager") + assert not hasattr(runtime_module, "PluginWorkerRuntime") + assert not hasattr(runtime_module, "SupervisorRuntime") + assert not hasattr(runtime_module, "WorkerSession") + assert not hasattr(runtime_module, "LoadedPlugin") + assert not hasattr(runtime_module, "run_supervisor") + + def test_runtime_module_all_matches_narrow_public_surface(self): + """runtime.__all__ should stay aligned with the narrowed advanced API.""" + assert runtime_module.__all__ == [ + "CapabilityRouter", + "HandlerDispatcher", + "MessageHandler", + "Peer", + "StdioTransport", + "StreamExecution", + "Transport", + "WebSocketClientTransport", + "WebSocketServerTransport", + ] + class TestCliModule: """Tests for cli.py and __main__.py."""