509 lines
14 KiB
Markdown
509 lines
14 KiB
Markdown
|
|
# 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` |
|