From a3c4c6b0960d9970897fd287a36ef388ef2a8d44 Mon Sep 17 00:00:00 2001 From: whatevertogo Date: Fri, 13 Mar 2026 19:31:14 +0800 Subject: [PATCH] Refactor legacy API and LLM compatibility logic - Moved legacy LLM and tool compatibility logic from `_legacy_api.py` to a new module `_legacy_llm.py` for better organization and separation of concerns. - Updated `_legacy_api.py` to import necessary components from `_legacy_llm.py`, removing redundant code. - Enhanced database client functionality by adding support for batch read/write operations and change event subscriptions. - Improved documentation in the database client and capability router to reflect new features. - Refined environment management process in the loader to better handle plugin grouping and virtual environment management. --- ARCHITECTURE.md | 1901 +++-------------- refactor.md | 844 +------- src-new/astrbot_sdk/_legacy_api.py | 186 +- src-new/astrbot_sdk/_legacy_llm.py | 199 ++ src-new/astrbot_sdk/clients/db.py | 7 +- src-new/astrbot_sdk/runtime/bootstrap.py | 4 +- .../astrbot_sdk/runtime/capability_router.py | 3 + src-new/astrbot_sdk/runtime/loader.py | 12 +- 8 files changed, 511 insertions(+), 2645 deletions(-) create mode 100644 src-new/astrbot_sdk/_legacy_llm.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e7f888438..336494c48 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,1688 +1,289 @@ -# AstrBot SDK v4 架构与实现文档 +# AstrBot SDK v4 当前架构文档 -## 目录 +本文描述仓库 **当前实现**,是 `src-new/astrbot_sdk` / `src-new/astrbot` / `tests_v4` 的唯一主文档。 +`refactor.md` 仅保留历史设计意图和演进说明,不再描述现状。 -1. [架构概览](#架构概览) -2. [目录结构](#目录结构) -3. [核心模块详解](#核心模块详解) - - [协议层 (protocol/)](#协议层-protocol) - - [运行时层 (runtime/)](#运行时层-runtime) - - [客户端层 (clients/)](#客户端层-clients) - - [API 层 (api/)](#api层-api) - - [核心文件](#核心文件) -4. [五大硬性协议规则](#五大硬性协议规则) -5. [数据流与通信模型](#数据流与通信模型) -6. [扩展机制](#扩展机制) -7. [实现状态](#实现状态) +## 1. 目标与边界 ---- +AstrBot SDK v4 当前同时承担两件事: -## 架构概览 +1. 提供一套原生 v4 插件模型:`Star`、`Context`、`MessageEvent`、capability clients、v4 protocol。 +2. 维持旧插件兼容:`astrbot_sdk.api.*`、`astrbot_sdk.compat`、`astrbot.api.*` 以及选定的 `astrbot.core.*` facade 继续可用。 -AstrBot SDK v4 采用分层架构设计,从上到下分为: +因此,compat 现在不是可忽略的旁路,而是一个受控的长期子系统。当前架构目标是: -``` -┌─────────────────────────────────────────────────────┐ -│ 用户层 (User Layer) │ -│ 插件开发者编写的 Star 类 │ -├─────────────────────────────────────────────────────┤ -│ API 层 (API Layer) │ -│ Star, Context, decorators, filter, events │ -├─────────────────────────────────────────────────────┤ -│ 翻译层 (Translation Layer) │ -│ HandlerDispatcher, Loader, LegacyAdapter │ -├─────────────────────────────────────────────────────┤ -│ 通信层 (Communication Layer) │ -│ Peer, Transport, CapabilityRouter │ -└─────────────────────────────────────────────────────┘ +- v4 原生 API 仍保持清晰、窄导出、协议优先。 +- legacy 兼容逻辑尽量收口到私有边界,而不是扩散到 runtime 主干。 +- 兼容导入路径继续可用,但不把旧应用整棵树重新复制进来。 +- 文档明确区分“等价兼容”“降级兼容”“仅导入兼容”。 + +## 2. 当前分层模型 + +```text +插件作者 + ├─ 原生 v4: astrbot_sdk.{Star, Context, MessageEvent, decorators} + └─ legacy compat: astrbot_sdk.api.* / astrbot_sdk.compat / astrbot.api.* + +高层 API + ├─ 原生 clients: llm / memory / db / platform + └─ legacy facade: LegacyContext / LegacyStar / message components / filter namespace + +执行边界 + ├─ runtime 主干: loader / bootstrap / handler_dispatcher / capability_router / peer + ├─ compat 执行边界: _legacy_runtime.py + ├─ legacy 行为承接: _legacy_api.py + └─ 会话等待器: _session_waiter.py + +协议与传输 + ├─ protocol.messages / protocol.descriptors + ├─ Peer + └─ StdioTransport / WebSocket transports ``` -### 核心设计原则 +### 当前最重要的架构判断 -1. **协议优先**: 所有通信通过标准化的协议消息 -2. **能力抽象**: 通过 Capability 系统暴露核心功能 -3. **双向通信**: Plugin ↔ Core 的对称通信模型 -4. **向后兼容**: LegacyAdapter 提供 v3 兼容层 +- `astrbot_sdk.__init__` 只导出推荐的 v4 入口。 +- `astrbot_sdk.runtime.__init__` 只导出高级运行时原语,不把 loader/bootstrap 等编排细节提升为根级稳定 API。 +- `astrbot_sdk.protocol.__init__` 只导出 v4 原生协议模型;legacy JSON-RPC 适配器留在 `protocol.legacy_adapter` 子模块。 +- runtime 主干通过 `_legacy_runtime.py` 执行 compat filters / hooks / 生命周期桥接,不直接展开更多 legacy 细节。 ---- +## 3. 目录结构 -## 目录结构 - -``` -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 +```text +src-new/ +├── astrbot_sdk/ +│ ├── __init__.py # v4 推荐顶层入口 +│ ├── context.py # Context / CancelToken +│ ├── decorators.py # on_command / on_message / provide_capability ... +│ ├── events.py # MessageEvent / PlainTextResult +│ ├── errors.py # AstrBotError / ErrorCodes +│ ├── star.py # Star 基类与 handler 收集 +│ ├── compat.py # 旧顶层兼容重导出 +│ ├── _legacy_api.py # LegacyContext / LegacyStar / CommandComponent +│ ├── _legacy_llm.py # legacy LLM/tool 兼容辅助 +│ ├── _legacy_runtime.py # compat 执行边界 +│ ├── _session_waiter.py # legacy session_waiter 兼容执行 +│ ├── _shared_preferences.py # 共享偏好兼容辅助 +│ │ +│ ├── clients/ +│ │ ├── _proxy.py # CapabilityProxy +│ │ ├── llm.py +│ │ ├── memory.py +│ │ ├── db.py # 包含 get_many / set_many / watch +│ │ └── platform.py +│ │ +│ ├── protocol/ +│ │ ├── __init__.py # 仅导出原生 v4 协议模型 +│ │ ├── descriptors.py # handlers / capabilities / builtin schema registry +│ │ ├── messages.py # initialize / invoke / result / event / cancel +│ │ └── legacy_adapter.py # v3 JSON-RPC ↔ v4 适配 +│ │ +│ ├── runtime/ +│ │ ├── __init__.py # Peer / Transport / CapabilityRouter / HandlerDispatcher +│ │ ├── peer.py +│ │ ├── transport.py +│ │ ├── capability_router.py +│ │ ├── handler_dispatcher.py +│ │ ├── loader.py +│ │ ├── environment_groups.py # 共享环境规划与分组环境管理 +│ │ └── bootstrap.py +│ │ +│ └── api/ # astrbot_sdk.api.* 兼容层 +│ ├── basic/ +│ ├── components/ +│ ├── event/ +│ ├── message/ +│ ├── platform/ +│ ├── provider/ +│ └── star/ │ -├── 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 +└── astrbot/ # 旧包名 facade,受控兼容面 + ├── api/ + └── core/ ``` ---- +## 4. 核心执行链 -## 核心模块详解 +### 4.1 插件发现与 worker 启动 -### 协议层 (protocol/) +1. `runtime.loader.discover_plugins()` 扫描插件目录,兼容 `plugin.yaml` 和 legacy `main.py` 插件。 +2. `PluginEnvironmentManager.plan()` 基于 `runtime.python` 和 `requirements.txt` 规划共享环境分组。 +3. `GroupEnvironmentManager` 负责准备分组环境;worker 仍然保持“一插件一进程”,只是可共享同一个 Python 环境。 +4. `load_plugin()` 加载组件,v4 `Star` 直接实例化,legacy 组件复用同一 `LegacyContext`。 +5. legacy component 注册通过 `_legacy_runtime` 把 compat hooks / LLM tools / context functions 绑定到共享 `LegacyContext`。 +6. `PluginWorkerRuntime` 创建 `Peer`、`HandlerDispatcher`、`CapabilityDispatcher`,初始化后向 supervisor 发送 `initialize`。 +7. worker 启动/停止时的 compat lifecycle hooks 统一由 `_legacy_runtime` 执行。 -协议层负责消息格式定义和 legacy 兼容转换,是 v4 新引入的抽象层。 +### 4.2 handler.invoke 调用链 -#### `descriptors.py` - 描述符定义 +1. 上游通过 capability `"handler.invoke"` 调 worker。 +2. `HandlerDispatcher` 构造本地 `Context` 和 `MessageEvent`,先尝试把消息路由给 `_session_waiter`。 +3. 若命中 legacy compat handler,则由 `_legacy_runtime` 应用 custom filters、结果装饰、发送后 hook、错误 hook。 +4. handler 返回值支持: + - `MessageEventResult` + - `MessageChain` + - `PlainTextResult` + - `str` + - `{"text": ...}` +5. 发送链路优先使用 `ctx.platform.send_chain()` 或 `event.reply()`。 -定义了 Handler 和 Capability 的元数据结构,以及内置能力的 JSON Schema 常量。 +### 4.3 capability 调用链 -**核心类型:** +1. 插件代码通过 `ctx.llm.*`、`ctx.db.*`、`ctx.memory.*`、`ctx.platform.*` 访问上游能力。 +2. clients 通过 `CapabilityProxy` 转成 `Peer.invoke()` / `Peer.invoke_stream()`。 +3. supervisor 侧 `CapabilityRouter` 处理内建能力;worker 也可以通过 `@provide_capability()` 暴露插件自定义 capability。 +4. 插件自定义 capability 由 `CapabilityDispatcher` 在 worker 内分发执行。 + +### 4.4 session_waiter + +`_session_waiter.py` 提供 legacy `@session_waiter` 的最小可运行兼容实现。 +它不是单纯导入桩,而是按 session 维度把后续消息重新路由给等待中的 compat 回调。 + +## 5. 协议契约 + +### 5.1 五条硬规则 + +1. 所有协议消息统一使用 `id` 关联请求与响应。 +2. `EventMessage` 只用于 `stream=true` 的调用。 +3. 插件 handler 回调统一走 capability `"handler.invoke"`。 +4. `CancelMessage` 表示“请求停止”,调用方仍需等待终止态。 +5. `initialize` 失败后连接进入不可用状态。 + +### 5.2 版本语义 + +当前实现里必须区分两个概念: + +- `protocol_version`:**线协议版本**。当前 wire contract 使用 `"1.0"`。 +- `PeerInfo.version`:**软件/实现版本标识**。当前 runtime 常用 `"v4"` 作为软件版本字符串。 + +二者不是同一个字段,也不应混写成同一含义。 + +### 5.3 主要消息 + +- `InitializeMessage` +- `InvokeMessage` +- `ResultMessage` +- `EventMessage` +- `CancelMessage` + +`InitializeMessage` 由 `Peer.initialize()` 发起,成功响应是 +`ResultMessage(kind="initialize_result", success=True, output=InitializeOutput(...))`。 + +## 6. 当前内建 capabilities + +当前协议注册表和 `CapabilityRouter` 内建 capability 一致,共 18 个: + +| 命名空间 | Capability | 流式 | +|---|---|---| +| `llm` | `llm.chat` | 否 | +| `llm` | `llm.chat_raw` | 否 | +| `llm` | `llm.stream_chat` | 是 | +| `memory` | `memory.search` | 否 | +| `memory` | `memory.save` | 否 | +| `memory` | `memory.get` | 否 | +| `memory` | `memory.delete` | 否 | +| `db` | `db.get` | 否 | +| `db` | `db.set` | 否 | +| `db` | `db.delete` | 否 | +| `db` | `db.list` | 否 | +| `db` | `db.get_many` | 否 | +| `db` | `db.set_many` | 否 | +| `db` | `db.watch` | 是 | +| `platform` | `platform.send` | 否 | +| `platform` | `platform.send_image` | 否 | +| `platform` | `platform.send_chain` | 否 | +| `platform` | `platform.get_members` | 否 | + +说明: + +- `SessionRef` 是结构化发送目标 schema,不是 capability。 +- `internal.*` 与 `handler.*` 命名空间保留给框架内部使用,不属于公开内建 capability 列表。 + +## 7. 兼容层现状 + +### 7.1 等价或接近等价的兼容面 + +以下兼容面当前是实际可运行的,不只是 import stub: + +- `astrbot_sdk.api.*` 常用导入路径 +- `astrbot_sdk.compat` +- `astrbot.api.*` 以及选定的 `astrbot.core.*` facade +- `LegacyContext` / `LegacyStar` / `CommandComponent` +- `filter.command` / `regex` / `permission` +- `event_message_type` / `platform_adapter_type` +- 常用 compat hooks: + - `after_message_sent` + - `on_astrbot_loaded` + - `on_platform_loaded` + - `on_decorating_result` + - `on_llm_request` + - `on_llm_response` + - `on_waiting_llm_request` + - `on_using_llm_tool` + - `on_llm_tool_respond` + - `on_plugin_error` + - `on_plugin_loaded` + - `on_plugin_unloaded` +- message components 兼容导出、别名构造和常用工厂 +- `session_waiter` +- 旧插件共享单一 `LegacyContext` + +### 7.2 降级兼容 + +这些能力可以运行,但不保证与历史实现完全等价: + +- `command_group`:当前会展平成普通命令名,不复刻旧的树状命令帮助与多层执行链。 +- legacy JSON-RPC handshake 转 v4 handler 描述时,只能近似恢复旧触发信息,原始 payload 会保留在 metadata 里。 +- `astrbot.core.*` 的深层 facade 只覆盖受支持的导入路径,不等于整个旧应用树。 +- `tool_loop_agent()` 当前是 compat local tool loop,并非完整复刻旧应用内部 agent 体系。 + +### 7.3 仅导入兼容或明确不支持 + +以下路径或能力要么只有导入兼容,要么明确不实现旧语义: + +- `astrbot.api.agent()`:显式 `NotImplementedError` +- `astrbot.core.provider.provider` 中的 provider 基类与 embeddings/rerank 方法:导入可用,但方法是 stub +- 没有可映射执行链路的旧 `filter.*` helper:显式 `NotImplementedError` + +兼容原则是“尽量保留可运行的旧插件路径”,不是“重新实现整个旧 AstrBot 应用”。 + +## 8. 对插件作者的导入建议 + +### 推荐的新代码 ```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 +from astrbot_sdk import Star, Context, MessageEvent +from astrbot_sdk.decorators import on_command, on_message, provide_capability ``` -**内置能力 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 +from astrbot_sdk.api.event.filter import filter ``` -**兼容层入口:** +### 旧包名 facade ```python -# 通过 astrbot_sdk.api 和 astrbot_sdk.compat 访问旧版 API -from astrbot_sdk.api.star import Context, Star -from astrbot_sdk.compat import LegacyContext, CommandComponent +from astrbot.api.star import Star +from astrbot.core.utils.session_waiter import session_waiter ``` ---- +只有在需要兼容现有旧插件时才应继续使用这些路径;新插件应直接使用 v4 顶层入口。 -## 关键设计决策 +## 9. 测试与维护约定 -### 运行时模块导出策略 +- 当前主测试目录是 `tests_v4/`,覆盖 protocol、runtime、clients、compat facade、legacy plugin integration、top-level imports 与 integration flows。 +- 文档维护规则: + - capability 集合变化时,同时更新本文档与对应测试。 + - compat 支持级别变化时,同时更新本文档、`CLAUDE.md` / `AGENTS.md` 备注以及相关契约测试。 + - `refactor.md` 不再承载现状;出现冲突时,一律以本文档和代码/测试为准。 -`runtime/__init__.py` 仅暴露高级运行时原语(`Peer`, `Transport`, `CapabilityRouter`, `HandlerDispatcher`),而将 loader/bootstrap 等编排细节保留在子模块中。 +## 10. 当前建议的后续演进方向 -**原因:** -- 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] -``` +1. 继续把 runtime 对 compat 的认知收口到 `_legacy_runtime.py`。 +2. 继续拆薄 `_legacy_api.py`,让 `LegacyContext` 更偏向 facade 和 orchestration。 +3. 保持 `src-new/astrbot` 为受控 facade,不要把旧应用整棵树重新复制进来。 +4. 用契约测试保护 capability 注册表、compat hook 执行和 facade 导入矩阵,避免文档再次漂移。 diff --git a/refactor.md b/refactor.md index a7a40d2e0..f59ba99d6 100644 --- a/refactor.md +++ b/refactor.md @@ -1,825 +1,55 @@ -# AstrBot SDK 重构架构设计 v4 +# AstrBot SDK v4 重构设计(历史说明) ---- +本文档保留最初的 v4 重构意图与设计取舍,**不再作为当前实现文档**。 +当前代码、兼容面、能力集合、目录结构与版本语义,请以 [ARCHITECTURE.md](D:/GitObjectsOwn/astrbot-sdk/ARCHITECTURE.md) 为准。 -## 一、全局架构图 +## 1. 这份文档现在的用途 -``` -╔══════════════════════════════════════════════════════════════════════╗ -║ 插件作者的世界 ║ -║ ║ -║ class MyPlugin(Star): ║ -║ @on_command("hello") ║ -║ async def hello(self, event: MessageEvent, ctx: Context): ║ -║ reply = await ctx.llm.chat(event.text) ║ -║ await event.reply(reply) ║ -║ ║ -╚══════════════════════════════════════════════════════════════════════╝ - │ Star / 装饰器 / Event │ Context / Clients - ▼ ▼ -┌─────────────────────┐ ┌────────────────────────────────┐ -│ Handler 系统 │ │ Capability 调用系统 │ -│ │ │ │ -│ HandlerDescriptor │ │ ctx.llm.chat() │ -│ HandlerDispatcher │ │ ctx.memory.search() │ -│ │ │ ctx.db.get() │ -│ 插件 → 主进程 │ │ ctx.platform.send() │ -│ "我能响应这些事件" │ │ │ -│ │ │ 插件 → 主进程 │ -│ │ │ "帮我调用这个能力" │ -└──────────┬──────────┘ └──────────────┬─────────────────┘ - │ │ - └─────────────────┬────────────────┘ - ▼ -┌──────────────────────────────────────────────────────────────────┐ -│ 通信层 │ -│ │ -│ 所有消息统一使用 id 字段关联请求与响应 │ -│ │ -│ Peer.initialize(handlers=[...]) │ -│ Peer.invoke("llm.chat", input) → result │ -│ Peer.invoke_stream("llm.stream_chat", input) → event* │ -│ Peer.invoke("handler.invoke", {handler_id, event}) │ -│ │ -│ Transport: StdioTransport / WebSocketTransport │ -└──────────────────────────────────────────────────────────────────┘ - │ JSON 消息流 - ▼ -┌──────────────────────────────────────────────────────────────────┐ -│ 主进程(AstrBot Core) │ -│ │ -│ CapabilityRouter ──► "llm.chat" ──► LLM Service │ -│ ──► "db.get" ──► Storage │ -│ ──► "handler.invoke" ──► 转发给插件 │ -│ │ -│ HandlerDispatcher ◄── 外部消息 ──► 匹配订阅 ──► 回调插件 │ -└──────────────────────────────────────────────────────────────────┘ +- 记录最初为什么要做 v4 分层与协议化重构 +- 保留当时的重要设计原则,供后续判断“方向有没有跑偏” +- 帮助阅读历史提交和旧讨论 - ┌─────────────────────┐ - │ compat.py(旁路) │ ← 不是核心层 - │ 旧 API → 转发新 API │ 新代码不感知它 - └─────────────────────┘ -``` +它**不再负责**描述当前仓库现状。 ---- +## 2. 仍然有效的核心原则 -## 二、两个核心概念的区分 +以下原则仍然是当前实现的主线: -``` -┌──────────────────────────────────────────────────────────────┐ -│ HandlerDescriptor │ -│ 方向:插件 ──► 主进程(initialize 时声明) │ -│ 含义:插件订阅"我能响应哪些事件" │ -│ 例子:@on_command("hello") → 订阅 /hello 命令 │ -└──────────────────────────────────────────────────────────────┘ +- 协议优先:插件与宿主通过显式协议消息交互 +- 统一 `id`:所有请求/响应使用单一关联字段 +- `handler.invoke`:handler 回调不引入额外消息类型 +- `event` 只服务于 `stream=true` +- runtime 根导出保持窄接口 +- legacy 适配与原生 v4 协议模型分开管理 -┌──────────────────────────────────────────────────────────────┐ -│ CapabilityInvocation │ -│ 方向:插件 ──► 主进程(运行时按需调用) │ -│ 含义:插件请求"帮我执行这个能力" │ -│ 例子:ctx.llm.chat() → invoke "llm.chat" │ -└──────────────────────────────────────────────────────────────┘ +## 3. 已经演化的地方 -┌──────────────────────────────────────────────────────────────┐ -│ CapabilityDescriptor │ -│ 方向:主进程 ──► 插件(initialize_result 时返回) │ -│ 含义:主进程声明"我提供哪些能力" │ -│ 例子:{ name: "llm.chat", supports_stream: false, ... } │ -└──────────────────────────────────────────────────────────────┘ -``` +最初方案中的下列假设,当前已经不再成立或只部分成立: -| | HandlerDescriptor | CapabilityDescriptor | CapabilityInvocation | -|---|---|---|---| -| 谁发 | 插件 | 主进程 | 插件 | -| 何时 | initialize 时 | initialize_result 时 | 运行时 | -| 主进程动作 | 注册订阅 | 告知可用能力 | 执行并返回结果 | +- `compat.py` 不是当前 compat 的全部实现,compat 已演化为长期维护子系统 +- runtime 不能完全“感知不到 compat”,但 compat 执行细节应继续收口到 `_legacy_runtime.py` +- 环境管理不再只是“每插件一个独立 venv”,现在有 `runtime.environment_groups` 做共享环境规划 +- capability 集合已经扩展,当前不止早期文档中的那一组 +- 旧包名兼容不再只有 `astrbot_sdk.api.*`,还包括受控的 `src-new/astrbot` facade ---- +## 4. 当前维护约定 -## 三、分层职责 +如果你要修改实现,请按下面的顺序看文档: -``` -┌─────────────────────────────────────────────────────┐ -│ Layer 1:用户层 │ -│ Star / 装饰器 / MessageEvent │ -│ 插件作者只接触这一层 │ -│ 不知道:RPC、进程、序列化、订阅协议 │ -│ │ -│ 处理器发现机制: │ -│ - 装饰器将元数据附加到函数属性 __astrbot_handler_meta__ │ -│ - Star.__init_subclass__ 自动收集到 __handlers__ │ -│ - loader 扫描时从 __handlers__ 构建 HandlerDescriptor │ -└──────────────────────────┬──────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Layer 2:API 层 │ -│ Context / LLMClient / DBClient / MemoryClient │ -│ PlatformClient │ -│ 把能力包装成类型化 API │ -│ 不知道:JSON 格式、id、transport │ -└──────────────────────────┬──────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Layer 3:翻译层 │ -│ CapabilityProxy │ -│ API 调用 → Peer.invoke(name, input) │ -│ → Peer.invoke_stream(name, input) │ -│ output dict → 返回类型 │ -│ 无业务逻辑,一一对应 │ -└──────────────────────────┬──────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────┐ -│ Layer 4:通信层 │ -│ Peer / Transport / Protocol Messages │ -│ 可靠收发消息 │ -│ 不知道业务,只知道消息格式 │ -└─────────────────────────────────────────────────────┘ +1. 先看 `ARCHITECTURE.md` +2. 再看相关代码和 `tests_v4` +3. 最后把本文档当作历史背景材料 - ※ compat.py 不是第五层,是用户层和 API 层的旁路入口。 - 新代码不 import 它,可整体删除。 - ※ `invoke_stream()` 是 SDK 侧便利方法,线协议仍然只发送 - `invoke { stream: true }`。 -``` +如果 `ARCHITECTURE.md` 与本文档冲突: ---- +- 以 `ARCHITECTURE.md` 为准 +- 若仍有歧义,以代码和测试为准 -## 四、目录结构 +## 5. 对后续重构的约束 -``` -astrbot_sdk/ -│ -├── star.py -├── context.py -├── decorators.py -├── events.py -├── errors.py -├── compat.py ← 旁路,不是核心层 -│ -├── clients/ -│ ├── llm.py -│ ├── memory.py -│ ├── db.py -│ └── platform.py -│ -├── runtime/ -│ ├── peer.py -│ ├── transport.py -│ ├── capability_router.py -│ ├── handler_dispatcher.py -│ ├── loader.py -│ └── bootstrap.py -│ -└── protocol/ - ├── messages.py ← 所有协议消息类型 - ├── descriptors.py ← HandlerDescriptor / CapabilityDescriptor - └── legacy_adapter.py ← 旧线协议翻译,只做翻译无业务逻辑 -``` +后续清理实现时,应继续坚持: ---- - -## 五、协议消息定义(完整版) - -### 五条硬规则 - -**规则一:统一使用 `id` 字段关联所有请求与响应** -``` -所有消息只用一个关联字段:id -不区分 request_id / invocation_id,全部统一成 id。 -发送方生成 id,接收方响应时原样带回,双方按 id 配对。 -``` - -**规则二:event 只用于 stream=true 的调用** -``` -stream=false 的调用只能以单个 result 结束。 -stream=true 的调用只能以 event 序列结束。 -stream=false 的调用不得发送 event(started/delta/completed/failed)。 -违反此规则的实现视为协议错误。 -``` - -**规则三:插件 handler 回调走统一 invoke,不新增消息类型** -``` -主进程触发插件处理器时: - capability: "handler.invoke" - input: { handler_id: str, event: { 纯数据 } } - -ctx 不通过线协议传输。 -ctx 由插件进程本地重建并注入处理器。 -看到处理器签名有 ctx 参数,不要误以为需要从主进程发过来。 -``` - -**规则四:cancel 是"请求停止",不是"立即停止"** -``` -收到 cancel 后: - 若调用已结束 → 忽略,不报错 - 若调用仍在执行 → 尽力中断,发送统一终止态 - -统一终止态: - stream=true: event { phase: "failed", error: { code: "cancelled" } } - stream=false: result { success: false, error: { code: "cancelled" } } - -调用方收到 cancel 后必须等待终止态,不能认为发完 cancel 就已结束。 -``` - -**规则五:initialize 失败后连接进入不可用状态** -``` -initialize 失败(协议版本不兼容 / handlers 非法 / 元信息缺失)时: - 返回 result { kind: "initialize_result", success: false, error: {...} } - 连接进入不可用状态 - 除关闭连接外,不得继续发送普通 invoke - 对端收到失败的 initialize_result 后应立即关闭连接 -``` - ---- - -### 消息格式 - -**initialize** -```json -{ - "type": "initialize", - "id": "msg_001", - "protocol_version": "1.0", - "peer": { - "name": "my-plugin", - "role": "plugin", - "version": "1.2.0" - }, - "handlers": [ "HandlerDescriptor ..." ], - "metadata": {} -} -``` - -**initialize_result(成功)** -```json -{ - "type": "result", - "id": "msg_001", - "kind": "initialize_result", - "success": true, - "output": { - "peer": { "name": "astrbot-core", "role": "core" }, - "capabilities": [ "CapabilityDescriptor ..." ], - "metadata": {} - } -} -``` - -**initialize_result(失败)** -```json -{ - "type": "result", - "id": "msg_001", - "kind": "initialize_result", - "success": false, - "error": { - "code": "protocol_version_mismatch", - "message": "服务端支持协议版本 1.0,客户端请求版本 2.0", - "hint": "请升级 astrbot_sdk 至最新版本", - "retryable": false - } -} -``` -※ 失败后连接进入不可用状态,对端应立即关闭连接。 - -**invoke(普通能力)** -```json -{ - "type": "invoke", - "id": "msg_002", - "capability": "llm.chat", - "input": { "prompt": "hi", "system": null }, - "stream": false -} -``` - -**invoke(流式能力)** -```json -{ - "type": "invoke", - "id": "msg_003", - "capability": "llm.stream_chat", - "input": { "prompt": "hi" }, - "stream": true -} -``` - -**invoke(handler 回调)** -```json -{ - "type": "invoke", - "id": "msg_010", - "capability": "handler.invoke", - "input": { - "handler_id": "handler_abc123", - "event": { - "text": "/hello", - "user_id": "u_001", - "group_id": null, - "platform": "qq" - } - }, - "stream": false -} -``` -※ input.event 只含纯数据字段。ctx 由插件进程本地构建并注入,不经过线协议传输。 - -**result(成功)** -```json -{ - "type": "result", - "id": "msg_002", - "success": true, - "output": { "text": "你好!" } -} -``` - -**result(失败)** -```json -{ - "type": "result", - "id": "msg_002", - "success": false, - "error": { - "code": "llm_not_configured", - "message": "未找到可用的大模型配置", - "hint": "请在管理面板的「模型管理」中添加模型", - "retryable": false - } -} -``` - -**event 序列(stream=true 专用)** -```json -{ "type": "event", "id": "msg_003", "phase": "started" } -{ "type": "event", "id": "msg_003", "phase": "delta", "data": { "text": "你" } } -{ "type": "event", "id": "msg_003", "phase": "delta", "data": { "text": "好" } } -{ "type": "event", "id": "msg_003", "phase": "completed", "output": { "text": "你好" } } -``` - -**event(取消终止态)** -```json -{ - "type": "event", - "id": "msg_003", - "phase": "failed", - "error": { - "code": "cancelled", - "message": "调用被取消", - "hint": "", - "retryable": false - } -} -``` - -**cancel** -```json -{ - "type": "cancel", - "id": "msg_003", - "reason": "user_cancelled" -} -``` - ---- - -## 六、描述符定义 - -### HandlerDescriptor - -``` -HandlerDescriptor -{ - id: str 唯一标识,主进程回调时填入 handler_id - trigger: CommandTrigger - | MessageTrigger - | EventTrigger - | ScheduleTrigger - priority: int 默认 0,越大越先执行 - permissions: { - require_admin: bool - level: int - } -} -``` - -trigger 判别联合:不同 type 只允许对应字段出现,其他字段必须省略。 - -``` -CommandTrigger -{ - type: "command" - command: str 必填 - aliases: [str] 可选,默认 [] - description: str 可选 -} - -MessageTrigger -{ - type: "message" - regex: str | null 可选 - keywords: [str] 可选,默认 [] - platforms: [str] 可选,默认 [](空表示所有平台) -} - -EventTrigger -{ - type: "event" - event_type: str 必填 -} - -ScheduleTrigger -{ - type: "schedule" - cron: str | null - interval_seconds: int | null -} -规则:cron 和 interval_seconds 必须且只能有一个非 null -``` - -### CapabilityDescriptor - -主进程在 initialize_result.output.capabilities 中返回。 - -``` -CapabilityDescriptor -{ - name: str capability name,如 "llm.chat" - description: str 一句话说明 - input_schema: JSONSchema | null 输入结构定义 - output_schema: JSONSchema | null 输出结构定义 - supports_stream: bool 是否支持 stream=true 调用 - cancelable: bool 是否支持 cancel -} - -schema 治理规则: - 内建核心 capability(llm.* / db.* / memory.* / platform.*) - 必须提供 input_schema 和 output_schema - 兼容期或动态注册的 capability - 允许为 null,但应在路线图中补全 - 不得以"动态能力"为由长期保持 null -``` - -示例: -```json -{ - "name": "llm.chat", - "description": "发送对话请求,返回模型回复文本", - "input_schema": { - "type": "object", - "properties": { - "prompt": { "type": "string" }, - "system": { "type": "string" }, - "model": { "type": "string" }, - "temperature": { "type": "number" } - }, - "required": ["prompt"] - }, - "output_schema": { - "type": "object", - "properties": { - "text": { "type": "string" } - }, - "required": ["text"] - }, - "supports_stream": false, - "cancelable": false -} -``` - ---- - -## 七、Capability Name 约定 - -``` -格式:{namespace}.{method} - -内建 capability 列表: - llm.chat ctx.llm.chat() - llm.chat_raw ctx.llm.chat_raw() - llm.stream_chat ctx.llm.stream_chat() - memory.search ctx.memory.search() - memory.save ctx.memory.save() - memory.delete ctx.memory.delete() - db.get ctx.db.get() - db.set ctx.db.set() - db.delete ctx.db.delete() - db.list ctx.db.list() - platform.send ctx.platform.send() - platform.send_image ctx.platform.send_image() - platform.get_members ctx.platform.get_members() - -保留命名空间(插件不可使用这些前缀): - handler.* 框架内部:处理器回调 - system.* 框架内部:系统级操作 - internal.* 框架内部:保留扩展 - -命名规则: - 全小写,点分隔命名空间,下划线分隔单词,不用驼峰 - capability name 是协议约定,手写定义,不自动从方法名推导 - 方法名重构不影响协议;协议变更需同步更新方法名和文档 -``` - ---- - -## 八、错误模型 - -```python -@dataclass -class AstrBotError(Exception): - code: str # 机器可读,如 "llm_not_configured" - message: str # 发生了什么 - hint: str # 用户怎么修 - retryable: bool # True = 可重试(超时、网络抖动、临时不可用) - # False = 重试无意义(权限不足、能力不存在、配置缺失) -``` - -``` -可重试(retryable=true) 不可重试(retryable=false) -───────────────────────── ──────────────────────────── -CapabilityTimeout LLMNotConfigured -NetworkError CapabilityNotFound -LLMTemporaryError PermissionDenied - LLMError(模型返回错误) - InvalidInput - Cancelled - ProtocolVersionMismatch -``` - -**Star.on_error 默认兜底:** -``` -AstrBotError retryable=true → 回复"请求失败,请稍后重试" -AstrBotError retryable=false → 回复 error.hint -其他异常 → 回复"出了点问题,请联系插件作者" -所有情况均打完整 traceback 日志 -插件作者覆盖 on_error 可完全自定义 -``` - ---- - -## 九、Context 设计规则 - -```python -class Context: - - # 第一类:插件常用能力 Client(稳定,只扩展不删除) - llm: LLMClient - memory: MemoryClient - db: DBClient - platform: PlatformClient - - # 第二类:少量基础运行时信息 - plugin_id: str - logger: Logger # 自动带插件名前缀 - cancel_token: ... # 取消当前调用 - - # ❌ 不直接挂顶层: - # tools / runtime / scheduler / http / - # storage / persona / workflow / config - # 有需要时设计专属 Client 后再加 -``` - ---- - -## 十、LLM Client 分层 - -```python -class LLMClient: - - async def chat( - self, - prompt: str, - *, - system: str | None = None, - history: list[ChatMessage] | None = None, - model: str | None = None, - temperature: float | None = None, - ) -> str: - """发送对话请求,返回回复文本。爱好者场景首选。""" - - async def chat_raw( - self, - prompt: str, - **kwargs, - ) -> LLMResponse: - """返回完整响应,含 usage / finish_reason / tool_calls。""" - - async def stream_chat( - self, - prompt: str, - *, - system: str | None = None, - history: list[ChatMessage] | None = None, - ) -> AsyncGenerator[str, None]: - """流式对话,逐字返回文本片段。""" -``` - -chat() 和 chat_raw() 是唯二入口,不再增加第三种变体。 - ---- - -## 十一、关键数据流 - -### 11.1 插件加载与握手 - -``` -框架启动 - → loader.py 扫描目录,发现 Star 子类 - → 收集 __handlers__,转成 HandlerDescriptor 列表 - → Peer 发送 initialize { id: "msg_001", handlers: [...] } - → 主进程注册事件订阅 - → 主进程返回 initialize_result { id: "msg_001", - success: true, - capabilities: [...] } - → 插件 CapabilityProxy 缓存 capabilities - → 插件就绪 - -握手失败时: - → 主进程返回 initialize_result { success: false, error: {...} } - → 连接进入不可用状态 - → 插件进程关闭连接,打错误日志 - → 不发送任何 invoke -``` - -### 11.2 外部消息触发处理器 - -``` -外部用户发送 /hello - → 主进程 HandlerDispatcher 匹配订阅 - → 主进程发送 invoke { - id: "msg_010", - capability: "handler.invoke", - input: { - handler_id: "handler_abc", - event: { text: "/hello", user_id: "u_001", ... } - }, - stream: false - } - → 插件 handler_dispatcher 找到处理器方法 - → 本地构建 ctx,注入 event 和 ctx,执行处理器 - → 处理器内调用 ctx.llm.chat()(进入 11.3) -``` - -注:ctx 在插件进程本地构建,不经过线协议传输。 - -### 11.3 非流式能力调用 - -``` -ctx.llm.chat("hi") - → CapabilityProxy 构造 input - → Peer.invoke("llm.chat", {prompt:"hi"}) id="msg_020" - → 发送 { type:"invoke", id:"msg_020", capability:"llm.chat", - input:{...}, stream:false } - ← 收到 { type:"result", id:"msg_020", success:true, - output:{text:"你好"} } - → 解包 output.text → 返回 str - -※ 非流式调用不会收到任何 event 消息 -``` - -### 11.4 流式能力调用 - -``` -async for chunk in ctx.llm.stream_chat("hi"): - → Peer.invoke_stream("llm.stream_chat", {...}) - (底层仍发送 invoke + stream=true,id="msg_030") - ← event { id:"msg_030", phase:"started" } - ← event { id:"msg_030", phase:"delta", data:{text:"你"} } → yield "你" - ← event { id:"msg_030", phase:"delta", data:{text:"好"} } → yield "好" - ← event { id:"msg_030", phase:"completed", output:{text:"你好"} } - → 生成器结束 - -※ stream=true 不会收到 result 消息,只收到 event 序列 -``` - ---- - -## 十二、兼容层 - -``` -compat.py 三条铁律: - 1. 新代码不 import compat.py - 2. compat.py 只 import 新代码 - 3. compat.py 里只有"转发",无业务逻辑 - -旧 API 映射: - -旧写法 新写法 -────────────────────────────────────────────────────────────── -CommandComponent → Star -context.llm_generate(prompt) → ctx.llm.chat(prompt) -context.tool_loop_agent(...) → ctx.llm.chat_raw(...) 含 tools -context.send_message(session, mc) → ctx.platform.send(session, text) -context.put_kv_data(key, value) → ctx.db.set(key, value) -context.get_kv_data(key) → ctx.db.get(key) -@filter.command("hello") → @on_command("hello") -@filter.regex("pattern") → @on_message(regex="pattern") -@filter.permission(ADMIN) → @require_admin -yield event.plain_result("hi") → await event.reply("hi") - -deprecated warning 格式(每个方法只打一次): - [AstrBot] 警告:context.llm_generate() 已过时。 - 请替换为:ctx.llm.chat(prompt) - 迁移文档:https://docs.astrbot.app/migration/v3 - -流式兼容: - 旧 yield 写法只在 compat 层兜底处理 - 新 API 只推荐 AsyncGenerator,不双轨并行 - -legacy_adapter.py 职责边界: - 只翻译旧线协议消息 ↔ 新线协议消息 - 不含业务逻辑,不被新代码 import - 生命周期结束时整个删掉,新代码零修改 -``` - ---- - -## 十三、迁移计划 - -``` -阶段 0:立骨架(当前可开始) -────────────────────────────────────────────────────── - 做什么: - ✦ 新建 star / context / decorators / events / errors / clients - ✦ protocol/descriptors.py 写清 HandlerDescriptor(判别联合) - 和 CapabilityDescriptor(含 schema 治理规则) - ✦ Peer 用 mock 占位(invoke 返回假数据) - ✦ 写 compat.py - - 验收: - ✦ 旧插件加载不报错 - ✦ 新写法能跑通基本流程 - ✦ IDE 对 ctx.llm / ctx.db 有完整补全 - - -阶段 1:接通信层 -────────────────────────────────────────────────────── - 做什么: - ✦ 实现 Transport / Peer(统一 id 字段) - ✦ 实现 capability_router + handler_dispatcher - ✦ 实现 legacy_adapter(旧协议翻译) - ✦ clients/ 接上真实 capability 调用 - ✦ 实现 cancel 语义(请求停止,等终止态) - ✦ 实现 initialize 失败处理(连接不可用 + 关闭) - - 验收: - ✦ 端到端调用成功 - ✦ 流式响应正常,stream=false 不出现 event 消息 - ✦ initialize 失败时连接正确关闭 - ✦ retryable 错误触发自动提示,不可重试触发 hint - - -阶段 2:清理旧实现 -────────────────────────────────────────────────────── - 做什么: - ✦ 删除 api/star/context.py(旧 Context) - ✦ 删除 runtime/rpc/ 旧角色划分 - ✦ 删除 runtime/stars/filter/ 旧装饰器实现 - ✦ deprecated warning 升级为更显眼提示 - - 验收: - ✦ 旧插件仍通过 compat.py 运行 - ✦ 核心路径无旧抽象引用 - - -阶段 3:废弃旧 API(下一大版本) -────────────────────────────────────────────────────── - 做什么: - ✦ deprecated warning 变启动报错 - ✦ 生态迁移完成后删除 compat.py 和 legacy_adapter.py - - 验收: - ✦ 删除 compat.py 后新代码零修改 -``` - ---- - -## 十四、设计决策记录 - -| 问题 | 决策 | 理由 | -|------|------|------| -| 关联字段用什么名 | 统一 `id` | 防止 request_id / invocation_id 在 initialize_result 处产生歧义 | -| event 能用于非流式吗 | 不能,硬规则 | 防止"非流式先发 started 再发 result"污染处理逻辑 | -| initialize 失败后能继续发 invoke 吗 | 不能,连接进入不可用状态 | 防止在无效连接上堆积调用 | -| handler 回调走什么机制 | handler.invoke,不新增消息类型 | 协议保持五种消息 | -| ctx 从哪来 | 插件进程本地构建,不经线协议 | ctx 含运行时状态不可序列化,且无需传输 | -| HandlerDescriptor trigger 结构 | 判别联合 | 防止大量可空字段,方便校验和类型推导 | -| CapabilityDescriptor schema 是否可为 null | 可以,但内建 capability 必须提供 | 防止所有人偷懒填 null 导致 schema 形同虚设 | -| 保留命名空间 | handler.* / system.* / internal.* | 集中声明,防止插件误用或冲突 | -| 错误模型 | code + message + hint + retryable | retryable 区分策略差异,hint 直接告诉用户怎么修 | -| cancel 语义 | 请求停止,等终止态 | 避免实现侧歧义,调用方行为确定 | -| compat 定位 | 旁路入口,不是核心层 | 新代码不感知,可整体删除 | -| Context 扩展规则 | 只放常用能力 Client + 少量运行时信息 | 防止变成圣诞树 | -| chat() 返回类型 | str;进阶用 chat_raw() | 爱好者不拆包装,进阶有专用入口,两个定死 | -| 序列化 | 默认 JSON,不用 pickle | 跨语言,安全,可观测 | -| invoke vs invoke_stream | 分离两个方法而非 stream 参数 | API 更清晰,类型安全,避免运行时分支错误 | -| 处理器注册机制 | 函数属性 + __init_subclass__ 收集 | 避免装饰器时序问题,支持继承,loader 统一扫描 | -| MessageEvent.reply() | 依赖注入 reply_handler | Event 保持纯数据结构,reply 逻辑从外部注入 | - ---- - -*本文档是代码的源头。Python SDK、通信层、主进程三端有分歧时,以本文档为准。* - -*v4 修正:补充 event 只用于 stream=true 的硬规则;initialize 失败场景和连接不可用状态;ctx 不经线协议传输的明确说明;CapabilityDescriptor schema 治理规则;保留命名空间集中声明(handler.* / system.* / internal.*);initialize_result 失败示例;invoke_stream() 分离为独立方法;处理器发现机制使用函数属性 + __init_subclass__;MessageEvent.reply() 依赖注入模式。* +- 不破坏旧插件现有兼容面 +- 不把 legacy 逻辑重新扩散进 runtime 主干 +- 不把 `src-new/astrbot` 扩张成旧应用整棵树 +- 不让文档再次脱离代码与测试 diff --git a/src-new/astrbot_sdk/_legacy_api.py b/src-new/astrbot_sdk/_legacy_api.py index 6c6a56d02..2388c5269 100644 --- a/src-new/astrbot_sdk/_legacy_api.py +++ b/src-new/astrbot_sdk/_legacy_api.py @@ -7,7 +7,6 @@ from __future__ import annotations -import ast import inspect import json from collections import defaultdict @@ -18,6 +17,12 @@ from typing import Any from loguru import logger +from ._legacy_llm import ( + CompatLLMToolManager, + _CompatProviderRequest, + _legacy_llm_response, + _tool_parameters_from_legacy_args, +) from .api.basic.astrbot_config import AstrBotConfig from .api.provider.entities import LLMResponse from .context import Context as NewContext @@ -68,185 +73,6 @@ class _CompatHookEntry: handler: Callable[..., Any] -@dataclass(slots=True) -class _CompatToolSpec: - name: str - description: str - parameters: dict[str, Any] - handler: Callable[..., Any] - active: bool = True - - -@dataclass(slots=True) -class _CompatProviderRequest: - prompt: str | None = None - session_id: str | None = "" - image_urls: list[str] | None = None - contexts: list[dict[str, Any]] | None = None - system_prompt: str = "" - conversation: Any | None = None - tool_calls_result: Any | None = None - model: str | None = None - - -def _tool_parameters_from_legacy_args( - func_args: list[dict[str, Any]], -) -> dict[str, Any]: - parameters: dict[str, Any] = {"type": "object", "properties": {}, "required": []} - for item in func_args: - if not isinstance(item, dict): - continue - name = str(item.get("name", "")) - if not name: - continue - schema = {key: value for key, value in item.items() if key != "name"} - parameters["properties"][name] = schema - parameters["required"].append(name) - return parameters - - -class CompatLLMToolManager: - """旧版 llm tool manager 的最小兼容实现。""" - - def __init__(self) -> None: - self.func_list: list[_CompatToolSpec] = [] - - def add_tool( - self, - *, - name: str, - description: str, - parameters: dict[str, Any], - handler: Callable[..., Any], - ) -> None: - self.remove_func(name) - self.func_list.append( - _CompatToolSpec( - name=name, - description=description, - parameters=parameters, - handler=handler, - ) - ) - - def add_func( - self, - name: str, - func_args: list[dict[str, Any]], - desc: str, - handler: Callable[..., Any], - ) -> None: - self.add_tool( - name=name, - description=desc, - parameters=_tool_parameters_from_legacy_args(func_args), - handler=handler, - ) - - def remove_func(self, name: str) -> None: - self.func_list = [tool for tool in self.func_list if tool.name != name] - - def get_func(self, name: str) -> _CompatToolSpec | None: - for tool in self.func_list: - if tool.name == name: - return tool - return None - - def activate_llm_tool(self, name: str) -> bool: - tool = self.get_func(name) - if tool is None: - return False - tool.active = True - return True - - def deactivate_llm_tool(self, name: str) -> bool: - tool = self.get_func(name) - if tool is None: - return False - tool.active = False - return True - - def get_func_desc_openai_style(self) -> list[dict[str, Any]]: - return [ - { - "type": "function", - "function": { - "name": tool.name, - "description": tool.description, - "parameters": tool.parameters, - }, - } - for tool in self.func_list - if tool.active - ] - - -def _legacy_tool_calls( - response_payload: dict[str, Any] | None, -) -> tuple[list[dict[str, Any]], list[str], list[str]]: - tool_calls = list((response_payload or {}).get("tool_calls") or []) - tool_args: list[dict[str, Any]] = [] - tool_names: list[str] = [] - tool_ids: list[str] = [] - for tool_call in tool_calls: - if not isinstance(tool_call, dict): - continue - function_payload = tool_call.get("function") - if isinstance(function_payload, dict): - name = str(function_payload.get("name") or "") - raw_arguments = function_payload.get("arguments") - else: - name = str(tool_call.get("name") or "") - raw_arguments = tool_call.get("arguments") - if isinstance(raw_arguments, str): - try: - arguments = json.loads(raw_arguments) - except json.JSONDecodeError: - try: - arguments = ast.literal_eval(raw_arguments) - except (SyntaxError, ValueError): - arguments = {} - elif isinstance(raw_arguments, dict): - arguments = raw_arguments - else: - arguments = {} - if not isinstance(arguments, dict): - arguments = {} - tool_names.append(name) - tool_args.append(arguments) - tool_ids.append(str(tool_call.get("id") or f"tool-{len(tool_ids) + 1}")) - return tool_args, tool_names, tool_ids - - -def _legacy_llm_response(response: Any) -> LLMResponse: - if isinstance(response, LLMResponse): - return response - - model_dump = getattr(response, "model_dump", None) - if callable(model_dump): - payload = model_dump() - elif isinstance(response, dict): - payload = dict(response) - else: - payload = { - "text": getattr(response, "text", ""), - "usage": getattr(response, "usage", None), - "finish_reason": getattr(response, "finish_reason", None), - "tool_calls": getattr(response, "tool_calls", []), - } - - tool_args, tool_names, tool_ids = _legacy_tool_calls(payload) - return LLMResponse( - role=str(payload.get("role") or "assistant"), - completion_text=str(payload.get("text") or ""), - tools_call_args=tool_args, - tools_call_name=tool_names, - tools_call_ids=tool_ids, - raw_completion=response, - _new_record=payload, - ) - - class LegacyConversationManager: """旧版会话管理器的兼容实现。 diff --git a/src-new/astrbot_sdk/_legacy_llm.py b/src-new/astrbot_sdk/_legacy_llm.py new file mode 100644 index 000000000..efb74c377 --- /dev/null +++ b/src-new/astrbot_sdk/_legacy_llm.py @@ -0,0 +1,199 @@ +"""legacy LLM 与 tool 兼容辅助。 + +这个模块只承接 ``_legacy_api.py`` 中相对独立的旧 LLM/tool 兼容逻辑: + +- 旧版 tool manager 与 tool schema 组装 +- 旧 provider 请求对象 +- 新响应到旧 ``LLMResponse`` 的转换 + +它不暴露新的公开 API,只用于减轻 ``LegacyContext`` 所在模块的职责。 +""" + +from __future__ import annotations + +import ast +import json +from collections.abc import Callable +from dataclasses import dataclass +from typing import Any + +from .api.provider.entities import LLMResponse + + +@dataclass(slots=True) +class _CompatToolSpec: + name: str + description: str + parameters: dict[str, Any] + handler: Callable[..., Any] + active: bool = True + + +@dataclass(slots=True) +class _CompatProviderRequest: + prompt: str | None = None + session_id: str | None = "" + image_urls: list[str] | None = None + contexts: list[dict[str, Any]] | None = None + system_prompt: str = "" + conversation: Any | None = None + tool_calls_result: Any | None = None + model: str | None = None + + +def _tool_parameters_from_legacy_args( + func_args: list[dict[str, Any]], +) -> dict[str, Any]: + parameters: dict[str, Any] = {"type": "object", "properties": {}, "required": []} + for item in func_args: + if not isinstance(item, dict): + continue + name = str(item.get("name", "")) + if not name: + continue + schema = {key: value for key, value in item.items() if key != "name"} + parameters["properties"][name] = schema + parameters["required"].append(name) + return parameters + + +class CompatLLMToolManager: + """旧版 llm tool manager 的最小兼容实现。""" + + def __init__(self) -> None: + self.func_list: list[_CompatToolSpec] = [] + + def add_tool( + self, + *, + name: str, + description: str, + parameters: dict[str, Any], + handler: Callable[..., Any], + ) -> None: + self.remove_func(name) + self.func_list.append( + _CompatToolSpec( + name=name, + description=description, + parameters=parameters, + handler=handler, + ) + ) + + def add_func( + self, + name: str, + func_args: list[dict[str, Any]], + desc: str, + handler: Callable[..., Any], + ) -> None: + self.add_tool( + name=name, + description=desc, + parameters=_tool_parameters_from_legacy_args(func_args), + handler=handler, + ) + + def remove_func(self, name: str) -> None: + self.func_list = [tool for tool in self.func_list if tool.name != name] + + def get_func(self, name: str) -> _CompatToolSpec | None: + for tool in self.func_list: + if tool.name == name: + return tool + return None + + def activate_llm_tool(self, name: str) -> bool: + tool = self.get_func(name) + if tool is None: + return False + tool.active = True + return True + + def deactivate_llm_tool(self, name: str) -> bool: + tool = self.get_func(name) + if tool is None: + return False + tool.active = False + return True + + def get_func_desc_openai_style(self) -> list[dict[str, Any]]: + return [ + { + "type": "function", + "function": { + "name": tool.name, + "description": tool.description, + "parameters": tool.parameters, + }, + } + for tool in self.func_list + if tool.active + ] + + +def _legacy_tool_calls( + response_payload: dict[str, Any] | None, +) -> tuple[list[dict[str, Any]], list[str], list[str]]: + tool_calls = list((response_payload or {}).get("tool_calls") or []) + tool_args: list[dict[str, Any]] = [] + tool_names: list[str] = [] + tool_ids: list[str] = [] + for tool_call in tool_calls: + if not isinstance(tool_call, dict): + continue + function_payload = tool_call.get("function") + if isinstance(function_payload, dict): + name = str(function_payload.get("name") or "") + raw_arguments = function_payload.get("arguments") + else: + name = str(tool_call.get("name") or "") + raw_arguments = tool_call.get("arguments") + if isinstance(raw_arguments, str): + try: + arguments = json.loads(raw_arguments) + except json.JSONDecodeError: + try: + arguments = ast.literal_eval(raw_arguments) + except (SyntaxError, ValueError): + arguments = {} + elif isinstance(raw_arguments, dict): + arguments = raw_arguments + else: + arguments = {} + if not isinstance(arguments, dict): + arguments = {} + tool_names.append(name) + tool_args.append(arguments) + tool_ids.append(str(tool_call.get("id") or f"tool-{len(tool_ids) + 1}")) + return tool_args, tool_names, tool_ids + + +def _legacy_llm_response(response: Any) -> LLMResponse: + if isinstance(response, LLMResponse): + return response + + model_dump = getattr(response, "model_dump", None) + if callable(model_dump): + payload = model_dump() + elif isinstance(response, dict): + payload = dict(response) + else: + payload = { + "text": getattr(response, "text", ""), + "usage": getattr(response, "usage", None), + "finish_reason": getattr(response, "finish_reason", None), + "tool_calls": getattr(response, "tool_calls", []), + } + + tool_args, tool_names, tool_ids = _legacy_tool_calls(payload) + return LLMResponse( + role=str(payload.get("role") or "assistant"), + completion_text=str(payload.get("text") or ""), + tools_call_args=tool_args, + tools_call_name=tool_names, + tools_call_ids=tool_ids, + raw_completion=response, + _new_record=payload, + ) diff --git a/src-new/astrbot_sdk/clients/db.py b/src-new/astrbot_sdk/clients/db.py index 161c0c8bc..7c8c4c4df 100644 --- a/src-new/astrbot_sdk/clients/db.py +++ b/src-new/astrbot_sdk/clients/db.py @@ -12,12 +12,17 @@ Context.db.set(key, value) Context.db.get(key) Context.db.delete(key) - Context.db.list(prefix) # 新增:列出键 + Context.db.list(prefix) # 列出键 + Context.db.get_many(keys) # 批量读取 + Context.db.set_many(items) # 批量写入 + Context.db.watch(prefix) # 订阅变更流 功能说明: - 数据永久存储,除非用户显式删除 - 值类型支持任意 JSON 数据 - 支持前缀查询键列表 + - 支持批量读写 + - 支持订阅变更事件 """ from __future__ import annotations diff --git a/src-new/astrbot_sdk/runtime/bootstrap.py b/src-new/astrbot_sdk/runtime/bootstrap.py index 99e6df6d7..4b9560e50 100644 --- a/src-new/astrbot_sdk/runtime/bootstrap.py +++ b/src-new/astrbot_sdk/runtime/bootstrap.py @@ -735,7 +735,9 @@ class PluginWorkerRuntime: runtime_context, ) - async def _run_legacy_worker_startup_hooks(self, *, metadata: dict[str, Any]) -> None: + async def _run_legacy_worker_startup_hooks( + self, *, metadata: dict[str, Any] + ) -> None: await run_legacy_worker_startup_hooks( [*self.loaded_plugin.handlers, *self.loaded_plugin.capabilities], context=self._lifecycle_context, diff --git a/src-new/astrbot_sdk/runtime/capability_router.py b/src-new/astrbot_sdk/runtime/capability_router.py index de33aa696..921504503 100644 --- a/src-new/astrbot_sdk/runtime/capability_router.py +++ b/src-new/astrbot_sdk/runtime/capability_router.py @@ -21,6 +21,9 @@ 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: 发送消息链 diff --git a/src-new/astrbot_sdk/runtime/loader.py b/src-new/astrbot_sdk/runtime/loader.py index 57af25c36..9c41a7117 100644 --- a/src-new/astrbot_sdk/runtime/loader.py +++ b/src-new/astrbot_sdk/runtime/loader.py @@ -18,11 +18,11 @@ 5. 返回 PluginDiscoveryResult 环境管理流程: - 1. 检查 .venv 目录是否存在 - 2. 检查 Python 版本是否匹配 - 3. 检查指纹是否变化(requirements 内容) - 4. 必要时重建虚拟环境 - 5. 使用 uv 安装依赖 + 1. 对插件集合做共享环境规划 + 2. 按 Python 版本和依赖兼容性构建环境分组 + 3. 为每个分组生成 lock/source/metadata 工件 + 4. 必要时重建或同步分组虚拟环境 + 5. 将单个插件映射到所属分组环境 插件加载流程: 1. 将插件目录添加到 sys.path @@ -57,7 +57,7 @@ 新版 loader.py: - PluginSpec 描述插件规范 - - PluginEnvironmentManager 管理虚拟环境 + - PluginEnvironmentManager 管理分组共享环境 - load_plugin() 加载并解析组件 - LoadedHandler 封装处理器和描述符 - 支持新旧 Star 组件兼容