Files
JARVIS/development-doc/plan/agent-update/phase-9-skills-registry.md
WIN-JHFT4D3SIVT\caoxiaozhu a3fe4d24fc 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
2026-04-04 22:56:27 +08:00

14 KiB
Raw Blame History

Phase 9Skills 注册表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 结构

# 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

# 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

# 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

# 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 接口

# 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