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:
2026-04-04 22:56:27 +08:00
parent e5bd492d74
commit a3fe4d24fc
35 changed files with 8501 additions and 0 deletions

View 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",
]

View 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

View 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

View 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}",
)

View 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)

View 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