diff --git a/PROJECT_ARCHITECTURE.md b/PROJECT_ARCHITECTURE.md new file mode 100644 index 000000000..352641f60 --- /dev/null +++ b/PROJECT_ARCHITECTURE.md @@ -0,0 +1,981 @@ +# AstrBot SDK 项目完整架构分析文档 + +> 作者:whatevertogo +> 更新时间:2026-03-14 + +--- + +## 目录 + +1. [项目概述](#项目概述) +2. [目录结构](#目录结构) +3. [核心架构层次](#核心架构层次) +4. [协议层设计](#协议层设计) +5. [运行时架构](#运行时架构) +6. [客户端层设计](#客户端层设计) +7. [新旧架构对比](#新旧架构对比) +8. [兼容层设计](#兼容层设计) +9. [插件开发指南](#插件开发指南) +10. [关键设计模式](#关键设计模式) + +--- + +## 项目概述 + +AstrBot SDK 是一个基于 Python 3.12+ 的机器人插件开发框架,采用**进程隔离**和**能力路由**架构,支持插件的动态加载、独立运行和跨进程通信。 + +### 核心特性 + +| 特性 | 描述 | +|------|------| +| 进程隔离 | 每个插件运行在独立 Worker 进程,崩溃不影响其他插件 | +| 环境分组 | 多插件可共享同一 Python 虚拟环境,节省资源 | +| 能力路由 | 显式声明的 Capability 系统,支持 JSON Schema 验证 | +| 流式支持 | 原生支持流式 LLM 调用和增量结果返回 | +| 向后兼容 | 完整的旧版 API 兼容层,支持无修改迁移 | +| 协议优先 | 基于 v4 协议的统一通信模型,支持多种传输方式 | + +### 技术栈 + +- **Python**: 3.12+ +- **异步框架**: asyncio +- **Web 框架**: aiohttp +- **数据验证**: pydantic +- **日志**: loguru +- **配置**: pyyaml +- **LLM**: openai, anthropic, google-genai +- **包管理**: uv (环境分组) + +--- + +## 目录结构 + +``` +astrbot-sdk/ +├── src/ # 旧版实现 (已停止更新) +│ └── astrbot_sdk/ # 旧版 SDK +├── src-new/ # 新版 v4 实现 (当前活跃) +│ └── astrbot_sdk/ # v4 SDK 主包 +│ ├── __init__.py # 顶层公共 API +│ ├── star.py # v4 原生插件基类 +│ ├── context.py # 运行时上下文 +│ ├── decorators.py # v4 原生装饰器 +│ ├── events.py # v4 原生事件对象 +│ ├── errors.py # 统一错误模型 +│ ├── cli.py # 命令行工具 +│ ├── testing.py # 测试辅助模块 +│ │ +│ ├── clients/ # 能力客户端层 +│ │ ├── __init__.py +│ │ ├── _proxy.py # CapabilityProxy 能力代理 +│ │ ├── llm.py # LLM 客户端 +│ │ ├── memory.py # 记忆存储客户端 +│ │ ├── db.py # KV 存储客户端 +│ │ ├── platform.py # 平台消息客户端 +│ │ ├── http.py # HTTP 注册客户端 +│ │ └── metadata.py # 插件元数据客户端 +│ │ +│ ├── protocol/ # 协议层 +│ │ ├── messages.py # v4 协议消息模型 +│ │ ├── descriptors.py # Handler/Capability 描述符 +│ │ └── legacy_adapter.py # JSON-RPC ↔ v4 适配器 +│ │ +│ ├── runtime/ # 运行时层 +│ │ ├── __init__.py +│ │ ├── peer.py # 协议对等端 +│ │ ├── transport.py # 传输抽象与实现 +│ │ ├── handler_dispatcher.py # Handler 执行分发 +│ │ ├── capability_router.py # Capability 路由 +│ │ ├── loader.py # 插件加载 +│ │ ├── bootstrap.py # 启动引导 +│ │ └── environment_groups.py # 环境分组管理 +│ │ +│ ├── api/ # 旧版 API 兼容 facade +│ │ ├── basic/ # 基础配置与对话管理 +│ │ ├── components/ # 命令组件 +│ │ ├── event/ # 事件类型与过滤器 +│ │ ├── message/ # 消息链 +│ │ ├── message_components.py # 消息组件别名 +│ │ ├── platform/ # 平台元数据 +│ │ ├── provider/ # Provider 实体 +│ │ └── star/ # Star 基类与上下文 +│ │ +│ ├── compat.py # 顶层兼容入口 +│ ├── _legacy_api.py # 旧版 API 兼容实现 +│ ├── _legacy_runtime.py # Legacy 执行边界 +│ ├── _legacy_loader.py # Legacy 插件发现 +│ ├── _legacy_llm.py # Legacy LLM/tool 兼容 +│ ├── _session_waiter.py # session_waiter 兼容 +│ └── _shared_preferences.py # 共享偏好兼容 +│ +├── src-new/astrbot/ # 旧包名兼容 facade +│ ├── api/ # astrbot.api.* 兼容 +│ │ ├── event/ +│ │ ├── star/ +│ │ ├── components/ +│ │ ├── platform/ +│ │ ├── provider/ +│ │ └── util/ +│ └── core/ # astrbot.core.* 兼容 +│ ├── platform/ +│ ├── provider/ +│ ├── message/ +│ ├── utils/ +│ ├── agent/ +│ └── db/ +│ +├── tests_v4/ # v4 测试套件 +│ ├── unit/ # 单元测试 +│ ├── integration/ # 集成测试 +│ └── external_plugin_matrix.json # 外部插件兼容矩阵 +│ +├── test_plugin/ # 测试插件样本 +│ ├── new/ # v4 原生插件示例 +│ │ ├── plugin.yaml +│ │ └── commands/ +│ │ └── hello.py +│ │ +│ └── old/ # 旧版兼容插件示例 +│ ├── plugin.yaml +│ └── main.py +│ +├── astrBot/ # 参考 AstrBot 应用 +│ +├── pyproject.toml # 项目配置 +├── ARCHITECTURE.md # 架构文档 +├── refactor.md # 重构历史 +└── run_tests.py # 测试入口 +``` + +--- + +## 核心架构层次 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 用户层 (Plugin Developer) │ +├─────────────────────────────────────────────────────────────────┤ +│ 新代码入口: astrbot_sdk.{Star, Context, MessageEvent} │ +│ 旧代码入口: astrbot_sdk.api.* / astrbot.api.* │ +└────────────────────┬────────────────────────────────────────┘ + │ +┌──────────────────▼───────────────────────────────────────────┐ +│ 高层 API (High-Level API) │ +├─────────────────────────────────────────────────────────────────┤ +│ 原生客户端: clients/{llm, memory, db, platform, ...} │ +│ 兼容 Facade: _legacy_api.py (LegacyContext, ...) │ +└──────────────────┬───────────────────────────────────────────┘ + │ +┌──────────────────▼───────────────────────────────────────────┐ +│ 执行边界 (Execution Boundary) │ +├─────────────────────────────────────────────────────────────────┤ +│ runtime 主干: │ +│ - loader.py (插件发现、加载) │ +│ - bootstrap.py (Supervisor/Worker 启动) │ +│ - handler_dispatcher.py (Handler 执行分发) │ +│ - capability_router.py (Capability 路由) │ +│ - peer.py (协议对等端) │ +│ - transport.py (传输抽象) │ +│ compat 私有实现: │ +│ - _legacy_runtime.py (legacy 执行适配) │ +│ - _legacy_loader.py (legacy 插件发现) │ +│ - _session_waiter.py (session_waiter 兼容) │ +└──────────────────┬───────────────────────────────────────────┘ + │ +┌──────────────────▼───────────────────────────────────────────┐ +│ 协议与传输 (Protocol & Transport) │ +├─────────────────────────────────────────────────────────────────┤ +│ protocol/ │ +│ - messages.py (协议消息模型) │ +│ - descriptors.py (Handler/Capability 描述符) │ +│ - legacy_adapter.py (JSON-RPC ↔ v4 适配器) │ +│ transport 实现: │ +│ - StdioTransport (标准输入输出) │ +│ - WebSocketServerTransport (WebSocket 服务端) │ +│ - WebSocketClientTransport (WebSocket 客户端) │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 层次职责 + +| 层次 | 职责 | 主要模块 | +|------|------|---------| +| 用户层 | 插件开发者 API | `Star`, `Context`, `MessageEvent`, 装饰器 | +| 高层 API | 类型化的能力客户端 | `clients/`, `_legacy_api.py` | +| 执行边界 | 插件加载、路由、分发 | `runtime/loader.py`, `runtime/bootstrap.py` | +| 协议层 | 消息模型、描述符 | `protocol/` | +| 传输层 | 底层通信抽象 | `runtime/transport.py` | + +--- + +## 协议层设计 + +### 消息模型 + +v4 协议定义了 5 种消息类型: + +| 消息类型 | 用途 | 关键字段 | +|---------|------|---------| +| `InitializeMessage` | 握手初始化 | `protocol_version`, `peer`, `handlers`, `provided_capabilities` | +| `InvokeMessage` | 调用能力 | `capability`, `input`, `stream` | +| `ResultMessage` | 返回结果 | `success`, `output`, `error` | +| `EventMessage` | 流式事件 | `phase` (started/delta/completed/failed), `data` | +| `CancelMessage` | 取消调用 | `reason` | + +### 握手流程 + +``` +Worker (Plugin) Supervisor (Core) + | | + | InitializeMessage | + |----------------------------->| + | | 创建 CapabilityRouter + | | 注册 handler.invoke + | | + | ResultMessage(kind="init") | + |<-----------------------------| + | | 等待 handler.invoke 调用 + | | 执行 CapabilityRouter.execute() + | | + | InvokeMessage(handler.invoke) | + |<-----------------------------| + | HandlerDispatcher.invoke() | + | 执行用户 handler | + | | + | ResultMessage(output) | + |----------------------------->| +``` + +### 描述符模型 + +#### HandlerDescriptor + +```python +{ + "id": "plugin.module:handler_name", + "trigger": { + "type": "command", + "command": "hello", + "aliases": ["hi"], + "description": "打招呼命令" + }, + "priority": 0, + "permissions": { + "require_admin": False, + "level": 0 + } +} +``` + +#### CapabilityDescriptor + +```python +{ + "name": "llm.chat", + "description": "发送对话请求,返回文本", + "input_schema": { + "type": "object", + "properties": { + "prompt": {"type": "string"} + }, + "required": ["prompt"] + }, + "output_schema": { + "type": "object", + "properties": { + "text": {"type": "string"} + }, + "required": ["text"] + }, + "supports_stream": False, + "cancelable": False +} +``` + +### 内置 Capabilities (18个) + +| 命名空间 | 能力 | 说明 | +|----------|------|------| +| `llm` | `chat` | 同步对话,返回文本 | +| `llm` | `chat_raw` | 同步对话,返回完整响应 | +| `llm` | `stream_chat` | 流式对话 | +| `memory` | `search` | 搜索记忆 | +| `memory` | `save` | 保存记忆 | +| `memory` | `get` | 读取单条记忆 | +| `memory` | `delete` | 删除记忆 | +| `db` | `get` | 读取 KV | +| `db` | `set` | 写入 KV | +| `db` | `delete` | 删除 KV | +| `db` | `list` | 列出 KV 键 | +| `db` | `get_many` | 批量读取 KV | +| `db` | `set_many` | 批量写入 KV | +| `db` | `watch` | 订阅 KV 变更 | +| `platform` | `send` | 发送消息 | +| `platform` | `send_image` | 发送图片 | +| `platform` | `send_chain` | 发送消息链 | +| `platform` | `get_members` | 获取群成员 | + +--- + +## 运行时架构 + +### 组件关系图 + +``` + ┌──────────────┐ + │ AstrBot │ + │ Core │ + └──────┬─────┘ + │ + ┌──────▼─────┐ + │ Supervisor │ + │ Runtime │ + └──────┬─────┘ + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ + │ Peer │ │ Peer │ │ Peer │ + │ (stdio) │ │ (stdio) │ │ (stdio) │ + └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ + │ │ │ + ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ + │ Worker │ │ Worker │ │ Worker │ + │ Runtime │ │ Runtime │ │ Runtime │ + └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ + │ │ │ + ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ + │ Plugin A │ │ Plugin B │ │ Plugin C │ + │ (v4/old) │ │ (v4/old) │ │ (v4/old) │ + └───────────┘ └───────────┘ └───────────┘ +``` + +### SupervisorRuntime + +职责:管理多个 Worker 进程,聚合所有 handler + +```python +class SupervisorRuntime: + def __init__(self, *, transport, plugins_dir, env_manager): + self.transport = transport # 与 Core 的传输层 + self.plugins_dir = plugins_dir # 插件目录 + self.capability_router = CapabilityRouter() # 能力路由器 + self.peer = Peer(...) # 与 Core 的对等端 + self.worker_sessions = {} # Worker 会话映射 + self.handler_to_worker = {} # Handler → Worker 映射 + + async def start(self): + # 1. 发现所有插件 + discovery = discover_plugins(self.plugins_dir) + + # 2. 规划环境分组 + plan_result = self.env_manager.plan(discovery.plugins) + + # 3. 为每个分组启动 Worker + for group in plan_result.groups: + session = WorkerSession(group=group, ...) + await session.start() + self.worker_sessions[group.id] = session + + # 4. 聚合所有 handler 和 capability + await self.peer.initialize( + handlers=[...], + provided_capabilities=self.capability_router.descriptors() + ) +``` + +### WorkerSession + +职责:管理单个 Worker 进程的生命周期 + +```python +class WorkerSession: + def __init__(self, *, group, env_manager, capability_router): + self.group = group # 环境分组 + self.peer = Peer(...) # 与 Worker 的对等端 + self.capability_router = capability_router + self.handlers = [] # Worker 注册的 handlers + self.provided_capabilities = [] # Worker 提供的 capabilities + + async def start(self): + # 启动 Worker 子进程 + python_path = self.env_manager.prepare_group_environment(self.group) + transport = StdioTransport( + command=[python_path, "-m", "astrbot_sdk", "worker", "--group-metadata", ...] + ) + self.peer = Peer(transport=transport, ...) + + # 等待 Worker 初始化完成 + await self.peer.start() + await self.peer.wait_until_remote_initialized() + + # 获取 Worker 的注册信息 + self.handlers = list(self.peer.remote_handlers) + self.provided_capabilities = list(self.peer.remote_provided_capabilities) + + async def invoke_capability(self, capability_name, payload, *, request_id): + # 转发能力调用到 Worker + return await self.peer.invoke(capability_name, payload, request_id=request_id) +``` + +### PluginWorkerRuntime + +职责:Worker 进程内的插件加载与执行 + +```python +class PluginWorkerRuntime: + def __init__(self, *, plugin_dir, transport): + self.plugin = load_plugin_spec(plugin_dir) + self.loaded_plugin = load_plugin(self.plugin) + self.peer = Peer(transport=transport, ...) + self.dispatcher = HandlerDispatcher(...) + self.capability_dispatcher = CapabilityDispatcher(...) + + async def start(self): + # 1. 向 Supervisor 注册 handlers 和 capabilities + await self.peer.initialize( + handlers=[h.descriptor for h in self.loaded_plugin.handlers], + provided_capabilities=[c.descriptor for c in self.loaded_plugin.capabilities] + ) + + # 2. 执行 on_start 生命周期 + await self._run_lifecycle("on_start") + + # 3. 设置消息处理器 + self.peer.set_invoke_handler(self._handle_invoke) + self.peer.set_cancel_handler(self._handle_cancel) + + async def _handle_invoke(self, message, cancel_token): + if message.capability == "handler.invoke": + return await self.dispatcher.invoke(message, cancel_token) + return await self.capability_dispatcher.invoke(message, cancel_token) +``` + +### HandlerDispatcher + +职责:将 handler.invoke 请求转成真实 Python 调用 + +```python +class HandlerDispatcher: + def __init__(self, *, plugin_id, peer, handlers): + self._handlers = {item.descriptor.id: item for item in handlers} + self._peer = peer + self._active = {} # request_id → (task, cancel_token) + + async def invoke(self, message, cancel_token): + # 1. 查找 handler + loaded = self._handlers[message.input["handler_id"]] + + # 2. 创建上下文 + ctx = Context(peer=self._peer, plugin_id=plugin_id, cancel_token=cancel_token) + event = MessageEvent.from_payload(message.input["event"], context=ctx) + + # 3. 构建参数 (支持类型注解注入) + args = self._build_args(loaded.callable, event, ctx) + + # 4. 执行 handler + result = loaded.callable(*args) + + # 5. 处理返回值 + await self._consume_result(result, event, ctx) +``` + +**参数注入优先级**: +1. 按类型注解注入(`MessageEvent`, `Context`) +2. 按参数名注入(`event`, `ctx`, `context`) +3. 从 legacy_args 注入(命令参数等) + +### CapabilityRouter + +职责:能力注册、发现和执行路由 + +```python +class CapabilityRouter: + def __init__(self): + self._registrations = {} # capability_name → registration + self.db_store = {} # 内置 KV 存储 + self.memory_store = {} # 内置记忆存储 + self._register_builtin_capabilities() + + def register(self, descriptor, *, call_handler, stream_handler, finalize): + """注册能力""" + self._registrations[descriptor.name] = _CapabilityRegistration( + descriptor=descriptor, + call_handler=call_handler, + stream_handler=stream_handler, + finalize=finalize + ) + + async def execute(self, capability, payload, *, stream, cancel_token, request_id): + """执行能力调用""" + registration = self._registrations[capability] + + if stream: + # 流式调用 + raw_execution = registration.stream_handler(request_id, payload, cancel_token) + return StreamExecution(iterator=raw_execution, finalize=finalize) + else: + # 同步调用 + output = await registration.call_handler(request_id, payload, cancel_token) + return output +``` + +### 环境分组管理 + +```python +class EnvironmentPlanner: + def plan(self, plugins): + """根据 Python 版本和依赖兼容性分组""" + # 1. 按版本分组 + # 2. 按依赖兼容性合并 + # 3. 生成分组元数据 + return EnvironmentPlanResult(groups=[...]) + +class GroupEnvironmentManager: + def prepare(self, group): + """准备分组虚拟环境""" + # 1. 生成 lock/source/metadata 工件 + # 2. 必要时重建虚拟环境 + # 3. 返回 Python 解释器路径 + return venv_python_path +``` + +--- + +## 客户端层设计 + +### 客户端架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ User Plugin │ +├─────────────────────────────────────────────────────────────┤ +│ ctx.llm.chat() │ +│ ctx.memory.save() │ +│ ctx.db.set() │ +│ ctx.platform.send() │ +└────────────┬──────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ CapabilityProxy │ +│ - call(name, payload) │ +│ - stream(name, payload) │ +└────────────┬──────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ Peer │ +│ - invoke(capability, payload, stream=False) │ +│ - invoke_stream(capability, payload) │ +└────────────┬──────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ Transport │ +│ - send(json_string) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### CapabilityProxy + +职责:封装 Peer 的能力调用接口 + +```python +class CapabilityProxy: + def __init__(self, peer): + self._peer = peer + + async def call(self, name, payload): + """普通能力调用""" + # 1. 检查能力是否可用 + descriptor = self._peer.remote_capability_map.get(name) + if descriptor is None: + raise AstrBotError.capability_not_found(name) + + # 2. 调用 Peer.invoke + return await self._peer.invoke(name, payload, stream=False) + + async def stream(self, name, payload): + """流式能力调用""" + # 1. 检查流式支持 + descriptor = self._peer.remote_capability_map.get(name) + if not descriptor.supports_stream: + raise AstrBotError.invalid_input(f"{name} 不支持 stream") + + # 2. 调用 Peer.invoke_stream + event_stream = await self._peer.invoke_stream(name, payload) + async for event in event_stream: + if event.phase == "delta": + yield event.data +``` + +### LLMClient + +```python +class LLMClient: + def __init__(self, proxy: CapabilityProxy): + self._proxy = proxy + + async def chat(self, prompt, *, system=None, history=None, **kwargs) -> str: + """发送聊天请求,返回文本""" + output = await self._proxy.call("llm.chat", { + "prompt": prompt, + "system": system, + "history": self._serialize_history(history), + **kwargs + }) + return output["text"] + + async def chat_raw(self, prompt, **kwargs) -> LLMResponse: + """发送聊天请求,返回完整响应""" + output = await self._proxy.call("llm.chat_raw", {"prompt": prompt, **kwargs}) + return LLMResponse.model_validate(output) + + async def stream_chat(self, prompt, **kwargs) -> AsyncGenerator[str]: + """流式聊天""" + async for delta in self._proxy.stream("llm.stream_chat", {"prompt": prompt, **kwargs}): + yield delta["text"] +``` + +### 其他客户端 + +| 客户端 | 主要方法 | 对应 Capability | +|--------|---------|-----------------| +| `MemoryClient` | `search()`, `save()`, `get()`, `delete()` | `memory.*` | +| `DBClient` | `get()`, `set()`, `delete()`, `list()`, `get_many()`, `set_many()`, `watch()` | `db.*` | +| `PlatformClient` | `send()`, `send_image()`, `send_chain()`, `get_members()` | `platform.*` | +| `HTTPClient` | `register_webhook()` | - | +| `MetadataClient` | `get_plugin_info()` | - | + +--- + +## 新旧架构对比 + +### 协议对比 + +| 特性 | 旧版 JSON-RPC | 新版 v4 协议 | +|------|---------------|--------------| +| 消息格式 | `{"jsonrpc": "2.0", ...}` | `{"type": "invoke", ...}` | +| 方法区分 | `method` 字段 | `type` 字段 | +| 错误码 | 整数 (`-32000`) | 字符串 (`"internal_error"`) | +| 流式支持 | 独立 notification 方法 | 统一 `EventMessage` phase | +| 握手 | `handshake` method | `InitializeMessage` type | +| 能力声明 | 隐式(method 名称) | 显式 `CapabilityDescriptor` | + +### 运行时对比 + +| 特性 | 旧版 | 新版 | +|------|------|------| +| Peer 抽象 | 分离 `JSONRPCClient/Server` | 统一 `Peer` | +| Handler 分发 | 直接调用 `handler(event)` | `HandlerDispatcher` 参数注入 | +| 能力路由 | 无显式路由 | `CapabilityRouter` | +| 环境管理 | 无 | `PluginEnvironmentManager` 分组 | +| 传输层 | 每个实现处理 JSON-RPC | 传输层只处理字符串 | + +### 代码对比 + +#### 旧版 Handler + +```python +from astrbot.api.star import Star +from astrbot.api.event import AstrMessageEvent + +class MyPlugin(Star): + @command_handler("hello", aliases=["hi"]) + def hello_handler(self, event: AstrMessageEvent): + reply = self.call_context_function("llm_generate", prompt=event.message_plain) + event.reply(reply) +``` + +#### 新版 Handler + +```python +from astrbot_sdk import Star, Context, MessageEvent +from astrbot_sdk.decorators import on_command + +class MyPlugin(Star): + @on_command("hello", aliases=["hi"]) + async def hello(self, event: MessageEvent, ctx: Context) -> None: + reply = await ctx.llm.chat(event.text) + await event.reply(reply) +``` + +--- + +## 兼容层设计 + +### 兼容架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Legacy Plugin │ +├─────────────────────────────────────────────────────────────┤ +│ import astrbot.api.star as star │ +│ import astrbot.api.event as event │ +│ from astrbot.api.star import Star, Context │ +└────────────┬───────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ astrbot_sdk.compat │ +│ - 顶层兼容入口,统一旧导入路径 │ +└────────────┬───────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ astrbot_sdk.api.* (Facade) │ +│ - 提供 astrbot.api.* 导入路径 │ +│ - 调用 _legacy_api.py 中的实现 │ +└────────────┬───────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ astrbot_sdk._legacy_api.py │ +│ - LegacyContext, LegacyStar 等 │ +│ - 兼容的 API 实现 │ +└────────────┬───────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ astrbot_sdk._legacy_runtime.py │ +│ - LegacyRuntimeAdapter, LegacyWorkerRuntimeBridge │ +│ - 处理 legacy hook 和 result │ +└────────────┬───────────────────────────────────────────────┘ + │ +┌────────────▼──────────────────────────────────────────────┐ +│ runtime.HandlerDispatcher │ +│ - 调用 legacy_runtime.dispatch_result() │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 兼容模块职责 + +| 模块 | 职责 | +|------|------| +| `compat.py` | 顶层兼容入口,统一旧导入路径 | +| `api/*` | `astrbot.api.*` 导入路径 facade | +| `_legacy_api.py` | LegacyContext, LegacyStar, 兼容 API 实现 | +| `_legacy_runtime.py` | LegacyRuntimeAdapter, 处理 legacy hook/result | +| `_legacy_loader.py` | Legacy 插件发现与 main.py 包装 | +| `_legacy_llm.py` | Legacy LLM/tool 兼容 | +| `_session_waiter.py` | session_waiter 兼容执行 | +| `_shared_preferences.py` | 共享偏好兼容 | + +### LegacyContext + +```python +class LegacyContext: + """旧版 Context 的兼容实现""" + + def __init__(self, *, peer, plugin_id, logger, ...): + self.peer = peer + self.plugin_id = plugin_id + + # 旧版方法 + def call_context_function(self, function_name, **kwargs): + """调用上下文函数(兼容旧版)""" + # 通过 capability 调用实现 + + def send_message(self, target, message_chain): + """发送消息(兼容旧版)""" + + # ... 其他旧版方法 +``` + +### 旧包名兼容 + +```python +# src-new/astrbot/__init__.py +# 提供 astrbot.api.* 和 astrbot.core.* 导入路径 + +# 用户代码 +from astrbot.api.star import Star +from astrbot.api.event import AstrMessageEvent +from astrbot.api.event.filter import filter + +# 实际实现 +from astrbot_sdk.api.star import Star as _Star +from astrbot_sdk.api.event import AstrMessageEvent as _AstrMessageEvent +# ... +``` + +--- + +## 插件开发指南 + +### v4 原生插件 + +#### plugin.yaml + +```yaml +_schema_version: 2 +name: my_plugin +author: your_name +version: 1.0.0 +runtime: + python: "3.12" +components: + - class: main:MyPlugin +``` + +#### main.py + +```python +from astrbot_sdk import Star, Context, MessageEvent +from astrbot_sdk.decorators import on_command, on_message, provide_capability + +class MyPlugin(Star): + # 命令处理器 + @on_command("hello", aliases=["hi"]) + async def hello(self, event: MessageEvent, ctx: Context) -> None: + await event.reply(f"你好,{event.user_id}!") + + # 消息处理器 + @on_message(keywords=["帮助"]) + async def help(self, event: MessageEvent, ctx: Context) -> None: + await event.reply("可用命令:hello, help") + + # 提供能力 + @provide_capability( + "my_plugin.calculate", + description="执行计算", + input_schema={ + "type": "object", + "properties": {"x": {"type": "number"}}, + "required": ["x"] + }, + output_schema={ + "type": "object", + "properties": {"result": {"type": "number"}}, + "required": ["result"] + } + ) + async def calculate_capability( + self, + payload: dict, + ctx: Context + ) -> dict: + x = payload.get("x", 0) + return {"result": x * 2} +``` + +### 旧版兼容插件 + +#### plugin.yaml + +```yaml +name: my_old_plugin +version: 1.0.0 +components: + - class: main:MyOldPlugin +``` + +#### main.py + +```python +from astrbot.api.star import Star +from astrbot.api.event import AstrMessageEvent + +class MyOldPlugin(Star): + # 旧版装饰器仍然支持 + @command_handler("old_hello") + def old_hello_handler(self, event: AstrMessageEvent): + # 旧版 API 调用 + reply = self.call_context_function("llm_generate", prompt="你好") + event.reply(reply) + + # 生命周期钩子 + async def on_start(self): + self.put_kv_data("started", True) + + async def on_stop(self): + self.put_kv_data("started", False) +``` + +### 生命周期钩子 + +| 钩子 | 说明 | +|------|------| +| `on_start()` | 插件启动时调用 | +| `on_stop()` | 插件停止时调用 | +| `on_error(exc, event, ctx)` | Handler 执行出错时调用 | + +--- + +## 关键设计模式 + +### 1. 协议优先模式 + +- 所有跨进程通信都通过 v4 协议 +- 传输层只处理字符串,协议由 Peer 层处理 +- 支持多种传输方式(Stdio, WebSocket) + +### 2. 能力路由模式 + +- 显式声明 Capability 和输入/输出 Schema +- 通过 CapabilityRouter 统一路由 +- 支持同步和流式两种调用模式 + +### 3. 环境分组模式 + +- 多插件可共享同一 Python 虚拟环境 +- 按版本和依赖兼容性自动分组 +- 节省资源,加快启动速度 + +### 4. 参数注入模式 + +- HandlerDispatcher 支持类型注解注入 +- 优先级:类型注解 > 参数名 > legacy_args +- 支持可选类型 `Optional[Type]` + +### 5. 取消传播模式 + +- CancelToken 统一取消机制 +- 跨进程取消通过 CancelMessage +- 早到取消避免竞态条件 + +### 6. 兼容桥接模式 + +- 多层兼容桥接:`compat.py` → `api/*` → `_legacy_api.py` → `_legacy_runtime.py` +- 每层只负责一个兼容维度的转换 +- 新旧代码可以共存 + +### 7. 插件隔离模式 + +- 每个插件运行在独立 Worker 进程 +- 崩溃不影响其他插件 +- 支持 GroupWorkerRuntime 共享环境 + +--- + +## 附录:关键文件速查 + +| 文件 | 核心类/函数 | 说明 | +|------|------------|------| +| `src-new/astrbot_sdk/__init__.py` | `Star`, `Context`, `MessageEvent` | 顶层入口 | +| `src-new/astrbot_sdk/star.py` | `Star` | v4 原生插件基类 | +| `src-new/astrbot_sdk/context.py` | `Context` | 运行时上下文 | +| `src-new/astrbot_sdk/decorators.py` | `on_command`, `on_message` | v4 装饰器 | +| `src-new/astrbot_sdk/runtime/peer.py` | `Peer` | 协议对等端 | +| `src-new/astrbot_sdk/runtime/bootstrap.py` | `SupervisorRuntime` | 启动引导 | +| `src-new/astrbot_sdk/runtime/loader.py` | `load_plugin()` | 插件加载 | +| `src-new/astrbot_sdk/runtime/handler_dispatcher.py` | `HandlerDispatcher` | Handler 执行分发 | +| `src-new/astrbot_sdk/runtime/capability_router.py` | `CapabilityRouter` | Capability 路由 | +| `src-new/astrbot_sdk/protocol/messages.py` | `InitializeMessage`, `InvokeMessage` | 协议消息 | +| `src-new/astrbot_sdk/protocol/descriptors.py` | `HandlerDescriptor`, `CapabilityDescriptor` | 描述符 | +| `src-new/astrbot_sdk/clients/_proxy.py` | `CapabilityProxy` | 能力代理 | +| `src-new/astrbot_sdk/_legacy_runtime.py` | `LegacyRuntimeAdapter` | Legacy 执行适配 | + +--- + +## 更新日志 + +### 2026-03-14 +- 添加环境分组详细说明 +- 完善 CapabilityRouter 内置能力列表 +- 添加客户端层架构图 +- 补充新旧代码对比示例 + +### 2026-03-13 +- 初始版本 +- 完成整体架构分析 +- 新旧对比整理 + +--- + +> 本文档基于 AstrBot SDK 当前版本 (`refact1/refactsome`) 整理 +> 如有疑问请查阅源代码或提交 Issue diff --git a/src-new/astrbot_sdk/api/__init__.py b/src-new/astrbot_sdk/api/__init__.py index 9933bb2fe..5589c4694 100644 --- a/src-new/astrbot_sdk/api/__init__.py +++ b/src-new/astrbot_sdk/api/__init__.py @@ -1,9 +1,18 @@ -"""过渡期 ``astrbot_sdk.api`` 兼容 facade。 +"""过渡期兼容实现层 ``astrbot_sdk.api``。 -这个包仅用于承接旧插件的历史导入路径,方便外部插件逐步迁移到 v4 顶层 API。 -它不是新的推荐入口,也不应继续承载新的运行时实现逻辑。 +这个包承载旧插件所需的兼容模型与逻辑实现,供 legacy 执行路径直接使用。 +它不是简单的重导出层,而是维护旧语义所必需的实现模块: -迁移目标: +- ``astrbot_sdk.api.event``:``AstrMessageEvent``、``filter``、``MessageEventResult`` +- ``astrbot_sdk.api.message``:``MessageChain``、消息组件模型 +- ``astrbot_sdk.api.provider``:``LLMResponse`` 兼容实体 +- ``astrbot_sdk.api.basic``、``platform``、``components``:配置、平台、组件兼容面 + +与 ``src-new/astrbot/`` 对比: +- 此包(``astrbot_sdk.api``)是真正的兼容实现,包含运行时逻辑 +- ``src-new/astrbot/`` 是薄 facade,只做导入路径转发 + +迁移目标(仅在有明确迁移计划时移除): - ``astrbot_sdk.context.Context`` - ``astrbot_sdk.events.MessageEvent`` @@ -12,10 +21,9 @@ 维护约束: -- ``astrbot_sdk.api.*`` 保持为受控兼容面,后续会随迁移推进逐步移除 -- 包内模块优先作为 facade / 重导出层存在,但允许少量必须保留行为的 compat 模块 -- compat 真实行为应尽量收口到顶层 private compat 模块 -- 新增运行时逻辑应优先放在 v4 主路径,由 compat 层按需转发或包装 +- 新增运行时逻辑优先放在 v4 主路径,由此层按需包装 +- compat 真实执行边界收口在顶层私有模块(``_legacy_runtime.py`` 等) +- 新插件应直接使用 ``astrbot_sdk`` 顶层 v4 API """ from loguru import logger diff --git a/src-new/astrbot_sdk/api/event/astr_message_event.py b/src-new/astrbot_sdk/api/event/astr_message_event.py index aaa943ed1..552add8eb 100644 --- a/src-new/astrbot_sdk/api/event/astr_message_event.py +++ b/src-new/astrbot_sdk/api/event/astr_message_event.py @@ -137,7 +137,18 @@ class AstrMessageEvent(MessageEvent): session_id=self.session_id, ) self.unified_msg_origin = str(self.session) - self.platform = self.platform_meta or self.platform + + def to_payload(self) -> dict[str, Any]: + """Override to guarantee ``platform`` in the wire payload is always a string id. + + ``MessageEvent.to_payload()`` serialises ``self.platform`` verbatim. + Since ``AstrMessageEvent`` may receive a ``PlatformMetadata`` object + via ``platform_meta``, we normalise it through ``get_platform_id()`` + so the wire format stays a clean ``str | None``. + """ + payload = super().to_payload() + payload["platform"] = self.get_platform_id() or None + return payload @classmethod def from_payload(