Compare commits
11 Commits
de141cb1b7
...
e1704f87ea
| Author | SHA1 | Date | |
|---|---|---|---|
| e1704f87ea | |||
| 1b69bc2719 | |||
| 45ba4f5a49 | |||
| f2bfa3f377 | |||
| 3100643534 | |||
| 1f105e7497 | |||
| dc1d566db3 | |||
| 3eec18b8a9 | |||
| 1c4a7ba003 | |||
| 42433c3062 | |||
| 0144d9e58e |
@ -1303,18 +1303,18 @@ class MainTerminal:
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "todo_create",
|
"name": "todo_create",
|
||||||
"description": "创建待办列表,将多步骤任务拆解为最多 8 条可执行项。建立前需向用户同步当前理解与约束,并在概述中记录关键目标。",
|
"description": "创建待办列表,将多步骤任务拆解为最多 8 条可执行项。概述请控制在 50 字以内,直接说明清单目标;任务列表只写 2~4 条明确步骤。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"overview": {"type": "string", "description": "对任务的思考与概述"},
|
"overview": {"type": "string", "description": "一句话概述待办清单要完成的目标,50 字以内。"},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "任务列表,依次对应 task1~task8",
|
"description": "任务列表,建议 2~4 条,每条写清“动词+对象+目标”。",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {"type": "string", "description": "单个任务描述"}
|
"title": {"type": "string", "description": "单个任务描述,写成可执行的步骤"}
|
||||||
},
|
},
|
||||||
"required": ["title"]
|
"required": ["title"]
|
||||||
},
|
},
|
||||||
@ -1369,6 +1369,20 @@ class MainTerminal:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "close_sub_agent",
|
||||||
|
"description": "强制关闭指定子智能体,适用于长时间无响应、超时或卡死的任务。使用前请确认必要的日志/文件已保留,操作会立即终止该任务。",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"task_id": {"type": "string", "description": "子智能体任务ID"},
|
||||||
|
"agent_id": {"type": "integer", "description": "子智能体编号(1~5),若缺少 task_id 可用"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@ -1397,7 +1411,7 @@ class MainTerminal:
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "wait_sub_agent",
|
"name": "wait_sub_agent",
|
||||||
"description": "等待指定子智能体任务结束(或超时)。任务完成后会返回交付目录,并将结果复制到指定的项目文件夹。",
|
"description": "等待指定子智能体任务结束(或超时)。任务完成后会返回交付目录,并将结果复制到指定的项目文件夹。调用时 `timeout_seconds` 应不少于对应子智能体的 `timeout_seconds`,否则可能提前终止等待。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -1976,13 +1990,29 @@ class MainTerminal:
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif tool_name == "wait_sub_agent":
|
elif tool_name == "wait_sub_agent":
|
||||||
|
wait_timeout = arguments.get("timeout_seconds")
|
||||||
|
if not wait_timeout:
|
||||||
|
task_ref = self.sub_agent_manager.lookup_task(
|
||||||
|
task_id=arguments.get("task_id"),
|
||||||
|
agent_id=arguments.get("agent_id")
|
||||||
|
)
|
||||||
|
if task_ref:
|
||||||
|
wait_timeout = task_ref.get("timeout_seconds")
|
||||||
result = self.sub_agent_manager.wait_for_completion(
|
result = self.sub_agent_manager.wait_for_completion(
|
||||||
task_id=arguments.get("task_id"),
|
task_id=arguments.get("task_id"),
|
||||||
agent_id=arguments.get("agent_id"),
|
agent_id=arguments.get("agent_id"),
|
||||||
timeout_seconds=arguments.get("timeout_seconds")
|
timeout_seconds=wait_timeout
|
||||||
)
|
)
|
||||||
self._record_sub_agent_message(result.get("system_message"), result.get("task_id"), inline=False)
|
self._record_sub_agent_message(result.get("system_message"), result.get("task_id"), inline=False)
|
||||||
|
|
||||||
|
elif tool_name == "close_sub_agent":
|
||||||
|
result = self.sub_agent_manager.terminate_sub_agent(
|
||||||
|
task_id=arguments.get("task_id"),
|
||||||
|
agent_id=arguments.get("agent_id")
|
||||||
|
)
|
||||||
|
message = result.get("message") or result.get("error")
|
||||||
|
self._record_sub_agent_message(message, result.get("task_id"), inline=False)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
result = {"success": False, "error": f"未知工具: {tool_name}"}
|
result = {"success": False, "error": f"未知工具: {tool_name}"}
|
||||||
|
|
||||||
|
|||||||
@ -58,6 +58,6 @@ TOOL_CATEGORIES: Dict[str, ToolCategory] = {
|
|||||||
),
|
),
|
||||||
"sub_agent": ToolCategory(
|
"sub_agent": ToolCategory(
|
||||||
label="子智能体",
|
label="子智能体",
|
||||||
tools=["create_sub_agent", "wait_sub_agent"],
|
tools=["create_sub_agent", "wait_sub_agent", "close_sub_agent"],
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,10 +74,10 @@ class SubAgentManager:
|
|||||||
"error": f"该对话已使用过编号 {agent_id},请更换新的子智能体代号。"
|
"error": f"该对话已使用过编号 {agent_id},请更换新的子智能体代号。"
|
||||||
}
|
}
|
||||||
|
|
||||||
if self._active_task_count() >= SUB_AGENT_MAX_ACTIVE:
|
if self._active_task_count(conversation_id) >= SUB_AGENT_MAX_ACTIVE:
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": f"已有 {SUB_AGENT_MAX_ACTIVE} 个子智能体在运行,请稍后再试。",
|
"error": f"该对话已存在 {SUB_AGENT_MAX_ACTIVE} 个运行中的子智能体,请稍后再试。",
|
||||||
}
|
}
|
||||||
|
|
||||||
task_id = self._generate_task_id(agent_id)
|
task_id = self._generate_task_id(agent_id)
|
||||||
@ -174,8 +174,10 @@ class SubAgentManager:
|
|||||||
if not task:
|
if not task:
|
||||||
return {"success": False, "error": "未找到对应的子智能体任务"}
|
return {"success": False, "error": "未找到对应的子智能体任务"}
|
||||||
|
|
||||||
if task.get("status") in TERMINAL_STATUSES and task.get("final_result"):
|
if task.get("status") in TERMINAL_STATUSES or task.get("status") == "terminated":
|
||||||
return task["final_result"]
|
if task.get("final_result"):
|
||||||
|
return task["final_result"]
|
||||||
|
return {"success": False, "status": task.get("status"), "message": "子智能体已结束。"}
|
||||||
|
|
||||||
timeout_seconds = timeout_seconds or task.get("timeout_seconds") or SUB_AGENT_DEFAULT_TIMEOUT
|
timeout_seconds = timeout_seconds or task.get("timeout_seconds") or SUB_AGENT_DEFAULT_TIMEOUT
|
||||||
deadline = time.time() + timeout_seconds
|
deadline = time.time() + timeout_seconds
|
||||||
@ -188,7 +190,7 @@ class SubAgentManager:
|
|||||||
time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL)
|
time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if status in {"completed", "failed", "timeout"}:
|
if status in {"completed", "failed", "timeout", "terminated"}:
|
||||||
break
|
break
|
||||||
|
|
||||||
time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL)
|
time.sleep(SUB_AGENT_STATUS_POLL_INTERVAL)
|
||||||
@ -206,6 +208,36 @@ class SubAgentManager:
|
|||||||
self._save_state()
|
self._save_state()
|
||||||
return finalize_result
|
return finalize_result
|
||||||
|
|
||||||
|
def terminate_sub_agent(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
task_id: Optional[str] = None,
|
||||||
|
agent_id: Optional[int] = None,
|
||||||
|
) -> Dict:
|
||||||
|
"""强制关闭指定子智能体。"""
|
||||||
|
task = self._select_task(task_id, agent_id)
|
||||||
|
if not task:
|
||||||
|
return {"success": False, "error": "未找到对应的子智能体任务"}
|
||||||
|
|
||||||
|
task_id = task["task_id"]
|
||||||
|
response = self._call_service("POST", f"/tasks/{task_id}/terminate", timeout=10)
|
||||||
|
response["task_id"] = task_id
|
||||||
|
if response.get("success"):
|
||||||
|
task["status"] = "terminated"
|
||||||
|
task["final_result"] = {
|
||||||
|
"success": False,
|
||||||
|
"status": "terminated",
|
||||||
|
"task_id": task_id,
|
||||||
|
"agent_id": task.get("agent_id"),
|
||||||
|
"message": response.get("message") or "子智能体已被强制关闭。",
|
||||||
|
}
|
||||||
|
self._save_state()
|
||||||
|
if "system_message" not in response:
|
||||||
|
response["system_message"] = response.get("message") or "🛑 子智能体已被手动关闭。"
|
||||||
|
elif "system_message" not in response:
|
||||||
|
response["system_message"] = response.get("message")
|
||||||
|
return response
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# 内部工具方法
|
# 内部工具方法
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@ -245,8 +277,17 @@ class SubAgentManager:
|
|||||||
suffix = uuid.uuid4().hex[:6]
|
suffix = uuid.uuid4().hex[:6]
|
||||||
return f"sub_{agent_id}_{int(time.time())}_{suffix}"
|
return f"sub_{agent_id}_{int(time.time())}_{suffix}"
|
||||||
|
|
||||||
def _active_task_count(self) -> int:
|
def _active_task_count(self, conversation_id: Optional[str] = None) -> int:
|
||||||
return len([t for t in self.tasks.values() if t.get("status") in {"pending", "running"}])
|
active = [
|
||||||
|
t for t in self.tasks.values()
|
||||||
|
if t.get("status") in {"pending", "running"}
|
||||||
|
]
|
||||||
|
if conversation_id:
|
||||||
|
active = [
|
||||||
|
t for t in active
|
||||||
|
if t.get("conversation_id") == conversation_id
|
||||||
|
]
|
||||||
|
return len(active)
|
||||||
|
|
||||||
def _copy_reference_files(self, references: List[str], dest_dir: Path) -> Tuple[List[str], List[str]]:
|
def _copy_reference_files(self, references: List[str], dest_dir: Path) -> Tuple[List[str], List[str]]:
|
||||||
copied = []
|
copied = []
|
||||||
@ -308,12 +349,25 @@ class SubAgentManager:
|
|||||||
return candidates[0]
|
return candidates[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def lookup_task(self, *, task_id: Optional[str] = None, agent_id: Optional[int] = None) -> Optional[Dict]:
|
||||||
|
"""只读查询任务信息,供 wait_sub_agent 自动调整超时时间。"""
|
||||||
|
task = self._select_task(task_id, agent_id)
|
||||||
|
if not task:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"task_id": task.get("task_id"),
|
||||||
|
"agent_id": task.get("agent_id"),
|
||||||
|
"status": task.get("status"),
|
||||||
|
"timeout_seconds": task.get("timeout_seconds"),
|
||||||
|
"conversation_id": task.get("conversation_id"),
|
||||||
|
}
|
||||||
|
|
||||||
def poll_updates(self) -> List[Dict]:
|
def poll_updates(self) -> List[Dict]:
|
||||||
"""检查运行中的子智能体任务,返回新完成的结果。"""
|
"""检查运行中的子智能体任务,返回新完成的结果。"""
|
||||||
updates: List[Dict] = []
|
updates: List[Dict] = []
|
||||||
pending_tasks = [
|
pending_tasks = [
|
||||||
task for task in self.tasks.values()
|
task for task in self.tasks.values()
|
||||||
if task.get("status") not in TERMINAL_STATUSES
|
if task.get("status") not in TERMINAL_STATUSES.union({"terminated"})
|
||||||
]
|
]
|
||||||
logger.debug(f"[SubAgentManager] 待检查任务: {len(pending_tasks)}")
|
logger.debug(f"[SubAgentManager] 待检查任务: {len(pending_tasks)}")
|
||||||
if not pending_tasks:
|
if not pending_tasks:
|
||||||
@ -358,7 +412,7 @@ class SubAgentManager:
|
|||||||
|
|
||||||
def _finalize_task(self, task: Dict, service_payload: Dict, status: str) -> Dict:
|
def _finalize_task(self, task: Dict, service_payload: Dict, status: str) -> Dict:
|
||||||
existing_result = task.get("final_result")
|
existing_result = task.get("final_result")
|
||||||
if existing_result and task.get("status") in TERMINAL_STATUSES:
|
if existing_result and task.get("status") in TERMINAL_STATUSES.union({"terminated"}):
|
||||||
return existing_result
|
return existing_result
|
||||||
|
|
||||||
task["status"] = status
|
task["status"] = status
|
||||||
@ -367,6 +421,21 @@ class SubAgentManager:
|
|||||||
deliverables_dir = Path(service_payload.get("deliverables_dir") or task.get("deliverables_dir", ""))
|
deliverables_dir = Path(service_payload.get("deliverables_dir") or task.get("deliverables_dir", ""))
|
||||||
logger.debug(f"[SubAgentManager] finalize task={task['task_id']} status={status}")
|
logger.debug(f"[SubAgentManager] finalize task={task['task_id']} status={status}")
|
||||||
|
|
||||||
|
if status == "terminated":
|
||||||
|
system_message = service_payload.get("system_message") or "🛑 子智能体已被手动关闭。"
|
||||||
|
result = {
|
||||||
|
"success": False,
|
||||||
|
"task_id": task["task_id"],
|
||||||
|
"agent_id": task["agent_id"],
|
||||||
|
"status": "terminated",
|
||||||
|
"message": message or "子智能体已被手动关闭。",
|
||||||
|
"details": service_payload,
|
||||||
|
"sub_conversation_id": task.get("sub_conversation_id"),
|
||||||
|
"system_message": system_message,
|
||||||
|
}
|
||||||
|
task["final_result"] = result
|
||||||
|
return result
|
||||||
|
|
||||||
if status != "completed":
|
if status != "completed":
|
||||||
result = {
|
result = {
|
||||||
"success": False,
|
"success": False,
|
||||||
@ -497,6 +566,9 @@ class SubAgentManager:
|
|||||||
"""返回子智能体任务概览,用于前端展示。"""
|
"""返回子智能体任务概览,用于前端展示。"""
|
||||||
overview: List[Dict[str, Any]] = []
|
overview: List[Dict[str, Any]] = []
|
||||||
for task_id, task in self.tasks.items():
|
for task_id, task in self.tasks.items():
|
||||||
|
status = task.get("status")
|
||||||
|
if status not in {"pending", "running"}:
|
||||||
|
continue
|
||||||
if conversation_id and task.get("conversation_id") != conversation_id:
|
if conversation_id and task.get("conversation_id") != conversation_id:
|
||||||
continue
|
continue
|
||||||
snapshot = {
|
snapshot = {
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
当你通过 `create_sub_agent`/`wait_sub_agent` 管理子智能体时,请遵循以下规则:
|
当你通过 `create_sub_agent`/`wait_sub_agent` 管理子智能体时,请遵循以下规则:
|
||||||
|
|
||||||
1. **何时创建**:当单个回复难以在时限内完成、需要长时间探索/编写大量文件或会阻塞主智能体上下文时,再考虑创建子智能体。特别是涉及大量信息搜集、网页提取、跨多篇资料的阅读与总结、需要输出结构化文件的任务,应该优先使用子智能体,避免主对话被海量搜索结果占满。
|
1. **何时创建**:当单个回复难以在时限内完成、需要长时间探索/编写大量文件或会阻塞主智能体上下文时,再考虑创建子智能体。特别是涉及大量信息搜集、网页提取、跨多篇资料的阅读与总结、需要输出结构化文件的任务,应该优先使用子智能体,避免主对话被海量搜索结果占满。子智能体适合“单一方向”的独立任务,比如按车企拆分调研、按章节拆分报告;不要让一个子智能体承担多个平行方向的需求。
|
||||||
2. **描述方式**:调用 `create_sub_agent` 前,先总结任务目标(summary)、详细分工(task),并指明交付目录(target_dir)以及需要一并提供的参考文件列表。任务描述要清晰、可执行,不要把问题交给子智能体自行理解。
|
2. **描述方式**:调用 `create_sub_agent` 前,先总结任务目标(summary)、详细分工(task),并指明交付目录(target_dir)以及需要一并提供的参考文件列表。任务描述要清晰、可执行,不要把问题交给子智能体自行理解。
|
||||||
3. **参考目录**:主智能体可将必要的文件列入 `reference_files`;这些文件会在子智能体的 `references/` 目录下以只读方式提供,适合提供需求文档、接口约束或已有实现片段。不要传递包含敏感信息或过于庞大的目录。
|
3. **参考目录**:主智能体可将必要的文件列入 `reference_files`;这些文件会在子智能体的 `references/` 目录下以只读方式提供,适合提供需求文档、接口约束或已有实现片段。不要传递包含敏感信息或过于庞大的目录。
|
||||||
4. **交付目录要求**:子智能体只能在其 `deliverables/` 下输出成果,主智能体最终会把该目录复制到 `target_project_dir/子任务ID_deliverables`。交付目录必须包含:
|
4. **交付目录要求**:子智能体只能在其 `deliverables/` 下输出成果,主智能体最终会把该目录复制到 `target_project_dir/子任务ID_deliverables`。交付目录必须包含:
|
||||||
@ -14,4 +14,11 @@
|
|||||||
- 深度调研/多份长文总结:600 秒(上限依据配置)
|
- 深度调研/多份长文总结:600 秒(上限依据配置)
|
||||||
6. **善后**:记录系统返回的 `system_message`,同步给用户;若交付不满足预期,可在主流程中补充说明或直接修改复制出的成果。
|
6. **善后**:记录系统返回的 `system_message`,同步给用户;若交付不满足预期,可在主流程中补充说明或直接修改复制出的成果。
|
||||||
|
|
||||||
|
**拆分示例**
|
||||||
|
- “调研比亚迪/吉利/奇瑞/长安/长城新能源品牌情况” → 建议为每家车企创建一个子智能体,各自总结销量与经营情况,主智能体负责合并。
|
||||||
|
- “对 6 个 API 做差异分析” → 可按 API 或功能模块拆分,每个子智能体负责一组接口。
|
||||||
|
- “阅读 20 篇行业报告并整理要点” → 可按主题或时间段划分,避免单个子智能体上下文爆炸。
|
||||||
|
- “把 8 个用户反馈邮件整理成 FAQ” → 可按邮件批次或问题类型分配给不同子智能体,保证每份输出精简清晰。
|
||||||
|
- “提取 3 份 PDF 报告的参数表并生成对比 Markdown” → 每个子智能体负责一份 PDF 的提取与结构化,再由主流程合并成总对比表。
|
||||||
|
|
||||||
牢记:主智能体与子智能体完全隔离,只能通过上述API交互。提供明确任务、参考和交付标准,才能让子智能体按预期产出可直接交付的结果。
|
牢记:主智能体与子智能体完全隔离,只能通过上述API交互。提供明确任务、参考和交付标准,才能让子智能体按预期产出可直接交付的结果。
|
||||||
|
|||||||
21
prompts/thinking_mode_guidelines.txt
Normal file
21
prompts/thinking_mode_guidelines.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
你现在处于「智能思考模式」。以下规则仅在本模式下生效:
|
||||||
|
|
||||||
|
1. **思考阶段**
|
||||||
|
- 先思考分析用户需求:要解决什么问题、现有信息缺口、潜在风险。
|
||||||
|
- 评估是否需要执行以下操作,并说明原因:
|
||||||
|
* 阅读或聚焦哪些文件?需要查看哪些片段?
|
||||||
|
* 是否进行 `read_file type=search/extract`、`web_search`、`extract_webpage` 或其他工具?
|
||||||
|
* 是否需要创建/修改/删除文件、运行终端命令或脚本?
|
||||||
|
* 是否需要创建/等待/关闭子智能体?
|
||||||
|
* 是否需要更新主记忆或任务记忆?
|
||||||
|
|
||||||
|
2. **正式输出阶段**
|
||||||
|
- 直接向用户说明你的计划:描述每一步准备做什么、需要哪些工具或文件。
|
||||||
|
- 如果暂时不执行某些操作,也要写明原因,比如“当前信息充足,无需额外读取”。
|
||||||
|
- 如需向用户确认信息或获取材料,可在计划里直接提问。
|
||||||
|
- 后续真正执行操作时,遵循该计划,若有变动要向用户解释。
|
||||||
|
|
||||||
|
3. **其他注意事项**
|
||||||
|
- 不要一上来就连续执行命令,先让用户看懂你的下一步安排。
|
||||||
|
- 若判断无需任何工具或修改,也要明确说明理由。
|
||||||
|
- 保持语气专业但亲切,让用户清楚你即将采取的行动。
|
||||||
@ -1303,18 +1303,18 @@ class MainTerminal:
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "todo_create",
|
"name": "todo_create",
|
||||||
"description": "创建待办列表,将多步骤任务拆解为最多 8 条可执行项。建立前需向用户同步当前理解与约束,并在概述中记录关键目标。",
|
"description": "创建待办列表,将多步骤任务拆解为最多 8 条可执行项。概述需控制在 50 字以内并直接说明目标,任务项写成 2~4 条“动词+对象+目标”的步骤。",
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"overview": {"type": "string", "description": "对任务的思考与概述"},
|
"overview": {"type": "string", "description": "一句话概述本次清单的目标(<=50 字)。"},
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "任务列表,依次对应 task1~task8",
|
"description": "任务列表,建议 2~4 条,依次对应 task1~task8",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"title": {"type": "string", "description": "单个任务描述"}
|
"title": {"type": "string", "description": "单个任务描述,写成可执行动作。"}
|
||||||
},
|
},
|
||||||
"required": ["title"]
|
"required": ["title"]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -9,12 +9,12 @@
|
|||||||
|
|
||||||
## 如何编写
|
## 如何编写
|
||||||
1. **概述**:一句话写清楚你正在完成的目标(≤50 字)。
|
1. **概述**:一句话写清楚你正在完成的目标(≤50 字)。
|
||||||
2. **任务项**:2~4 条即可,按执行顺序罗列。每条必须描述“对哪个对象做什么动作”,例如 “读取 sales.xlsx,统计月度汇总”。
|
2. **任务项**:2~4 条即可,按执行顺序罗列。每条必须描述“对哪个对象做什么动作”。
|
||||||
3. **粒度**:避免含糊词(“处理”、“完善”等);能在十分钟内完成的最小可执行步骤即可。
|
3. **粒度**:避免含糊词(“处理”、“完善”等);能在十分钟内完成的最小可执行步骤即可。
|
||||||
|
|
||||||
## 使用流程
|
## 使用流程
|
||||||
1. **先规划**:在创建清单前,用自然语言写下你准备执行的流程,让自己确认无遗漏。
|
1. **先规划**:在创建清单前,用自然语言写下你准备执行的流程,让自己确认无遗漏。
|
||||||
2. **todo_create**:把概述与任务数组一次性写对,创建后尽量不要反复删除重建。
|
2. **todo_create**:把概述与任务数组一次性写对,创建后尽量不要反复删除重建。概述 ≤ 50 字,说明清单目标;任务数组写 2~4 条“动词+对象+目标”的步骤。
|
||||||
3. **todo_update_task**:每完成一项立刻勾选;若步骤发生变化,先写明原因再修改对应任务。
|
3. **todo_update_task**:每完成一项立刻勾选;若步骤发生变化,先写明原因再修改对应任务。
|
||||||
4. **todo_finish**:所有任务完成后调用。若仍有未完项但必须停止,先调用 `todo_finish`,再用 `todo_finish_confirm` 说明原因与后续建议。
|
4. **todo_finish**:所有任务完成后调用。若仍有未完项但必须停止,先调用 `todo_finish`,再用 `todo_finish_confirm` 说明原因与后续建议。
|
||||||
|
|
||||||
|
|||||||
@ -175,6 +175,7 @@ async function bootstrapApp() {
|
|||||||
|
|
||||||
// 停止功能状态
|
// 停止功能状态
|
||||||
stopRequested: false,
|
stopRequested: false,
|
||||||
|
terminating: false,
|
||||||
|
|
||||||
// 路由相关
|
// 路由相关
|
||||||
initialRouteResolved: false,
|
initialRouteResolved: false,
|
||||||
@ -633,6 +634,37 @@ async function bootstrapApp() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async terminateSubAgentTask() {
|
||||||
|
if (!this.subAgentTaskId || this.terminating) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const shouldTerminate = window.confirm('确定要立即关闭子智能体吗?此操作无法撤销。');
|
||||||
|
if (!shouldTerminate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.terminating = true;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`/tasks/${encodeURIComponent(this.subAgentTaskId)}/terminate`, { method: 'POST' });
|
||||||
|
let data = null;
|
||||||
|
try {
|
||||||
|
data = await resp.json();
|
||||||
|
} catch (err) {
|
||||||
|
data = { success: false, error: await resp.text() };
|
||||||
|
}
|
||||||
|
if (!resp.ok || !data.success) {
|
||||||
|
const message = (data && (data.error || data.message)) || '关闭失败';
|
||||||
|
alert(`关闭子智能体失败:${message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.addSystemMessage('🛑 子智能体已被手动关闭。');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('关闭子智能体失败:', error);
|
||||||
|
alert(`关闭子智能体失败:${error.message || error}`);
|
||||||
|
} finally {
|
||||||
|
this.terminating = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
formatPathDisplay(path) {
|
formatPathDisplay(path) {
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return '—';
|
return '—';
|
||||||
@ -647,7 +679,8 @@ async function bootstrapApp() {
|
|||||||
pending: '等待中',
|
pending: '等待中',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
failed: '失败',
|
failed: '失败',
|
||||||
timeout: '超时'
|
timeout: '超时',
|
||||||
|
terminated: '已关闭'
|
||||||
};
|
};
|
||||||
return map[status] || status;
|
return map[status] || status;
|
||||||
},
|
},
|
||||||
|
|||||||
@ -59,6 +59,13 @@
|
|||||||
<span class="thinking-mode" v-else>
|
<span class="thinking-mode" v-else>
|
||||||
状态: {{ formatSubAgentStatus(subAgentTaskInfo.status) }}
|
状态: {{ formatSubAgentStatus(subAgentTaskInfo.status) }}
|
||||||
</span>
|
</span>
|
||||||
|
<button v-if="isSubAgentView"
|
||||||
|
type="button"
|
||||||
|
class="btn stop-btn terminate-btn"
|
||||||
|
@click="terminateSubAgentTask"
|
||||||
|
:disabled="terminating">
|
||||||
|
{{ terminating ? '关闭中...' : '关闭子智能体' }}
|
||||||
|
</button>
|
||||||
<span class="connection-status" :class="{ connected: isConnected }">
|
<span class="connection-status" :class="{ connected: isConnected }">
|
||||||
<span class="status-dot" :class="{ active: isConnected }"></span>
|
<span class="status-dot" :class="{ active: isConnected }"></span>
|
||||||
{{ isConnected ? '已连接' : '未连接' }}
|
{{ isConnected ? '已连接' : '未连接' }}
|
||||||
@ -228,39 +235,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Token区域包装器 -->
|
|
||||||
<div class="token-wrapper" v-if="currentConversationId">
|
|
||||||
<!-- Token统计显示面板 -->
|
|
||||||
<div class="token-display-panel" :class="{ collapsed: tokenPanelCollapsed }">
|
|
||||||
<div class="token-panel-content">
|
|
||||||
<div class="token-stats">
|
|
||||||
<div class="token-item">
|
|
||||||
<span class="token-label">当前上下文</span>
|
|
||||||
<span class="token-value current">{{ formatTokenCount(currentContextTokens || 0) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="token-separator"></div>
|
|
||||||
|
|
||||||
<div class="token-item">
|
|
||||||
<span class="token-label">累计输入</span>
|
|
||||||
<span class="token-value input">{{ formatTokenCount(currentConversationTokens.cumulative_input_tokens || 0) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="token-item">
|
|
||||||
<span class="token-label">累计输出</span>
|
|
||||||
<span class="token-value output">{{ formatTokenCount(currentConversationTokens.cumulative_output_tokens || 0) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 独立的切换按钮 -->
|
|
||||||
<button @click="toggleTokenPanel" class="token-toggle-btn" :class="{ collapsed: tokenPanelCollapsed }">
|
|
||||||
<span v-if="!tokenPanelCollapsed">▲</span>
|
|
||||||
<span v-else>▼</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="messages-area" ref="messagesArea">
|
<div class="messages-area" ref="messagesArea">
|
||||||
<div v-for="(msg, index) in messages" :key="index" class="message-block">
|
<div v-for="(msg, index) in messages" :key="index" class="message-block">
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
--claude-shadow: 0 14px 36px rgba(61, 57, 41, 0.12);
|
--claude-shadow: 0 14px 36px rgba(61, 57, 41, 0.12);
|
||||||
--claude-success: #76b086;
|
--claude-success: #76b086;
|
||||||
--claude-warning: #d99845;
|
--claude-warning: #d99845;
|
||||||
|
--claude-danger: #d65a5a;
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
@ -86,6 +87,21 @@ body {
|
|||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminate-btn {
|
||||||
|
background: var(--claude-danger);
|
||||||
|
color: #fff6f6;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminate-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
.connection-status {
|
.connection-status {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: var(--claude-text-secondary);
|
color: var(--claude-text-secondary);
|
||||||
@ -1373,11 +1389,6 @@ body {
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 输入区域 */
|
|
||||||
.token-wrapper {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-area {
|
.input-area {
|
||||||
background: rgba(255, 255, 255, 0.82);
|
background: rgba(255, 255, 255, 0.82);
|
||||||
border-top: 1px solid var(--claude-border);
|
border-top: 1px solid var(--claude-border);
|
||||||
@ -1530,9 +1541,6 @@ body {
|
|||||||
.messages-area {
|
.messages-area {
|
||||||
padding: 16px 18px;
|
padding: 16px 18px;
|
||||||
}
|
}
|
||||||
.token-wrapper {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.input-area {
|
.input-area {
|
||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
@ -1862,230 +1870,7 @@ body {
|
|||||||
color: var(--claude-text-secondary);
|
color: var(--claude-text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.token-count {
|
|
||||||
color: var(--claude-accent);
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
/* ========================================= */
|
|
||||||
/* Token 统计面板样式(无缝一体版)*/
|
|
||||||
/* ========================================= */
|
|
||||||
|
|
||||||
/* Token区域包装器 */
|
|
||||||
.token-wrapper {
|
|
||||||
position: relative;
|
|
||||||
z-index: 5;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 当前对话信息栏 - 移除底部边框 */
|
|
||||||
.current-conversation-info {
|
|
||||||
position: relative;
|
|
||||||
z-index: 10;
|
|
||||||
background: var(--claude-panel);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
border-bottom: none; /* 移除边框,让它和下面的面板融为一体 */
|
|
||||||
padding: 12px 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
border-radius: 0; /* 顶部保持直角 */
|
|
||||||
box-shadow: 0 12px 28px rgba(61, 57, 41, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Token面板 - 与标题栏完全一体,底部圆角 */
|
|
||||||
.token-display-panel {
|
|
||||||
background: var(--claude-panel);
|
|
||||||
backdrop-filter: blur(18px);
|
|
||||||
border: none;
|
|
||||||
border-radius: 0 0 16px 16px;
|
|
||||||
box-shadow: 0 8px 18px rgba(189, 93, 58, 0.12);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 展开状态 */
|
|
||||||
.token-display-panel:not(.collapsed) {
|
|
||||||
height: 80px;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 收起状态 */
|
|
||||||
.token-display-panel.collapsed {
|
|
||||||
height: 0;
|
|
||||||
opacity: 0;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-panel-content {
|
|
||||||
padding: 16px 24px;
|
|
||||||
height: 100%;
|
|
||||||
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-display-panel.collapsed .token-panel-content {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-stats {
|
|
||||||
display: flex;
|
|
||||||
gap: 32px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 13px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-label {
|
|
||||||
color: var(--claude-text-secondary);
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 500;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-value {
|
|
||||||
color: var(--claude-text);
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 18px;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-value.current {
|
|
||||||
color: var(--claude-accent);
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.token-value.input { color: var(--claude-success); }
|
|
||||||
.token-value.output { color: var(--claude-warning); }
|
|
||||||
|
|
||||||
.token-separator {
|
|
||||||
width: 1px;
|
|
||||||
height: 35px;
|
|
||||||
background: linear-gradient(to bottom,
|
|
||||||
transparent,
|
|
||||||
rgba(218, 119, 86, 0.25) 20%,
|
|
||||||
rgba(218, 119, 86, 0.25) 80%,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 切换按钮 - 独立定位 */
|
|
||||||
.token-toggle-btn {
|
|
||||||
position: absolute;
|
|
||||||
right: 24px;
|
|
||||||
bottom: -18px; /* 相对于wrapper底部 */
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 2px solid rgba(218, 119, 86, 0.3);
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
z-index: 15;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 展开状态 */
|
|
||||||
.token-toggle-btn:not(.collapsed) {
|
|
||||||
background: linear-gradient(135deg, #ffffff 0%, rgba(255, 248, 242, 0.9) 100%);
|
|
||||||
color: var(--claude-accent);
|
|
||||||
box-shadow: 0 3px 10px rgba(189, 93, 58, 0.18);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 收起状态 - 在标题栏下方露出一半 */
|
|
||||||
.token-toggle-btn.collapsed {
|
|
||||||
background: linear-gradient(135deg, var(--claude-accent) 0%, var(--claude-accent-strong) 100%);
|
|
||||||
color: #fff8f2;
|
|
||||||
border-color: rgba(255, 248, 242, 0.55);
|
|
||||||
box-shadow: 0 3px 11px rgba(189, 93, 58, 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-toggle-btn:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
box-shadow: 0 5px 16px rgba(189, 93, 58, 0.26);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-toggle-btn.collapsed:hover {
|
|
||||||
background: linear-gradient(135deg, var(--claude-button-hover) 0%, var(--claude-button-active) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-toggle-btn:active {
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 箭头样式 - 移除浮动动画 */
|
|
||||||
.token-toggle-btn span {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 移除动画效果 */
|
|
||||||
/* .token-toggle-btn:not(.collapsed) span {
|
|
||||||
animation: arrowBounceUp 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-toggle-btn.collapsed span {
|
|
||||||
animation: arrowBounceDown 2s ease-in-out infinite;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* 保留动画定义,但不使用 */
|
|
||||||
@keyframes arrowBounceUp {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(-3px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes arrowBounceDown {
|
|
||||||
0%, 100% { transform: translateY(0); }
|
|
||||||
50% { transform: translateY(3px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 响应式调整 */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.token-stats {
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-item {
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-value {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-value.current {
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-label {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token-toggle-btn {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
font-size: 12px;
|
|
||||||
right: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* Markdown列表样式 - 修复偏左问题 */
|
/* Markdown列表样式 - 修复偏左问题 */
|
||||||
.text-content ul,
|
.text-content ul,
|
||||||
.text-content ol {
|
.text-content ol {
|
||||||
|
|||||||
@ -74,6 +74,7 @@ user_terminals: Dict[str, WebTerminal] = {}
|
|||||||
terminal_rooms: Dict[str, set] = {}
|
terminal_rooms: Dict[str, set] = {}
|
||||||
connection_users: Dict[str, str] = {}
|
connection_users: Dict[str, str] = {}
|
||||||
stop_flags: Dict[str, Dict[str, Any]] = {}
|
stop_flags: Dict[str, Dict[str, Any]] = {}
|
||||||
|
terminated_tasks: set = set()
|
||||||
|
|
||||||
DEFAULT_PORT = 8092
|
DEFAULT_PORT = 8092
|
||||||
|
|
||||||
@ -82,6 +83,8 @@ sub_agent_tasks: Dict[str, Dict[str, Any]] = {}
|
|||||||
sub_agent_terminals: Dict[str, SubAgentTerminal] = {}
|
sub_agent_terminals: Dict[str, SubAgentTerminal] = {}
|
||||||
sub_agent_rooms: Dict[str, set] = defaultdict(set)
|
sub_agent_rooms: Dict[str, set] = defaultdict(set)
|
||||||
sub_agent_connections: Dict[str, str] = {}
|
sub_agent_connections: Dict[str, str] = {}
|
||||||
|
SUB_AGENT_TERMINAL_STATUSES = {"completed", "failed", "timeout"}
|
||||||
|
STOPPING_GRACE_SECONDS = 30
|
||||||
|
|
||||||
def format_read_file_result(result_data: Dict) -> str:
|
def format_read_file_result(result_data: Dict) -> str:
|
||||||
"""格式化 read_file 工具的输出,便于在Web端展示。"""
|
"""格式化 read_file 工具的输出,便于在Web端展示。"""
|
||||||
@ -244,8 +247,52 @@ def build_finish_tool_reminder(meta: Dict[str, Any]) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_active_sub_agent_count() -> int:
|
def cleanup_inactive_sub_agent_tasks(force: bool = False):
|
||||||
return len([task for task in sub_agent_tasks.values() if task.get("status") in {"pending", "running"}])
|
"""移除已结束或长期停止中的子智能体,避免占用名额。"""
|
||||||
|
now = time.time()
|
||||||
|
for task_id, task in list(sub_agent_tasks.items()):
|
||||||
|
status = (task.get("status") or "").lower()
|
||||||
|
if status in SUB_AGENT_TERMINAL_STATUSES:
|
||||||
|
_purge_sub_agent_task(task_id)
|
||||||
|
continue
|
||||||
|
if status == "stopping":
|
||||||
|
updated_at = task.get("updated_at") or task.get("created_at") or now
|
||||||
|
if force or (now - updated_at) > STOPPING_GRACE_SECONDS:
|
||||||
|
_purge_sub_agent_task(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
def _purge_sub_agent_task(task_id: str):
|
||||||
|
"""移除本地记录与相关连接。"""
|
||||||
|
sub_agent_tasks.pop(task_id, None)
|
||||||
|
terminal = sub_agent_terminals.pop(task_id, None)
|
||||||
|
if terminal and hasattr(terminal, "close"):
|
||||||
|
try:
|
||||||
|
terminal.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
room_sids = sub_agent_rooms.pop(task_id, set())
|
||||||
|
for sid in list(room_sids):
|
||||||
|
sub_agent_connections.pop(sid, None)
|
||||||
|
for sid, current in list(sub_agent_connections.items()):
|
||||||
|
if current == task_id:
|
||||||
|
sub_agent_connections.pop(sid, None)
|
||||||
|
for sid in list(stop_flags.keys()):
|
||||||
|
if task_id in sid:
|
||||||
|
stop_flags.pop(sid, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_active_sub_agent_count(conversation_id: Optional[str] = None) -> int:
|
||||||
|
cleanup_inactive_sub_agent_tasks()
|
||||||
|
normalized = _normalize_conversation_id(conversation_id)
|
||||||
|
count = 0
|
||||||
|
for task in sub_agent_tasks.values():
|
||||||
|
if task.get("status") not in {"pending", "running"}:
|
||||||
|
continue
|
||||||
|
if normalized:
|
||||||
|
if _normalize_conversation_id(task.get("parent_conversation_id")) != normalized:
|
||||||
|
continue
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
def find_sub_agent_conversation_file(conv_id: str) -> Optional[Path]:
|
def find_sub_agent_conversation_file(conv_id: str) -> Optional[Path]:
|
||||||
@ -1984,12 +2031,15 @@ def create_sub_agent_task(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
if not payload.get(field):
|
if not payload.get(field):
|
||||||
return {"success": False, "error": f"缺少必要参数: {field}"}
|
return {"success": False, "error": f"缺少必要参数: {field}"}
|
||||||
|
|
||||||
|
cleanup_inactive_sub_agent_tasks()
|
||||||
task_id = payload["task_id"]
|
task_id = payload["task_id"]
|
||||||
if task_id in sub_agent_tasks:
|
if task_id in sub_agent_tasks:
|
||||||
return {"success": False, "error": f"任务 {task_id} 已存在"}
|
return {"success": False, "error": f"任务 {task_id} 已存在"}
|
||||||
|
|
||||||
if get_active_sub_agent_count() >= SUB_AGENT_MAX_ACTIVE:
|
parent_conv = payload.get("parent_conversation_id")
|
||||||
return {"success": False, "error": f"已存在 {SUB_AGENT_MAX_ACTIVE} 个运行中的子智能体,请稍后再试"}
|
if get_active_sub_agent_count(parent_conv) >= SUB_AGENT_MAX_ACTIVE:
|
||||||
|
limit_note = f"同一对话最多可同时运行 {SUB_AGENT_MAX_ACTIVE} 个子智能体"
|
||||||
|
return {"success": False, "error": f"已存在 {SUB_AGENT_MAX_ACTIVE} 个运行中的子智能体,请稍后再试({limit_note})。"}
|
||||||
|
|
||||||
workspace_dir = Path(payload["workspace_dir"]).resolve()
|
workspace_dir = Path(payload["workspace_dir"]).resolve()
|
||||||
references_dir = Path(payload["references_dir"]).resolve()
|
references_dir = Path(payload["references_dir"]).resolve()
|
||||||
@ -2016,6 +2066,7 @@ def create_sub_agent_task(payload: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
metadata=metadata,
|
metadata=metadata,
|
||||||
message_callback=None,
|
message_callback=None,
|
||||||
)
|
)
|
||||||
|
terminal.task_id = task_id
|
||||||
|
|
||||||
def _finish_hook(result: Dict[str, Any]):
|
def _finish_hook(result: Dict[str, Any]):
|
||||||
reason = result.get("reason", "")
|
reason = result.get("reason", "")
|
||||||
@ -2083,6 +2134,38 @@ def stop_sub_agent_task(task_id: str) -> Dict[str, Any]:
|
|||||||
return {"success": True, "message": "已发送停止指令"}
|
return {"success": True, "message": "已发送停止指令"}
|
||||||
|
|
||||||
|
|
||||||
|
def force_terminate_sub_agent(task_id: str) -> Dict[str, Any]:
|
||||||
|
cleanup_inactive_sub_agent_tasks()
|
||||||
|
task = sub_agent_tasks.get(task_id)
|
||||||
|
if not task:
|
||||||
|
return {"success": False, "error": "任务不存在"}
|
||||||
|
|
||||||
|
client_sid = task.get("last_client_sid")
|
||||||
|
if client_sid and client_sid in stop_flags:
|
||||||
|
stop_flags[client_sid]["stop"] = True
|
||||||
|
|
||||||
|
terminated_tasks.add(task_id)
|
||||||
|
terminal = sub_agent_terminals.get(task_id)
|
||||||
|
if terminal:
|
||||||
|
try:
|
||||||
|
reset_system_state(terminal)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
update_sub_agent_task(task_id, status="terminated")
|
||||||
|
broadcast_sub_agent_event(task_id, "sub_agent_status", {
|
||||||
|
"status": "terminated",
|
||||||
|
"message": "子智能体已被手动关闭"
|
||||||
|
})
|
||||||
|
_purge_sub_agent_task(task_id)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"status": "terminated",
|
||||||
|
"message": "子智能体已被手动关闭",
|
||||||
|
"system_message": "🛑 子智能体已被手动关闭。"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
# 子智能体任务API(主智能体调用,免登录)
|
# 子智能体任务API(主智能体调用,免登录)
|
||||||
# ==========================================
|
# ==========================================
|
||||||
@ -2140,6 +2223,13 @@ def api_stop_sub_agent(task_id: str):
|
|||||||
return jsonify(result), status_code
|
return jsonify(result), status_code
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/tasks/<task_id>/terminate', methods=['POST'])
|
||||||
|
def api_terminate_sub_agent(task_id: str):
|
||||||
|
result = force_terminate_sub_agent(task_id)
|
||||||
|
status_code = 200 if result.get("success") else 400
|
||||||
|
return jsonify(result), status_code
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tasks/<task_id>/conversation', methods=['GET'])
|
@app.route('/tasks/<task_id>/conversation', methods=['GET'])
|
||||||
def api_get_sub_agent_conversation(task_id: str):
|
def api_get_sub_agent_conversation(task_id: str):
|
||||||
info = get_task_record(task_id)
|
info = get_task_record(task_id)
|
||||||
@ -2356,6 +2446,7 @@ def process_message_task(terminal: WebTerminal, message: str, sender, client_sid
|
|||||||
stop_flags[client_sid]['task'] = task
|
stop_flags[client_sid]['task'] = task
|
||||||
stop_flags[client_sid]['terminal'] = terminal
|
stop_flags[client_sid]['terminal'] = terminal
|
||||||
|
|
||||||
|
task_id = getattr(terminal, "task_id", None)
|
||||||
try:
|
try:
|
||||||
loop.run_until_complete(task)
|
loop.run_until_complete(task)
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
@ -2365,6 +2456,9 @@ def process_message_task(terminal: WebTerminal, message: str, sender, client_sid
|
|||||||
'reason': 'user_requested'
|
'reason': 'user_requested'
|
||||||
})
|
})
|
||||||
reset_system_state(terminal)
|
reset_system_state(terminal)
|
||||||
|
finally:
|
||||||
|
if task_id:
|
||||||
|
terminated_tasks.discard(task_id)
|
||||||
|
|
||||||
loop.close()
|
loop.close()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -3063,6 +3157,10 @@ async def handle_task_with_sender(terminal: WebTerminal, message, sender, client
|
|||||||
for iteration in range(max_iterations):
|
for iteration in range(max_iterations):
|
||||||
total_iterations += 1
|
total_iterations += 1
|
||||||
debug_log(f"\n--- 迭代 {iteration + 1}/{max_iterations} 开始 ---")
|
debug_log(f"\n--- 迭代 {iteration + 1}/{max_iterations} 开始 ---")
|
||||||
|
task_id = getattr(web_terminal, "task_id", None)
|
||||||
|
if task_id and task_id in terminated_tasks:
|
||||||
|
sender('system_message', {'content': '🛑 子智能体已被手动关闭。'})
|
||||||
|
break
|
||||||
|
|
||||||
# 检查是否超过总工具调用限制
|
# 检查是否超过总工具调用限制
|
||||||
if total_tool_calls >= MAX_TOTAL_TOOL_CALLS:
|
if total_tool_calls >= MAX_TOTAL_TOOL_CALLS:
|
||||||
@ -4153,6 +4251,7 @@ def mark_task_completed(task_id: str, reason: Optional[str] = None):
|
|||||||
"status": "completed",
|
"status": "completed",
|
||||||
"reason": reason or ""
|
"reason": reason or ""
|
||||||
})
|
})
|
||||||
|
cleanup_inactive_sub_agent_tasks()
|
||||||
|
|
||||||
|
|
||||||
def mark_task_failed(task_id: str, message: str):
|
def mark_task_failed(task_id: str, message: str):
|
||||||
@ -4161,6 +4260,7 @@ def mark_task_failed(task_id: str, message: str):
|
|||||||
"status": "failed",
|
"status": "failed",
|
||||||
"message": message
|
"message": message
|
||||||
})
|
})
|
||||||
|
cleanup_inactive_sub_agent_tasks()
|
||||||
|
|
||||||
|
|
||||||
def mark_task_timeout(task_id: str):
|
def mark_task_timeout(task_id: str):
|
||||||
@ -4172,6 +4272,7 @@ def mark_task_timeout(task_id: str):
|
|||||||
"status": "timeout",
|
"status": "timeout",
|
||||||
"message": "任务已超时"
|
"message": "任务已超时"
|
||||||
})
|
})
|
||||||
|
cleanup_inactive_sub_agent_tasks()
|
||||||
|
|
||||||
|
|
||||||
def start_sub_agent_monitor():
|
def start_sub_agent_monitor():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user