mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
feat(openspec): add abp-plugin-loader change for remaining ABP tasks
This commit is contained in:
2
openspec/changes/abp-plugin-loader/.openspec.yaml
Normal file
2
openspec/changes/abp-plugin-loader/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-27
|
||||
106
openspec/changes/abp-plugin-loader/design.md
Normal file
106
openspec/changes/abp-plugin-loader/design.md
Normal file
@@ -0,0 +1,106 @@
|
||||
## Context
|
||||
|
||||
ABP 协议第一阶段完成了核心握手、传输层和事件系统。当前 `OutOfProcessPluginLoader` 已实现,但:
|
||||
1. `PluginLoader` trait 未定义,InProcess 模式缺失
|
||||
2. `tools/list` 端点未实现,工具发现依赖它
|
||||
3. Python FFI 绑定未完成,Python 层无法调用 Rust 核心
|
||||
4. 配置集成未落地,无 PluginRegistry
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 实现 `PluginLoader` trait,统一进程内/外加载逻辑
|
||||
- 实现 `InProcessPluginLoader`,Python 模块直接加载
|
||||
- 实现 `tools/list` 端点和工具 Schema 验证
|
||||
- 完成 Python FFI 绑定链路
|
||||
- 落地 config.yaml 插件配置和 PluginRegistry
|
||||
|
||||
**Non-Goals:**
|
||||
- 不实现 Stars → ABP 插件迁移(下一阶段)
|
||||
- 不实现 WebUI 插件配置界面(config.yaml 落地即可)
|
||||
- 不改变 Rust 核心源码(仅调用已有接口)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. PluginLoader Trait 设计
|
||||
|
||||
**决定**:定义抽象 trait,包含 `load()`、`unload()`、`reload()`、`get_plugin_info()` 方法。
|
||||
|
||||
```python
|
||||
class PluginLoader(ABC):
|
||||
@abstractmethod
|
||||
async def load(self, config: PluginConfig) -> PluginInstance: ...
|
||||
|
||||
@abstractmethod
|
||||
async def unload(self, plugin_id: str) -> None: ...
|
||||
|
||||
@abstractmethod
|
||||
async def reload(self, plugin_id: str) -> PluginInstance: ...
|
||||
```
|
||||
|
||||
**替代方案**:
|
||||
- 工厂模式(被否定):增加抽象层级,当前场景不需要
|
||||
- 统一接口参数(被否定):进程内/外行为差异大,分离更清晰
|
||||
|
||||
### 2. InProcessPluginLoader 实现
|
||||
|
||||
**决定**:Python 模块直接导入,插件实例化后存入 `Dict[str, PluginInstance]`。
|
||||
|
||||
**理由**:
|
||||
- 无进程通信开销,性能最优
|
||||
- 与项目 "Rust 核心 + Python 胶水层" 架构一致
|
||||
- 插件需遵循 `ABPPluginProtocol` 接口约定
|
||||
|
||||
### 3. 工具发现架构
|
||||
|
||||
**决定**:`ToolRegistry` 统一管理,`tools/list` 聚合所有插件工具。
|
||||
|
||||
```
|
||||
ToolRegistry
|
||||
├── register(plugin_id, tools)
|
||||
├── unregister(plugin_id)
|
||||
├── list_tools() -> List[ToolDef]
|
||||
└── call_tool(name, args) -> ToolResult
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 中心化管理避免冲突
|
||||
- Schema 验证在注册时执行
|
||||
- 跨插件工具调用统一入口
|
||||
|
||||
### 4. FFI 绑定链路
|
||||
|
||||
**决定**:Python → Rust `_core.so` → ABP PluginLoader。
|
||||
|
||||
```
|
||||
Python PluginManager
|
||||
→ ctypes / cffi 调用 _core.so
|
||||
→ Rust abp_plugin_loader_* 函数
|
||||
→ 返回 Python 对象(通过 .pyi 类型提示)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- Rust 源码待提交,当前仅使用编译产物
|
||||
- `_core.pyi` 提供类型检查
|
||||
- anyio 异步调用通过 `run_in_executor` 封装
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| Rust 源码未提交,接口可能变 | FFI 调用失败 | 接口版本化,核心保持向后兼容 |
|
||||
| InProcess 插件崩溃影响主进程 | 稳定性下降 | 进程内插件加超时保护 + 错误隔离 |
|
||||
| 工具 Schema 验证复杂度 | 开发体验下降 | JSON Schema 仅校验格式,不校验业务逻辑 |
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. **Phase 1**:PluginLoader trait + InProcessPluginLoader(对现有 OutOfProcess 透明)
|
||||
2. **Phase 2**:tools/list + ToolRegistry
|
||||
3. **Phase 3**:FFI 绑定 + config.yaml 集成
|
||||
4. **Phase 4**:测试覆盖 + 文档
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Q1: InProcess 插件是否需要独立的沙箱隔离?(当前:无)
|
||||
- Q2: 工具注册时 Schema 校验严格程度?(当前:格式校验 + 必需字段)
|
||||
- Q3: PluginRegistry 是否需要持久化?(当前:内存,进程重启丢失)
|
||||
32
openspec/changes/abp-plugin-loader/proposal.md
Normal file
32
openspec/changes/abp-plugin-loader/proposal.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## Why
|
||||
|
||||
ABP 协议实现第一阶段完成了核心握手、传输层和事件系统。当前 PluginLoader trait 和 InProcess 插件加载尚未实现,工具发现依赖 `tools/list`,FFI 绑定也未完成。这些是 ABP 插件系统真正可用的最后几块拼图。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **PluginLoader trait**:定义插件加载抽象,支持进程内/外两种模式
|
||||
- **InProcessPluginLoader**:实现 Python 模块直接加载(无需进程通信)
|
||||
- **tools/list 端点**:插件暴露可用工具列表
|
||||
- **工具 Schema 验证**:JSON Schema Draft-07 校验
|
||||
- **跨插件工具发现**:统一聚合所有插件的工具
|
||||
- **FFI 绑定**:Python 胶水层调用 Rust ABP 核心
|
||||
- **配置集成**:config.yaml 插件配置落地,PluginRegistry 实现
|
||||
- **测试完善**:单元测试 + 集成测试覆盖
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `abp-plugin-loader`: 插件加载器 trait 和实现(InProcess/OutOfProcess)
|
||||
- `abp-tool-discovery`: 工具注册、Schema 验证、跨插件发现
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `abp-protocol`: FFI 绑定补充(Python → Rust 核心调用链路)
|
||||
- `abp-tool-router`: 增加 `tools/list` 端点实现
|
||||
|
||||
## Impact
|
||||
|
||||
- **新增**:`astrbot/core/plugin/` 扩展(loader 模块)
|
||||
- **修改**:`astrbot/core/plugin/plugin_manager.py`(FFI 绑定)
|
||||
- **依赖**:Rust 核心 `_core.so` 已编译,源码待提交 `astrbot/rust/src/`
|
||||
@@ -0,0 +1,57 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: PluginLoader Trait
|
||||
ABP 插件加载器抽象接口,定义进程内/外插件的统一加载方式。
|
||||
|
||||
#### Scenario: Load In-Process Plugin
|
||||
- **WHEN** 配置指定 `load_mode: "in_process"`
|
||||
- **THEN** `InProcessPluginLoader` 直接导入 Python 模块
|
||||
- **AND** 实例化插件,传入 context、user_config、data_dirs
|
||||
- **AND** 将插件实例注册到 `PluginRegistry`
|
||||
|
||||
#### Scenario: Load Out-of-Process Plugin
|
||||
- **WHEN** 配置指定 `load_mode: "out_of_process"`
|
||||
- **THEN** `OutOfProcessPluginLoader` 启动插件子进程
|
||||
- **AND** 建立传输层连接(Stdio/Unix Socket/HTTP)
|
||||
- **AND** 执行握手协议交换配置
|
||||
|
||||
#### Scenario: Unload Plugin
|
||||
- **WHEN** 调用 `loader.unload(plugin_id)`
|
||||
- **THEN** 关闭插件连接/释放模块引用
|
||||
- **AND** 从 `PluginRegistry` 注销插件
|
||||
|
||||
#### Scenario: Reload Plugin
|
||||
- **WHEN** 调用 `loader.reload(plugin_id)`
|
||||
- **THEN** 卸载现有插件实例
|
||||
- **AND** 重新加载并初始化插件
|
||||
|
||||
### Requirement: In-Process Plugin Loading
|
||||
进程内插件直接加载为 Python 模块,由 Python 胶水层管理生命周期。
|
||||
|
||||
#### Scenario: In-Process Plugin Invocation
|
||||
- **WHEN** 调用进程内插件方法(如 `handle_event`)
|
||||
- **THEN** 直接通过 Python 对象调用
|
||||
- **AND** 不经过序列化/反序列化
|
||||
- **AND** 无进程通信开销
|
||||
|
||||
#### Scenario: In-Process Plugin Context
|
||||
- **WHEN** 插件实例化时
|
||||
- **THEN** 主进程传入 context 对象
|
||||
- **AND** 传入 user_config(来自握手)
|
||||
- **AND** 传入 data_dirs(数据目录路径)
|
||||
|
||||
### Requirement: PluginRegistry
|
||||
全局插件实例注册表,管理所有已加载插件。
|
||||
|
||||
#### Scenario: Register Plugin Instance
|
||||
- **WHEN** 插件加载成功时
|
||||
- **THEN** `PluginRegistry` 存储 plugin_id → PluginInstance 映射
|
||||
- **AND** 插件暴露的工具注册到 `ToolRegistry`
|
||||
|
||||
#### Scenario: Query Plugin by ID
|
||||
- **WHEN** 调用 `registry.get_plugin(plugin_id)`
|
||||
- **THEN** 返回插件实例或 None
|
||||
|
||||
#### Scenario: List All Plugins
|
||||
- **WHEN** 调用 `registry.list_plugins()`
|
||||
- **THEN** 返回所有已注册插件的元数据列表
|
||||
@@ -0,0 +1,58 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: tools/list Endpoint
|
||||
插件通过 `tools/list` 端点暴露可用工具列表。
|
||||
|
||||
#### Scenario: List Plugin Tools
|
||||
- **WHEN** 主进程调用 `tools/list` 方法
|
||||
- **THEN** 插件返回 `{ "tools": [ToolDef, ...] }`
|
||||
- **AND** 每个 ToolDef 包含 name、description、parameters(JSON Schema)
|
||||
|
||||
#### Scenario: Empty Tools List
|
||||
- **WHEN** 插件无可用工具
|
||||
- **THEN** 返回 `{ "tools": [] }`
|
||||
|
||||
### Requirement: Tool Schema Validation
|
||||
注册工具时进行 JSON Schema Draft-07 格式校验。
|
||||
|
||||
#### Scenario: Valid Tool Schema
|
||||
- **WHEN** 工具定义通过 Schema 校验
|
||||
- **THEN** 工具注册到 `ToolRegistry`
|
||||
- **AND** 可被 `tools/call` 调用
|
||||
|
||||
#### Scenario: Invalid Tool Schema
|
||||
- **WHEN** 工具定义 Schema 校验失败
|
||||
- **THEN** 记录警告日志
|
||||
- **AND** 跳过该工具(不注册)
|
||||
|
||||
### Requirement: ToolRegistry
|
||||
中心化工具注册表,支持跨插件工具发现和调用。
|
||||
|
||||
#### Scenario: Register Tool
|
||||
- **WHEN** 插件调用 `registry.register(plugin_id, tools)`
|
||||
- **THEN** 工具按 plugin_id 隔离存储
|
||||
- **AND** 执行 Schema 校验
|
||||
|
||||
#### Scenario: Discover All Tools
|
||||
- **WHEN** 调用 `registry.list_tools()`
|
||||
- **THEN** 返回所有插件注册的工具
|
||||
- **AND** 每个工具标记来源 plugin_id
|
||||
|
||||
#### Scenario: Call Tool
|
||||
- **WHEN** 调用 `registry.call_tool(tool_name, args)`
|
||||
- **THEN** 查找工具所属插件
|
||||
- **AND** 转发调用到对应插件
|
||||
- **AND** 返回执行结果
|
||||
|
||||
#### Scenario: Tool Not Found
|
||||
- **WHEN** 调用不存在的工具
|
||||
- **THEN** 抛出 `ToolNotFoundError` 错误
|
||||
- **AND** 错误码 -32203
|
||||
|
||||
### Requirement: Cross-Plugin Tool Discovery
|
||||
聚合多个插件的工具,提供统一发现接口。
|
||||
|
||||
#### Scenario: Aggregate Tools from Multiple Plugins
|
||||
- **WHEN** 多个插件注册了工具
|
||||
- **THEN** `ToolRegistry` 聚合所有工具
|
||||
- **AND** 工具名可配置为 `plugin_name/tool_name` 格式避免冲突
|
||||
45
openspec/changes/abp-plugin-loader/tasks.md
Normal file
45
openspec/changes/abp-plugin-loader/tasks.md
Normal file
@@ -0,0 +1,45 @@
|
||||
## 1. PluginLoader Trait & InProcessPluginLoader
|
||||
|
||||
- [ ] 1.1 Define `PluginLoader` abstract base class in `astrbot/core/plugin/loader.py`
|
||||
- [ ] 1.2 Define `PluginInstance` dataclass (plugin_id, instance, metadata)
|
||||
- [ ] 1.3 Implement `InProcessPluginLoader.load()` (Python module import)
|
||||
- [ ] 1.4 Implement `InProcessPluginLoader.unload()` (remove from registry)
|
||||
- [ ] 1.5 Implement `InProcessPluginLoader.reload()` (reload module)
|
||||
- [ ] 1.6 Create `PluginRegistry` class (Dict[str, PluginInstance] + singleton)
|
||||
- [ ] 1.7 Add `register()` and `unregister()` methods to PluginRegistry
|
||||
|
||||
## 2. Tool Discovery & Registry
|
||||
|
||||
- [ ] 2.1 Create `astrbot/core/plugin/tool_registry.py`
|
||||
- [ ] 2.2 Implement `ToolDef` dataclass (name, description, parameters schema)
|
||||
- [ ] 2.3 Implement `ToolRegistry.register(plugin_id, tools)` with Schema validation
|
||||
- [ ] 2.4 Implement `ToolRegistry.list_tools()` (aggregate all plugins)
|
||||
- [ ] 2.5 Implement `ToolRegistry.call_tool(tool_name, args)`
|
||||
- [ ] 2.6 Implement `tools/list` JSON-RPC endpoint in plugin base class
|
||||
- [ ] 2.7 Add JSON Schema Draft-07 validation (jsonschema library)
|
||||
|
||||
## 3. FFI Bindings
|
||||
|
||||
- [ ] 3.1 Audit existing `_core.pyi` for missing ABP types
|
||||
- [ ] 3.2 Add `plugin_loader_*` FFI function signatures to rust-ffi.md (if missing)
|
||||
- [ ] 3.3 Implement Python → Rust plugin loader calls via ctypes
|
||||
- [ ] 3.4 Add async wrapper using `run_in_executor` for FFI calls
|
||||
- [ ] 3.5 Update `plugin_manager.py` to use FFI bindings
|
||||
|
||||
## 4. Configuration Integration
|
||||
|
||||
- [ ] 4.1 Define `plugins` section in `config.yaml` schema
|
||||
- [ ] 4.2 Implement `PluginConfig` dataclass parsing
|
||||
- [ ] 4.3 Integrate PluginRegistry initialization into AstrBot startup
|
||||
- [ ] 4.4 Add connection pooling for Unix Socket transport (task 3.4 from original)
|
||||
- [ ] 4.5 Write integration test for config → plugin loading flow
|
||||
|
||||
## 5. Testing
|
||||
|
||||
- [ ] 5.1 Write unit tests for PluginLoader trait
|
||||
- [ ] 5.2 Write unit tests for InProcessPluginLoader
|
||||
- [ ] 5.3 Write unit tests for ToolRegistry
|
||||
- [ ] 5.4 Write integration tests for in-process plugin loading
|
||||
- [ ] 5.5 Write integration tests for tools/list + tools/call flow
|
||||
- [ ] 5.6 Run `ruff check .` and `ruff format .`
|
||||
- [ ] 5.7 Run `uvx ty check` for type validation
|
||||
Reference in New Issue
Block a user