Files
AstrBot/ARCHITECTURE.md

1689 lines
54 KiB
Markdown
Raw Blame History

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