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
14 KiB
14 KiB
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 结构
# 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 |