# AstrBot SDK v4 架构与实现文档 ## 目录 1. [架构概览](#架构概览) 2. [目录结构](#目录结构) 3. [核心模块详解](#核心模块详解) - [协议层 (protocol/)](#协议层-protocol) - [运行时层 (runtime/)](#运行时层-runtime) - [客户端层 (clients/)](#客户端层-clients) - [API 层 (api/)](#api层-api) - [核心文件](#核心文件) 4. [五大硬性协议规则](#五大硬性协议规则) 5. [数据流与通信模型](#数据流与通信模型) 6. [扩展机制](#扩展机制) 7. [实现状态](#实现状态) --- ## 架构概览 AstrBot SDK v4 采用分层架构设计,从上到下分为: ``` ┌─────────────────────────────────────────────────────┐ │ 用户层 (User Layer) │ │ 插件开发者编写的 Star 类 │ ├─────────────────────────────────────────────────────┤ │ API 层 (API Layer) │ │ Star, Context, decorators, filter, events │ ├─────────────────────────────────────────────────────┤ │ 翻译层 (Translation Layer) │ │ HandlerDispatcher, Loader, LegacyAdapter │ ├─────────────────────────────────────────────────────┤ │ 通信层 (Communication Layer) │ │ Peer, Transport, CapabilityRouter │ └─────────────────────────────────────────────────────┘ ``` ### 核心设计原则 1. **协议优先**: 所有通信通过标准化的协议消息 2. **能力抽象**: 通过 Capability 系统暴露核心功能 3. **双向通信**: Plugin ↔ Core 的对称通信模型 4. **向后兼容**: LegacyAdapter 提供 v3 兼容层 --- ## 目录结构 ``` src-new/astrbot_sdk/ ├── __init__.py # 顶层导出 (Star, Context, decorators, events, errors) ├── __main__.py # CLI 入口点 ├── cli.py # Click 命令行工具 ├── star.py # Star 基类与 Handler 发现 ├── context.py # 运行时 Context 与 CancelToken ├── decorators.py # 装饰器 @on_command, @on_message, @provide_capability 等 ├── events.py # MessageEvent 事件定义 ├── errors.py # AstrBotError 错误模型与 ErrorCodes 常量 ├── compat.py # 兼容层导出 (LegacyContext, CommandComponent) ├── _legacy_api.py # Legacy Context, CommandComponent, LegacyConversationManager │ ├── protocol/ # 协议层 (已完成) │ ├── __init__.py # 公共入口,导出所有协议类型 │ ├── descriptors.py # HandlerDescriptor, CapabilityDescriptor │ │ # 内置能力 JSON Schema 常量 (16 个能力) │ ├── messages.py # 五种协议消息类型 │ └── legacy_adapter.py # v3 JSON-RPC ↔ v4 协议双向转换 │ ├── runtime/ # 运行时层 (已完成) │ ├── __init__.py # 公共入口,导出高级原语 │ ├── peer.py # 核心通信端点,远程元数据缓存 │ ├── transport.py # 传输层实现 (Stdio/WebSocket) │ ├── loader.py # 插件加载器、环境管理、配置规范化 │ ├── handler_dispatcher.py # Handler 分发器 + CapabilityDispatcher │ ├── capability_router.py # Capability 路由器,15 个内置能力 │ └── bootstrap.py # Supervisor/Worker 运行时,WebSocket 服务 │ ├── clients/ # 客户端层 (已完成) │ ├── __init__.py # 导出所有客户端 │ ├── _proxy.py # CapabilityProxy 代理 │ ├── llm.py # LLM 客户端 (chat/chat_raw/stream_chat) │ ├── db.py # 数据库客户端 (get/set/delete/list) │ ├── memory.py # 记忆客户端 (search/get/save/delete) │ └── platform.py # 平台客户端 (支持 SessionRef) │ └── api/ # API 层 - 兼容层 ├── __init__.py # 子模块导出 ├── basic/ # 基础实体与配置 │ ├── astrbot_config.py │ ├── conversation_mgr.py │ └── entities.py ├── components/ # 组件导出 │ └── command.py # CommandComponent 导出 ├── event/ # 事件相关 │ ├── astr_message_event.py │ ├── astrbot_message.py │ ├── event_result.py │ ├── event_type.py │ ├── filter.py # filter 命名空间 │ ├── message_session.py │ └── message_type.py ├── message/ # 消息链 │ ├── chain.py │ └── components.py ├── message_components/ # 消息组件兼容导出 ├── platform/ # 平台元数据 │ └── platform_metadata.py ├── provider/ # Provider 实体 │ └── entities.py └── star/ # Star 相关 ├── context.py # Legacy Context 导出 └── star.py ``` --- ## 核心模块详解 ### 协议层 (protocol/) 协议层负责消息格式定义和 legacy 兼容转换,是 v4 新引入的抽象层。 #### `descriptors.py` - 描述符定义 定义了 Handler 和 Capability 的元数据结构,以及内置能力的 JSON Schema 常量。 **核心类型:** ```python # 权限配置 class Permissions(_DescriptorBase): require_admin: bool = False level: int = 0 # 会话引用 (新增) class SessionRef(_DescriptorBase): conversation_id: str platform: str | None = None raw: dict[str, Any] | None = None # 四种 Trigger 类型 (discriminated union) class CommandTrigger: type: Literal["command"] = "command" command: str aliases: list[str] = [] description: str | None = None platforms: list[str] = [] class MessageTrigger: type: Literal["message"] = "message" regex: str | None = None keywords: list[str] = [] platforms: list[str] = [] message_types: list[str] = [] class EventTrigger: type: Literal["event"] = "event" event_type: str # 字符串形式,如 "message" class ScheduleTrigger: type: Literal["schedule"] = "schedule" cron: str | None = None interval_seconds: int | None = None # Trigger 联合类型 Trigger = Annotated[ CommandTrigger | MessageTrigger | EventTrigger | ScheduleTrigger, Field(discriminator="type"), ] # Handler 描述符 class HandlerDescriptor(_DescriptorBase): id: str trigger: Trigger kind: Literal["handler", "hook", "tool", "session"] = "handler" contract: str | None = None priority: int = 0 permissions: Permissions # Capability 描述符 class CapabilityDescriptor(_DescriptorBase): name: str description: str input_schema: dict[str, Any] | None = None output_schema: dict[str, Any] | None = None supports_stream: bool = False cancelable: bool = False ``` **内置能力 Schema 常量 (16 个能力):** ```python # LLM 相关 (3 个) LLM_CHAT_INPUT_SCHEMA / LLM_CHAT_OUTPUT_SCHEMA LLM_CHAT_RAW_INPUT_SCHEMA / LLM_CHAT_RAW_OUTPUT_SCHEMA LLM_STREAM_CHAT_INPUT_SCHEMA / LLM_STREAM_CHAT_OUTPUT_SCHEMA # Memory 相关 (4 个) MEMORY_SEARCH_INPUT_SCHEMA / MEMORY_SEARCH_OUTPUT_SCHEMA MEMORY_SAVE_INPUT_SCHEMA / MEMORY_SAVE_OUTPUT_SCHEMA MEMORY_GET_INPUT_SCHEMA / MEMORY_GET_OUTPUT_SCHEMA MEMORY_DELETE_INPUT_SCHEMA / MEMORY_DELETE_OUTPUT_SCHEMA # DB 相关 (4 个) DB_GET_INPUT_SCHEMA / DB_GET_OUTPUT_SCHEMA DB_SET_INPUT_SCHEMA / DB_SET_OUTPUT_SCHEMA DB_DELETE_INPUT_SCHEMA / DB_DELETE_OUTPUT_SCHEMA DB_LIST_INPUT_SCHEMA / DB_LIST_OUTPUT_SCHEMA # Platform 相关 (4 个) PLATFORM_SEND_INPUT_SCHEMA / PLATFORM_SEND_OUTPUT_SCHEMA PLATFORM_SEND_IMAGE_INPUT_SCHEMA / PLATFORM_SEND_IMAGE_OUTPUT_SCHEMA PLATFORM_SEND_CHAIN_INPUT_SCHEMA / PLATFORM_SEND_CHAIN_OUTPUT_SCHEMA PLATFORM_GET_MEMBERS_INPUT_SCHEMA / PLATFORM_GET_MEMBERS_OUTPUT_SCHEMA # SessionRef Schema (新增) SESSION_REF_SCHEMA # 汇总字典 BUILTIN_CAPABILITY_SCHEMAS: dict[str, dict[str, JSONSchema]] ``` **命名空间规范:** ```python RESERVED_CAPABILITY_NAMESPACES = ("handler", "system", "internal") RESERVED_CAPABILITY_PREFIXES = tuple(f"{namespace}." for namespace in RESERVED_CAPABILITY_NAMESPACES) CAPABILITY_NAME_PATTERN = r"^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$" ``` --- #### `messages.py` - 协议消息 定义五种协议消息类型,遵循**统一 id 字段**原则。 **消息类型:** | 类型 | 用途 | 关键字段 | |------|------|----------| | `InitializeMessage` | 初始化握手 | `peer`, `handlers`, `provided_capabilities`, `metadata` | | `InvokeMessage` | 调用 Capability | `capability`, `input`, `stream` | | `ResultMessage` | 返回结果 | `success`, `output`, `error` | | `EventMessage` | 流式事件 | `phase` (started/delta/completed/failed) | | `CancelMessage` | 取消请求 | `reason` | **核心结构:** ```python class ErrorPayload(_MessageBase): code: str message: str hint: str = "" retryable: bool = False class PeerInfo(_MessageBase): name: str role: Literal["plugin", "supervisor", "core"] version: str = "4.0" class InitializeMessage(_MessageBase): type: Literal["initialize"] = "initialize" id: str protocol_version: str = "4.0" peer: PeerInfo handlers: list[HandlerDescriptor] = [] provided_capabilities: list[CapabilityDescriptor] = [] metadata: dict[str, Any] = {} class InitializeOutput(_MessageBase): peer: PeerInfo capabilities: list[CapabilityDescriptor] = [] metadata: dict[str, Any] = {} class ResultMessage(_MessageBase): type: Literal["result"] = "result" id: str kind: str # "initialize_result" 或 capability 名称 success: bool output: dict[str, Any] | None = None error: ErrorPayload | None = None class InvokeMessage(_MessageBase): type: Literal["invoke"] = "invoke" id: str capability: str input: dict[str, Any] = {} stream: bool = False class EventMessage(_MessageBase): type: Literal["event"] = "event" id: str phase: Literal["started", "delta", "completed", "failed"] data: dict[str, Any] | None = None output: dict[str, Any] | None = None error: ErrorPayload | None = None class CancelMessage(_MessageBase): type: Literal["cancel"] = "cancel" id: str reason: str = "user_cancelled" ``` **核心函数:** ```python def parse_message(payload: str | bytes | dict) -> ProtocolMessage: """解析 JSON 为协议消息对象""" ``` --- #### `legacy_adapter.py` - 协议适配器 实现 v3 JSON-RPC 与 v4 协议的双向转换。 **核心类型:** ```python class LegacyRequest(_LegacyMessageBase): jsonrpc: Literal["2.0"] = "2.0" id: str | None = None method: str params: dict[str, Any] = {} class LegacySuccessResponse(_LegacyMessageBase): jsonrpc: Literal["2.0"] = "2.0" id: str | None = None result: Any class LegacyErrorResponse(_LegacyMessageBase): jsonrpc: Literal["2.0"] = "2.0" id: str | None = None error: LegacyErrorData LegacyMessage = LegacyRequest | LegacySuccessResponse | LegacyErrorResponse LegacyToV4Message = InitializeMessage | InvokeMessage | CancelMessage | None ``` **核心方法:** ```python def parse_legacy_message(payload: str | dict) -> LegacyMessage: """解析 legacy JSON-RPC 消息""" def legacy_message_to_v4(legacy: LegacyMessage) -> LegacyToV4Message: """Legacy JSON-RPC → v4 Message""" def initialize_to_legacy_handshake_response(message: InitializeMessage, output: InitializeOutput) -> dict: """v4 Initialize → Legacy Response""" def invoke_to_legacy_request(message: InvokeMessage) -> dict: """v4 Invoke → Legacy Request""" def result_to_legacy_response(message: ResultMessage) -> dict: """v4 Result → Legacy Response""" def event_to_legacy_notification(message: EventMessage) -> dict: """v4 Event → Legacy Notification""" def cancel_to_legacy_request(message: CancelMessage) -> dict: """v4 Cancel → Legacy Request""" ``` **转换映射表:** | 旧版 JSON-RPC | v4 协议消息 | |---------------|-------------| | `handshake` | `InitializeMessage` | | `call_handler` | `InvokeMessage(capability="handler.invoke")` | | `call_context_function` | `InvokeMessage(capability="internal.legacy.call_context_function")` | | `handler_stream_start/delta/end` | `EventMessage(phase="started/delta/completed/failed")` | | `cancel` | `CancelMessage` | **常量:** ```python LEGACY_JSONRPC_VERSION = "2.0" LEGACY_CONTEXT_CAPABILITY = "internal.legacy.call_context_function" LEGACY_HANDSHAKE_METADATA_KEY = "legacy_handshake_payload" LEGACY_PLUGIN_KEYS_METADATA_KEY = "legacy_plugin_keys" LEGACY_ADAPTER_MESSAGE_EVENT = 3 ``` --- ### 运行时层 (runtime/) 运行时层负责把协议、传输、插件加载和生命周期管理拼成一条完整执行链。 #### `__init__.py` - 模块导出 ```python from .capability_router import CapabilityRouter, StreamExecution from .handler_dispatcher import HandlerDispatcher from .peer import Peer from .transport import ( MessageHandler, StdioTransport, Transport, WebSocketClientTransport, WebSocketServerTransport, ) __all__ = [ "CapabilityRouter", "HandlerDispatcher", "MessageHandler", "Peer", "StdioTransport", "StreamExecution", "Transport", "WebSocketClientTransport", "WebSocketServerTransport", ] ``` **设计原则:** - **精简导出**: 仅暴露相对稳定的运行时构件 - **高级原语**: `Peer`、`Transport`、`CapabilityRouter`、`HandlerDispatcher` - **子模块隔离**: loader/bootstrap 编排细节保留在子模块中,不作为根级稳定契约 - **插件作者指引**: 大多数插件应使用顶层 `astrbot_sdk` 或 `astrbot_sdk.api` **不在根级导出的类型:** | 类型 | 所在子模块 | 原因 | |------|-----------|------| | `LoadedPlugin`, `LoadedHandler` | `loader` | 加载器内部数据结构 | | `PluginSpec`, `PluginDiscoveryResult` | `loader` | 插件发现元数据 | | `SupervisorRuntime`, `WorkerSession` | `bootstrap` | 运行时编排细节 | | `run_supervisor`, `run_plugin_worker` | `bootstrap` | 启动函数 | --- #### `peer.py` - 核心通信端点 实现 Plugin ↔ Core 的对称通信模型。 **核心方法:** ```python class Peer: # 生命周期 async def start(self) -> None: ... async def stop(self) -> None: ... async def wait_closed(self) -> None: ... async def wait_until_remote_initialized(self, timeout: float = 30.0) -> None: ... # 初始化 async def initialize(self, handlers, metadata, provided_capabilities) -> InitializeOutput: ... # Capability 调用 async def invoke(self, capability, payload, stream=False) -> dict: ... async def invoke_stream(self, capability, payload) -> AsyncIterator[EventMessage]: ... # 取消 async def cancel(self, request_id, reason="user_cancelled") -> None: ... # Handler 设置 def set_initialize_handler(self, handler: InitializeHandler): ... def set_invoke_handler(self, handler: InvokeHandler): ... def set_cancel_handler(self, handler: CancelHandler): ... ``` **远程元数据缓存 (新增):** ```python # 初始化后可用 remote_peer: PeerInfo # 远端身份信息 remote_handlers: list[HandlerDescriptor] # 远端声明的处理器 remote_provided_capabilities: list[CapabilityDescriptor] # 远端提供的内置能力 remote_capabilities: list[CapabilityDescriptor] # 远端能力描述符 remote_capability_map: dict[str, CapabilityDescriptor] # 能力名到描述符的映射 remote_metadata: dict[str, Any] # 握手元数据 ``` **内部状态:** ```python self._pending_results: dict[str, asyncio.Future[ResultMessage]] # 普通调用 self._pending_streams: dict[str, asyncio.Queue] # 流式调用 self._inbound_tasks: dict[str, tuple[Task, CancelToken]] # 入站任务 self._remote_initialized: asyncio.Event # 远端初始化状态 self._unusable: bool # 连接是否不可用 ``` **错误处理方法:** ```python async def _fail_connection(self, error: AstrBotError) -> None: """统一处理连接失败""" async def _reject_initialize(self, message, error) -> None: """拒绝初始化并标记连接不可用""" ``` --- #### `transport.py` - 传输层实现 抽象传输层,支持多种通信方式。 **类层次:** ``` Transport (ABC) ├── StdioTransport # 进程间通信 (stdin/stdout) ├── WebSocketServerTransport # WebSocket 服务端 └── WebSocketClientTransport # WebSocket 客户端 ``` **StdioTransport 特性:** - 支持作为父进程启动子进程 (`command` 参数) - 支持直接读写 stdin/stdout - 自动处理进程生命周期 **WebSocket 特性:** - 心跳机制 (通过 `heartbeat` 参数配置) - 单连接限制 (Server 端) - 自动重连需要外部实现 --- #### `loader.py` - 插件加载器 负责插件发现、环境准备、配置规范化和实例化。 **核心类型:** ```python @dataclass class PluginSpec: name: str plugin_dir: Path manifest_path: Path requirements_path: Path python_version: str manifest_data: dict[str, Any] @dataclass class LoadedHandler: descriptor: HandlerDescriptor callable: Any owner: Any legacy_context: Any | None = None @dataclass class LoadedCapability: # 新增 descriptor: CapabilityDescriptor callable: Any owner: Any stream_handler: Any | None = None @dataclass class LoadedPlugin: plugin: PluginSpec handlers: list[LoadedHandler] capabilities: list[LoadedCapability] # 新增 instances: list[Any] ``` **核心函数:** ```python def discover_plugins(plugins_dir: Path) -> PluginDiscoveryResult: """扫描插件目录,发现所有有效插件""" def load_plugin_spec(plugin_dir: Path) -> PluginSpec: """从插件目录加载插件规范""" def load_plugin(plugin: PluginSpec) -> LoadedPlugin: """加载插件,返回 Handler 和 Capability 列表""" class PluginEnvironmentManager: """使用 uv 管理插件虚拟环境""" def prepare_environment(self, plugin: PluginSpec) -> Path: """准备插件 Python 环境,返回 python 路径""" ``` **配置规范化 (新增):** ```python def _load_plugin_config(plugin: PluginSpec) -> dict[str, Any]: """加载插件配置""" def _normalize_config_value(value, schema) -> Any: """规范化配置值,严格类型检查""" # 支持 object, list, dict, int, float, bool, string 类型 # 排除 bool 伪装成 int 的情况 ``` **Legacy 上下文共享 (重要修复):** ```python # 同一插件共享一个 LegacyContext 实例 shared_legacy_context: LegacyContext | None = None # 这与旧版 StarManager 行为一致 ``` **Handler ID 格式:** ``` {plugin_name}:{module}.{ClassName}.{method_name} ``` **插件模块隔离 (重要):** ```python def import_string(path: str, plugin_dir: Path | None = None) -> Any: """导入模块属性,支持插件目录隔离""" module_name, attr = path.split(":", 1) _prepare_plugin_import(module_name, plugin_dir) module = import_module(module_name) return getattr(module, attr) def _prepare_plugin_import(module_name: str, plugin_dir: Path | None) -> None: """准备插件导入环境,处理模块缓存冲突""" # 1. 将 plugin_dir 加入 sys.path # 2. 检测缓存模块是否属于当前插件 # 3. 若缓存模块属于其他插件,清理冲突的根包 def _purge_module_root(root_name: str) -> None: """清理冲突的模块缓存""" for module_name in list(sys.modules): if module_name == root_name or module_name.startswith(f"{root_name}."): sys.modules.pop(module_name, None) def _module_belongs_to_plugin(module: Any, plugin_dir: Path) -> bool: """检查模块是否属于指定插件目录""" # 通过 __file__ 或 __path__ 判断模块位置 ``` **设计说明**: 当多个插件使用相同的顶层包名(如 `commands.*`)时,`import_string` 会自动清理冲突的缓存模块,确保每个插件加载自己的代码而非其他插件的缓存。 **Legacy Handler 顺序保持:** ```python def _iter_discoverable_names(instance: Any) -> list[str]: """返回可发现的名称列表,保持声明顺序""" # 1. 优先返回 __handlers__ 中声明的名称(保持顺序) handler_names = list(dict.fromkeys(_iter_handler_names(instance))) # 2. 然后返回 dir() 中发现的额外名称(按字母排序) known_names = set(handler_names) extra_names = sorted(name for name in dir(instance) if name not in known_names) return [*handler_names, *extra_names] ``` **设计说明**: 旧版插件期望 handler 按声明顺序注册。使用 `sorted()` 或 `dir()` 遍历会改变顺序,导致命令冲突时错误的 handler 被优先触发。 --- #### `handler_dispatcher.py` - Handler 分发器 处理 `handler.invoke` Capability 的调用。 ```python class HandlerDispatcher: async def invoke(self, message, cancel_token) -> dict[str, Any]: """调用指定 Handler""" async def cancel(self, request_id: str) -> None: """取消正在执行的 Handler""" async def _run_handler(self, loaded, event, ctx) -> None: """执行 Handler,处理同步/异步/生成器返回值""" def _build_args(self, handler, event, ctx) -> list[Any]: """根据签名注入 event 和 ctx 参数""" ``` **CapabilityDispatcher (新增):** ```python class CapabilityDispatcher: """与 HandlerDispatcher 并行的 capability 分发器""" async def dispatch(self, capability, payload, cancel_token, request_id): """分发 capability 调用""" # 支持参数注入: Context, CancelToken, payload # 支持流式 capability (async generator 或 StreamExecution) # 支持取消传播 ``` **参数注入规则:** | 参数名 | 注入值 | |--------|--------| | `event` | `MessageEvent` 实例 | | `ctx` / `context` | `Context` 实例 | | 类型注解 `MessageEvent` | `MessageEvent` 实例 | | 类型注解 `Context` | `Context` 实例 | | 类型注解 `CancelToken` | `CancelToken` 实例 | | `legacy_args` | 命令参数、regex 捕获组 | **结果处理:** | 返回类型 | 处理方式 | |----------|----------| | `MessageEventResult` | 调用 `platform.send_chain` | | `MessageChain` | 调用 `platform.send_chain` | | `PlainTextResult` | 调用 `event.reply` | | `dict` 含 "text" 键 | 提取 text 并回复 | --- #### `capability_router.py` - Capability 路由器 管理和路由 Capability 调用。 ```python class CapabilityRouter: def register(self, descriptor, call_handler=None, stream_handler=None, exposed=True): """注册 Capability""" def unregister(self, name: str) -> bool: """注销 Capability""" def contains(self, name: str) -> bool: """检查能力是否存在""" async def execute(self, capability, payload, stream, cancel_token, request_id): """执行 Capability 调用""" @dataclass class StreamExecution: iterator: AsyncIterator[dict[str, Any]] finalize: Callable[[list[dict]], dict[str, Any]] ``` **SessionRef 目标解析 (新增):** ```python def resolve_target(payload: dict) -> dict[str, Any]: """从 payload.target 解析会话引用,支持 session 字段和 payload 转换""" ``` **内置 Capabilities (15 个):** | Capability | 功能 | 流式 | |------------|------|------| | `llm.chat` | 对话 (返回文本) | 否 | | `llm.chat_raw` | 对话 (返回完整响应) | 否 | | `llm.stream_chat` | 流式对话 | 是 | | `memory.search` | 搜索记忆 | 否 | | `memory.get` | 获取记忆 | 否 | | `memory.save` | 保存记忆 | 否 | | `memory.delete` | 删除记忆 | 否 | | `db.get` | 读取 KV | 否 | | `db.set` | 写入 KV | 否 | | `db.delete` | 删除 KV | 否 | | `db.list` | 列出 KV | 否 | | `platform.send` | 发送消息 | 否 | | `platform.send_image` | 发送图片 | 否 | | `platform.send_chain` | 发送消息链 | 否 | | `platform.get_members` | 获取群成员 | 否 | --- #### `bootstrap.py` - 运行时启动器 定义三种运行模式。 **运行模式:** | 模式 | 类 | 用途 | |------|-----|------| | Supervisor | `SupervisorRuntime` | 管理多插件,聚合 Handler | | Worker | `PluginWorkerRuntime` | 单插件进程,处理 Handler 调用 | | WebSocket | `run_websocket_server()` | 开发调试用 WebSocket 服务 | ```python class WorkerSession: """Supervisor 管理的单插件会话""" async def start(self) -> None: ... async def invoke_handler(self, handler_id, event_payload, request_id) -> dict: ... async def cancel(self, request_id) -> None: ... class SupervisorRuntime: """Supervisor 运行时""" async def start(self) -> None: # 1. 发现插件 # 2. 为每个插件启动 Worker 进程 # 3. 聚合 Handler 并向 Core 初始化 def _handle_worker_closed(self, worker_name: str) -> None: """处理 Worker 连接关闭""" class PluginWorkerRuntime: """Worker 运行时""" async def start(self) -> None: # 1. 加载插件 # 2. 创建 Dispatcher # 3. 向 Supervisor 初始化 async def _run_lifecycle(self, method_name: str, ctx) -> None: """运行生命周期回调 (on_start/on_stop)""" ``` **连接关闭处理 (新增):** ```python # WorkerSession 新增 on_closed 回调参数 # 清理逻辑包括: # - 从 handler_to_worker 移除对应 handlers # - 从 capability_router 注销 capabilities # - 从 loaded_plugins 移除 # - 取消 stale requests ``` **Handler 冲突检测 (新增):** ```python def _register_handler(self, handler_id: str, worker_name: str) -> bool: """检测 handler ID 冲突并输出警告""" ``` --- ### 客户端层 (clients/) 客户端层提供类型安全的 Capability 调用接口。 #### `_proxy.py` - Capability 代理 ```python class CapabilityProxy: async def call(self, name: str, payload: dict) -> dict[str, Any]: """普通调用""" async def stream(self, name: str, payload: dict) -> AsyncIterator[dict[str, Any]]: """流式调用""" def _ensure_available(self, name: str, *, stream: bool) -> None: """确保能力可用且支持指定调用模式""" ``` **设计要点:** - 从 `peer.__dict__` 读取 `remote_capability_map` 和 `remote_peer`,避免 `MagicMock` 误判 - 支持 `phase="delta"` 事件的流式响应处理 --- #### `llm.py` - LLM 客户端 ```python class ChatMessage(BaseModel): role: Literal["user", "assistant", "system"] content: str class LLMResponse(BaseModel): text: str usage: dict[str, Any] | None = None finish_reason: str | None = None tool_calls: list[dict[str, Any]] = [] class LLMClient: async def chat(self, prompt, system=None, history=None, model=None, temperature=None, **kwargs) -> str: """简单对话,返回文本""" async def chat_raw(self, prompt, **kwargs) -> LLMResponse: """完整对话,返回结构化响应""" async def stream_chat(self, prompt, system=None, history=None, **kwargs) -> AsyncGenerator[str, None]: """流式对话""" ``` **支持参数:** - `prompt` - 用户输入 - `system` - 系统提示词 - `history` - 对话历史 (`ChatMessage` 列表) - `model` - 指定模型 - `temperature` - 采样温度 (0-1) - `**kwargs` - 额外透传参数(如 `image_urls`、`tools`) --- #### `db.py` - 数据库客户端 ```python class DBClient: async def get(self, key: str) -> dict[str, Any] | None: ... async def set(self, key: str, value: dict[str, Any]) -> None: ... async def delete(self, key: str) -> None: ... async def list(self, prefix: str | None = None) -> list[str]: ... ``` **与旧版对比:** - 旧版:`Context.put_kv_data()`, `Context.get_kv_data()` - 新版:`ctx.db.set()`, `ctx.db.get()`, `ctx.db.list()` --- #### `memory.py` - 记忆客户端 ```python class MemoryClient: async def search(self, query: str) -> list[dict[str, Any]]: ... async def get(self, key: str) -> dict[str, Any] | None: ... async def save(self, key: str, value: dict[str, Any] | None = None, **extra) -> None: ... async def delete(self, key: str) -> None: ... ``` **与 DBClient 区别:** - MemoryClient 支持**向量语义搜索** - 适用于存储用户偏好、对话摘要、AI 推理缓存 --- #### `platform.py` - 平台客户端 ```python class PlatformClient: async def send(self, session: str | SessionRef, text: str) -> dict[str, Any]: ... async def send_image(self, session: str | SessionRef, image_url: str) -> dict[str, Any]: ... async def send_chain(self, session: str | SessionRef, chain: list[dict]) -> dict[str, Any]: ... async def get_members(self, session: str | SessionRef) -> list[dict[str, Any]]: ... ``` **SessionRef 支持 (新增):** ```python def _build_target_payload(self, session: str | SessionRef) -> dict: """支持 SessionRef 类型,提取 target 字段""" ``` --- ### API 层 (api/) API 层作为兼容层,通过 thin re-export 方式暴露旧版 API。 #### 兼容层设计 ```python # api/__init__.py from . import basic, components, event, message, message_components, platform, provider, star # api/star/context.py - Legacy Context 导出 from ..._legacy_api import LegacyContext as Context # api/components/command.py - CommandComponent 导出 from ..._legacy_api import CommandComponent # api/event/filter.py - filter 命名空间 class _FilterNamespace: command = staticmethod(command) regex = staticmethod(regex) permission = staticmethod(permission) event_message_type = staticmethod(event_message_type) platform_adapter_type = staticmethod(platform_adapter_type) filter = _FilterNamespace() # api/message/chain.py - MessageChain 兼容类 class MessageChain: def message(self, text) -> "MessageChain": ... def at(self, name, qq) -> "MessageChain": ... def at_all(self) -> "MessageChain": ... def url_image(self, url) -> "MessageChain": ... def file_image(self, path) -> "MessageChain": ... def base64_image(self, base64_str) -> "MessageChain": ... def use_t2i(self, use_t2i) -> "MessageChain": ... def to_payload(self) -> list[dict]: ... def get_plain_text(self) -> str: ... def is_plain_text_only(self) -> bool: ... # api/message/components.py - 消息组件 class Plain, Image, At, AtAll, Reply, Node, Face, File, Record, Video, ... ComponentTypes: dict[str, type[BaseMessageComponent]] ``` **不支持的功能 (显式报错):** - `custom_filter` - 自定义过滤器 - `after_message_sent` - 消息发送后钩子 - `on_astrbot_loaded` / `on_platform_loaded` - 加载钩子 - `on_decorating_result` - 结果装饰钩子 - `on_llm_request` / `on_llm_response` - LLM 钩子 - `command_group` - 命令组 --- ### 核心文件 #### 顶层导出 (`__init__.py`) ```python from .context import Context from .decorators import on_command, on_event, on_message, on_schedule, provide_capability, require_admin from .errors import AstrBotError from .events import MessageEvent from .star import Star __all__ = [ "AstrBotError", "Context", "MessageEvent", "Star", "on_command", "on_event", "on_message", "on_schedule", "provide_capability", "require_admin", ] ``` **设计原则**: 旧版兼容能力由 `astrbot_sdk.api` 与 `astrbot_sdk.compat` 承接。 --- #### `star.py` - Star 基类 ```python class Star: __handlers__: tuple[str, ...] = () def __init_subclass__(cls, **kwargs): """收集子类的 Handler 方法名到 __handlers__""" async def on_start(self, ctx) -> None: """生命周期钩子:启动时""" async def on_stop(self, ctx) -> None: """生命周期钩子:停止时""" async def on_error(self, error, event, ctx) -> None: """错误处理钩子""" @classmethod def __astrbot_is_new_star__(cls) -> bool: return True # 新版 Star 返回 True ``` **Handler 发现机制:** 1. `@on_command` 等装饰器在方法上设置 `__astrbot_handler_meta__` 2. `Star.__init_subclass__` 遍历 MRO 收集带 meta 的方法名 3. Loader 读取 `__handlers__` 并构建 `HandlerDescriptor` --- #### `decorators.py` - 装饰器 ```python # 命令触发 @on_command("hello", aliases=["hi"], description="问候") def hello_handler(event): ... # 消息触发 @on_message(regex=r"^ping", keywords=["ping"], platforms=["qq"]) def ping_handler(event): ... # 事件触发 @on_event("group_join") def join_handler(event): ... # 定时触发 @on_schedule(cron="0 9 * * *") # 或 interval_seconds=60 def scheduled_handler(): ... # 权限 @on_command("admin") @require_admin def admin_handler(event): ... # 声明能力 (新增) @provide_capability( name="my.custom_action", description="自定义操作", input_schema={...}, output_schema={...}, ) async def custom_action(payload, ctx, cancel_token): ... ``` --- #### `context.py` - 运行时 Context ```python @dataclass(slots=True) class CancelToken: def cancel(self) -> None: ... @property def cancelled(self) -> bool: ... async def wait(self) -> None: ... def raise_if_cancelled(self) -> None: ... class Context: def __init__(self, *, peer, plugin_id, cancel_token=None, logger=None): proxy = CapabilityProxy(peer) self.peer = peer self.llm = LLMClient(proxy) self.memory = MemoryClient(proxy) self.db = DBClient(proxy) self.platform = PlatformClient(proxy) self.plugin_id = plugin_id self.logger = logger or base_logger.bind(plugin_id=plugin_id) self.cancel_token = cancel_token or CancelToken() ``` --- #### `events.py` - 事件定义 ```python @dataclass class PlainTextResult: text: str ReplyHandler = Callable[[str], Awaitable[None]] class MessageEvent: def __init__(self, *, text, user_id, group_id, platform, session_id, raw, context, reply_handler): self.text = text self.user_id = user_id self.group_id = group_id self.platform = platform self.session_id = session_id or group_id or user_id or "" self.raw = raw or {} self._reply_handler = reply_handler @classmethod def from_payload(cls, payload, context=None, reply_handler=None) -> "MessageEvent": """从 payload 构造""" def to_payload(self) -> dict[str, Any]: """序列化为 payload""" async def reply(self, text: str) -> None: """回复消息 (依赖注入 reply_handler)""" def bind_reply_handler(self, reply_handler: ReplyHandler) -> None: """绑定回复处理器""" def plain_result(self, text: str) -> PlainTextResult: """创建纯文本结果""" @property def session_ref(self) -> SessionRef: """获取 SessionRef 对象 (新增)""" ``` --- #### `errors.py` - 错误模型 ```python class ErrorCodes: """错误码常量类""" # 非重试错误 LLM_NOT_CONFIGURED = "llm_not_configured" CAPABILITY_NOT_FOUND = "capability_not_found" PERMISSION_DENIED = "permission_denied" LLM_ERROR = "llm_error" INVALID_INPUT = "invalid_input" CANCELLED = "cancelled" PROTOCOL_VERSION_MISMATCH = "protocol_version_mismatch" PROTOCOL_ERROR = "protocol_error" INTERNAL_ERROR = "internal_error" # 可重试错误 CAPABILITY_TIMEOUT = "capability_timeout" NETWORK_ERROR = "network_error" LLM_TEMPORARY_ERROR = "llm_temporary_error" @dataclass class AstrBotError(Exception): code: str message: str hint: str = "" retryable: bool = False @classmethod def cancelled(cls, message="调用被取消") -> "AstrBotError": ... @classmethod def capability_not_found(cls, name: str) -> "AstrBotError": ... @classmethod def invalid_input(cls, message: str) -> "AstrBotError": ... @classmethod def protocol_version_mismatch(cls, message: str) -> "AstrBotError": ... @classmethod def protocol_error(cls, message: str) -> "AstrBotError": ... @classmethod def internal_error(cls, message: str) -> "AstrBotError": ... def to_payload(self) -> dict[str, object]: ... @classmethod def from_payload(cls, payload) -> "AstrBotError": ... ``` --- #### `_legacy_api.py` - 兼容层 ```python class LegacyConversationManager: """旧版会话管理器兼容实现""" async def new_conversation(self, unified_msg_origin, ...) -> str: ... async def switch_conversation(self, unified_msg_origin, conversation_id) -> None: ... async def delete_conversation(self, unified_msg_origin, conversation_id) -> None: ... async def get_curr_conversation_id(self, unified_msg_origin) -> str | None: ... async def get_conversation(self, unified_msg_origin, conversation_id, ...) -> dict | None: ... async def get_conversations(self, ...) -> list[dict]: ... async def update_conversation(self, unified_msg_origin, conversation_id, ...) -> None: ... async def add_message_pair(self, cid, user_message, assistant_message) -> None: ... class LegacyContext: """v3 Context 兼容实现""" def __init__(self, plugin_id: str): self.plugin_id = plugin_id self._runtime_context: NewContext | None = None self.conversation_manager = LegacyConversationManager(self) def bind_runtime_context(self, runtime_context: NewContext) -> None: ... def _register_component(self, *components) -> None: ... async def execute_registered_function(self, func_full_name, args) -> Any: ... async def call_context_function(self, func_full_name, args) -> dict: ... async def llm_generate(self, chat_provider_id, prompt, ...) -> LLMResponse: ... async def tool_loop_agent(self, chat_provider_id, prompt, ...) -> LLMResponse: ... async def send_message(self, session, message_chain) -> None: ... async def put_kv_data(self, key, value) -> None: ... async def get_kv_data(self, key, default=None) -> Any: ... async def delete_kv_data(self, key) -> None: ... class LegacyStar(Star): """旧版 astrbot.api.star.Star 兼容基类""" def __init__(self, context: LegacyContext | None = None, config: Any | None = None): ... @classmethod def __astrbot_is_new_star__(cls) -> bool: return False class CommandComponent(LegacyStar): """v3 插件基类 (LegacyStar 的别名)""" def register(name=None, author=None, desc=None, version=None, repo=None): """旧版插件元数据装饰器兼容入口""" ``` **已废弃方法 (抛出显式迁移错误):** - `get_filtered_conversations()` - `get_human_readable_context()` - `add_llm_tools()` --- #### `cli.py` - 命令行接口 ```python @click.group() def cli(): ... @cli.command() @click.option("--plugins-dir", default="plugins") def run(plugins_dir: Path): """启动 Supervisor""" asyncio.run(run_supervisor(plugins_dir=plugins_dir)) @cli.command(hidden=True) @click.option("--plugin-dir", required=True) def worker(plugin_dir: Path): """启动 Worker (内部命令)""" asyncio.run(run_plugin_worker(plugin_dir=plugin_dir)) @cli.command(hidden=True) @click.option("--port", default=8765) @click.option("--host", default="localhost") @click.option("--path", default="/ws") def websocket(port: int, host: str, path: str): """启动 WebSocket 服务 (调试用)""" ``` --- ## 五大硬性协议规则 ### 1. 统一 id 字段 **规则**: 所有协议消息必须有 `id` 字段。 ```python class InitializeMessage(_MessageBase): type: Literal["initialize"] = "initialize" id: str # 必须有 ... class ResultMessage(_MessageBase): type: Literal["result"] = "result" id: str # 必须有 ... ``` ### 2. event 仅用于 stream=true **规则**: `EventMessage` 只在流式调用中使用。 ```python # Peer._handle_result if queue is not None: # stream=true 的 pending stream await queue.put(AstrBotError.protocol_error("stream=true 调用不应收到 result")) # Peer._handle_event if future is not None: # stream=false 的 pending result future.set_exception(AstrBotError.protocol_error("stream=false 调用不应收到 event")) ``` ### 3. handler.invoke 用于回调 **规则**: 插件 Handler 调用通过 `handler.invoke` Capability。 ```python # HandlerDispatcher 中 if message.capability != "handler.invoke": raise AstrBotError.capability_not_found(message.capability) ``` ### 4. cancel 作为 request-stop **规则**: 取消请求发送 `CancelMessage`,等待终端事件。 ```python async def cancel(self, request_id: str, reason: str = "user_cancelled") -> None: await self._send(CancelMessage(id=request_id, reason=reason)) # Worker 收到后 token.cancel() task.cancel() ``` ### 5. initialize 失败处理 **规则**: 初始化失败后连接进入不可用状态并关闭。 ```python async def _reject_initialize(self, message, error): await self._send(ResultMessage(id=message.id, kind="initialize_result", success=False, error=...)) self._unusable = True self._remote_initialized.set() await self.stop() ``` --- ## 数据流与通信模型 ### 初始化流程 ``` ┌────────┐ ┌────────┐ │ Core │ │ Plugin │ └───┬────┘ └───┬────┘ │ │ │──── InitializeMessage ───────────────>│ │ {id, peer, handlers, │ │ provided_capabilities, metadata} │ │ │ │<─── ResultMessage ────────────────────│ │ {id, kind="initialize_result", │ │ success, output: {peer, │ │ capabilities, metadata}} │ │ │ ``` ### Capability 调用流程 (普通) ``` ┌────────┐ ┌────────┐ │ Core │ │ Plugin │ └───┬────┘ └───┬────┘ │ │ │──── InvokeMessage ───────────────────>│ │ {id, capability, input, stream=false}│ │ │ │<─── ResultMessage ────────────────────│ │ {id, success, output/error} │ │ │ ``` ### Capability 调用流程 (流式) ``` ┌────────┐ ┌────────┐ │ Core │ │ Plugin │ └───┬────┘ └───┬────┘ │ │ │──── InvokeMessage ───────────────────>│ │ {id, capability, input, stream=true}│ │ │ │<─── EventMessage(phase="started") ────│ │ │ │<─── EventMessage(phase="delta") ──────│ │ {data: {...}} │ │<─── EventMessage(phase="delta") ──────│ │ ... │ │ │ │<─── EventMessage(phase="completed") ──│ │ {output: {...}} │ │ │ ``` ### 取消流程 ``` ┌────────┐ ┌────────┐ │ Core │ │ Plugin │ └───┬────┘ └───┬────┘ │ │ │──── CancelMessage ───────────────────>│ │ {id, reason} │ │ │ │<─── EventMessage(phase="failed") ─────│ │ {error: {code: "cancelled"}} │ │ │ ``` --- ## 扩展机制 ### 添加新 Capability 1. 在 `protocol/descriptors.py` 中定义 Schema 常量: ```python MY_CAPABILITY_INPUT_SCHEMA = {"type": "object", "properties": {...}, "required": [...]} MY_CAPABILITY_OUTPUT_SCHEMA = {"type": "object", "properties": {...}} BUILTIN_CAPABILITY_SCHEMAS["my.custom_action"] = { "input": MY_CAPABILITY_INPUT_SCHEMA, "output": MY_CAPABILITY_OUTPUT_SCHEMA, } ``` 2. 在 `CapabilityRouter._register_builtin_capabilities()` 中注册: ```python self.register( CapabilityDescriptor( name="my.custom_action", description="自定义操作", input_schema=MY_CAPABILITY_INPUT_SCHEMA, output_schema=MY_CAPABILITY_OUTPUT_SCHEMA, supports_stream=False, cancelable=False, ), call_handler=my_handler, exposed=True, ) ``` 3. 在 `clients/` 中添加客户端封装(可选)。 ### 添加新 Trigger 类型 1. 在 `descriptors.py` 中定义新的 Trigger 类: ```python class CustomTrigger(_DescriptorBase): type: Literal["custom"] = "custom" custom_field: str ``` 2. 更新 `Trigger` 联合类型: ```python Trigger = Annotated[ CommandTrigger | MessageTrigger | EventTrigger | ScheduleTrigger | CustomTrigger, Field(discriminator="type"), ] ``` 3. 在 `decorators.py` 中添加装饰器: ```python def on_custom(custom_field: str): def decorator(func): meta = _get_or_create_meta(func) meta.trigger = CustomTrigger(custom_field=custom_field) return func return decorator ``` ### 添加新 Transport 1. 继承 `Transport` 基类: ```python class MyTransport(Transport): async def start(self) -> None: ... async def stop(self) -> None: ... async def send(self, payload: str) -> None: ... ``` 2. 在 `runtime/__init__.py` 中导出。 --- ## 实现状态 ### 已完成模块 | 模块 | 文件 | 状态 | 说明 | |------|------|------|------| | **协议层** | `protocol/` | ✅ 完成 | | | | `descriptors.py` | ✅ | Handler/Capability 描述符 + 16 个内置 Schema 常量 | | | `messages.py` | ✅ | 5 种消息类型 + parse_message | | | `legacy_adapter.py` | ✅ | JSON-RPC ↔ v4 双向转换 | | **运行时层** | `runtime/` | ✅ 完成 | | | | `peer.py` | ✅ | 对称通信端点 + 取消 + 流式 + 远程元数据缓存 | | | `transport.py` | ✅ | Stdio + WebSocket Server/Client | | | `loader.py` | ✅ | 插件发现 + 环境管理 + 配置规范化 + 模块隔离 + Handler 顺序保持 | | | `handler_dispatcher.py` | ✅ | Handler 分发 + CapabilityDispatcher + 参数注入增强 | | | `capability_router.py` | ✅ | 能力路由 + 15 个内置能力 + SessionRef 支持 | | | `bootstrap.py` | ✅ | Supervisor + Worker + WebSocket + 连接关闭处理 | | **客户端层** | `clients/` | ✅ 完成 | | | | `_proxy.py` | ✅ | CapabilityProxy 代理 | | | `llm.py` | ✅ | LLM 客户端 (chat/chat_raw/stream_chat) | | | `memory.py` | ✅ | Memory 客户端 (search/get/save/delete) | | | `db.py` | ✅ | DB 客户端 (get/set/delete/list) | | | `platform.py` | ✅ | Platform 客户端 (支持 SessionRef) | | **API 层** | `api/` | ✅ 完成 | 兼容层 | | | `star/context.py` | ✅ | LegacyContext 导出 | | | `components/command.py` | ✅ | CommandComponent 导出 | | | `event/filter.py` | ✅ | filter 命名空间 + 平台/消息类型过滤 | | | `message/chain.py` | ✅ | MessageChain + to_payload | | | `message/components.py` | ✅ | 20+ 消息组件类型 | | | `basic/astrbot_config.py` | ✅ | AstrBotConfig + save_config | | | `basic/` | ✅ | 基础实体与配置 | | | `platform/` | ✅ | 平台元数据 | | | `provider/` | ✅ | Provider 实体 | | **核心文件** | 根目录 | ✅ 完成 | | | | `__init__.py` | ✅ | 顶层导出 | | | `star.py` | ✅ | Star 基类 + Handler 发现 | | | `context.py` | ✅ | Context + CancelToken | | | `decorators.py` | ✅ | on_command/on_message/on_event/on_schedule/provide_capability | | | `events.py` | ✅ | MessageEvent + SessionRef | | | `errors.py` | ✅ | AstrBotError + ErrorCodes | | | `_legacy_api.py` | ✅ | LegacyContext + LegacyStar + register + LegacyConversationManager | | | `cli.py` | ✅ | Click 命令行工具 | | | `__main__.py` | ✅ | python -m astrbot_sdk 入口 | ### 测试覆盖 测试文件位于 `tests_v4/` 目录,共 **40 个** Python 文件: ``` tests_v4/ ├── conftest.py # pytest 配置与共享 fixtures ├── helpers.py # 测试辅助函数 │ ├── # 协议层测试 ├── test_protocol_messages.py # 协议消息模型 ├── test_protocol_descriptors.py # 描述符测试 ├── test_protocol_package.py # 协议包测试 ├── test_protocol_legacy_adapter.py # Legacy 适配器测试 ├── test_legacy_adapter.py # LegacyAdapter 运行时测试 │ ├── # 运行时层测试 ├── test_peer.py # Peer 测试 ├── test_transport.py # Transport 测试 ├── test_capability_router.py # CapabilityRouter 测试 ├── test_handler_dispatcher.py # HandlerDispatcher 测试 ├── test_loader.py # 加载器测试(含模块隔离、Handler 顺序) ├── test_bootstrap.py # Bootstrap 测试 ├── test_runtime.py # 运行时测试 ├── test_runtime_integration.py # 运行时集成测试 │ ├── # 核心模块测试 ├── test_context.py # Context 测试 ├── test_events.py # 事件测试 ├── test_decorators.py # 装饰器测试 │ ├── # API 层测试 ├── test_api_modules.py # API 模块测试 ├── test_api_decorators.py # API 装饰器测试 ├── test_api_event_filter.py # filter 命名空间测试 ├── test_api_legacy_context.py # Legacy Context 测试 ├── test_api_message_components.py # 消息组件测试 ├── test_api_contract.py # API 契约测试 │ ├── # 客户端层测试 ├── test_clients_module.py # 客户端模块测试 ├── test_llm_client.py # LLM 客户端测试 ├── test_memory_client.py # Memory 客户端测试 ├── test_db_client.py # DB 客户端测试 ├── test_platform_client.py # Platform 客户端测试 ├── test_capability_proxy.py # CapabilityProxy 测试 │ ├── # 兼容性迁移测试 ├── test_script_migrations.py # 脚本迁移测试 ├── test_supervisor_migration.py # Supervisor 迁移测试 ├── test_legacy_plugin_integration.py # 旧版插件集成测试 ├── test_top_level_modules.py # 顶层模块测试(含 runtime 导出验证) │ ├── # 入口点测试 ├── test_entrypoints.py # 入口点测试 ├── test_conftest_fixtures.py # pytest fixtures 测试 └── __init__.py # 包初始化文件 ``` **测试重点:** | 测试文件 | 重点测试内容 | |---------|-------------| | `test_loader.py` | 插件模块隔离(多插件同名包)、Legacy Handler 顺序保持 | | `test_top_level_modules.py` | `runtime.__init__` 导出验证、确保不暴露内部数据结构 | | `test_legacy_plugin_integration.py` | 旧版插件完整兼容性、LegacyContext 共享 | --- ## 版本兼容性 | 组件 | v3 | v4 | |------|----|----| | 插件基类 | `CommandComponent` | `Star` | | Context | `LegacyContext` | `Context` | | 装饰器 | `@filter.command` | `@on_command` | | 协议 | JSON-RPC 2.0 | 自定义协议 | | 通信 | 单向 | 双向对称 | **兼容策略**: `LegacyAdapter` 实现协议转换,`CommandComponent` 继承 `Star` 并标记 `__astrbot_is_new_star__ = False`。 **迁移指南:** ```python # 旧版 (将在未来版本废弃) from astrbot_sdk.api.event import AstrMessageEvent from astrbot_sdk.api.star.context import Context # 新版 (推荐) from astrbot_sdk.events import MessageEvent from astrbot_sdk.context import Context ``` **兼容层入口:** ```python # 通过 astrbot_sdk.api 和 astrbot_sdk.compat 访问旧版 API from astrbot_sdk.api.star import Context, Star from astrbot_sdk.compat import LegacyContext, CommandComponent ``` --- ## 关键设计决策 ### 运行时模块导出策略 `runtime/__init__.py` 仅暴露高级运行时原语(`Peer`, `Transport`, `CapabilityRouter`, `HandlerDispatcher`),而将 loader/bootstrap 等编排细节保留在子模块中。 **原因:** - loader/bootstrap 数据结构(`LoadedPlugin`, `WorkerSession` 等)属于内部实现细节 - 避免意外的 API 契约,便于未来重构 - 插件作者通常不需要直接使用这些低级原语 ### 插件模块隔离 当多个插件使用相同的顶层包名时,`import_string()` 会自动清理冲突的缓存模块。 **问题:** 插件 A 定义 `commands.hello`,插件 B 也定义 `commands.world`。若插件 A 先加载,`sys.modules["commands"]` 指向插件 A 的目录,插件 B 会错误地使用插件 A 的代码。 **解决:** `_prepare_plugin_import()` 检测缓存模块是否属于当前插件目录,若不属于则清理冲突的根包。 ### Legacy Handler 顺序保持 `_iter_discoverable_names()` 保持 handler 的声明顺序,而非使用 `sorted()` 或 `set()` 遍历。 **原因:** 旧版 `StarManager` 按声明顺序注册 handler。当多个 handler 匹配同一命令时,第一个注册的 handler 被优先触发。改变顺序会导致不同的 handler 被触发。 **实现:** ```python # 保持 __handlers__ 中的顺序 handler_names = list(dict.fromkeys(_iter_handler_names(instance))) # 额外发现的名称按字母排序 extra_names = sorted(name for name in dir(instance) if name not in known_names) return [*handler_names, *extra_names] ```