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
This commit is contained in:
16
backend/app/agents/skills/__init__.py
Normal file
16
backend/app/agents/skills/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Skills 注册表 - Phase 9"""
|
||||
|
||||
from app.agents.skills.registry import SkillRegistry, get_skill_registry
|
||||
from app.agents.skills.metadata import SkillMetadata
|
||||
from app.agents.skills.loaders.local_loader import LocalSkillLoader
|
||||
from app.agents.skills.loaders.plugin_loader import PluginSkillLoader
|
||||
from app.agents.skills.mcp_builder import MCPSkillBuilder
|
||||
|
||||
__all__ = [
|
||||
"SkillRegistry",
|
||||
"SkillMetadata",
|
||||
"LocalSkillLoader",
|
||||
"PluginSkillLoader",
|
||||
"MCPSkillBuilder",
|
||||
"get_skill_registry",
|
||||
]
|
||||
100
backend/app/agents/skills/loaders/local_loader.py
Normal file
100
backend/app/agents/skills/loaders/local_loader.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""本地 Skills 加载器 - Phase 9.2"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from app.agents.skills.metadata import SkillMetadata
|
||||
|
||||
|
||||
class LocalSkillLoader:
|
||||
"""本地 Skills 加载器
|
||||
|
||||
从 skills_dir 目录加载 SKILL.md 文件。
|
||||
"""
|
||||
|
||||
def __init__(self, skills_dir: str):
|
||||
self.skills_dir = skills_dir
|
||||
|
||||
def load_all(self) -> list[SkillMetadata]:
|
||||
"""加载所有本地 Skills
|
||||
|
||||
Returns:
|
||||
Skill 元数据列表
|
||||
"""
|
||||
skills = []
|
||||
|
||||
if not os.path.exists(self.skills_dir):
|
||||
return skills
|
||||
|
||||
for root, dirs, files in os.walk(self.skills_dir):
|
||||
# 跳过隐藏目录
|
||||
dirs[:] = [d for d in dirs if not d.startswith(".")]
|
||||
|
||||
if "SKILL.md" in files:
|
||||
skill = self._load_skill_from_dir(root)
|
||||
if skill:
|
||||
skills.append(skill)
|
||||
|
||||
return skills
|
||||
|
||||
def _load_skill_from_dir(self, skill_dir: str) -> SkillMetadata | None:
|
||||
"""从目录加载 Skill
|
||||
|
||||
Args:
|
||||
skill_dir: Skill 目录
|
||||
|
||||
Returns:
|
||||
Skill 元数据
|
||||
"""
|
||||
skill_path = os.path.join(skill_dir, "SKILL.md")
|
||||
|
||||
try:
|
||||
with open(skill_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
# 解析 frontmatter
|
||||
metadata = self._parse_frontmatter(content)
|
||||
|
||||
# 获取 Skill 名称(目录名)
|
||||
name = os.path.basename(skill_dir)
|
||||
|
||||
return SkillMetadata(
|
||||
name=metadata.get("name", name),
|
||||
description=metadata.get("description", ""),
|
||||
version=metadata.get("version", "1.0.0"),
|
||||
author=metadata.get("author", ""),
|
||||
tags=metadata.get("tags", []),
|
||||
triggers=metadata.get("triggers", []),
|
||||
content=content,
|
||||
source="local",
|
||||
source_id=skill_dir,
|
||||
)
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _parse_frontmatter(self, content: str) -> dict[str, Any]:
|
||||
"""解析 frontmatter"""
|
||||
metadata = {}
|
||||
|
||||
# 匹配 --- 包裹的 frontmatter
|
||||
match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
|
||||
if match:
|
||||
frontmatter = match.group(1)
|
||||
|
||||
for line in frontmatter.split("\n"):
|
||||
if ":" in line:
|
||||
key, value = line.split(":", 1)
|
||||
key = key.strip()
|
||||
value = value.strip()
|
||||
|
||||
# 处理列表
|
||||
if value.startswith("[") and value.endswith("]"):
|
||||
value = [v.strip().strip('"').strip("'") for v in value[1:-1].split(",")]
|
||||
elif value.lower() in ("true", "false"):
|
||||
value = value.lower() == "true"
|
||||
|
||||
metadata[key] = value
|
||||
|
||||
return metadata
|
||||
51
backend/app/agents/skills/loaders/plugin_loader.py
Normal file
51
backend/app/agents/skills/loaders/plugin_loader.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""插件 Skills 加载器 - Phase 9.2"""
|
||||
|
||||
from app.agents.skills.metadata import SkillMetadata
|
||||
from app.agents.plugins.manager import get_plugin_manager
|
||||
|
||||
|
||||
class PluginSkillLoader:
|
||||
"""插件 Skills 加载器
|
||||
|
||||
从已安装的插件中加载 Skills。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.plugin_manager = get_plugin_manager()
|
||||
|
||||
def load_all(self) -> list[SkillMetadata]:
|
||||
"""从所有已启用的插件加载 Skills
|
||||
|
||||
Returns:
|
||||
Skill 元数据列表
|
||||
"""
|
||||
skills = []
|
||||
|
||||
for plugin in self.plugin_manager.list_plugins():
|
||||
if not self.plugin_manager.is_enabled(plugin.id):
|
||||
continue
|
||||
|
||||
# 从插件加载 Skills
|
||||
plugin_skills = self._load_from_plugin(plugin)
|
||||
skills.extend(plugin_skills)
|
||||
|
||||
return skills
|
||||
|
||||
def _load_from_plugin(self, plugin: Any) -> list[SkillMetadata]:
|
||||
"""从单个插件加载 Skills"""
|
||||
skills = []
|
||||
|
||||
for skill_name in plugin.skills:
|
||||
skill = SkillMetadata(
|
||||
name=f"{plugin.id}/{skill_name}",
|
||||
description=f"Skill from plugin: {plugin.name}",
|
||||
version=plugin.version,
|
||||
author=plugin.author,
|
||||
tags=["plugin", plugin.id],
|
||||
content=f"# {skill_name}\n\nFrom plugin: {plugin.name}",
|
||||
source="plugin",
|
||||
source_id=plugin.id,
|
||||
)
|
||||
skills.append(skill)
|
||||
|
||||
return skills
|
||||
100
backend/app/agents/skills/mcp_builder.py
Normal file
100
backend/app/agents/skills/mcp_builder.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""MCP Skill Builder - Phase 9.3"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.agents.skills.metadata import SkillMetadata
|
||||
|
||||
|
||||
class MCPSkillBuilder:
|
||||
"""MCP Skill Builder
|
||||
|
||||
从 MCP 服务器发现和构建 Skills。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._skills: dict[str, SkillMetadata] = {}
|
||||
|
||||
def discover_skills_from_mcp(self, mcp_servers: list[dict[str, Any]]) -> list[SkillMetadata]:
|
||||
"""从 MCP 服务器发现 Skills
|
||||
|
||||
Args:
|
||||
mcp_servers: MCP 服务器配置列表
|
||||
|
||||
Returns:
|
||||
发现的 Skill 元数据列表
|
||||
"""
|
||||
skills = []
|
||||
|
||||
for server in mcp_servers:
|
||||
server_skills = self._discover_from_server(server)
|
||||
skills.extend(server_skills)
|
||||
|
||||
return skills
|
||||
|
||||
def _discover_from_server(self, server: dict[str, Any]) -> list[SkillMetadata]:
|
||||
"""从单个 MCP 服务器发现 Skills"""
|
||||
skills = []
|
||||
server_name = server.get("name", "unknown")
|
||||
tools = server.get("tools", [])
|
||||
|
||||
# 按工具分组
|
||||
tool_groups: dict[str, list[str]] = {}
|
||||
for tool in tools:
|
||||
group = tool.get("group", "default")
|
||||
if group not in tool_groups:
|
||||
tool_groups[group] = []
|
||||
tool_groups[group].append(tool)
|
||||
|
||||
# 为每个组创建一个 Skill
|
||||
for group_name, group_tools in tool_groups.items():
|
||||
skill = self._tool_to_skill(group_name, group_tools, server_name)
|
||||
skills.append(skill)
|
||||
|
||||
return skills
|
||||
|
||||
def _tool_to_skill(self, group: str, tools: list[dict[str, Any]], server: str) -> SkillMetadata:
|
||||
"""将 MCP 工具转换为 Skill"""
|
||||
tool_summaries = []
|
||||
for tool in tools:
|
||||
name = tool.get("name", "unknown")
|
||||
description = tool.get("description", "")
|
||||
input_schema = tool.get("inputSchema", {})
|
||||
|
||||
tool_summaries.append(f"### {name}\n{description}\n\nInput: {input_schema}")
|
||||
|
||||
content = f"""# MCP Skill: {group}
|
||||
|
||||
来自 MCP 服务器: {server}
|
||||
|
||||
## 工具列表
|
||||
|
||||
{chr(10).join(tool_summaries)}
|
||||
|
||||
## 使用说明
|
||||
|
||||
使用这些工具前请确保理解每个工具的输入输出格式。
|
||||
"""
|
||||
|
||||
return SkillMetadata(
|
||||
name=f"mcp-{server}-{group}",
|
||||
description=f"MCP skill from {server}: {group}",
|
||||
version="1.0.0",
|
||||
tags=["mcp", server, group],
|
||||
triggers=[group, server],
|
||||
content=content,
|
||||
source="mcp",
|
||||
source_id=f"{server}:{group}",
|
||||
)
|
||||
|
||||
def _group_to_skill(self, group: str, tools: list[str], server: str) -> SkillMetadata:
|
||||
"""将 MCP 工具组转换为 Skill"""
|
||||
return SkillMetadata(
|
||||
name=f"mcp-{server}-{group}",
|
||||
description=f"MCP skill from {server}: {group}",
|
||||
version="1.0.0",
|
||||
tags=["mcp", server, group],
|
||||
triggers=[group, server],
|
||||
content=f"# {group}\n\nTools: {', '.join(tools)}",
|
||||
source="mcp",
|
||||
source_id=f"{server}:{group}",
|
||||
)
|
||||
38
backend/app/agents/skills/metadata.py
Normal file
38
backend/app/agents/skills/metadata.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""Skill 元数据定义 - Phase 9.1"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class SkillMetadata:
|
||||
"""Skill 元数据"""
|
||||
|
||||
name: str # Skill 名称
|
||||
description: str # 描述
|
||||
version: str = "1.0.0" # 版本
|
||||
author: str = "" # 作者
|
||||
tags: list[str] = field(default_factory=list) # 标签
|
||||
triggers: list[str] = field(default_factory=list) # 触发关键词
|
||||
content: str = "" # Skill 内容(markdown)
|
||||
source: str = "local" # 来源:local, plugin, mcp, bundled
|
||||
source_id: str = "" # 来源 ID
|
||||
enabled: bool = True # 是否启用
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"version": self.version,
|
||||
"author": self.author,
|
||||
"tags": self.tags,
|
||||
"triggers": self.triggers,
|
||||
"content": self.content,
|
||||
"source": self.source,
|
||||
"source_id": self.source_id,
|
||||
"enabled": self.enabled,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> "SkillMetadata":
|
||||
return cls(**data)
|
||||
133
backend/app/agents/skills/registry.py
Normal file
133
backend/app/agents/skills/registry.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Skills 注册表 - Phase 9.1"""
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from app.agents.skills.metadata import SkillMetadata
|
||||
from app.agents.skills.loaders.local_loader import LocalSkillLoader
|
||||
|
||||
|
||||
class SkillRegistry:
|
||||
"""Skills 注册表
|
||||
|
||||
管理所有 Skills 的注册、发现和加载。
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._skills: dict[str, SkillMetadata] = {}
|
||||
self._loaders: list[Any] = []
|
||||
|
||||
def load_all(self, skills_dir: str | None = None) -> int:
|
||||
"""加载所有 Skills
|
||||
|
||||
Args:
|
||||
skills_dir: Skills 目录,None 则使用默认目录
|
||||
|
||||
Returns:
|
||||
加载的 Skill 数量
|
||||
"""
|
||||
if skills_dir is None:
|
||||
skills_dir = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "..", ".claude", "skills"
|
||||
)
|
||||
|
||||
count = 0
|
||||
|
||||
# 本地加载器
|
||||
local_loader = LocalSkillLoader(skills_dir)
|
||||
local_skills = local_loader.load_all()
|
||||
for skill in local_skills:
|
||||
self.register(skill)
|
||||
count += 1
|
||||
|
||||
# 插件加载器
|
||||
for loader in self._loaders:
|
||||
try:
|
||||
external_skills = loader.load_all()
|
||||
for skill in external_skills:
|
||||
self.register(skill)
|
||||
count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return count
|
||||
|
||||
def register(self, skill: SkillMetadata) -> None:
|
||||
"""注册 Skill"""
|
||||
self._skills[skill.name] = skill
|
||||
|
||||
def unregister(self, name: str) -> bool:
|
||||
"""注销 Skill"""
|
||||
if name in self._skills:
|
||||
del self._skills[name]
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_skill(self, name: str) -> SkillMetadata | None:
|
||||
"""获取 Skill"""
|
||||
return self._skills.get(name)
|
||||
|
||||
def search(self, query: str) -> list[SkillMetadata]:
|
||||
"""搜索 Skills
|
||||
|
||||
Args:
|
||||
query: 搜索关键词
|
||||
|
||||
Returns:
|
||||
匹配的 Skills 列表
|
||||
"""
|
||||
query_lower = query.lower()
|
||||
results = []
|
||||
|
||||
for skill in self._skills.values():
|
||||
if not skill.enabled:
|
||||
continue
|
||||
|
||||
# 匹配名称、描述、标签
|
||||
if (
|
||||
query_lower in skill.name.lower()
|
||||
or query_lower in skill.description.lower()
|
||||
or any(query_lower in tag.lower() for tag in skill.tags)
|
||||
or any(query_lower in trigger.lower() for trigger in skill.triggers)
|
||||
):
|
||||
results.append(skill)
|
||||
|
||||
return results
|
||||
|
||||
def get_skill_context(self, names: list[str]) -> str:
|
||||
"""获取 Skill 上下文
|
||||
|
||||
Args:
|
||||
names: Skill 名称列表
|
||||
|
||||
Returns:
|
||||
拼接的 Skill 内容
|
||||
"""
|
||||
contexts = []
|
||||
|
||||
for name in names:
|
||||
skill = self._skills.get(name)
|
||||
if skill and skill.enabled:
|
||||
contexts.append(f"# {skill.name}\n\n{skill.content}")
|
||||
|
||||
return "\n\n---\n\n".join(contexts)
|
||||
|
||||
def add_loader(self, loader: Any) -> None:
|
||||
"""添加加载器"""
|
||||
self._loaders.append(loader)
|
||||
|
||||
def list_all(self) -> list[SkillMetadata]:
|
||||
"""列出所有 Skills"""
|
||||
return list(self._skills.values())
|
||||
|
||||
|
||||
# 全局单例
|
||||
_registry: SkillRegistry | None = None
|
||||
|
||||
|
||||
def get_skill_registry() -> SkillRegistry:
|
||||
"""获取全局 Skills 注册表"""
|
||||
global _registry
|
||||
if _registry is None:
|
||||
_registry = SkillRegistry()
|
||||
return _registry
|
||||
Reference in New Issue
Block a user