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
16 KiB
16 KiB
Phase 8:插件生态(Plugin Ecosystem)
日期:2026-04-04 状态:待开始 前置依赖:Phase 6(工具系统重构)、Phase 7(Hook 拦截层) 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)
{
"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
# 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
# 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
# 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
# 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
# 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 接口
# 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/* |