# 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)

```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/*` |
