Phase 7-10: CustomHookLoader, MCPSkillLoader, SkillTriggerDetector, TeamMember, WebSocketManager

This commit is contained in:
2026-04-05 10:56:21 +08:00
parent d18167826e
commit fca7a7cf3d
11 changed files with 958 additions and 14 deletions

View File

@@ -0,0 +1,12 @@
"""Skills 加载器包"""
from app.agents.skills.loaders.local_loader import LocalSkillLoader
from app.agents.skills.loaders.plugin_loader import PluginSkillLoader
from app.agents.skills.loaders.mcp_loader import MCPSkillLoader, get_mcp_skill_loader
__all__ = [
"LocalSkillLoader",
"PluginSkillLoader",
"MCPSkillLoader",
"get_mcp_skill_loader",
]

View File

@@ -0,0 +1,169 @@
"""MCP Skill 加载器 - Phase 9.2
从 MCP (Model Context Protocol) 服务器发现和加载 Skills。
"""
import os
from typing import Any
from app.agents.skills.metadata import SkillMetadata
class MCPSkillLoader:
"""MCP Skill 加载器
从 MCP 服务器发现可用的 Skills。
"""
def __init__(self, mcp_servers: list[dict[str, Any]] | None = None):
"""
Args:
mcp_servers: MCP 服务器列表,每项包含 name, command, env 等
"""
self.mcp_servers = mcp_servers or []
self._discovered_skills: dict[str, SkillMetadata] = {}
def discover_skills(self) -> list[SkillMetadata]:
"""从所有配置的 MCP 服务器发现 Skills
Returns:
发现的 Skill 列表
"""
skills = []
for server in self.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
Args:
server: 服务器配置
Returns:
Skill 列表
"""
skills = []
server_name = server.get("name", "unknown")
# 模拟从 MCP 服务器获取工具列表
# 实际实现时,这里会调用 MCP 服务器的 list_tools 接口
try:
tools = self._call_mcp_list_tools(server)
for tool in tools:
skill = self._tool_to_skill(tool, server_name)
if skill:
skills.append(skill)
self._discovered_skills[skill.name] = skill
except Exception:
pass
return skills
def _call_mcp_list_tools(self, server: dict[str, Any]) -> list[dict[str, Any]]:
"""调用 MCP 服务器的 list_tools 接口
Args:
server: 服务器配置
Returns:
工具列表
"""
# TODO: 实现实际的 MCP 协议调用
# 目前返回空列表,实际使用时需要实现 MCP 客户端
return []
def _tool_to_skill(self, tool: dict[str, Any], server: str) -> SkillMetadata | None:
"""将 MCP 工具转换为 Skill
Args:
tool: MCP 工具定义
server: 服务器名
Returns:
Skill 元数据或 None
"""
tool_name = tool.get("name")
if not tool_name:
return None
return SkillMetadata(
id=f"mcp_{server}_{tool_name}",
name=f"{server}:{tool_name}",
description=tool.get("description", f"MCP tool: {tool_name}"),
version="1.0.0",
content=self._generate_skill_content(tool),
triggers=[f"@{server}", f"/{tool_name}"],
tools=[tool_name],
tags=["mcp", server],
enabled=True,
)
def _generate_skill_content(self, tool: dict[str, Any]) -> str:
"""生成 Skill 内容
Args:
tool: MCP 工具定义
Returns:
Skill 内容字符串
"""
name = tool.get("name", "unknown")
description = tool.get("description", "No description")
input_schema = tool.get("inputSchema", {})
content = f"""# MCP Tool: {name}
**Description**: {description}
**Server**: {tool.get("server", "unknown")}
**Input Schema**:
```json
{input_schema}
```
**Usage**:
Use the `/{name}` command or `@{tool.get("server", "server")}` to invoke this tool.
**Examples**:
```
/{name} arg1=value1 arg2=value2
@{tool.get("server", "server")} {name} --arg1 value1
```
"""
return content
def get_skill(self, name: str) -> SkillMetadata | None:
"""获取已发现的 Skill
Args:
name: Skill 名称
Returns:
Skill 元数据或 None
"""
return self._discovered_skills.get(name)
def list_skills(self) -> list[SkillMetadata]:
"""列出所有已发现的 Skills
Returns:
Skill 列表
"""
return list(self._discovered_skills.values())
# 全局加载器
_loader: MCPSkillLoader | None = None
def get_mcp_skill_loader() -> MCPSkillLoader:
"""获取全局 MCP Skill 加载器"""
global _loader
if _loader is None:
_loader = MCPSkillLoader()
return _loader

View File

@@ -8,8 +8,9 @@ from typing import Any
class SkillMetadata:
"""Skill 元数据"""
name: str # Skill 名称
description: str # 描述
id: str = "" # Skill ID
name: str = "" # Skill 名称
description: str = "" # 描述
version: str = "1.0.0" # 版本
author: str = "" # 作者
tags: list[str] = field(default_factory=list) # 标签
@@ -18,9 +19,11 @@ class SkillMetadata:
source: str = "local" # 来源local, plugin, mcp, bundled
source_id: str = "" # 来源 ID
enabled: bool = True # 是否启用
tools: list[str] = field(default_factory=list) # 关联的工具
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"description": self.description,
"version": self.version,
@@ -31,6 +34,7 @@ class SkillMetadata:
"source": self.source,
"source_id": self.source_id,
"enabled": self.enabled,
"tools": self.tools,
}
@classmethod

View File

@@ -0,0 +1,140 @@
"""Skill 触发检测器 - Phase 9.5
检测消息中的 Skill 触发条件。
"""
import re
from typing import Any
from app.agents.skills.metadata import SkillMetadata
class SkillTriggerDetector:
"""Skill 触发检测器
检测用户消息中是否触发了某个 Skill。
"""
def __init__(self):
self._skills: dict[str, SkillMetadata] = {}
def register_skill(self, skill: SkillMetadata) -> None:
"""注册 Skill
Args:
skill: Skill 元数据
"""
self._skills[skill.name] = skill
def unregister_skill(self, name: str) -> bool:
"""注销 Skill
Args:
name: Skill 名称
Returns:
是否成功
"""
if name in self._skills:
del self._skills[name]
return True
return False
def detect_triggered_skills(self, message: str) -> list[str]:
"""检测触发的 Skills
Args:
message: 用户消息
Returns:
触发的 Skill 名称列表
"""
triggered = []
message_lower = message.lower()
for skill in self._skills.values():
if not skill.enabled:
continue
if self._matches_triggers(message, message_lower, skill):
triggered.append(skill.name)
return triggered
def _matches_triggers(self, message: str, message_lower: str, skill: SkillMetadata) -> bool:
"""检查消息是否匹配 Skill 触发条件
Args:
message: 原始消息
message_lower: 小写消息
skill: Skill 元数据
Returns:
是否匹配
"""
for trigger in skill.triggers:
trigger_lower = trigger.lower()
# 前缀匹配,如 "/code" 或 "@git"
if trigger_lower.startswith("/") or trigger_lower.startswith("@"):
if message_lower.startswith(trigger_lower):
return True
# 命令格式,如 "//analyze"
if trigger_lower.startswith("//"):
pattern = trigger_lower[2:]
if re.search(rf"\b{re.escape(pattern)}\b", message_lower):
return True
# 关键词匹配
if trigger_lower in message_lower:
return True
return False
def get_skill_prompt(self, skill_name: str) -> str | None:
"""获取 Skill 的提示词
Args:
skill_name: Skill 名称
Returns:
Skill 内容或 None
"""
skill = self._skills.get(skill_name)
if skill:
return skill.content
return None
def get_triggered_skill_context(self, message: str) -> str:
"""获取触发的 Skills 上下文
Args:
message: 用户消息
Returns:
拼接的 Skill 上下文
"""
triggered = self.detect_triggered_skills(message)
if not triggered:
return ""
contexts = []
for skill_name in triggered:
skill = self._skills.get(skill_name)
if skill:
contexts.append(f"# {skill.name}\n\n{skill.content}")
return "\n\n---\n\n".join(contexts)
# 全局检测器
_detector: SkillTriggerDetector | None = None
def get_skill_trigger_detector() -> SkillTriggerDetector:
"""获取全局 Skill 触发检测器"""
global _detector
if _detector is None:
_detector = SkillTriggerDetector()
return _detector