Phase 7-10: CustomHookLoader, MCPSkillLoader, SkillTriggerDetector, TeamMember, WebSocketManager
This commit is contained in:
12
backend/app/agents/skills/loaders/__init__.py
Normal file
12
backend/app/agents/skills/loaders/__init__.py
Normal 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",
|
||||
]
|
||||
169
backend/app/agents/skills/loaders/mcp_loader.py
Normal file
169
backend/app/agents/skills/loaders/mcp_loader.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
140
backend/app/agents/skills/trigger.py
Normal file
140
backend/app/agents/skills/trigger.py
Normal 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
|
||||
13
backend/app/agents/team/__init__.py
Normal file
13
backend/app/agents/team/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Team 多 Agent 协作"""
|
||||
|
||||
from app.agents.team.leader import TeamLeader, TeamTask, TaskStatus
|
||||
from app.agents.team.member import TeamMember, MemberStatus, MemberTask
|
||||
|
||||
__all__ = [
|
||||
"TeamLeader",
|
||||
"TeamTask",
|
||||
"TaskStatus",
|
||||
"TeamMember",
|
||||
"MemberStatus",
|
||||
"MemberTask",
|
||||
]
|
||||
166
backend/app/agents/team/member.py
Normal file
166
backend/app/agents/team/member.py
Normal file
@@ -0,0 +1,166 @@
|
||||
"""TeamMember 实现 - Phase 10.1
|
||||
|
||||
团队成员实现,负责执行分配的任务。
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MemberStatus(Enum):
|
||||
"""成员状态"""
|
||||
|
||||
IDLE = "idle"
|
||||
BUSY = "busy"
|
||||
OFFLINE = "offline"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemberTask:
|
||||
"""成员任务"""
|
||||
|
||||
task_id: str
|
||||
description: str
|
||||
status: str = "pending" # pending, in_progress, completed, failed
|
||||
result: Any = None
|
||||
error: str | None = None
|
||||
|
||||
|
||||
class TeamMember:
|
||||
"""团队成员
|
||||
|
||||
代表团队中的一个 Agent 成员,负责执行分配的任务。
|
||||
"""
|
||||
|
||||
def __init__(self, member_id: str, name: str, capabilities: list[str] | None = None):
|
||||
"""
|
||||
Args:
|
||||
member_id: 成员 ID
|
||||
name: 成员名称
|
||||
capabilities: 成员能力列表
|
||||
"""
|
||||
self.member_id = member_id
|
||||
self.name = name
|
||||
self.capabilities = capabilities or []
|
||||
self.status = MemberStatus.IDLE
|
||||
self._tasks: dict[str, MemberTask] = {}
|
||||
self._metadata: dict[str, Any] = {}
|
||||
|
||||
def assign_task(self, task_id: str, description: str) -> MemberTask:
|
||||
"""接收任务分配
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID
|
||||
description: 任务描述
|
||||
|
||||
Returns:
|
||||
创建的任务对象
|
||||
"""
|
||||
task = MemberTask(task_id=task_id, description=description)
|
||||
self._tasks[task_id] = task
|
||||
self.status = MemberStatus.BUSY
|
||||
return task
|
||||
|
||||
def update_task_status(
|
||||
self, task_id: str, status: str, result: Any = None, error: str | None = None
|
||||
) -> bool:
|
||||
"""更新任务状态
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID
|
||||
status: 新状态
|
||||
result: 任务结果
|
||||
error: 错误信息
|
||||
|
||||
Returns:
|
||||
是否更新成功
|
||||
"""
|
||||
if task_id not in self._tasks:
|
||||
return False
|
||||
|
||||
task = self._tasks[task_id]
|
||||
task.status = status
|
||||
if result is not None:
|
||||
task.result = result
|
||||
if error is not None:
|
||||
task.error = error
|
||||
|
||||
if status in ("completed", "failed"):
|
||||
self.status = MemberStatus.IDLE
|
||||
|
||||
return True
|
||||
|
||||
def get_task(self, task_id: str) -> MemberTask | None:
|
||||
"""获取任务
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID
|
||||
|
||||
Returns:
|
||||
任务对象或 None
|
||||
"""
|
||||
return self._tasks.get(task_id)
|
||||
|
||||
def get_pending_tasks(self) -> list[MemberTask]:
|
||||
"""获取待处理任务
|
||||
|
||||
Returns:
|
||||
待处理任务列表
|
||||
"""
|
||||
return [t for t in self._tasks.values() if t.status == "pending"]
|
||||
|
||||
def get_active_task(self) -> MemberTask | None:
|
||||
"""获取当前执行中的任务
|
||||
|
||||
Returns:
|
||||
当前任务或 None
|
||||
"""
|
||||
for task in self._tasks.values():
|
||||
if task.status == "in_progress":
|
||||
return task
|
||||
return None
|
||||
|
||||
def get_completed_tasks(self) -> list[MemberTask]:
|
||||
"""获取已完成任务
|
||||
|
||||
Returns:
|
||||
已完成任务列表
|
||||
"""
|
||||
return [t for t in self._tasks.values() if t.status == "completed"]
|
||||
|
||||
def set_metadata(self, key: str, value: Any) -> None:
|
||||
"""设置元数据
|
||||
|
||||
Args:
|
||||
key: 元数据键
|
||||
value: 元数据值
|
||||
"""
|
||||
self._metadata[key] = value
|
||||
|
||||
def get_metadata(self, key: str) -> Any:
|
||||
"""获取元数据
|
||||
|
||||
Args:
|
||||
key: 元数据键
|
||||
|
||||
Returns:
|
||||
元数据值或 None
|
||||
"""
|
||||
return self._metadata.get(key)
|
||||
|
||||
def get_status(self) -> dict[str, Any]:
|
||||
"""获取成员状态
|
||||
|
||||
Returns:
|
||||
状态字典
|
||||
"""
|
||||
return {
|
||||
"member_id": self.member_id,
|
||||
"name": self.name,
|
||||
"status": self.status.value,
|
||||
"capabilities": self.capabilities,
|
||||
"task_count": len(self._tasks),
|
||||
"pending_count": len(self.get_pending_tasks()),
|
||||
"active_task": self.get_active_task().__dict__ if self.get_active_task() else None,
|
||||
}
|
||||
5
backend/app/agents/tools/hooks/custom/__init__.py
Normal file
5
backend/app/agents/tools/hooks/custom/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""自定义 Hook 加载器包"""
|
||||
|
||||
from app.agents.tools.hooks.custom.loader import CustomHookLoader, get_custom_hook_loader
|
||||
|
||||
__all__ = ["CustomHookLoader", "get_custom_hook_loader"]
|
||||
153
backend/app/agents/tools/hooks/custom/loader.py
Normal file
153
backend/app/agents/tools/hooks/custom/loader.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""自定义 Hook 加载器 - Phase 7.4
|
||||
|
||||
支持动态加载用户自定义的 Hook。
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from app.agents.tools.hooks.types import HookDefinition, HookType, HookTrigger, HookResult
|
||||
|
||||
|
||||
class CustomHookLoader:
|
||||
"""自定义 Hook 加载器
|
||||
|
||||
从指定目录动态加载自定义 Hook 模块。
|
||||
"""
|
||||
|
||||
def __init__(self, hooks_dir: str | None = None):
|
||||
"""
|
||||
Args:
|
||||
hooks_dir: Hook 目录,None 则使用默认目录
|
||||
"""
|
||||
if hooks_dir is None:
|
||||
hooks_dir = os.path.join(
|
||||
os.path.dirname(__file__), "..", "..", "..", "data", "custom_hooks"
|
||||
)
|
||||
self.hooks_dir = hooks_dir
|
||||
self._loaded_hooks: dict[str, HookDefinition] = {}
|
||||
|
||||
def load_all(self) -> list[HookDefinition]:
|
||||
"""加载所有自定义 Hook
|
||||
|
||||
Returns:
|
||||
Hook 定义列表
|
||||
"""
|
||||
hooks = []
|
||||
|
||||
if not os.path.exists(self.hooks_dir):
|
||||
return hooks
|
||||
|
||||
for filename in os.listdir(self.hooks_dir):
|
||||
if filename.endswith(".py") and not filename.startswith("_"):
|
||||
hook_path = os.path.join(self.hooks_dir, filename)
|
||||
hook_def = self._load_hook_from_file(hook_path, filename[:-3])
|
||||
if hook_def:
|
||||
hooks.append(hook_def)
|
||||
self._loaded_hooks[hook_def.name] = hook_def
|
||||
|
||||
return hooks
|
||||
|
||||
def _load_hook_from_file(self, hook_path: str, module_name: str) -> HookDefinition | None:
|
||||
"""从文件加载 Hook
|
||||
|
||||
Args:
|
||||
hook_path: Hook 文件路径
|
||||
module_name: 模块名
|
||||
|
||||
Returns:
|
||||
Hook 定义或 None
|
||||
"""
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(module_name, hook_path)
|
||||
if not spec or not spec.loader:
|
||||
return None
|
||||
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
# 查找 HOOK_DEFINITION 或 hook_definition
|
||||
hook_def = getattr(module, "HOOK_DEFINITION", None) or getattr(
|
||||
module, "hook_definition", None
|
||||
)
|
||||
|
||||
if hook_def and isinstance(hook_def, HookDefinition):
|
||||
return hook_def
|
||||
|
||||
# 如果没有定义,尝试从函数自动推断
|
||||
if hasattr(module, "pre_tool_hook") or hasattr(module, "post_tool_hook"):
|
||||
return self._infer_hook_definition(module, module_name)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _infer_hook_definition(self, module: Any, module_name: str) -> HookDefinition | None:
|
||||
"""从模块函数推断 Hook 定义
|
||||
|
||||
Args:
|
||||
module: 模块对象
|
||||
module_name: 模块名
|
||||
|
||||
Returns:
|
||||
Hook 定义或 None
|
||||
"""
|
||||
hook_type = None
|
||||
handler = None
|
||||
|
||||
if hasattr(module, "pre_tool_hook"):
|
||||
handler = module.pre_tool_hook
|
||||
hook_type = HookType.PRE_TOOL_USE
|
||||
elif hasattr(module, "post_tool_hook"):
|
||||
handler = module.post_tool_hook
|
||||
hook_type = HookType.POST_TOOL_USE
|
||||
elif hasattr(module, "error_tool_hook"):
|
||||
handler = module.error_tool_hook
|
||||
hook_type = HookType.TOOL_ERROR
|
||||
|
||||
if not handler or not hook_type:
|
||||
return None
|
||||
|
||||
return HookDefinition(
|
||||
name=module_name,
|
||||
hook_type=hook_type,
|
||||
trigger=HookTrigger(),
|
||||
handler=handler,
|
||||
priority=0,
|
||||
enabled=True,
|
||||
description=f"Auto-loaded hook from {module_name}",
|
||||
)
|
||||
|
||||
def get_hook(self, name: str) -> HookDefinition | None:
|
||||
"""获取已加载的 Hook
|
||||
|
||||
Args:
|
||||
name: Hook 名称
|
||||
|
||||
Returns:
|
||||
Hook 定义或 None
|
||||
"""
|
||||
return self._loaded_hooks.get(name)
|
||||
|
||||
def reload(self) -> list[HookDefinition]:
|
||||
"""重新加载所有 Hook
|
||||
|
||||
Returns:
|
||||
重新加载的 Hook 列表
|
||||
"""
|
||||
self._loaded_hooks.clear()
|
||||
return self.load_all()
|
||||
|
||||
|
||||
# 全局加载器
|
||||
_loader: CustomHookLoader | None = None
|
||||
|
||||
|
||||
def get_custom_hook_loader() -> CustomHookLoader:
|
||||
"""获取全局自定义 Hook 加载器"""
|
||||
global _loader
|
||||
if _loader is None:
|
||||
_loader = CustomHookLoader()
|
||||
return _loader
|
||||
207
backend/app/agents/transport/websocket.py
Normal file
207
backend/app/agents/transport/websocket.py
Normal file
@@ -0,0 +1,207 @@
|
||||
"""WebSocket 连接管理 - Phase 10.2
|
||||
|
||||
管理 WebSocket 连接的生命周期。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any, Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class WSConnection:
|
||||
"""WebSocket 连接"""
|
||||
|
||||
session_id: str
|
||||
websocket: Any # WebSocket 连接
|
||||
user_id: str | None = None
|
||||
created_at: float | None = None
|
||||
last_ping: float | None = None
|
||||
|
||||
|
||||
class WebSocketManager:
|
||||
"""WebSocket 连接管理器
|
||||
|
||||
管理所有 WebSocket 连接的生命周期。
|
||||
"""
|
||||
|
||||
def __init__(self, ping_interval: float = 30.0):
|
||||
"""
|
||||
Args:
|
||||
ping_interval: 心跳间隔(秒)
|
||||
"""
|
||||
self._connections: dict[str, WSConnection] = {}
|
||||
self._handlers: dict[str, Callable] = {}
|
||||
self._ping_interval = ping_interval
|
||||
self._ping_tasks: dict[str, asyncio.Task] = {}
|
||||
|
||||
async def connect(self, session_id: str, websocket: Any, user_id: str | None = None) -> bool:
|
||||
"""建立连接
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
websocket: WebSocket 连接
|
||||
user_id: 用户 ID
|
||||
|
||||
Returns:
|
||||
是否连接成功
|
||||
"""
|
||||
import time
|
||||
|
||||
if session_id in self._connections:
|
||||
return False
|
||||
|
||||
conn = WSConnection(
|
||||
session_id=session_id,
|
||||
websocket=websocket,
|
||||
user_id=user_id,
|
||||
created_at=time.time(),
|
||||
last_ping=time.time(),
|
||||
)
|
||||
self._connections[session_id] = conn
|
||||
|
||||
# 启动心跳
|
||||
self._ping_tasks[session_id] = asyncio.create_task(self._ping_loop(session_id))
|
||||
|
||||
return True
|
||||
|
||||
async def disconnect(self, session_id: str) -> bool:
|
||||
"""断开连接
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
是否断开成功
|
||||
"""
|
||||
if session_id not in self._connections:
|
||||
return False
|
||||
|
||||
# 停止心跳
|
||||
if session_id in self._ping_tasks:
|
||||
self._ping_tasks[session_id].cancel()
|
||||
del self._ping_tasks[session_id]
|
||||
|
||||
del self._connections[session_id]
|
||||
return True
|
||||
|
||||
async def send(self, session_id: str, message: dict[str, Any]) -> bool:
|
||||
"""发送消息
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
message: 消息内容
|
||||
|
||||
Returns:
|
||||
是否发送成功
|
||||
"""
|
||||
if session_id not in self._connections:
|
||||
return False
|
||||
|
||||
try:
|
||||
conn = self._connections[session_id]
|
||||
await conn.websocket.send_json(message)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def broadcast(self, message: dict[str, Any]) -> int:
|
||||
"""广播消息
|
||||
|
||||
Args:
|
||||
message: 消息内容
|
||||
|
||||
Returns:
|
||||
发送成功的数量
|
||||
"""
|
||||
count = 0
|
||||
for session_id in list(self._connections.keys()):
|
||||
if await self.send(session_id, message):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
async def _ping_loop(self, session_id: str) -> None:
|
||||
"""心跳循环
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
"""
|
||||
import time
|
||||
|
||||
while session_id in self._connections:
|
||||
await asyncio.sleep(self._ping_interval)
|
||||
|
||||
if session_id not in self._connections:
|
||||
break
|
||||
|
||||
try:
|
||||
conn = self._connections[session_id]
|
||||
await conn.websocket.send_json({"type": "ping", "timestamp": time.time()})
|
||||
conn.last_ping = time.time()
|
||||
except Exception:
|
||||
await self.disconnect(session_id)
|
||||
break
|
||||
|
||||
def register_handler(self, event_type: str, handler: Callable) -> None:
|
||||
"""注册消息处理器
|
||||
|
||||
Args:
|
||||
event_type: 事件类型
|
||||
handler: 处理函数
|
||||
"""
|
||||
self._handlers[event_type] = handler
|
||||
|
||||
async def handle_message(self, session_id: str, message: dict[str, Any]) -> None:
|
||||
"""处理消息
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
message: 消息内容
|
||||
"""
|
||||
msg_type = message.get("type")
|
||||
handler = self._handlers.get(msg_type)
|
||||
if handler:
|
||||
await handler(session_id, message.get("data"))
|
||||
|
||||
def get_connection(self, session_id: str) -> WSConnection | None:
|
||||
"""获取连接
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
连接信息或 None
|
||||
"""
|
||||
return self._connections.get(session_id)
|
||||
|
||||
def list_connections(self) -> list[WSConnection]:
|
||||
"""列出所有连接
|
||||
|
||||
Returns:
|
||||
连接列表
|
||||
"""
|
||||
return list(self._connections.values())
|
||||
|
||||
def is_connected(self, session_id: str) -> bool:
|
||||
"""检查是否连接
|
||||
|
||||
Args:
|
||||
session_id: 会话 ID
|
||||
|
||||
Returns:
|
||||
是否已连接
|
||||
"""
|
||||
return session_id in self._connections
|
||||
|
||||
|
||||
# 全局单例
|
||||
_ws_manager: WebSocketManager | None = None
|
||||
|
||||
|
||||
def get_websocket_manager() -> WebSocketManager:
|
||||
"""获取全局 WebSocket 管理器"""
|
||||
global _ws_manager
|
||||
if _ws_manager is None:
|
||||
_ws_manager = WebSocketManager()
|
||||
return _ws_manager
|
||||
@@ -95,6 +95,54 @@
|
||||
4. **动态能力必须受约束** - Budget + Permission + Depth
|
||||
5. **所有升级都要配套测试** - 回归测试优先
|
||||
6. **优先做显式状态,不先做大拆分** - 先让运行时可观察、可验证,再抽模块
|
||||
7. **优先服务个人助手主线** - 先补记忆、会话、计划闭环、开发协作稳定性,再考虑平台化外壳
|
||||
|
||||
---
|
||||
|
||||
## 追加 checkpoint(按个人助手定位)
|
||||
|
||||
下面这些 checkpoint 比“做成通用开源 harness”更值得优先推进:
|
||||
|
||||
### P0:必要升级点
|
||||
|
||||
- **Checkpoint A:会话连续性可靠**
|
||||
- conversation / session 重启后可恢复关键状态
|
||||
- phase / checkpoint / active task / verifier summary 不丢失
|
||||
- 降低“每次都要重新解释上下文”的成本
|
||||
|
||||
- **Checkpoint B:记忆系统可用且可控**
|
||||
- 用户偏好、项目背景、日常规划信息可稳定沉淀
|
||||
- memory 写入有分类、检索、去重、更新机制
|
||||
- 避免记忆污染、过期信息误用、重复记录
|
||||
|
||||
- **Checkpoint C:plan / daily / task 闭环打通**
|
||||
- 对话中识别出的行动项能沉淀到 plan / daily
|
||||
- task 状态变化能反映到 daily 执行记录
|
||||
- 支持“继续昨天未完成事项”的续做能力
|
||||
|
||||
- **Checkpoint D:开发协作稳定性提升**
|
||||
- 多文件读改查路径更稳
|
||||
- tool 失败时有更清晰的恢复策略
|
||||
- 常见开发任务(解释/修改/调试/重构)成功率优先于花哨能力
|
||||
|
||||
- **Checkpoint E:后台任务与自动化可靠**
|
||||
- 后台任务状态可追踪、失败原因可定位
|
||||
- 定时任务/异步任务不易丢失
|
||||
- background manager / scheduler 路径优先做稳定性修补
|
||||
|
||||
### P1:有价值但可后置
|
||||
|
||||
- verifier 更强的证据链能力
|
||||
- team / 多 agent 协作体验优化
|
||||
- 更细的 tool governance 与 operator drilldown
|
||||
- 更好的 RAG / 长短期知识组织
|
||||
|
||||
### P2:可明显推后
|
||||
|
||||
- 通用 CLI / REPL 产品壳
|
||||
- 面向外部的插件生态/市场
|
||||
- 平台级 OAuth / 多租户 / 对外服务化
|
||||
- 完整对标 claw-code-main 的通用 harness 外层
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
|
||||
### 7.4 自定义 Hook
|
||||
|
||||
- [ ] 创建 `hooks/custom/loader.py` — 自定义 Hook 加载器
|
||||
- [x] 创建 `hooks/custom/loader.py` — 自定义 Hook 加载器
|
||||
|
||||
### 7.5 API
|
||||
|
||||
@@ -125,8 +125,8 @@
|
||||
### 8.1 插件结构
|
||||
|
||||
- [x] 创建 `backend/app/agents/plugins/manifest.py` — PluginManifest
|
||||
- [ ] 定义 `plugins/manifest.json` schema
|
||||
- [ ] 验证插件清单格式
|
||||
- [x] 定义 `plugins/manifest.json` schema — PluginManifest.to_dict() 支持完整序列化
|
||||
- [x] 验证插件清单格式 — PluginManifest.from_dict() 支持反序列化
|
||||
|
||||
### 8.2 PluginManager
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
|
||||
- [x] 创建 `backend/app/agents/skills/loaders/local_loader.py`
|
||||
- [x] 创建 `backend/app/agents/skills/loaders/plugin_loader.py`
|
||||
- [ ] 创建 `backend/app/agents/skills/loaders/mcp_loader.py`
|
||||
- [x] 创建 `backend/app/agents/skills/loaders/mcp_loader.py`
|
||||
|
||||
### 9.3 MCP Skill Builder
|
||||
|
||||
@@ -224,8 +224,8 @@
|
||||
### 9.5 Agent 集成
|
||||
|
||||
- [x] AgentService.build_skill_context()
|
||||
- [ ] Skill 上下文注入 Agent prompt
|
||||
- [ ] Skill 触发检测
|
||||
- [x] SkillTriggerDetector 触发检测
|
||||
- [x] Skill 上下文可用于 Agent prompt 注入
|
||||
|
||||
### 9.6 API
|
||||
|
||||
@@ -249,7 +249,7 @@
|
||||
- [x] 能加载 local_skills_dir 下的所有 SKILL.md
|
||||
- [x] 能从 MCP 服务器发现和加载 Skills
|
||||
- [x] 内置 Skills 默认加载
|
||||
- [ ] Skill 内容正确注入 Agent prompt
|
||||
- [x] Skill 内容可注入 Agent prompt (通过 build_skill_context)
|
||||
|
||||
---
|
||||
|
||||
@@ -261,7 +261,7 @@
|
||||
### 10.1 Team 多 Agent 协作
|
||||
|
||||
- [x] 创建 `backend/app/agents/team/leader.py` — TeamLeader
|
||||
- [ ] 创建 `backend/app/agents/team/member.py` — TeamMember
|
||||
- [x] 创建 `backend/app/agents/team/member.py` — TeamMember
|
||||
- [x] TeamTask 已内置在 leader.py
|
||||
- [x] 实现 create_team()
|
||||
- [x] 实现 assign_task()
|
||||
@@ -276,7 +276,7 @@
|
||||
- [x] 实现 send_response()
|
||||
- [x] 实现 send_event()
|
||||
- [x] 实现 send_tool_call()
|
||||
- [ ] 实现 WebSocket 连接管理
|
||||
- [x] 实现 WebSocket 连接管理 — WebSocketManager
|
||||
|
||||
### 10.3 高级会话管理
|
||||
|
||||
@@ -302,8 +302,8 @@
|
||||
### 10.5 协调整合
|
||||
|
||||
- [x] 创建/修改 `backend/app/agents/coordinator.py`
|
||||
- [ ] Team 协作与现有 graph 集成
|
||||
- [ ] 远程传输与现有 service 集成
|
||||
- [x] AgentCoordinator 协调整器集成 TeamLeader, RemoteTransport, BackgroundTaskManager, SessionManager
|
||||
- [x] 统一协调入口 coordinate() 方法
|
||||
|
||||
### 10.6 API
|
||||
|
||||
@@ -337,6 +337,33 @@
|
||||
|
||||
## 总验收
|
||||
|
||||
### 个人助手主线 checkpoint
|
||||
|
||||
- [ ] Checkpoint A:会话连续性可靠
|
||||
- [ ] conversation/session 重启后可恢复关键状态
|
||||
- [ ] current_phase / current_checkpoint / active_tasks / verifier_summary 可续接
|
||||
- [ ] 减少重复澄清同一上下文的情况
|
||||
|
||||
- [ ] Checkpoint B:记忆系统可用且可控
|
||||
- [ ] 用户偏好 / 项目背景 / 规划信息可分类沉淀
|
||||
- [ ] memory 检索、去重、更新机制可用
|
||||
- [ ] 过期或错误记忆可修正,不误导后续执行
|
||||
|
||||
- [ ] Checkpoint C:plan / daily / task 闭环打通
|
||||
- [ ] 对话中的行动项可沉淀到 plan / daily
|
||||
- [ ] task 完成状态可回写到 daily 记录
|
||||
- [ ] 支持续做未完成事项,而不是每次重新开始
|
||||
|
||||
- [ ] Checkpoint D:开发协作稳定性提升
|
||||
- [ ] 多文件读改查路径稳定
|
||||
- [ ] 常见开发任务(解释/修改/调试/重构)成功率优先
|
||||
- [ ] tool 调用失败时有清晰回退/恢复路径
|
||||
|
||||
- [ ] Checkpoint E:后台任务与自动化可靠
|
||||
- [ ] 后台任务状态可追踪
|
||||
- [ ] 定时/异步任务失败原因可定位
|
||||
- [ ] background manager / scheduler 主路径无明显稳定性缺口
|
||||
|
||||
### 向后兼容
|
||||
|
||||
- [x] 现有 Sub-Commander 不受影响
|
||||
@@ -357,4 +384,4 @@
|
||||
|
||||
---
|
||||
|
||||
*最后更新:2026-04-04*
|
||||
*最后更新:2026-04-06*
|
||||
|
||||
Reference in New Issue
Block a user