# Phase 9：Skills 注册表（Skills Registry）

日期：2026-04-04
状态：待开始
前置依赖：Phase 6（工具系统重构）
Demo参考：claw-code-main — skills/loadSkillsDir.ts, bundledSkills.ts, mcpSkillBuilders.ts

---

## 1. 阶段目标

建立完整的 **Skills 动态加载和注册系统**，支持：
- 本地文件 Skills（现有）
- 插件提供的 Skills
- MCP 动态发现的 Skills
- Skills 注册表和搜索
- Bundled Skills 集

**现有状态**：Jarvis 已有基础的 `skill_registry.py`，需要增强为完整系统。

---

## 2. Skills 架构

### 2.1 Skill 结构

```markdown
# SKILL.md

## Skill Metadata
name: my-skill
description: Does something useful
version: 1.0.0
author: Developer

## Triggers
- keywords: ["skill", "my"]
- patterns: ["/my-skill"]

## Capabilities
- tools: ["my_tool"]
- hooks: ["my_hook"]

## Configuration
```json
{
  "option1": "value1"
}
```
```

### 2.2 SkillRegistry 增强

```python
# backend/app/agents/skills/registry.py

@dataclass
class SkillMetadata:
    """Skill 元数据"""
    name: str
    description: str
    version: str
    author: str | None = None
    
    triggers: list[str] = field(default_factory=list)
    patterns: list[str] = field(default_factory=list)
    
    tools: list[str] = field(default_factory=list)
    hooks: list[str] = field(default_factory=list)
    commands: list[str] = field(default_factory=list)
    
    config_schema: dict | None = None
    source: SkillSource = SkillSource.LOCAL
    
    file_path: Path | None = None

class SkillSource(Enum):
    """Skill 来源"""
    LOCAL = "local"           # 本地文件
    PLUGIN = "plugin"        # 插件提供
    MCP = "mcp"              # MCP 动态发现
    BUNDLED = "bundled"      # 内置

class SkillRegistry:
    """Skills 注册表"""
    
    def __init__(
        self,
        local_skills_dir: Path,
        tool_registry: ToolRegistry,
        hook_manager: HookManager
    ):
        self.local_skills_dir = local_skills_dir
        self.tool_registry = tool_registry
        self.hook_manager = hook_manager
        
        self._skills: dict[str, SkillMetadata] = {}
        self._index: dict[str, list[str]] = defaultdict(list)  # keyword -> skill names
    
    async def load_all(self):
        """加载所有 Skills"""
        await self._load_local_skills()
        await self._load_bundled_skills()
    
    async def _load_local_skills(self):
        """加载本地 Skills"""
        if not self.local_skills_dir.exists():
            return
        
        for skill_file in self.local_skills_dir.rglob("SKILL.md"):
            try:
                metadata = await self._parse_skill_file(skill_file)
                metadata.source = SkillSource.LOCAL
                self._register_skill(metadata)
            except Exception as e:
                logger.warning(f"Failed to load skill from {skill_file}: {e}")
    
    async def _load_bundled_skills(self):
        """加载内置 Skills"""
        for skill_def in BUNDLED_SKILLS:
            metadata = SkillMetadata(
                name=skill_def["name"],
                description=skill_def["description"],
                version=skill_def["version"],
                author="Jarvis Team",
                triggers=skill_def.get("triggers", []),
                tools=skill_def.get("tools", []),
                source=SkillSource.BUNDLED
            )
            self._register_skill(metadata)
    
    async def _parse_skill_file(self, path: Path) -> SkillMetadata:
        """解析 SKILL.md 文件"""
        content = path.read_text(encoding="utf-8")
        
        # 简单的 frontmatter 解析
        metadata = SkillMetadata(
            name=path.parent.name,
            description="",
            version="1.0.0"
        )
        
        # 解析 triggers
        for line in content.split("\n"):
            if line.startswith("- keywords:"):
                keywords = line.split("[")[1].split("]")[0]
                metadata.triggers = [k.strip() for k in keywords.split(",")]
        
        return metadata
    
    def _register_skill(self, metadata: SkillMetadata):
        """注册 Skill"""
        self._skills[metadata.name] = metadata
        
        # 更新索引
        for keyword in metadata.triggers:
            self._index[keyword].append(metadata.name)
        
        # 注册关联的工具
        for tool_name in metadata.tools:
            self.tool_registry.register_tool_for_skill(metadata.name, tool_name)
    
    async def get_skill(self, name: str) -> SkillMetadata | None:
        """获取 Skill"""
        return self._skills.get(name)
    
    async def search(
        self,
        query: str,
        limit: int = 10
    ) -> list[SkillMetadata]:
        """搜索 Skills"""
        results = []
        
        query_lower = query.lower()
        
        # 精确匹配 name
        if query_lower in self._skills:
            results.append(self._skills[query_lower])
        
        # 关键词匹配
        for skill_name in self._index.get(query_lower, []):
            if skill_name not in results:
                results.append(self._skills[skill_name])
        
        # 描述匹配
        for skill in self._skills.values():
            if skill in results:
                continue
            if query_lower in skill.description.lower():
                results.append(skill)
        
        return results[:limit]
    
    async def get_skill_context(
        self,
        skill_name: str,
        context: dict
    ) -> str:
        """获取 Skill 上下文"""
        skill = self._skills.get(skill_name)
        if not skill:
            return ""
        
        # 读取 SKILL.md 内容
        if skill.file_path and skill.file_path.exists():
            content = skill.file_path.read_text(encoding="utf-8")
            # 移除 metadata 部分，只保留 instructions
            return content
        
        return ""
```

---

## 3. MCP Skill Builder

### 3.1 MCPSkillBuilder

```python
# backend/app/agents/skills/mcp_builder.py

class MCPSkillBuilder:
    """
    MCP Skill Builder
    
    从 MCP 服务器动态构建 Skills
    """
    
    def __init__(
        self,
        mcp_client: MCPClient,
        skill_registry: SkillRegistry
    ):
        self.mcp_client = mcp_client
        self.skill_registry = skill_registry
    
    async def discover_skills_from_mcp(
        self,
        mcp_server_id: str
    ) -> list[SkillMetadata]:
        """从 MCP 服务器发现 Skills"""
        # 获取 MCP 服务器提供的工具
        tools = await self.mcp_client.list_tools(mcp_server_id)
        
        skills = []
        
        # 按工具前缀分组
        tool_groups: dict[str, list] = defaultdict(list)
        for tool in tools:
            parts = tool.name.split("_")
            if len(parts) > 1:
                prefix = parts[0]
                tool_groups[prefix].append(tool)
            else:
                # 单工具，作为独立 skill
                skill = self._tool_to_skill(tool, mcp_server_id)
                skills.append(skill)
        
        # 创建分组 skill
        for prefix, group_tools in tool_groups.items():
            skill = self._group_to_skill(prefix, group_tools, mcp_server_id)
            skills.append(skill)
        
        return skills
    
    def _tool_to_skill(
        self,
        tool: MCPProtocolTool,
        server_id: str
    ) -> SkillMetadata:
        """将单个 MCP 工具转换为 Skill"""
        return SkillMetadata(
            name=f"{server_id}_{tool.name}",
            description=tool.description or f"MCP tool: {tool.name}",
            version="1.0.0",
            tools=[tool.name],
            source=SkillSource.MCP,
            config_schema={
                "server_id": server_id,
                "tool_name": tool.name
            }
        )
    
    def _group_to_skill(
        self,
        prefix: str,
        tools: list[MCPProtocolTool],
        server_id: str
    ) -> SkillMetadata:
        """将一组 MCP 工具转换为 Skill"""
        return SkillMetadata(
            name=f"{server_id}_{prefix}",
            description=f"MCP {prefix} tools: {', '.join(t.name for t in tools)}",
            version="1.0.0",
            tools=[t.name for t in tools],
            source=SkillSource.MCP,
            config_schema={
                "server_id": server_id,
                "prefix": prefix
            }
        )
```

---

## 4. 内置 Skills

```python
# backend/app/agents/skills/bundled.py

BUNDLED_SKILLS = [
    {
        "name": "code-analysis",
        "description": "代码分析技能 - 分析代码结构、复杂度、依赖关系",
        "version": "1.0.0",
        "triggers": ["分析代码", "代码分析", "code analysis"],
        "tools": ["grep", "glob", "lint"]
    },
    {
        "name": "git-helper",
        "description": "Git 操作助手 - 管理 Git 仓库和操作",
        "version": "1.0.0",
        "triggers": ["git", "版本控制", "commit"],
        "tools": ["git_status", "git_log", "git_diff"]
    },
    {
        "name": "web-research",
        "description": "网络研究技能 - 搜索和抓取网页内容",
        "version": "1.0.0",
        "triggers": ["搜索", "研究", "research", "web search"],
        "tools": ["web_search", "web_fetch"]
    },
    {
        "name": "file-management",
        "description": "文件管理技能 - 组织和管理文件",
        "version": "1.0.0",
        "triggers": ["文件", "整理", "file management"],
        "tools": ["glob", "file_read", "file_write", "organize"]
    },
    {
        "name": "task-planning",
        "description": "任务规划技能 - 拆解和规划复杂任务",
        "version": "1.0.0",
        "triggers": ["规划", "任务拆解", "task planning"],
        "tools": ["create_task", "get_tasks", "update_task"]
    }
]
```

---

## 5. Skill 与 Agent 集成

### 5.1 修改 AgentService

```python
# backend/app/services/agent_service.py (修改部分)

async def build_skill_context(
    self,
    skill_names: list[str],
    context: dict
) -> str:
    """构建 Skill 上下文"""
    parts = []
    
    for skill_name in skill_names:
        skill_context = await self.skill_registry.get_skill_context(
            skill_name, context
        )
        if skill_context:
            parts.append(f"\n\n=== Skill: {skill_name} ===\n{skill_context}")
    
    return "\n".join(parts)

async def chat(
    self,
    message: str,
    context: dict
) -> AgentResponse:
    """处理聊天消息（集成 Skills）"""
    # 1. 检测触发的 Skills
    triggered_skills = await self.skill_registry.search(message)
    
    # 2. 构建 Skill 上下文
    skill_context = await self.build_skill_context(
        [s.name for s in triggered_skills],
        context
    )
    
    # 3. 注入到 system prompt
    if skill_context:
        context["skill_context"] = skill_context
    
    # 4. 继续正常处理
    return await self._process_message(message, context)
```

---

## 6. API 接口

```python
# backend/app/routers/skills.py

@router.get("/api/skills")
async def list_skills(
    source: SkillSource | None = None,
    current_user: User = Depends(get_current_user)
):
    """列出所有 Skills"""
    skills = await skill_registry.list_all()
    
    if source:
        skills = [s for s in skills if s.source == source]
    
    return {
        "skills": [
            {
                "name": s.name,
                "description": s.description,
                "version": s.version,
                "source": s.source.value,
                "triggers": s.triggers
            }
            for s in skills
        ]
    }

@router.get("/api/skills/search")
async def search_skills(
    q: str,
    limit: int = 10
):
    """搜索 Skills"""
    results = await skill_registry.search(q, limit)
    return {"skills": results}

@router.get("/api/skills/{skill_name}")
async def get_skill(
    skill_name: str,
    current_user: User = Depends(get_current_user)
):
    """获取 Skill 详情"""
    skill = await skill_registry.get_skill(skill_name)
    if not skill:
        raise HTTPException(404, "Skill not found")
    
    return {
        "name": skill.name,
        "description": skill.description,
        "version": skill.version,
        "source": skill.source.value,
        "triggers": skill.triggers,
        "tools": skill.tools,
        "hooks": skill.hooks
    }
```

---

## 7. 文件结构

```
backend/app/agents/skills/
├── __init__.py
├── registry.py           # Skills 注册表（增强）
├── metadata.py           # Skill 元数据
├── mcp_builder.py       # MCP Skill Builder
├── bundled.py           # 内置 Skills
│
├── loaders/             # 加载器
│   ├── __init__.py
│   ├── local_loader.py  # 本地文件加载
│   ├── plugin_loader.py # 插件 Skill 加载
│   └── mcp_loader.py    # MCP Skill 加载
│
└── context.py           # Skill 上下文构建

backend/app/routers/
└── skills.py            # Skills API 路由
```

---

## 8. 验收标准

| 检查点 | 标准 |
|--------|------|
| 本地 Skills | 能加载 local_skills_dir 下的所有 SKILL.md |
| MCP Skills | 能从 MCP 服务器发现和加载 Skills |
| Bundled Skills | 内置 Skills 默认加载 |
| Skill 搜索 | 能按关键词搜索 Skills |
| Skill 上下文 | Skill 内容正确注入 Agent prompt |
| API 可用 | Skills API 可用 |

---

## 9. Demo 借鉴

| claw-code | Jarvis 对应 |
|-----------|------------|
| `src/skills/loadSkillsDir.ts` | `skills/loaders/local_loader.py` |
| `src/skills/bundledSkills.ts` | `skills/bundled.py` |
| `src/skills/mcpSkillBuilders.ts` | `skills/mcp_builder.py` |
| `/skills` command | `/api/skills` |
| Skill registry pipeline | `SkillRegistry` |
