feat(agents): Phase 7-10 hook system, plugins, skills, orchestration

Phase 7: Built-in Hooks (audit_log, dangerous_confirmation, security_scan)
Phase 8: Plugin system (PluginManager, PluginSandbox, PluginManifest)
Phase 9: Skills registry (SkillRegistry, local/plugin/MCP loaders)
Phase 10: TeamLeader, RemoteTransport, BackgroundTaskManager
This commit is contained in:
2026-04-04 22:56:27 +08:00
parent e5bd492d74
commit a3fe4d24fc
35 changed files with 8501 additions and 0 deletions

View File

@@ -0,0 +1,628 @@
# Phase 8插件生态Plugin Ecosystem
日期2026-04-04
状态:待开始
前置依赖Phase 6工具系统重构、Phase 7Hook 拦截层)
Demo参考claw-code-main — plugins/, PluginLifecycle
---
## 1. 阶段目标
建立完整的**插件生命周期管理系统**,允许第三方开发者通过插件扩展 Jarvis 的功能。
**核心能力**
- 插件安装/卸载/启用/禁用
- 插件沙箱隔离执行
- 插件提供的工具/Hook/命令注册
- 内置插件集
---
## 2. 插件架构
### 2.1 插件结构
```
my-plugin/
├── manifest.json # 插件清单
├── SKILL.md # 插件技能文档(可选)
├── README.md # 插件说明
├── tools/ # 插件提供的工具
│ ├── __init__.py
│ └── my_tool.py
├── hooks/ # 插件提供的 Hook
│ ├── __init__.py
│ └── my_hook.py
├── commands/ # 插件提供的命令
│ ├── __init__.py
│ └── my_command.py
└── static/ # 静态资源
└── icon.png
```
### 2.2 插件清单 (manifest.json)
```json
{
"id": "my-plugin",
"name": "My Plugin",
"version": "1.0.0",
"description": "A sample plugin for Jarvis",
"author": "Developer Name",
"license": "MIT",
"homepage": "https://github.com/developer/my-plugin",
"compatibility": {
"jarvis_version": ">=2.0.0",
"api_version": "1.0"
},
"capabilities": {
"tools": [
{
"name": "my_tool",
"description": "Does something useful",
"parameters": {
"type": "object",
"properties": {
"input": {"type": "string"}
},
"required": ["input"]
}
}
],
"hooks": [
{
"name": "my_pre_hook",
"type": "pre_tool_use",
"description": "Custom pre-hook"
}
],
"commands": [
{
"name": "/my-command",
"description": "Custom slash command"
}
]
},
"permissions": {
"network": false,
"filesystem": "read",
"database": false
},
"dependencies": {}
}
```
---
## 3. 核心组件
### 3.1 PluginManifest
```python
# backend/app/agents/plugins/manifest.py
@dataclass
class ToolSpec:
"""工具规格"""
name: str
description: str
parameters: dict
return_schema: dict | None = None
@dataclass
class HookSpec:
"""Hook 规格"""
name: str
type: HookType
description: str
filter_names: list[str] | None = None
@dataclass
class CommandSpec:
"""命令规格"""
name: str
description: str
parameters: list | None = None
@dataclass
class PluginManifest:
"""插件清单"""
id: str
name: str
version: str
description: str
author: str
license: str
homepage: str | None = None
compatibility: dict = field(default_factory=dict)
capabilities: dict = field(default_factory=dict)
permissions: dict = field(default_factory=dict)
dependencies: dict = field(default_factory=dict)
@classmethod
def from_file(cls, path: Path) -> "PluginManifest":
"""从文件加载"""
with open(path / "manifest.json") as f:
data = json.load(f)
return cls(**data)
def validate(self) -> list[str]:
"""验证清单有效性"""
errors = []
if not self.id:
errors.append("Plugin ID is required")
if not self.version:
errors.append("Plugin version is required")
if "tools" in self.capabilities:
for tool in self.capabilities["tools"]:
if "name" not in tool:
errors.append(f"Tool missing name in plugin {self.id}")
return errors
```
### 3.2 PluginManager
```python
# backend/app/agents/plugins/manager.py
class PluginState(Enum):
"""插件状态"""
INSTALLED = "installed"
ENABLED = "enabled"
DISABLED = "disabled"
ERROR = "error"
@dataclass
class Plugin:
"""插件实例"""
manifest: PluginManifest
path: Path
state: PluginState
module: Any = None
error_message: str | None = None
tools: list[ToolManifest] = field(default_factory=list)
hooks: list[HookConfig] = field(default_factory=list)
commands: list[CommandSpec] = field(default_factory=list)
class PluginManager:
"""插件管理器"""
def __init__(
self,
plugin_dir: Path,
tool_registry: ToolRegistry,
hook_manager: HookManager
):
self.plugin_dir = plugin_dir
self.tool_registry = tool_registry
self.hook_manager = hook_manager
self._plugins: dict[str, Plugin] = {}
self._hooks = []
async def install(self, plugin_path: Path | str) -> Plugin:
"""
安装插件
流程:
1. 验证插件清单
2. 检查兼容性
3. 复制到插件目录
4. 加载插件模块
"""
plugin_path = Path(plugin_path)
# 1. 加载清单
manifest = PluginManifest.from_file(plugin_path)
# 2. 验证
errors = manifest.validate()
if errors:
raise PluginValidationError(errors)
# 3. 检查版本兼容性
await self._check_compatibility(manifest)
# 4. 创建插件目录
target_dir = self.plugin_dir / manifest.id
if target_dir.exists():
raise PluginAlreadyExistsError(manifest.id)
shutil.copytree(plugin_path, target_dir)
# 5. 创建插件实例
plugin = Plugin(
manifest=manifest,
path=target_dir,
state=PluginState.INSTALLED
)
# 6. 加载模块
await self._load_plugin(plugin)
self._plugins[manifest.id] = plugin
return plugin
async def uninstall(self, plugin_id: str) -> bool:
"""卸载插件"""
plugin = self._plugins.get(plugin_id)
if not plugin:
raise PluginNotFoundError(plugin_id)
# 禁用插件
if plugin.state == PluginState.ENABLED:
await self.disable(plugin_id)
# 移除工具和 Hook
for tool in plugin.tools:
self.tool_registry.unregister(tool.name)
for hook in plugin.hooks:
self.hook_manager.remove_hook(hook)
# 删除插件目录
shutil.rmtree(plugin.path)
del self._plugins[plugin_id]
return True
async def enable(self, plugin_id: str) -> Plugin:
"""启用插件"""
plugin = self._plugins.get(plugin_id)
if not plugin:
raise PluginNotFoundError(plugin_id)
# 注册工具
for tool in plugin.tools:
self.tool_registry.register(
tool,
self._create_tool_executor(plugin, tool.name)
)
# 注册 Hook
for hook in plugin.hooks:
self.hook_manager.add_hook(hook)
plugin.state = PluginState.ENABLED
return plugin
async def disable(self, plugin_id: str) -> Plugin:
"""禁用插件"""
plugin = self._plugins.get(plugin_id)
if not plugin:
raise PluginNotFoundError(plugin_id)
# 注销工具
for tool in plugin.tools:
self.tool_registry.unregister(tool.name)
# 注销 Hook
for hook in plugin.hooks:
self.hook_manager.remove_hook(hook)
plugin.state = PluginState.DISABLED
return plugin
async def reload(self, plugin_id: str) -> Plugin:
"""重新加载插件"""
plugin = self._plugins.get(plugin_id)
if not plugin:
raise PluginNotFoundError(plugin_id)
was_enabled = plugin.state == PluginState.ENABLED
if was_enabled:
await self.disable(plugin_id)
await self._load_plugin(plugin)
if was_enabled:
await self.enable(plugin_id)
return plugin
async def list_plugins(self) -> list[Plugin]:
"""列出所有插件"""
return list(self._plugins.values())
```
---
## 4. 插件隔离
### 4.1 PluginSandbox
```python
# backend/app/agents/plugins/sandbox.py
class PluginSandbox:
"""插件沙箱"""
def __init__(self, plugin: Plugin, permissions: dict):
self.plugin = plugin
self.permissions = permissions
self._env = {}
async def load_module(self) -> Any:
"""在沙箱中加载插件模块"""
# 设置受限的环境
self._env = {
"__name__": f"plugin.{self.plugin.manifest.id}",
"__file__": str(self.plugin.path),
}
if not self.permissions.get("filesystem", False):
# 只读文件系统
self._env["__builtins__"] = {
k: v for k, v in __builtins__.items()
if k in ["print", "len", "range", "str", "int", "float", "list", "dict", "tuple", "set", "bool", "type", "isinstance", "hasattr", "getattr", "setattr", "open"]
}
# 加载模块
spec = importlib.util.spec_from_file_location(
f"plugin_{self.plugin.manifest.id}",
self.plugin.path / "__init__.py"
)
module = importlib.util.module_from_spec(spec)
sys.modules[f"plugin_{self.plugin.manifest.id}"] = module
try:
spec.loader.exec_module(module)
except Exception as e:
raise PluginLoadError(str(e))
return module
def check_permission(self, permission: str) -> bool:
"""检查权限"""
return self.permissions.get(permission, False)
```
---
## 5. 插件市场
### 5.1 PluginMarketplace
```python
# backend/app/services/plugin_marketplace.py
@dataclass
class MarketplacePlugin:
"""市场中的插件"""
manifest: PluginManifest
download_count: int
rating: float
reviews_count: int
last_updated: datetime
class PluginMarketplace:
"""插件市场"""
def __init__(self, http_client: httpx.AsyncClient):
self.http_client = http_client
self.cache: dict[str, MarketplacePlugin] = {}
self.cache_ttl = timedelta(hours=1)
async def search(
self,
query: str,
category: str | None = None,
limit: int = 20
) -> list[MarketplacePlugin]:
"""搜索插件"""
# TODO: 连接到实际的市场 API
return []
async def get_plugin(
self,
plugin_id: str
) -> MarketplacePlugin | None:
"""获取插件详情"""
# 检查缓存
if plugin_id in self.cache:
return self.cache[plugin_id]
# TODO: 从市场 API 获取
return None
async def download_plugin(
self,
plugin_id: str,
target_dir: Path
) -> Path:
"""下载插件"""
# TODO: 实现下载逻辑
pass
```
---
## 6. 内置插件
### 6.1 内置插件列表
| 插件 | 描述 | 提供的工具 |
|------|------|-----------|
| `code-helper` | 代码助手 | `lint`, `format`, `explain_code` |
| `git-helper` | Git 助手 | `git_status`, `git_log`, `git_diff` |
| `web-helper` | Web 助手 | `fetch_url`, `parse_html` |
| `file-organizer` | 文件整理 | `organize_files`, `cleanup_duplicates` |
### 6.2 BuiltinPlugins
```python
# backend/app/agents/plugins/builtins.py
BUILTIN_PLUGINS = [
{
"id": "code-helper",
"name": "Code Helper",
"version": "1.0.0",
"description": "代码辅助工具集",
"capabilities": {
"tools": [
{
"name": "lint",
"description": "检查代码风格",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string"},
"language": {"type": "string"}
}
}
},
{
"name": "format",
"description": "格式化代码",
"parameters": {
"type": "object",
"properties": {
"code": {"type": "string"},
"language": {"type": "string"}
}
}
}
]
}
}
]
```
---
## 7. API 接口
```python
# backend/app/routers/plugins.py
@router.get("/api/plugins")
async def list_plugins(
current_user: User = Depends(get_current_user)
):
"""列出用户已安装的插件"""
plugins = await plugin_manager.list_plugins()
return {
"plugins": [
{
"id": p.manifest.id,
"name": p.manifest.name,
"version": p.manifest.version,
"state": p.state.value,
"description": p.manifest.description
}
for p in plugins
]
}
@router.post("/api/plugins/install")
async def install_plugin(
request: InstallPluginRequest,
current_user: User = Depends(get_current_user)
):
"""安装插件"""
plugin = await plugin_manager.install(Path(request.path))
return {"plugin_id": plugin.manifest.id}
@router.post("/api/plugins/{plugin_id}/enable")
async def enable_plugin(
plugin_id: str,
current_user: User = Depends(get_current_user)
):
"""启用插件"""
plugin = await plugin_manager.enable(plugin_id)
return {"state": plugin.state.value}
@router.post("/api/plugins/{plugin_id}/disable")
async def disable_plugin(
plugin_id: str,
current_user: User = Depends(get_current_user)
):
"""禁用插件"""
plugin = await plugin_manager.disable(plugin_id)
return {"state": plugin.state.value}
@router.delete("/api/plugins/{plugin_id}")
async def uninstall_plugin(
plugin_id: str,
current_user: User = Depends(get_current_user)
):
"""卸载插件"""
await plugin_manager.uninstall(plugin_id)
return {"status": "ok"}
@router.get("/api/marketplace/plugins")
async def search_marketplace(
q: str | None = None,
category: str | None = None,
limit: int = 20
):
"""搜索插件市场"""
plugins = await marketplace.search(q, category, limit)
return {"plugins": plugins}
```
---
## 8. 文件结构
```
backend/app/agents/plugins/
├── __init__.py
├── manifest.py # 插件清单
├── manager.py # 插件管理器
├── sandbox.py # 插件沙箱
├── builtins/ # 内置插件
│ ├── __init__.py
│ ├── code_helper/
│ ├── git_helper/
│ └── web_helper/
├── marketplace.py # 插件市场
└── router.py # API 路由(可选)
backend/app/routers/
└── plugins.py # 插件 API 路由
```
---
## 9. 验收标准
| 检查点 | 标准 |
|--------|------|
| 插件安装 | 可以从目录安装插件 |
| 插件启用 | 插件的工具和 Hook 正确注册 |
| 插件禁用 | 插件的工具和 Hook 正确注销 |
| 插件卸载 | 插件目录正确删除 |
| 插件隔离 | 插件无法访问未授权资源 |
| 市场搜索 | 可以搜索插件市场 |
| 内置插件 | 内置插件默认安装 |
---
## 10. Demo 借鉴
| claw-code | Jarvis 对应 |
|-----------|------------|
| `src/plugins/builtinPlugins.ts` | `plugins/builtins.py` |
| `src/plugins/bundled/index.ts` | `plugins/builtins/` |
| `PluginLifecycle` | `PluginManager` |
| `PluginInstallationManager` | `PluginManager.install` |
| `/plugin`, `/reload-plugins` | `/api/plugins/*` |