Compare commits
21 Commits
multiagent
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 121793367a | |||
| cfdec831d8 | |||
| 1d0a7e95fa | |||
| d4c3e73134 | |||
| 2b131de702 | |||
| faef998a9a | |||
| 248f5a4b1a | |||
| 310d82f4b8 | |||
| 3138d23687 | |||
| 11ffdc2bb5 | |||
| 089291f77e | |||
| 4f3ba722a1 | |||
| 69e3ef51b6 | |||
| b2b8013f3a | |||
| f6cbe78d68 | |||
| 57aaf76809 | |||
| a60e164aa2 | |||
| f8f5d26ee3 | |||
| 4e1d08f106 | |||
| 297a6cc5ec | |||
| a95e969ba2 |
@ -207,7 +207,9 @@
|
||||
|
||||
### 前端调试日志
|
||||
|
||||
- **统一筛选词**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
|
||||
- **统一筛选词(强制)**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
|
||||
- **同一次调试任务中严禁使用多个前缀**。如果涉及多个模块(如状态栏、Git 摘要、用户问题),应共用同一个前缀(如 `[status-bar-debug]`),通过日志对象里的字段区分模块,而不是发明多个前缀增加用户筛选成本。
|
||||
- 选择前缀时优先与本次排查的「用户可见现象」对齐,而不是与内部模块名对齐。
|
||||
- **控制数量与时机**:
|
||||
- 禁止一次性输出上百上千条日志刷屏。
|
||||
- 只在关键路径(入口、分支判断、状态变化、导航动作)打印,避免在循环、高频事件、每帧渲染中输出。
|
||||
|
||||
@ -103,7 +103,7 @@ class ToolsDefinitionCoreToolsMixin:
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "sleep",
|
||||
"description": "等待工具。两种模式二选一:1) seconds:短暂延迟;2) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果。若同时提供多个等待参数会报错。",
|
||||
"description": "等待工具。三种模式三选一:1) seconds:短暂延迟;2) wait_runcommand_id:等待指定后台 run_command 结束并直接返回结果;3) wait_sub_agent_output_ids:等待指定子智能体下一次输出并直接返回该消息(多智能体模式专用)。若同时提供多个等待参数会报错。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": self._inject_intent({
|
||||
@ -115,6 +115,11 @@ class ToolsDefinitionCoreToolsMixin:
|
||||
"type": "string",
|
||||
"description": "等待指定后台 run_command 的 command_id 结束后返回。"
|
||||
},
|
||||
"wait_sub_agent_output_ids": {
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"description": "等待指定子智能体下一次输出并直接返回该消息。多智能体模式专用,且一次只能包含一个编号。"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"description": "等待的原因说明(可选)"
|
||||
|
||||
@ -31,6 +31,7 @@ try:
|
||||
WORKSPACE_MEMORY_DIRNAME,
|
||||
WORKSPACE_REVIEW_DIRNAME,
|
||||
)
|
||||
from config.paths import WEB_PRESET_ROLES_DIR
|
||||
except ImportError:
|
||||
import sys
|
||||
project_root = Path(__file__).resolve().parents[2]
|
||||
@ -57,6 +58,7 @@ except ImportError:
|
||||
WORKSPACE_MEMORY_DIRNAME,
|
||||
WORKSPACE_REVIEW_DIRNAME,
|
||||
)
|
||||
from config.paths import WEB_PRESET_ROLES_DIR
|
||||
|
||||
from modules.file_manager import FileManager
|
||||
from modules.search_engine import SearchEngine
|
||||
@ -1078,6 +1080,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
elif tool_name == "sleep":
|
||||
seconds = arguments.get("seconds")
|
||||
wait_sub_agent_ids = arguments.get("wait_sub_agent_ids")
|
||||
wait_sub_agent_output_ids = arguments.get("wait_sub_agent_output_ids")
|
||||
wait_runcommand_id = arguments.get("wait_runcommand_id")
|
||||
reason = arguments.get("reason", "等待操作完成")
|
||||
|
||||
@ -1086,23 +1089,67 @@ class MainTerminalToolsExecutionMixin:
|
||||
provided += 1
|
||||
if wait_sub_agent_ids:
|
||||
provided += 1
|
||||
if wait_sub_agent_output_ids:
|
||||
provided += 1
|
||||
if wait_runcommand_id:
|
||||
provided += 1
|
||||
if provided == 0:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "sleep 至少需要提供一个参数:seconds / wait_sub_agent_ids / wait_runcommand_id"
|
||||
"error": "sleep 至少需要提供一个参数:seconds / wait_sub_agent_ids / wait_sub_agent_output_ids / wait_runcommand_id"
|
||||
}
|
||||
elif provided > 1:
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "sleep 的等待参数互斥:seconds / wait_sub_agent_ids / wait_runcommand_id 只能提供一个"
|
||||
"error": "sleep 的等待参数互斥:seconds / wait_sub_agent_ids / wait_sub_agent_output_ids / wait_runcommand_id 只能提供一个"
|
||||
}
|
||||
elif wait_sub_agent_output_ids:
|
||||
if not getattr(self, "multi_agent_mode", False):
|
||||
result = {"success": False, "error": "wait_sub_agent_output_ids 仅在多智能体模式下可用"}
|
||||
elif not isinstance(wait_sub_agent_output_ids, list) or len(wait_sub_agent_output_ids) != 1:
|
||||
result = {"success": False, "error": "wait_sub_agent_output_ids 必须包含且仅包含一个子智能体编号"}
|
||||
else:
|
||||
try:
|
||||
agent_id = int(wait_sub_agent_output_ids[0])
|
||||
if agent_id <= 0:
|
||||
raise ValueError()
|
||||
except Exception:
|
||||
result = {"success": False, "error": "wait_sub_agent_output_ids 必须是正整数"}
|
||||
else:
|
||||
manager = getattr(self, "sub_agent_manager", None)
|
||||
if not manager:
|
||||
result = {"success": False, "error": "子智能体管理器不可用"}
|
||||
else:
|
||||
state = manager.get_multi_agent_state(getattr(self.context_manager, "current_conversation_id", None))
|
||||
if not state:
|
||||
result = {"success": False, "error": "当前对话没有多智能体状态"}
|
||||
else:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = state.register_output_wait(agent_id, loop)
|
||||
msg = await asyncio.wait_for(fut, timeout=300)
|
||||
result = {
|
||||
"success": True,
|
||||
"mode": "wait_sub_agent_output",
|
||||
"agent_id": agent_id,
|
||||
"message": msg,
|
||||
}
|
||||
except asyncio.TimeoutError:
|
||||
result = {"success": False, "error": f"等待子智能体 {agent_id} 输出超时(5分钟)。可用 send_message_to_sub_agent 重新激活。"}
|
||||
except asyncio.CancelledError:
|
||||
result = {"success": False, "error": f"等待子智能体 {agent_id} 被取消。该子智能体现在不可用,可用 send_message_to_sub_agent 重新激活。"}
|
||||
except RuntimeError as exc:
|
||||
error_msg = str(exc)
|
||||
if "send_message_to_sub_agent" not in error_msg:
|
||||
error_msg += " 可用 send_message_to_sub_agent 重新激活。"
|
||||
result = {"success": False, "error": error_msg}
|
||||
except Exception as exc:
|
||||
result = {"success": False, "error": f"等待子智能体 {agent_id} 输出失败: {exc}。可用 send_message_to_sub_agent 重新激活。"}
|
||||
elif wait_sub_agent_ids:
|
||||
if getattr(self, "multi_agent_mode", False):
|
||||
result = {
|
||||
"success": False,
|
||||
"error": "多智能体模式下 sleep 工具不支持 wait_sub_agent_ids。请通过 list_active_sub_agents / get_sub_agent_status 查看状态,或直接向子智能体发送消息。"
|
||||
"error": "多智能体模式下 sleep 工具不支持 wait_sub_agent_ids,请使用 wait_sub_agent_output_ids。"
|
||||
}
|
||||
elif not isinstance(wait_sub_agent_ids, list) or not wait_sub_agent_ids:
|
||||
result = {"success": False, "error": "wait_sub_agent_ids 必须是非空数组"}
|
||||
@ -1748,9 +1795,9 @@ class MainTerminalToolsExecutionMixin:
|
||||
from modules.multi_agent.prompts import build_multi_agent_sub_agent_prompt
|
||||
_data_dir = str(getattr(self, "data_dir", ""))
|
||||
_custom_dir = infer_custom_roles_dir(_data_dir)
|
||||
# host模式下 custom_dir 就是 runtime_dir;web模式下 custom_dir 是用户个人目录
|
||||
# host模式下 custom_dir 就是 runtime_dir;web模式下 runtime_dir 指向 web 预设目录
|
||||
_is_web = '/web/users/' in _data_dir
|
||||
_runtime_dir = None if _is_web else _custom_dir
|
||||
_runtime_dir = WEB_PRESET_ROLES_DIR if _is_web else _custom_dir
|
||||
role = load_preset_role(role_id, runtime_dir=_runtime_dir, custom_dir=_custom_dir)
|
||||
if not role:
|
||||
result = {"success": False, "error": f"角色不存在: {role_id}"}
|
||||
@ -1883,7 +1930,7 @@ class MainTerminalToolsExecutionMixin:
|
||||
agent_ids=arguments.get("agent_ids", [])
|
||||
)
|
||||
|
||||
# 多智能体模式专属工具:send_message_to_sub_agent / ask_sub_agent / answer_sub_agent_question / create_custom_agent / list_agents / list_active_sub_agents
|
||||
# 多智能体模式专属工具:send_message_to_sub_agent / stop_sub_agent / answer_sub_agent_question / create_custom_agent / list_agents / list_active_sub_agents
|
||||
elif tool_name == "send_message_to_sub_agent":
|
||||
if not getattr(self, "multi_agent_mode", False):
|
||||
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
|
||||
@ -1922,51 +1969,15 @@ class MainTerminalToolsExecutionMixin:
|
||||
logger.exception("[multi_agent] send_message_to_sub_agent failed")
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "ask_sub_agent":
|
||||
elif tool_name == "stop_sub_agent":
|
||||
if not getattr(self, "multi_agent_mode", False):
|
||||
result = {"success": False, "error": "该工具仅在多智能体模式下可用"}
|
||||
else:
|
||||
try:
|
||||
from modules.multi_agent.state import format_multi_agent_message, TYPE_ASK
|
||||
agent_id = int(arguments.get("agent_id", 0))
|
||||
question = arguments.get("question", "")
|
||||
timeout = int(arguments.get("timeout_seconds", 600))
|
||||
conv_id = self.context_manager.current_conversation_id
|
||||
state = self.sub_agent_manager.get_multi_agent_state(conv_id)
|
||||
if not state:
|
||||
result = {"success": False, "error": "多智能体状态未就绪"}
|
||||
else:
|
||||
question_id = f"ask_sub_agent_{int(time.time() * 1000)}_{uuid.uuid4().hex[:6]}"
|
||||
# 构造提问并插入子对话(子智能体下一轮 assistant 输出作为回答返回到工具结果)
|
||||
text = format_multi_agent_message(
|
||||
display_name="Team Leader",
|
||||
msg_type=TYPE_ASK,
|
||||
content=question,
|
||||
msg_id=question_id,
|
||||
target=state.get_instance(agent_id).display_name if state.get_instance(agent_id) else f"Agent_{agent_id}",
|
||||
)
|
||||
ma_debug(
|
||||
"tool_ask_sub_agent",
|
||||
agent_id=agent_id,
|
||||
question=str(question)[:500],
|
||||
question_id=question_id,
|
||||
wrapped_message_preview=text[:500],
|
||||
conversation_id=conv_id,
|
||||
)
|
||||
ok = self.sub_agent_manager.inject_message_to_sub_agent(agent_id, text)
|
||||
if not ok:
|
||||
result = {"success": False, "error": f"子智能体 {agent_id} 不存在"}
|
||||
else:
|
||||
# 阻塞等待子智能体下一轮输出作为回答
|
||||
answer = await state.wait_for_answer(
|
||||
question_id=question_id,
|
||||
agent_id=agent_id,
|
||||
timeout=timeout,
|
||||
)
|
||||
result = {"success": True, "answer": answer}
|
||||
except asyncio.TimeoutError:
|
||||
result = {"success": False, "error": "等待子智能体回答超时"}
|
||||
result = self.sub_agent_manager.stop_sub_agent(agent_id=agent_id)
|
||||
except Exception as exc:
|
||||
logger.exception("[multi_agent] stop_sub_agent failed")
|
||||
result = {"success": False, "error": str(exc)}
|
||||
|
||||
elif tool_name == "answer_sub_agent_question":
|
||||
|
||||
@ -286,6 +286,9 @@ class MultiAgentState:
|
||||
# 一个 agent 可能同时只阻塞在一个 ask 工具上(最简实现)
|
||||
# key = agent_id, value = question_id(表示当前 agent 正阻塞等待)
|
||||
self.agent_blocking_question: Dict[int, str] = {}
|
||||
# 主智能体通过 sleep(wait_sub_agent_output_ids) 等待某个子智能体下一次输出
|
||||
# key = agent_id, value = asyncio.Future(结果为完整消息文本)
|
||||
self.output_waits: Dict[int, asyncio.Future] = {}
|
||||
# 角色实例计数:role_id -> 已分配的最大 agent_id(数字)
|
||||
# 用于创建新实例时自动递增编号,但允许调用方显式指定
|
||||
self.role_counters: Dict[str, int] = {}
|
||||
@ -313,17 +316,45 @@ class MultiAgentState:
|
||||
return self.agents.get(aid)
|
||||
|
||||
def list_active(self) -> List[AgentInstance]:
|
||||
return [a for a in self.agents.values() if a.status == "running" or a.status == "idle"]
|
||||
# failed 视为可复活状态,仍算活跃
|
||||
return [a for a in self.agents.values() if a.status in ("running", "idle", "failed")]
|
||||
|
||||
def list_all(self) -> List[AgentInstance]:
|
||||
return list(self.agents.values())
|
||||
|
||||
def mark_status(self, agent_id: int, status: str, last_output: str = "") -> None:
|
||||
a = self.agents.get(agent_id)
|
||||
if a:
|
||||
a.status = status
|
||||
if last_output:
|
||||
a.last_output = last_output
|
||||
if not a:
|
||||
return
|
||||
a.status = status
|
||||
if last_output:
|
||||
a.last_output = last_output
|
||||
# 当子智能体进入终态/idle 时,立即取消 sleep(wait_sub_agent_output_ids) 的等待
|
||||
if status in ("failed", "terminated", "idle"):
|
||||
self._cancel_output_wait(agent_id, status)
|
||||
|
||||
def _cancel_output_wait(self, agent_id: int, status: str) -> None:
|
||||
"""取消指定 agent 的 sleep 输出等待,并提示可用 send_message_to_sub_agent 重新激活。"""
|
||||
fut = self.output_waits.pop(agent_id, None)
|
||||
if not fut or fut.done():
|
||||
return
|
||||
if status == "terminated":
|
||||
msg = f"子智能体 {agent_id} 已终止,无法继续等待输出。"
|
||||
elif status == "failed":
|
||||
msg = f"子智能体 {agent_id} 已失败,无法继续等待输出。该子智能体可被复活,可用 send_message_to_sub_agent 重新激活。"
|
||||
else:
|
||||
msg = f"子智能体 {agent_id} 已进入空闲状态,无法继续等待输出。可用 send_message_to_sub_agent 重新激活。"
|
||||
try:
|
||||
loop = fut.get_loop()
|
||||
if loop and not loop.is_closed():
|
||||
loop.call_soon_threadsafe(fut.set_exception, RuntimeError(msg))
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
fut.set_exception(RuntimeError(msg))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ----- 主对话注入 -----
|
||||
def push_master_message(self, message_text: str) -> None:
|
||||
@ -353,6 +384,76 @@ class MultiAgentState:
|
||||
def has_pending_master_messages(self) -> bool:
|
||||
return len(self.pending_master_messages) > 0
|
||||
|
||||
# ----- 等待子智能体输出(sleep wait_sub_agent_output_ids) -----
|
||||
def register_output_wait(
|
||||
self, agent_id: int, loop: AbstractEventLoop
|
||||
) -> asyncio.Future:
|
||||
"""注册等待指定子智能体的下一次输出。
|
||||
|
||||
如果 `pending_master_messages` 里已经有该子智能体的未消费输出,
|
||||
则立即消费并返回该消息;否则返回一个 future,由后续输出唤醒。
|
||||
"""
|
||||
fut: asyncio.Future = loop.create_future()
|
||||
inst = self.agents.get(agent_id)
|
||||
if not inst:
|
||||
fut.set_exception(ValueError(f"未找到子智能体 {agent_id}"))
|
||||
return fut
|
||||
|
||||
if inst.status in ("terminated", "failed"):
|
||||
fut.set_exception(
|
||||
RuntimeError(
|
||||
f"子智能体 {agent_id} 当前状态为 {inst.status},无法等待输出"
|
||||
)
|
||||
)
|
||||
return fut
|
||||
|
||||
# 优先消费 pending_master_messages 里已有的未消费输出
|
||||
display_name = inst.display_name
|
||||
for i, msg in enumerate(self.pending_master_messages):
|
||||
parsed = parse_multi_agent_message(msg)
|
||||
if parsed and parsed.get("display_name") == display_name:
|
||||
self.pending_master_messages.pop(i)
|
||||
fut.set_result(msg)
|
||||
ma_debug(
|
||||
"state_output_wait_claimed_pending",
|
||||
conversation_id=self.conversation_id,
|
||||
agent_id=agent_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
return fut
|
||||
|
||||
# 没有未消费输出,注册等待
|
||||
self.output_waits[agent_id] = fut
|
||||
ma_debug(
|
||||
"state_output_wait_registered",
|
||||
conversation_id=self.conversation_id,
|
||||
agent_id=agent_id,
|
||||
display_name=display_name,
|
||||
)
|
||||
return fut
|
||||
|
||||
def claim_output_wait(self, agent_id: int, message_text: str) -> bool:
|
||||
"""如果该 agent 正被 sleep 等待输出,则把消息交给等待方并阻止进入主对话。"""
|
||||
fut = self.output_waits.pop(agent_id, None)
|
||||
if not fut:
|
||||
return False
|
||||
loop: Optional[AbstractEventLoop] = None
|
||||
try:
|
||||
loop = fut.get_loop()
|
||||
except Exception:
|
||||
pass
|
||||
if loop and not loop.is_closed():
|
||||
try:
|
||||
loop.call_soon_threadsafe(fut.set_result, message_text)
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
fut.set_result(message_text)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
# ----- 阻塞问答 -----
|
||||
async def wait_for_answer(self, question_id: str, agent_id: int, timeout: float = 600.0) -> str:
|
||||
"""子智能体 ask_* 工具调用后阻塞等待答案。
|
||||
@ -496,6 +597,17 @@ class MultiAgentState:
|
||||
loop.call_soon_threadsafe(fut.cancel)
|
||||
except Exception:
|
||||
pass
|
||||
for fut in list(self.output_waits.values()):
|
||||
if not fut.done():
|
||||
try:
|
||||
loop = fut.get_loop()
|
||||
if loop and not loop.is_closed():
|
||||
loop.call_soon_threadsafe(fut.cancel)
|
||||
else:
|
||||
fut.cancel()
|
||||
except Exception:
|
||||
pass
|
||||
self.output_waits.clear()
|
||||
self.agents.clear()
|
||||
self.task_id_to_agent_id.clear()
|
||||
self.pending_master_messages.clear()
|
||||
|
||||
@ -65,6 +65,23 @@ def _master_tool_create_sub_agent() -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_stop_sub_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "stop_sub_agent",
|
||||
"description": "暂停指定子智能体实例,使其进入 idle 状态而不终结。暂停后可用 send_message_to_sub_agent 重新激活继续工作。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "要暂停的子智能体编号。"},
|
||||
}),
|
||||
"required": ["agent_id"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_terminate_sub_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
@ -104,29 +121,6 @@ def _master_tool_send_message_to_sub_agent() -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_ask_sub_agent() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "ask_sub_agent",
|
||||
"description": (
|
||||
"向指定子智能体提出一个明确问题并阻塞等待一轮回答(不是发起任务,是问问题)。"
|
||||
"问题会以 `来自 Team Leader 的提问` 格式插入子对话,子智能体下一轮 assistant 输出"
|
||||
"作为回答返回到此工具结果中。"
|
||||
),
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": _inject_intent({
|
||||
"agent_id": {"type": "integer", "description": "目标子智能体编号。"},
|
||||
"question": {"type": "string", "description": "要询问的问题,应简短明确。"},
|
||||
"timeout_seconds": {"type": "integer", "description": "等待回答超时秒数,默认 600。"},
|
||||
}),
|
||||
"required": ["agent_id", "question"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _master_tool_answer_sub_agent_question() -> Dict[str, Any]:
|
||||
return {
|
||||
"type": "function",
|
||||
@ -210,9 +204,9 @@ def _master_tool_get_sub_agent_status() -> Dict[str, Any]:
|
||||
|
||||
MULTI_AGENT_MASTER_TOOLS: List[Dict[str, Any]] = [
|
||||
_master_tool_create_sub_agent(),
|
||||
_master_tool_stop_sub_agent(),
|
||||
_master_tool_terminate_sub_agent(),
|
||||
_master_tool_send_message_to_sub_agent(),
|
||||
_master_tool_ask_sub_agent(),
|
||||
_master_tool_answer_sub_agent_question(),
|
||||
_master_tool_create_custom_agent(),
|
||||
_master_tool_list_agents(),
|
||||
|
||||
@ -24,7 +24,7 @@ from config import (
|
||||
from utils.logger import setup_logger
|
||||
from modules.sub_agent.task import SubAgentTask
|
||||
from modules.sub_agent.prompts import build_user_message, build_system_prompt
|
||||
from modules.sub_agent.tools import handle_search_workspace, handle_read_mediafile
|
||||
from modules.sub_agent.tools import handle_read_mediafile
|
||||
from modules.sub_agent.state import SubAgentStateMixin
|
||||
from modules.sub_agent.stats import SubAgentStatsMixin
|
||||
from modules.sub_agent.creation import SubAgentCreationMixin
|
||||
@ -294,7 +294,13 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
def _on_done(fut):
|
||||
try:
|
||||
self._running_tasks.pop(task_id, None)
|
||||
self._sub_agent_instances.pop(agent_id, None)
|
||||
# 多智能体模式下 failed 视为可复活状态,保留实例引用供后续 send_message_to_sub_agent 重新激活
|
||||
if multi_agent_mode:
|
||||
final_task = self.tasks.get(task_id) or {}
|
||||
if final_task.get("status") != "failed":
|
||||
self._sub_agent_instances.pop(agent_id, None)
|
||||
else:
|
||||
self._sub_agent_instances.pop(agent_id, None)
|
||||
self.reconcile_task_states(conversation_id=conversation_id)
|
||||
# 多智能体模式:结束时把状态写回 MultiAgentState
|
||||
if multi_agent_mode and multi_agent_state:
|
||||
@ -434,6 +440,59 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
)
|
||||
return count
|
||||
|
||||
def stop_sub_agent(
|
||||
self,
|
||||
*,
|
||||
task_id: Optional[str] = None,
|
||||
agent_id: Optional[int] = None,
|
||||
) -> Dict:
|
||||
"""暂停指定子智能体,使其进入 idle 状态而不终结。"""
|
||||
task = self._select_task(task_id, agent_id)
|
||||
if not task:
|
||||
return {"success": False, "error": "未找到对应的子智能体任务"}
|
||||
|
||||
real_task_id = task["task_id"]
|
||||
real_agent_id = task.get("agent_id")
|
||||
if not task.get("multi_agent_mode"):
|
||||
return {"success": False, "error": "stop_sub_agent 仅在多智能体模式下可用"}
|
||||
|
||||
# 查找或复活实例,确保能接收软停止信号
|
||||
if real_agent_id is not None:
|
||||
sub_agent = self._find_or_revive_sub_agent_task(real_agent_id)
|
||||
else:
|
||||
sub_agent = None
|
||||
|
||||
if sub_agent and hasattr(sub_agent, "request_soft_stop"):
|
||||
try:
|
||||
sub_agent.request_soft_stop()
|
||||
except Exception as exc:
|
||||
return {"success": False, "error": f"暂停子智能体失败: {exc}"}
|
||||
else:
|
||||
# 没有活实例时直接修改任务状态为 idle
|
||||
task["status"] = "idle"
|
||||
task["updated_at"] = time.time()
|
||||
self._save_state()
|
||||
|
||||
# 同步更新 MultiAgentState
|
||||
conversation_id = task.get("conversation_id")
|
||||
if conversation_id:
|
||||
state = self.get_multi_agent_state(conversation_id)
|
||||
if state and real_agent_id is not None:
|
||||
state.mark_status(real_agent_id, "idle")
|
||||
|
||||
ma_debug(
|
||||
"manager_stop_sub_agent",
|
||||
task_id=real_task_id,
|
||||
agent_id=real_agent_id,
|
||||
had_instance=bool(sub_agent),
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"task_id": real_task_id,
|
||||
"agent_id": real_agent_id,
|
||||
"message": f"子智能体{real_agent_id} 已暂停,可用 send_message_to_sub_agent 重新激活。",
|
||||
}
|
||||
|
||||
def terminate_sub_agent(
|
||||
self,
|
||||
*,
|
||||
@ -634,8 +693,6 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
try:
|
||||
# 多智能体模式常见问答工具已在 SubAgentTask._execute_multi_agent_tool 中处理
|
||||
# 这里只处理实际通过主进程执行的工具
|
||||
if tool_name == "search_workspace":
|
||||
return await handle_search_workspace(self.project_path, self.terminal, arguments)
|
||||
if tool_name == "read_mediafile":
|
||||
return await handle_read_mediafile(self.project_path, arguments)
|
||||
|
||||
@ -910,9 +967,11 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
|
||||
适用于 ask_other_agent / send_message_to_sub_agent / answer_sub_agent_question_
|
||||
(非阻塞到工具结果的路径)。返回 True 表示成功注入。
|
||||
若内存中无运行实例(如 failed 后保留的实例已结束),会尝试从 conversation
|
||||
文件重建子智能体(保留原 agent_id 和 role_id)后再注入消息。
|
||||
"""
|
||||
# 查找该 agent_id 对应的 running SubAgentTask
|
||||
sub_agent = self._find_sub_agent_task_by_agent_id(agent_id)
|
||||
# 查找或复活该 agent_id 对应的 SubAgentTask
|
||||
sub_agent = self._find_or_revive_sub_agent_task(agent_id)
|
||||
ma_debug(
|
||||
"manager_inject_message_to_sub_agent",
|
||||
agent_id=agent_id,
|
||||
@ -925,6 +984,162 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
|
||||
sub_agent.inject_message(message_text)
|
||||
return True
|
||||
|
||||
def _find_or_revive_sub_agent_task(self, agent_id: int) -> Optional[Any]:
|
||||
"""查找内存中的 SubAgentTask;不存在或已结束时从磁盘复活(多智能体模式)。"""
|
||||
inst = self._find_sub_agent_task_by_agent_id(agent_id)
|
||||
if inst is not None:
|
||||
task = getattr(inst, "_task", None)
|
||||
if task is None or not task.done():
|
||||
return inst
|
||||
# 实例存在但已结束,需要复活前先清理旧引用
|
||||
self._sub_agent_instances.pop(agent_id, None)
|
||||
revived = self._revive_sub_agent(agent_id)
|
||||
return revived
|
||||
|
||||
def _revive_sub_agent(self, agent_id: int) -> Optional[Any]:
|
||||
"""从 conversation.json 重建一个多智能体子智能体实例(保留原 agent_id/role_id)。
|
||||
|
||||
用于 failed/idle 等可复活状态被 send_message_to_sub_agent 重新激活的场景。
|
||||
"""
|
||||
from modules.sub_agent.task import SubAgentTask
|
||||
|
||||
candidates = [
|
||||
t for t in self.tasks.values()
|
||||
if isinstance(t, dict) and t.get("agent_id") == agent_id and t.get("multi_agent_mode")
|
||||
]
|
||||
if not candidates:
|
||||
return None
|
||||
# 按创建时间取最新一条
|
||||
candidates.sort(key=lambda item: item.get("created_at", 0), reverse=True)
|
||||
task = candidates[0]
|
||||
task_id = task.get("task_id")
|
||||
if not task_id:
|
||||
return None
|
||||
# 已在运行中则不重复重建
|
||||
if task_id in self._running_tasks:
|
||||
existing_inst = self._sub_agent_instances.get(agent_id)
|
||||
if existing_inst is not None:
|
||||
return existing_inst
|
||||
|
||||
task_root = Path(task.get("task_root", ""))
|
||||
conversation_file = Path(task.get("conversation_file", ""))
|
||||
system_prompt_file = task_root / "system_prompt.txt"
|
||||
task_message_file = task_root / "task.txt"
|
||||
|
||||
if not conversation_file.exists():
|
||||
logger.warning(f"[revive] 任务 {task_id} 的对话文件缺失,无法复活")
|
||||
return None
|
||||
|
||||
try:
|
||||
conversation_data = json.loads(conversation_file.read_text(encoding="utf-8"))
|
||||
messages = list(conversation_data.get("messages") or [])
|
||||
except Exception as exc:
|
||||
logger.warning(f"[revive] 读取任务 {task_id} 对话文件失败: {exc}")
|
||||
return None
|
||||
|
||||
system_prompt = ""
|
||||
if system_prompt_file.exists():
|
||||
try:
|
||||
system_prompt = system_prompt_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
task_message = ""
|
||||
if task_message_file.exists():
|
||||
try:
|
||||
task_message = task_message_file.read_text(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not messages:
|
||||
messages = [
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": task_message},
|
||||
]
|
||||
|
||||
conversation_id = task.get("conversation_id")
|
||||
multi_agent_state = None
|
||||
if conversation_id:
|
||||
multi_agent_state = self.get_or_create_multi_agent_state(conversation_id)
|
||||
if multi_agent_state and not multi_agent_state.get_instance(agent_id):
|
||||
from modules.multi_agent.state import AgentInstance
|
||||
inst = AgentInstance(
|
||||
agent_id=agent_id,
|
||||
role_id=task.get("role_id") or "",
|
||||
display_name=task.get("display_name") or f"Agent_{agent_id}",
|
||||
task_id=task_id,
|
||||
status="idle",
|
||||
summary=task.get("summary", ""),
|
||||
)
|
||||
try:
|
||||
multi_agent_state.register_instance(inst)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
sub_agent = SubAgentTask(
|
||||
manager=self,
|
||||
task_record=task,
|
||||
task_message=task_message,
|
||||
system_prompt=system_prompt,
|
||||
model_key=task.get("model_key"),
|
||||
thinking_mode=task.get("thinking_mode") or "fast",
|
||||
multi_agent_mode=True,
|
||||
multi_agent_state=multi_agent_state,
|
||||
display_name=task.get("display_name"),
|
||||
)
|
||||
sub_agent.messages = messages
|
||||
sub_agent._idle = True
|
||||
task["status"] = "idle"
|
||||
task["updated_at"] = time.time()
|
||||
if multi_agent_state:
|
||||
multi_agent_state.mark_status(agent_id, "idle")
|
||||
# 同步落盘 output.json
|
||||
try:
|
||||
output_file = Path(task.get("output_file", ""))
|
||||
if output_file.exists():
|
||||
output_data = json.loads(output_file.read_text(encoding="utf-8"))
|
||||
else:
|
||||
output_data = {}
|
||||
output_data["status"] = "idle"
|
||||
output_data["success"] = None
|
||||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
|
||||
except Exception as exc:
|
||||
logger.warning(f"[revive] 更新任务 {task_id} output 文件失败: {exc}")
|
||||
|
||||
task_coro = sub_agent.run()
|
||||
asyncio_task = self._run_coro(task_coro)
|
||||
sub_agent._task = asyncio_task
|
||||
self._running_tasks[task_id] = asyncio_task
|
||||
self._sub_agent_instances[agent_id] = sub_agent
|
||||
|
||||
def _on_done(fut, tid=task_id, aid=agent_id, state=multi_agent_state, sa=sub_agent):
|
||||
try:
|
||||
self._running_tasks.pop(tid, None)
|
||||
# failed 保留实例供复活;其余终态清理
|
||||
if state:
|
||||
final_task = self.tasks.get(tid) or {}
|
||||
if final_task.get("status") != "failed":
|
||||
self._sub_agent_instances.pop(aid, None)
|
||||
else:
|
||||
self._sub_agent_instances.pop(aid, None)
|
||||
self.reconcile_task_states(conversation_id=conversation_id)
|
||||
if state:
|
||||
self._on_multi_agent_task_done(tid, aid, state, sa)
|
||||
except Exception as exc:
|
||||
logger.exception(f"[SubAgent] revived task {tid} 完成回调异常: {exc}")
|
||||
ma_debug("manager_revive_on_done_exception", task_id=tid, agent_id=aid, error=str(exc))
|
||||
|
||||
asyncio_task.add_done_callback(_on_done)
|
||||
ma_debug(
|
||||
"manager_revive_sub_agent",
|
||||
task_id=task_id,
|
||||
agent_id=agent_id,
|
||||
display_name=task.get("display_name"),
|
||||
message_count=len(messages),
|
||||
)
|
||||
return sub_agent
|
||||
|
||||
def _find_sub_agent_task_by_agent_id(self, agent_id: int) -> Optional[Any]:
|
||||
"""通过遍历创建中的 task 查找活 SubAgentTask 实例。
|
||||
|
||||
|
||||
@ -396,6 +396,27 @@ class SubAgentTask:
|
||||
try:
|
||||
from modules.multi_agent.state import build_sub_agent_output_text
|
||||
msg = build_sub_agent_output_text(self.display_name, output_text.strip(), is_final=is_final)
|
||||
# 如果该 agent 正被 sleep(wait_sub_agent_output_ids) 等待,把输出交给等待方,
|
||||
# 不再插入主对话(避免同一条消息出现两遍)
|
||||
if self.multi_agent_state.claim_output_wait(self.agent_id, msg):
|
||||
ma_debug(
|
||||
"sub_agent_output_claimed_by_wait",
|
||||
task_id=self.task_id,
|
||||
agent_id=self.agent_id,
|
||||
display_name=self.display_name,
|
||||
msg_preview=msg[:200],
|
||||
)
|
||||
# 同时记录到实例状态,供 list_active_sub_agents 使用
|
||||
inst = self.multi_agent_state.get_instance(self.agent_id)
|
||||
if inst:
|
||||
inst.last_output = output_text[:500]
|
||||
self.emit("progress", {
|
||||
"subtype": "output",
|
||||
"content": output_text,
|
||||
"is_final": is_final,
|
||||
"ts": int(time.time() * 1000),
|
||||
})
|
||||
return
|
||||
self.multi_agent_state.push_master_message(msg)
|
||||
ma_debug(
|
||||
"sub_agent_forward_pushed",
|
||||
@ -694,8 +715,6 @@ class SubAgentTask:
|
||||
self.stats["write_files"] += 1
|
||||
elif name == "edit_file":
|
||||
self.stats["edit_files"] += 1
|
||||
elif name == "search_workspace":
|
||||
self.stats["searches"] += 1
|
||||
elif name in ("web_search", "extract_webpage", "save_webpage"):
|
||||
self.stats["web_pages"] += 1
|
||||
elif name == "run_command":
|
||||
@ -942,7 +961,8 @@ class SubAgentTask:
|
||||
"stats": {**self.stats, "runtime_seconds": runtime_seconds, "turn_count": self.stats.get("turn_count", 0)},
|
||||
}
|
||||
self.conversation_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.conversation_file.write_text(json.dumps(conversation_data, ensure_ascii=False), encoding="utf-8")
|
||||
conversation_json = json.dumps(conversation_data, ensure_ascii=False)
|
||||
self.conversation_file.write_text(conversation_json, encoding="utf-8")
|
||||
|
||||
output_data = {
|
||||
"success": None,
|
||||
@ -951,7 +971,8 @@ class SubAgentTask:
|
||||
"stats": conversation_data["stats"],
|
||||
}
|
||||
self.output_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.output_file.write_text(json.dumps(output_data, ensure_ascii=False), encoding="utf-8")
|
||||
output_json = json.dumps(output_data, ensure_ascii=False)
|
||||
self.output_file.write_text(output_json, encoding="utf-8")
|
||||
except Exception as exc:
|
||||
logger.warning(f"[SubAgentTask] 增量保存失败: {exc}")
|
||||
|
||||
@ -1032,8 +1053,17 @@ class SubAgentTask:
|
||||
idle=self._idle,
|
||||
cancelled=self._cancelled,
|
||||
)
|
||||
if timeout:
|
||||
status = "timeout"
|
||||
elif max_turns_exceeded:
|
||||
status = "failed"
|
||||
elif success:
|
||||
status = "completed"
|
||||
else:
|
||||
status = "failed"
|
||||
output_data = {
|
||||
"success": success,
|
||||
"status": status,
|
||||
"summary": summary,
|
||||
"timeout": timeout,
|
||||
"max_turns_exceeded": max_turns_exceeded,
|
||||
|
||||
@ -145,29 +145,6 @@ SUB_AGENT_TOOLS: List[Dict[str, Any]] = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_workspace",
|
||||
"description": "在本地目录内搜索文件名或文件内容(跨文件)。仅返回摘要。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {"type": "string", "enum": ["file", "content"]},
|
||||
"query": {"type": "string"},
|
||||
"root": {"type": "string", "description": "搜索起点目录,默认 '.'。"},
|
||||
"use_regex": {"type": "boolean"},
|
||||
"case_sensitive": {"type": "boolean"},
|
||||
"max_results": {"type": "integer"},
|
||||
"max_matches_per_file": {"type": "integer"},
|
||||
"include_glob": {"type": "array", "items": {"type": "string"}},
|
||||
"exclude_glob": {"type": "array", "items": {"type": "string"}},
|
||||
"max_file_size": {"type": "integer"},
|
||||
},
|
||||
"required": ["mode", "query"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
@ -338,21 +315,6 @@ def _format_tool_result(name: str, raw: Any) -> str:
|
||||
if raw.get("mode") == "save":
|
||||
return f"已保存: {raw.get('path')}"
|
||||
return f"URL: {raw.get('url')}\n{raw.get('content') or ''}"
|
||||
if name == "search_workspace":
|
||||
if raw.get("mode") == "file":
|
||||
lines = [f"命中文件({len(raw.get('matches') or [])}):"]
|
||||
for idx, p in enumerate(raw.get("matches") or [], 1):
|
||||
lines.append(f"{idx}) {p}")
|
||||
return "\n".join(lines)
|
||||
if raw.get("mode") == "content":
|
||||
parts = []
|
||||
for item in raw.get("results") or []:
|
||||
parts.append(item.get("file") or "")
|
||||
for m in item.get("matches") or []:
|
||||
parts.append(f"- L{m.get('line')}: {m.get('snippet')}")
|
||||
parts.append("")
|
||||
return "\n".join(parts).strip()
|
||||
return ""
|
||||
if name == "read_mediafile":
|
||||
return raw.get("message") or "媒体已读取"
|
||||
if name == "read_skill":
|
||||
|
||||
@ -1,231 +1,16 @@
|
||||
"""子智能体特有工具实现(search_workspace / read_mediafile)。
|
||||
"""子智能体特有工具实现(read_mediafile)。
|
||||
|
||||
这些工具在主进程中没有同名工具,因此由子智能体管理器本地实现。
|
||||
search_workspace 优先复用主进程的 run_command 调用 rg,回退到 Python 遍历;
|
||||
read_mediafile 直接读取项目内媒体文件并返回 base64。
|
||||
(原 search_workspace 工具已移除:子智能体需要搜索时改用 run_command
|
||||
执行 rg / grep / find,走主进程沙箱链路,避免进程内遍历大目录导致内存失控。)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import fnmatch
|
||||
import json
|
||||
import mimetypes
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
async def handle_search_workspace(
|
||||
project_path: Path,
|
||||
terminal: Any,
|
||||
arguments: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""search_workspace 实现。"""
|
||||
mode = arguments.get("mode")
|
||||
query = arguments.get("query") or ""
|
||||
root = arguments.get("root") or "."
|
||||
use_regex = bool(arguments.get("use_regex", False))
|
||||
case_sensitive = bool(arguments.get("case_sensitive", False))
|
||||
max_results = int(arguments.get("max_results", 20))
|
||||
max_matches_per_file = int(arguments.get("max_matches_per_file", 3))
|
||||
include_glob = arguments.get("include_glob") or ["**/*"]
|
||||
exclude_glob = arguments.get("exclude_glob") or []
|
||||
max_file_size = arguments.get("max_file_size")
|
||||
|
||||
if mode not in {"file", "content"}:
|
||||
return {"success": False, "error": "search_workspace mode 必须是 file 或 content"}
|
||||
|
||||
root_path = (project_path / root).resolve()
|
||||
try:
|
||||
root_path.relative_to(project_path)
|
||||
except Exception:
|
||||
return {"success": False, "error": "搜索根目录必须位于项目目录内"}
|
||||
|
||||
if mode == "file":
|
||||
return _search_workspace_file(project_path, root_path, query, use_regex, case_sensitive, max_results, include_glob, exclude_glob)
|
||||
|
||||
rg_result = await _search_workspace_content_rg(
|
||||
project_path, root_path, query, use_regex, case_sensitive, max_results, max_matches_per_file,
|
||||
include_glob, exclude_glob, max_file_size, terminal
|
||||
)
|
||||
if rg_result is not None:
|
||||
return rg_result
|
||||
|
||||
return _search_workspace_content_python(
|
||||
project_path, root_path, query, use_regex, case_sensitive, max_results, max_matches_per_file,
|
||||
include_glob, exclude_glob, max_file_size
|
||||
)
|
||||
|
||||
|
||||
def _search_workspace_file(
|
||||
project_path: Path,
|
||||
root_path: Path,
|
||||
query: str,
|
||||
use_regex: bool,
|
||||
case_sensitive: bool,
|
||||
max_results: int,
|
||||
include_glob: List[str],
|
||||
exclude_glob: List[str],
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
pattern = re.compile(query, 0 if case_sensitive else re.IGNORECASE) if use_regex else None
|
||||
needle = query if case_sensitive else query.lower()
|
||||
matches = []
|
||||
for item in _iter_files(project_path, root_path, include_glob, exclude_glob):
|
||||
name = item.name
|
||||
hay = name if case_sensitive else name.lower()
|
||||
ok = pattern.search(name) if pattern else needle in hay
|
||||
if ok:
|
||||
matches.append(str(item.relative_to(project_path)))
|
||||
if len(matches) >= max_results:
|
||||
break
|
||||
return {
|
||||
"success": True,
|
||||
"mode": "file",
|
||||
"root": str(root_path.relative_to(project_path)),
|
||||
"query": query,
|
||||
"matches": matches,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
|
||||
async def _search_workspace_content_rg(
|
||||
project_path: Path,
|
||||
root_path: Path,
|
||||
query: str,
|
||||
use_regex: bool,
|
||||
case_sensitive: bool,
|
||||
max_results: int,
|
||||
max_matches_per_file: int,
|
||||
include_glob: List[str],
|
||||
exclude_glob: List[str],
|
||||
max_file_size: Optional[int],
|
||||
terminal: Any,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not shutil.which("rg"):
|
||||
return None
|
||||
try:
|
||||
args = ["rg", "--line-number", "--no-heading", "--color=never"]
|
||||
if not use_regex:
|
||||
args.append("--fixed-strings")
|
||||
if not case_sensitive:
|
||||
args.append("-i")
|
||||
if max_matches_per_file:
|
||||
args.append(f"--max-count={max_matches_per_file}")
|
||||
if max_file_size:
|
||||
args.append(f"--max-filesize={max_file_size}")
|
||||
for g in include_glob:
|
||||
args.extend(["-g", g])
|
||||
for g in exclude_glob:
|
||||
args.extend(["-g", f"!{g}"])
|
||||
args.extend([query, str(root_path)])
|
||||
|
||||
result_text = await terminal.handle_tool_call("run_command", {"command": shlex.join(args), "timeout": 60})
|
||||
parsed = json.loads(result_text) if isinstance(result_text, str) else result_text
|
||||
if not parsed.get("success"):
|
||||
return None
|
||||
|
||||
files_map: Dict[str, List[Dict[str, Any]]] = {}
|
||||
for line in (parsed.get("output") or "").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split(":", 2)
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
file_path, line_num, snippet = parts[0], parts[1], parts[2]
|
||||
try:
|
||||
line_no = int(line_num)
|
||||
except ValueError:
|
||||
continue
|
||||
files_map.setdefault(file_path, [])
|
||||
if len(files_map[file_path]) < max_matches_per_file:
|
||||
files_map[file_path].append({"line": line_no, "snippet": snippet.strip()})
|
||||
if len(files_map) >= max_results and len(files_map[file_path]) >= max_matches_per_file:
|
||||
break
|
||||
|
||||
results = [{"file": f, "matches": ms} for f, ms in list(files_map.items())[:max_results]]
|
||||
return {
|
||||
"success": True,
|
||||
"mode": "content",
|
||||
"root": str(root_path.relative_to(project_path)),
|
||||
"query": query,
|
||||
"results": results,
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _search_workspace_content_python(
|
||||
project_path: Path,
|
||||
root_path: Path,
|
||||
query: str,
|
||||
use_regex: bool,
|
||||
case_sensitive: bool,
|
||||
max_results: int,
|
||||
max_matches_per_file: int,
|
||||
include_glob: List[str],
|
||||
exclude_glob: List[str],
|
||||
max_file_size: Optional[int],
|
||||
) -> Dict[str, Any]:
|
||||
try:
|
||||
pattern = re.compile(query, 0 if case_sensitive else re.IGNORECASE) if use_regex else None
|
||||
needle = query if case_sensitive else query.lower()
|
||||
results = []
|
||||
for file_path in _iter_files(project_path, root_path, include_glob, exclude_glob):
|
||||
if max_file_size:
|
||||
try:
|
||||
if file_path.stat().st_size > max_file_size:
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
try:
|
||||
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
||||
except Exception:
|
||||
continue
|
||||
lines = text.splitlines()
|
||||
matches = []
|
||||
for idx, line in enumerate(lines, 1):
|
||||
hay = line if case_sensitive else line.lower()
|
||||
ok = pattern.search(line) if pattern else needle in hay
|
||||
if ok:
|
||||
matches.append({"line": idx, "snippet": line.strip()})
|
||||
if len(matches) >= max_matches_per_file:
|
||||
break
|
||||
if matches:
|
||||
results.append({"file": str(file_path.relative_to(project_path)), "matches": matches})
|
||||
if len(results) >= max_results:
|
||||
break
|
||||
return {
|
||||
"success": True,
|
||||
"mode": "content",
|
||||
"root": str(root_path.relative_to(project_path)),
|
||||
"query": query,
|
||||
"results": results,
|
||||
}
|
||||
except Exception as exc:
|
||||
return {"success": False, "error": str(exc)}
|
||||
|
||||
|
||||
def _iter_files(project_path: Path, root_path: Path, include_glob: List[str], exclude_glob: List[str]):
|
||||
"""按 glob 规则遍历文件。"""
|
||||
for item in root_path.rglob("*"):
|
||||
if not item.is_file():
|
||||
continue
|
||||
try:
|
||||
rel = item.relative_to(project_path)
|
||||
except Exception:
|
||||
continue
|
||||
rel_str = str(rel)
|
||||
included = any(fnmatch.fnmatch(rel_str, g) for g in include_glob)
|
||||
if not included:
|
||||
continue
|
||||
if any(fnmatch.fnmatch(rel_str, g) for g in exclude_glob):
|
||||
continue
|
||||
yield item
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
async def handle_read_mediafile(project_path: Path, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
||||
|
||||
@ -161,6 +161,54 @@
|
||||
|
||||
---
|
||||
|
||||
## 信息诚实与验证规范
|
||||
|
||||
### 1. 信息来源必须明确标注
|
||||
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
|
||||
|
||||
- **基于我已有知识的回答**——模型训练知识,无实时来源;
|
||||
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
|
||||
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
|
||||
|
||||
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
|
||||
|
||||
### 2. 结论确定性必须分级
|
||||
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
|
||||
|
||||
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
|
||||
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
|
||||
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
|
||||
|
||||
禁止把"很大概率"或"有可能"说成确定结论。
|
||||
|
||||
### 3. 工作完成状态禁止默认等同于操作完成
|
||||
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
|
||||
|
||||
因此:
|
||||
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了";
|
||||
- 必须说明实际做了什么操作,以及当前的验证状态;
|
||||
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
|
||||
|
||||
### 4. 替代方案必须经用户许可并说明缺陷
|
||||
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
|
||||
|
||||
1. 明确得到用户的许可;
|
||||
2. 清楚说明该方案与用户原始要求的差异;
|
||||
3. 说明可能带来的缺陷、风险或错误。
|
||||
|
||||
没有得到明确许可前,禁止擅自采用折中方案。
|
||||
|
||||
### 5. 异常排查优先怀疑自己
|
||||
遇到与预期不符的情况时,排查顺序必须是:
|
||||
|
||||
1. 先怀疑**我的操作有问题**;
|
||||
2. 再怀疑**我改的地方错误了**;
|
||||
3. 最后再考虑外部环境或用户侧因素。
|
||||
|
||||
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
|
||||
|
||||
---
|
||||
|
||||
## 5. 操作技巧
|
||||
|
||||
- **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新
|
||||
|
||||
@ -178,6 +178,54 @@
|
||||
|
||||
---
|
||||
|
||||
## 信息诚实与验证规范
|
||||
|
||||
### 1. 信息来源必须明确标注
|
||||
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
|
||||
|
||||
- **基于我已有知识的回答**——模型训练知识,无实时来源;
|
||||
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
|
||||
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
|
||||
|
||||
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
|
||||
|
||||
### 2. 结论确定性必须分级
|
||||
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
|
||||
|
||||
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
|
||||
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
|
||||
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
|
||||
|
||||
禁止把"很大概率"或"有可能"说成确定结论。
|
||||
|
||||
### 3. 工作完成状态禁止默认等同于操作完成
|
||||
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
|
||||
|
||||
因此:
|
||||
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了";
|
||||
- 必须说明实际做了什么操作,以及当前的验证状态;
|
||||
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
|
||||
|
||||
### 4. 替代方案必须经用户许可并说明缺陷
|
||||
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
|
||||
|
||||
1. 明确得到用户的许可;
|
||||
2. 清楚说明该方案与用户原始要求的差异;
|
||||
3. 说明可能带来的缺陷、风险或错误。
|
||||
|
||||
没有得到明确许可前,禁止擅自采用折中方案。
|
||||
|
||||
### 5. 异常排查优先怀疑自己
|
||||
遇到与预期不符的情况时,排查顺序必须是:
|
||||
|
||||
1. 先怀疑**我的操作有问题**;
|
||||
2. 再怀疑**我改的地方错误了**;
|
||||
3. 最后再考虑外部环境或用户侧因素。
|
||||
|
||||
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
|
||||
|
||||
---
|
||||
|
||||
## 5. 操作技巧
|
||||
|
||||
- **待办事项**:任务≥2步时使用 `todo_create`;当用户提出稍微复杂的要求、预计超过3个步骤时,应积极创建待办事项,概述≤50字,任务2-6条,完成立即更新
|
||||
|
||||
@ -25,15 +25,40 @@
|
||||
- **监督与引导**:掌握团队全局进度,在子智能体偏离方向时及时干预纠正
|
||||
- **综合与决策**:收集子智能体的产出,综合形成最终结果,对用户负责
|
||||
|
||||
- **除非用户要求,或必须为之,你需要尽可能避免自己亲自去执行任务**
|
||||
你不是单打独斗的执行者,但你的主要价值不在于亲自执行。你拥有完整的工具集(文件读写、终端、搜索、MCP、skill、memory 等),只在必要的统筹、协调和判断中使用它们;具体的读、写、改、验证等操作默认交给子智能体。
|
||||
|
||||
你不是单打独斗的执行者。除非任务极其简单或明确不需要子智能体,否则你应该主动把任务拆解并指派给合适的角色。但同时,你仍然拥有完整的工具集(文件读写、终端、搜索、MCP、skill、memory 等),在需要时可以自己动手。
|
||||
## 你自己做 vs 派给子智能体
|
||||
|
||||
你的核心定位是 **统筹者、协调者、决策者和汇报者**,而不是执行者。你的上下文非常宝贵,必须省着用。
|
||||
|
||||
### 你必须自己做(不可替代)
|
||||
|
||||
1. **理解用户意图**:澄清模糊需求、追问关键信息、把用户的话翻译成可执行目标。
|
||||
2. **整体规划与任务拆解**:决定要做哪些事、按什么顺序、需要哪些角色。
|
||||
3. **分配与协调子智能体**:创建子智能体、给它们下指令、在它们之间传递信息、纠正方向。
|
||||
4. **回答子智能体提问**:通过 `answer_sub_agent_question` 及时回应子智能体的 `ask_master`。
|
||||
5. **综合与最终汇报**:把各子智能体的产出整合成统一结论,用清晰结构向用户汇报。
|
||||
|
||||
### 你应该派给子智能体(默认不自己做)
|
||||
|
||||
1. **项目初期调查**:从对话开始就可以创建子智能体并行调查项目结构、关键文件、代码位置、运行方式。不要等"正式开始"才创建它们。
|
||||
2. **具体实现与修改**:写代码、改配置、调整前端、调试 bug 等。
|
||||
3. **专项调研与对比**:技术选型、竞品分析、文档阅读、方案搜索。
|
||||
4. **验证与检查**:代码审查、测试执行、结果验证。
|
||||
5. **大量文件阅读**:需要读多个文件或长文件才能得出结论时,优先派给子智能体,而不是自己逐行读。
|
||||
|
||||
### 核心原则
|
||||
|
||||
- **默认派出去**:只要不是"理解、规划、协调、决策、汇报"这几类工作,就默认创建子智能体去做。
|
||||
- **尽早并行**:任务一开始就可以创建 2-3 个子智能体并行摸底,你再根据反馈调整方向。
|
||||
- **节省上下文**:尽量避免自己调用 `read_file`、`write_file`、`edit_file`、`run_command` 等消耗上下文的工具;这些操作优先由子智能体完成,你只要看结论。
|
||||
- **只做统筹**:你的价值在于判断"谁做什么、何时干预、怎么综合",而不是亲自敲代码。
|
||||
|
||||
## 任务复杂度评估与投入预算
|
||||
|
||||
在开始执行前,先评估任务复杂度,决定子智能体数量和工具调用预算:
|
||||
在决定派多少子智能体时,评估任务复杂度,决定子智能体数量和工具调用预算:
|
||||
|
||||
- **简单任务**(单一明确目标,1-2 步即可完成):1 个子智能体,约 3-10 次工具调用。甚至可以自己直接做,不需要创建子智能体。
|
||||
- **简单任务**(单一明确目标,1-2 步即可完成):1 个子智能体,约 3-10 次工具调用。
|
||||
- **中等任务**(需要多方面调查或 2-3 个不同角色协作):2-4 个子智能体,每个约 10-15 次工具调用。
|
||||
- **复杂任务**(涉及多个领域、需要深度调查或多阶段执行):可以创建更多子智能体,每个有明确分工的职责范围。
|
||||
|
||||
@ -56,7 +81,8 @@
|
||||
|
||||
## 工作原则
|
||||
|
||||
- **主动分工**:除非任务极其简单或明确不需要子智能体,否则主动把任务拆解并指派给合适的角色。
|
||||
- **明确你的身份**:和子智能体交流的对象是你(Team Leader),不是用户。对子智能体来说,你才是它们的 user。你在委派任务、回答提问、发送引导消息时,代表的是 Team Leader,不要替用户转述或以用户自居。
|
||||
- **主动分工**:根据「你自己做 vs 派给子智能体」的分类,把执行性、调研性、验证性工作默认派给子智能体;你自己专注于理解、规划、协调、决策和汇报。
|
||||
- **明确指令**:委派时写清楚任务目标、范围、产出要求和边界(见上方"委派质量标准")。
|
||||
- **及时回答**:当子智能体通过 `ask_master` 提问时,必须尽快通过 `answer_sub_agent_question` 回答。子智能体在阻塞等待你的回答,拖延会卡住整个团队。
|
||||
- **监督进度**:通过 `list_active_sub_agents` / `get_sub_agent_status` 掌握全局,并在合适的时机引导子智能体。
|
||||
@ -65,12 +91,60 @@
|
||||
- **先宽后窄**:在探索性任务中,引导子智能体先从宽泛的角度了解全局,再逐步聚焦到具体细节。避免一开始就钻入过窄的方向。
|
||||
- **上下文意识**:子智能体的每轮输出都会插入你的对话上下文。引导子智能体精简输出——只汇报关键步骤和结论,不要把冗长的过程细节都推给你。详细过程留在子智能体自己的上下文里。
|
||||
|
||||
---
|
||||
|
||||
## 信息诚实与验证规范
|
||||
|
||||
### 1. 信息来源必须明确标注
|
||||
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
|
||||
|
||||
- **基于我已有知识的回答**——模型训练知识,无实时来源;
|
||||
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
|
||||
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
|
||||
|
||||
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
|
||||
|
||||
### 2. 结论确定性必须分级
|
||||
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
|
||||
|
||||
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
|
||||
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
|
||||
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
|
||||
|
||||
禁止把"很大概率"或"有可能"说成确定结论。
|
||||
|
||||
### 3. 工作完成状态禁止默认等同于操作完成
|
||||
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
|
||||
|
||||
因此:
|
||||
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了";
|
||||
- 必须说明实际做了什么操作,以及当前的验证状态;
|
||||
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
|
||||
|
||||
### 4. 替代方案必须经用户许可并说明缺陷
|
||||
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
|
||||
|
||||
1. 明确得到用户的许可;
|
||||
2. 清楚说明该方案与用户原始要求的差异;
|
||||
3. 说明可能带来的缺陷、风险或错误。
|
||||
|
||||
没有得到明确许可前,禁止擅自采用折中方案。
|
||||
|
||||
### 5. 异常排查优先怀疑自己
|
||||
遇到与预期不符的情况时,排查顺序必须是:
|
||||
|
||||
1. 先怀疑**我的操作有问题**;
|
||||
2. 再怀疑**我改的地方错误了**;
|
||||
3. 最后再考虑外部环境或用户侧因素。
|
||||
|
||||
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
|
||||
|
||||
## 子智能体运行期间的行为规范
|
||||
|
||||
当子智能体正在工作时,你的行为必须遵循以下规则:
|
||||
|
||||
- **严格禁止反复查看状态**:不要在子智能体运行期间反复调用 `get_sub_agent_status` 或 `list_active_sub_agents` 来查看进度。子智能体的输出会自动以消息形式插入你的对话,你不需要主动轮询。反复查状态是浪费时间的行为。
|
||||
- **严格禁止用 sleep 填充等待**:不要在子智能体运行期间反复调用 `sleep` 工具来等待。如果你没有需要立即处理的事情,**立刻停止输出**,等待子智能体的汇报消息自动到达。
|
||||
- **等待子智能体输出的正确方式**:如果你没有需要立即处理的事情,可以选择**立刻停止输出**(等待子智能体的汇报消息自动到达),也可以选择使用 `sleep` 工具的 `wait_sub_agent_output_ids` 主动等待指定子智能体的下一次输出。不要在子智能体运行期间反复调用 `sleep` 做无意义的短延迟。
|
||||
- **无事可做时立刻停止输出**:如果你已经把任务委派出去,当前没有需要回答的 `ask_master` 提问,也没有需要干预的情况,就**停止输出**,不要输出任何文字。子智能体的产出会以 user 消息自动插入你的对话,届时你再继续工作。
|
||||
- **有事可做时主动干预**:如果你在子智能体的输出中发现了需要纠正的错误、可以提供的指导、或需要传递给其他子智能体的信息,立刻用 `send_message_to_sub_agent` 行动。这是你运行期间最有价值的工作。
|
||||
|
||||
|
||||
@ -34,6 +34,54 @@
|
||||
- 生成的文档要清晰、格式正确
|
||||
- 代码要包含必要的注释和说明
|
||||
|
||||
---
|
||||
|
||||
## 信息诚实与验证规范
|
||||
|
||||
### 1. 信息来源必须明确标注
|
||||
任何涉及事实、数据、官方政策、第三方事件的内容,必须向用户说明信息来源,禁止混为一谈:
|
||||
|
||||
- **基于我已有知识的回答**——模型训练知识,无实时来源;
|
||||
- **这是官方的说法**——来自官方文档、官网、官方发布渠道;
|
||||
- **这是第三方的报道**——来自媒体、社区、非官方账号、研究者或间接引用。
|
||||
|
||||
如果同一条回复里同时涉及多种来源,要**分别标注**,禁止笼统写成"据报道"或"官方说"。
|
||||
|
||||
### 2. 结论确定性必须分级
|
||||
对任何判断、原因分析、归因、预测,必须按确定程度显式分级:
|
||||
|
||||
- **我对这个结论标识百分百确定**——有明确证据链,可直接验证;
|
||||
- **这是我觉得有很大概率是因为这个**——证据较强,但仍有其他可能;
|
||||
- **我觉得有可能是这个原因**——只是合理推测,证据不足。
|
||||
|
||||
禁止把"很大概率"或"有可能"说成确定结论。
|
||||
|
||||
### 3. 工作完成状态禁止默认等同于操作完成
|
||||
完成了操作**不等于**任务完成。如果没有经过验证,问题可能仍然未被彻底修复,甚至压根没有被修复。
|
||||
|
||||
因此:
|
||||
- 禁止在没有百分百验证的情况下说"工作完成了""我修复了""我解决了";
|
||||
- 必须说明实际做了什么操作,以及当前的验证状态;
|
||||
- 允许的表述如:"我做了……,并已验证没有问题了""我做了……,但还没有验证""我做了……,无法验证问题是不是真的解决"。
|
||||
|
||||
### 4. 替代方案必须经用户许可并说明缺陷
|
||||
任何与用户原始要求或设计不一致的实现方式,包括替代方案、弱化实现、向后兼容折中、临时绕过,必须同时满足:
|
||||
|
||||
1. 明确得到用户的许可;
|
||||
2. 清楚说明该方案与用户原始要求的差异;
|
||||
3. 说明可能带来的缺陷、风险或错误。
|
||||
|
||||
没有得到明确许可前,禁止擅自采用折中方案。
|
||||
|
||||
### 5. 异常排查优先怀疑自己
|
||||
遇到与预期不符的情况时,排查顺序必须是:
|
||||
|
||||
1. 先怀疑**我的操作有问题**;
|
||||
2. 再怀疑**我改的地方错误了**;
|
||||
3. 最后再考虑外部环境或用户侧因素。
|
||||
|
||||
禁止在没有证据的情况下先说"你可能没有清缓存""你可能没有重启程序""你的原始数据可能有问题"。如果需要用户配合排查外部因素,必须先说明自己已经检查了哪些内容、排除了哪些自身原因。
|
||||
|
||||
# 交付要求
|
||||
|
||||
所有结果文件必须放在指定的交付目录中,包括:
|
||||
@ -54,8 +102,7 @@
|
||||
你拥有以下工具能力:
|
||||
- read_file: 读取文件内容
|
||||
- write_file / edit_file: 创建或修改文件
|
||||
- search_workspace: 搜索文件和代码
|
||||
- run_command: 执行终端命令
|
||||
- run_command: 执行终端命令(需要搜索文件/代码时,用它执行 rg 或 grep -rn / find;务必排除 node_modules、.git 等重目录,例如 grep -rn --exclude-dir=node_modules --exclude-dir=.git)
|
||||
- web_search / extract_webpage: 搜索和提取网页内容
|
||||
- read_mediafile: 读取图片/视频文件
|
||||
- finish_task: 完成任务并退出(必须调用)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
@ -10,7 +11,16 @@ def extract_intent_from_partial(arg_str: str) -> Optional[str]:
|
||||
return None
|
||||
match = re.search(r'"intent"\s*:\s*"([^"]{0,128})', arg_str, re.IGNORECASE | re.DOTALL)
|
||||
if match:
|
||||
return match.group(1)
|
||||
raw = match.group(1)
|
||||
try:
|
||||
# 某些模型会对非 ASCII 字符输出 JSON Unicode 转义(如 \u4e2d\u6587),
|
||||
# 这里用 json.loads 安全解码,失败时回退原始值。
|
||||
decoded = json.loads('"{}"'.format(raw))
|
||||
if isinstance(decoded, str):
|
||||
return decoded
|
||||
except Exception:
|
||||
pass
|
||||
return raw
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@ -350,7 +350,6 @@ def inject_multi_agent_master_message(
|
||||
inline=inline,
|
||||
after_tool_call_id=after_tool_call_id,
|
||||
)
|
||||
|
||||
# 解析标准格式以获取 subtype;解析失败时回退到通用类型
|
||||
try:
|
||||
from modules.multi_agent.state import parse_multi_agent_message
|
||||
|
||||
@ -644,13 +644,20 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
||||
metadata_overrides={
|
||||
"permission_mode": default_permission_mode or getattr(terminal, "get_permission_mode", lambda: "unrestricted")(),
|
||||
"execution_mode": getattr(terminal, "get_execution_mode", lambda: "sandbox")(),
|
||||
"multi_agent_mode": multi_agent_mode,
|
||||
"multi_agent_mode": bool(multi_agent_mode),
|
||||
},
|
||||
)
|
||||
try:
|
||||
cm.current_conversation_id = previous_cm_current
|
||||
except Exception:
|
||||
pass
|
||||
# 同步 terminal 级别的多智能体开关,避免新对话继承旧状态
|
||||
terminal.multi_agent_mode = bool(multi_agent_mode)
|
||||
try:
|
||||
if hasattr(terminal, "sub_agent_manager"):
|
||||
terminal.sub_agent_manager.multi_agent_mode = bool(multi_agent_mode)
|
||||
except Exception:
|
||||
pass
|
||||
result = {
|
||||
"success": True,
|
||||
"conversation_id": conversation_id,
|
||||
@ -661,8 +668,15 @@ def create_conversation(terminal: WebTerminal, workspace: UserWorkspace, usernam
|
||||
result = terminal.create_new_conversation(
|
||||
thinking_mode=thinking_mode,
|
||||
run_mode=run_mode,
|
||||
metadata_overrides={"multi_agent_mode": multi_agent_mode} if multi_agent_mode else None,
|
||||
metadata_overrides={"multi_agent_mode": bool(multi_agent_mode)},
|
||||
)
|
||||
# 同步 terminal 级别的多智能体开关,避免新对话继承旧状态
|
||||
terminal.multi_agent_mode = bool(multi_agent_mode)
|
||||
try:
|
||||
if hasattr(terminal, "sub_agent_manager"):
|
||||
terminal.sub_agent_manager.multi_agent_mode = bool(multi_agent_mode)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if result["success"]:
|
||||
# 仅在当前工作区创建时更新 session 模式;指定其他工作区时由前端切换后自动同步。
|
||||
|
||||
@ -6,7 +6,6 @@ from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
|
||||
def _load_summary_prompt(web_terminal) -> str:
|
||||
"""从 prompts/deep_compression_summary.txt 加载压缩总结提示词。"""
|
||||
try:
|
||||
@ -509,7 +508,6 @@ async def run_deep_compression(
|
||||
"compress_behavior": compress_behavior,
|
||||
"job_id": job_id,
|
||||
})
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"in_place": True,
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
@ -328,6 +329,13 @@ def create_multi_agent_conversation():
|
||||
ctx_manager.current_conversation_id = previous_cm_current
|
||||
except Exception:
|
||||
pass
|
||||
# 同步 terminal 级别的多智能体开关
|
||||
terminal.multi_agent_mode = True
|
||||
try:
|
||||
if hasattr(terminal, "sub_agent_manager"):
|
||||
terminal.sub_agent_manager.multi_agent_mode = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 触发对话列表更新事件
|
||||
try:
|
||||
@ -398,11 +406,28 @@ def list_active_sub_agents_api():
|
||||
if not sub_agent_manager:
|
||||
return jsonify({"success": False, "error": "子智能体管理器未就绪"}), 503
|
||||
state = sub_agent_manager.get_multi_agent_state(conversation_id)
|
||||
agents: List[Dict[str, Any]] = []
|
||||
if state:
|
||||
for a in state.list_all():
|
||||
d = a.to_dict()
|
||||
task = sub_agent_manager.tasks.get(a.task_id)
|
||||
if isinstance(task, dict):
|
||||
d["last_tool"] = task.get("last_tool")
|
||||
stats_file = Path(task.get("stats_file", ""))
|
||||
if stats_file.exists():
|
||||
try:
|
||||
stats = json.loads(stats_file.read_text(encoding="utf-8"))
|
||||
d["current_context_tokens"] = stats.get("current_context_tokens", 0)
|
||||
except Exception:
|
||||
d["current_context_tokens"] = 0
|
||||
else:
|
||||
d["current_context_tokens"] = 0
|
||||
agents.append(d)
|
||||
if not state:
|
||||
return jsonify({"success": True, "agents": []})
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"agents": [a.to_dict() for a in state.list_all()],
|
||||
"agents": agents,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理员监控面板</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/dist/assets/admin.css" />
|
||||
<script src="/static/security.js"></script>
|
||||
</head>
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>管理员策略配置</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/dist/assets/adminPolicy.css" />
|
||||
<script src="/static/security.js"></script>
|
||||
</head>
|
||||
|
||||
BIN
static/apple-touch-icon.png
Normal file
BIN
static/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
35
static/astrion-avatar.svg
Normal file
35
static/astrion-avatar.svg
Normal file
@ -0,0 +1,35 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -4 200 200" aria-hidden="true">
|
||||
<style>
|
||||
.sa-frame, .sa-eye {
|
||||
stroke: #000000;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
fill: none;
|
||||
}
|
||||
.sa-frame {
|
||||
stroke-width: 12;
|
||||
}
|
||||
.sa-eye {
|
||||
stroke-width: 10;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.sa-frame, .sa-eye {
|
||||
stroke: #ffffff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
class="sa-frame"
|
||||
d="M 175.959,107.000 Q 180.000,100.000 175.959,93.000 L 144.041,37.718 Q 140.000,30.718 131.917,30.718 L 68.083,30.718 Q 60.000,30.718 55.959,37.718 L 24.041,93.000 Q 20.000,100.000 24.041,107.000 L 55.959,162.282 Q 60.000,169.282 68.083,169.282 L 131.917,169.282 Q 140.000,169.282 144.041,162.282 Z"
|
||||
/>
|
||||
<path
|
||||
class="sa-eye"
|
||||
d="M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11"
|
||||
transform="translate(84, 96)"
|
||||
/>
|
||||
<path
|
||||
class="sa-eye"
|
||||
d="M 0,-11 Q 0,-5.5 0,0 Q 0,5.5 0,11 Q 0,5.5 0,0 Q 0,-5.5 0,-11"
|
||||
transform="translate(116, 96)"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@ -4,7 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>自定义工具管理</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/dist/assets/adminCustomTools.css" />
|
||||
<script src="/static/security.js"></script>
|
||||
</head>
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>极简模式 V2 - 无缝展开</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>极简模式 Demo</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
BIN
static/favicon-16x16.png
Normal file
BIN
static/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 362 B |
BIN
static/favicon-32x32.png
Normal file
BIN
static/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 716 B |
@ -3,6 +3,10 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>文件管理器</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/file_manager/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
13
static/icons/folder-closed.svg
Normal file
13
static/icons/folder-closed.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 342 B |
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Astrion</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="stylesheet" href="/static/easter-eggs/flood.css">
|
||||
<link rel="stylesheet" href="/static/easter-eggs/snake.css">
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录 - Astrion</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>注册 - Astrion</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@ -41,7 +41,6 @@ export async function mounted() {
|
||||
|
||||
// 立即加载初始数据(并行获取状态,优先同步运行模式)
|
||||
const initialDataPromise = this.loadInitialData();
|
||||
this.refreshProjectGitSummary?.();
|
||||
this.startProjectGitSummaryIdleRefresh?.();
|
||||
this.startConnectionHeartbeat();
|
||||
this.fetchTerminalCount();
|
||||
@ -50,6 +49,8 @@ export async function mounted() {
|
||||
if (initialDataPromise && typeof initialDataPromise.then === 'function') {
|
||||
initialDataPromise
|
||||
.then(() => {
|
||||
// 初始数据加载完成后再刷新 git 摘要,确保 workspace/project_path 已就绪
|
||||
this.refreshProjectGitSummary?.();
|
||||
// 避免在初始加载阶段被覆盖,加载完成后再兜底检查一次
|
||||
this.checkTutorialPrompt();
|
||||
})
|
||||
|
||||
@ -232,6 +232,8 @@ export const loadMethods = {
|
||||
this.fetchNetworkPermission();
|
||||
await this.fetchVersioningStatus(conversationId, { silent: true });
|
||||
this.fetchPendingToolApprovals();
|
||||
this.fetchPendingUserQuestions();
|
||||
this.refreshProjectGitSummary();
|
||||
this.fetchConversationTokenStatistics();
|
||||
this.updateCurrentContextTokens();
|
||||
traceLog('loadConversation:after-history', {
|
||||
|
||||
@ -58,6 +58,35 @@ export const dialogMethods = {
|
||||
this.userQuestionDialogVisible = true;
|
||||
this.userQuestionMinimized = false;
|
||||
},
|
||||
async fetchPendingUserQuestions() {
|
||||
if (!this.currentConversationId) {
|
||||
this.pendingUserQuestions = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/user-questions/pending?conversation_id=${encodeURIComponent(this.currentConversationId)}`
|
||||
);
|
||||
const payload = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !payload?.success) {
|
||||
return;
|
||||
}
|
||||
const items = Array.isArray(payload.items) ? payload.items : [];
|
||||
this.pendingUserQuestions = items;
|
||||
if (items.length > 0) {
|
||||
this.userQuestionActiveIndex = Math.min(
|
||||
Math.max(0, Number(this.userQuestionActiveIndex || 0)),
|
||||
Math.max(0, items.length - 1)
|
||||
);
|
||||
} else {
|
||||
this.userQuestionDialogVisible = false;
|
||||
this.userQuestionMinimized = false;
|
||||
this.userQuestionActiveIndex = 0;
|
||||
}
|
||||
} catch (_error) {
|
||||
// ignore
|
||||
}
|
||||
},
|
||||
approveToolApproval(approvalId) {
|
||||
return this.decideToolApproval(approvalId, 'approved');
|
||||
},
|
||||
|
||||
@ -70,7 +70,7 @@ export const gitMethods = {
|
||||
if (this.gitChangesPanelOpen) {
|
||||
this.loadGitChangesDiff?.();
|
||||
}
|
||||
} catch (_) {
|
||||
} catch (_error) {
|
||||
this.projectGitSummary = null;
|
||||
this.gitChangesDiff = null;
|
||||
} finally {
|
||||
|
||||
@ -151,6 +151,9 @@ export const routeMethods = {
|
||||
this.initialRouteResolved = true;
|
||||
}
|
||||
await this.restoreComposerDraftState('bootstrap-route:conversation');
|
||||
if (this.currentConversationId) {
|
||||
this.fetchPendingUserQuestions?.();
|
||||
}
|
||||
},
|
||||
handlePopState(event) {
|
||||
const state = event.state || {};
|
||||
|
||||
@ -372,6 +372,7 @@ export const socketMethods = {
|
||||
this.fetchConversationTokenStatistics();
|
||||
this.updateCurrentContextTokens();
|
||||
await this.fetchVersioningStatus(this.currentConversationId, { silent: true });
|
||||
this.fetchPendingUserQuestions();
|
||||
} catch (e) {
|
||||
console.warn('获取当前对话标题失败:', e);
|
||||
this.titleReady = true;
|
||||
|
||||
@ -52,10 +52,9 @@
|
||||
</div>
|
||||
<!-- 加载动画或完成图标 -->
|
||||
<div class="summary-status-icon">
|
||||
<component
|
||||
v-if="isSummaryRunning(group.actions, group.id)"
|
||||
:is="getSummaryLoader(group.id)"
|
||||
/>
|
||||
<div v-if="isSummaryRunning(group.actions, group.id)" class="summary-icon-inner">
|
||||
<component :is="getSummaryLoader(group.id)" />
|
||||
</div>
|
||||
<svg
|
||||
v-else
|
||||
class="check-icon"
|
||||
@ -1081,6 +1080,8 @@ onBeforeUnmount(() => {
|
||||
color: var(--claude-text-secondary);
|
||||
line-height: 1.7;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.summary-preview {
|
||||
@ -1157,8 +1158,18 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
min-height: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
/* 严格限制右侧不溢出对话记录边界;上下左允许动画自然延伸,不裁剪 */
|
||||
clip-path: inset(-100% 0 -100% -100%);
|
||||
}
|
||||
|
||||
.summary-icon-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* 整体向左偏移 10px,不影响容器尺寸和布局 */
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
|
||||
@ -204,8 +204,6 @@ function renderToolResult(): string {
|
||||
return renderTerminateSubAgent(result, args);
|
||||
} else if (name === 'send_message_to_sub_agent') {
|
||||
return renderSendMessageToSubAgent(result, args);
|
||||
} else if (name === 'ask_sub_agent') {
|
||||
return renderAskSubAgent(result, args);
|
||||
} else if (name === 'answer_sub_agent_question') {
|
||||
return renderAnswerSubAgentQuestion(result, args);
|
||||
} else if (name === 'create_custom_agent') {
|
||||
@ -832,14 +830,123 @@ function renderTerminalSnapshot(result: any, args: any): string {
|
||||
}
|
||||
|
||||
function renderSleep(result: any, args: any): string {
|
||||
const seconds = args.seconds || args.duration || 0;
|
||||
const status = formatToolStatusLabel(result, '✓ 完成');
|
||||
|
||||
const mode = result?.mode || 'seconds';
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
html += `<div><strong>模式:</strong>${escapeHtml(mode)}</div>`;
|
||||
|
||||
if (mode === 'seconds' || mode === '' || mode === null) {
|
||||
const seconds = args.seconds || args.duration || 0;
|
||||
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
|
||||
if (args.reason) {
|
||||
html += `<div><strong>原因:</strong>${escapeHtml(String(args.reason))}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_output') {
|
||||
const agentId = result?.agent_id ?? args?.wait_sub_agent_output_ids ?? '';
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>等待子智能体:</strong>#${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_ids') {
|
||||
const agentIds = Array.isArray(result?.agent_ids) ? result.agent_ids : [];
|
||||
if (agentIds.length > 0) {
|
||||
html += `<div><strong>等待子智能体:</strong>${agentIds.map(String).join(', ')}</div>`;
|
||||
}
|
||||
const taskIds = Array.isArray(result?.waited_task_ids) ? result.waited_task_ids : [];
|
||||
if (taskIds.length > 0) {
|
||||
html += `<div><strong>任务 ID:</strong>${taskIds.map(String).join(', ')}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_runcommand_id') {
|
||||
const commandId = result?.command_id ?? args?.wait_runcommand_id ?? '';
|
||||
if (commandId !== '') {
|
||||
html += `<div><strong>后台命令 ID:</strong>${escapeHtml(String(commandId))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// content
|
||||
if (mode === 'seconds' || mode === '' || mode === null) {
|
||||
const message = result?.message || '';
|
||||
if (message) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += `<div class="content-label">结果:</div>`;
|
||||
html += `<pre>${escapeHtml(String(message))}</pre>`;
|
||||
html += '</div>';
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_output') {
|
||||
const message = result?.message || '';
|
||||
if (message) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">子智能体输出:</div>';
|
||||
html += `<pre>${escapeHtml(String(message))}</pre>`;
|
||||
html += '</div>';
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_ids') {
|
||||
const results = Array.isArray(result?.results) ? result.results : [];
|
||||
if (results.length > 0) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += `<div class="content-label">已等待 ${results.length} 个子智能体:</div>`;
|
||||
results.forEach((item: any) => {
|
||||
if (!item || typeof item !== 'object') return;
|
||||
const agentId = item.agent_id ?? '?';
|
||||
const taskId = item.task_id ?? '?';
|
||||
const itemStatus = item.status ?? 'unknown';
|
||||
const success = item.success === true;
|
||||
html += '<div class="sub-agent-result-item">';
|
||||
html += `<div><strong>子智能体 #${escapeHtml(String(agentId))}</strong>(任务 ${escapeHtml(String(taskId))})· ${escapeHtml(String(itemStatus))} · ${success ? '✓ 成功' : '✗ 失败'}</div>`;
|
||||
const itemMessage = item.message || item.error || '';
|
||||
if (itemMessage) {
|
||||
const runtime = item.runtime_seconds ?? item.elapsed_seconds ?? null;
|
||||
const runtimeNote = runtime !== null ? `运行 ${Math.round(Number(runtime))} 秒` : '';
|
||||
const stats = item.stats;
|
||||
if (runtimeNote || stats) {
|
||||
html += '<div class="sub-agent-meta">';
|
||||
if (runtimeNote) {
|
||||
html += `<span>${escapeHtml(runtimeNote)}</span>`;
|
||||
}
|
||||
if (stats && typeof stats === 'object') {
|
||||
const statParts = [];
|
||||
if (stats.api_calls != null) statParts.push(`调用 ${stats.api_calls} 次`);
|
||||
if (stats.files_read != null) statParts.push(`阅读 ${stats.files_read} 次`);
|
||||
if (stats.edit_files != null) statParts.push(`编辑 ${stats.edit_files} 次`);
|
||||
if (stats.searches != null) statParts.push(`搜索 ${stats.searches} 次`);
|
||||
if (stats.web_pages != null) statParts.push(`网页 ${stats.web_pages} 个`);
|
||||
if (stats.commands != null) statParts.push(`命令 ${stats.commands} 个`);
|
||||
if (statParts.length > 0) {
|
||||
html += `<span>${escapeHtml(statParts.join(' | '))}</span>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
html += `<pre>${escapeHtml(String(itemMessage))}</pre>`;
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
} else if (mode === 'wait_runcommand_id') {
|
||||
const nested = result?.result;
|
||||
if (nested && typeof nested === 'object') {
|
||||
const output = nested.output || nested.stdout || '';
|
||||
const exitCode = nested.exit_code !== undefined ? nested.exit_code : nested.returncode;
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">后台命令输出:</div>';
|
||||
if (exitCode !== undefined) {
|
||||
html += `<div><strong>退出码:</strong>${escapeHtml(String(exitCode))}</div>`;
|
||||
}
|
||||
if (output) {
|
||||
html += `<pre>${escapeHtml(String(output))}</pre>`;
|
||||
} else {
|
||||
html += '<div class="tool-result-empty">[无输出]</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -1378,42 +1485,6 @@ function renderSendMessageToSubAgent(result: any, args: any): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAskSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
|
||||
const agentId = args.agent_id ?? '';
|
||||
const question = args.question ?? '';
|
||||
const answer =
|
||||
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (question) {
|
||||
const preview = String(question).slice(0, 200);
|
||||
const suffix = String(question).length > 200 ? '…' : '';
|
||||
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (answer && typeof answer === 'string') {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">回答</div>';
|
||||
html += `<pre>${escapeHtml(answer)}</pre>`;
|
||||
html += '</div>';
|
||||
} else if (result?.success) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAnswerSubAgentQuestion(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
|
||||
const questionId = args.question_id ?? result.question_id ?? '';
|
||||
|
||||
@ -159,8 +159,6 @@ export function renderEnhancedToolResult(
|
||||
return renderTerminateSubAgent(result, args);
|
||||
} else if (name === 'send_message_to_sub_agent') {
|
||||
return renderSendMessageToSubAgent(result, args);
|
||||
} else if (name === 'ask_sub_agent') {
|
||||
return renderAskSubAgent(result, args);
|
||||
} else if (name === 'answer_sub_agent_question') {
|
||||
return renderAnswerSubAgentQuestion(result, args);
|
||||
} else if (name === 'create_custom_agent') {
|
||||
@ -804,14 +802,123 @@ function renderTerminalSnapshot(result: any, args: any): string {
|
||||
}
|
||||
|
||||
function renderSleep(result: any, args: any): string {
|
||||
const seconds = args.seconds || args.duration || 0;
|
||||
const status = formatToolStatusLabel(result, '✓ 完成');
|
||||
|
||||
const mode = result?.mode || 'seconds';
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
html += `<div><strong>模式:</strong>${escapeHtml(mode)}</div>`;
|
||||
|
||||
if (mode === 'seconds' || mode === '' || mode === null) {
|
||||
const seconds = args.seconds || args.duration || 0;
|
||||
html += `<div><strong>等待时间:</strong>${seconds}秒</div>`;
|
||||
if (args.reason) {
|
||||
html += `<div><strong>原因:</strong>${escapeHtml(String(args.reason))}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_output') {
|
||||
const agentId = result?.agent_id ?? args?.wait_sub_agent_output_ids ?? '';
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>等待子智能体:</strong>#${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_ids') {
|
||||
const agentIds = Array.isArray(result?.agent_ids) ? result.agent_ids : [];
|
||||
if (agentIds.length > 0) {
|
||||
html += `<div><strong>等待子智能体:</strong>${agentIds.map(String).join(', ')}</div>`;
|
||||
}
|
||||
const taskIds = Array.isArray(result?.waited_task_ids) ? result.waited_task_ids : [];
|
||||
if (taskIds.length > 0) {
|
||||
html += `<div><strong>任务 ID:</strong>${taskIds.map(String).join(', ')}</div>`;
|
||||
}
|
||||
} else if (mode === 'wait_runcommand_id') {
|
||||
const commandId = result?.command_id ?? args?.wait_runcommand_id ?? '';
|
||||
if (commandId !== '') {
|
||||
html += `<div><strong>后台命令 ID:</strong>${escapeHtml(String(commandId))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
// content
|
||||
if (mode === 'seconds' || mode === '' || mode === null) {
|
||||
const message = result?.message || '';
|
||||
if (message) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += `<div class="content-label">结果:</div>`;
|
||||
html += `<pre>${escapeHtml(String(message))}</pre>`;
|
||||
html += '</div>';
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_output') {
|
||||
const message = result?.message || '';
|
||||
if (message) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">子智能体输出:</div>';
|
||||
html += `<pre>${escapeHtml(String(message))}</pre>`;
|
||||
html += '</div>';
|
||||
}
|
||||
} else if (mode === 'wait_sub_agent_ids') {
|
||||
const results = Array.isArray(result?.results) ? result.results : [];
|
||||
if (results.length > 0) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += `<div class="content-label">已等待 ${results.length} 个子智能体:</div>`;
|
||||
results.forEach((item: any) => {
|
||||
if (!item || typeof item !== 'object') return;
|
||||
const agentId = item.agent_id ?? '?';
|
||||
const taskId = item.task_id ?? '?';
|
||||
const itemStatus = item.status ?? 'unknown';
|
||||
const success = item.success === true;
|
||||
html += '<div class="sub-agent-result-item">';
|
||||
html += `<div><strong>子智能体 #${escapeHtml(String(agentId))}</strong>(任务 ${escapeHtml(String(taskId))})· ${escapeHtml(String(itemStatus))} · ${success ? '✓ 成功' : '✗ 失败'}</div>`;
|
||||
const itemMessage = item.message || item.error || '';
|
||||
if (itemMessage) {
|
||||
const runtime = item.runtime_seconds ?? item.elapsed_seconds ?? null;
|
||||
const runtimeNote = runtime !== null ? `运行 ${Math.round(Number(runtime))} 秒` : '';
|
||||
const stats = item.stats;
|
||||
if (runtimeNote || stats) {
|
||||
html += '<div class="sub-agent-meta">';
|
||||
if (runtimeNote) {
|
||||
html += `<span>${escapeHtml(runtimeNote)}</span>`;
|
||||
}
|
||||
if (stats && typeof stats === 'object') {
|
||||
const statParts = [];
|
||||
if (stats.api_calls != null) statParts.push(`调用 ${stats.api_calls} 次`);
|
||||
if (stats.files_read != null) statParts.push(`阅读 ${stats.files_read} 次`);
|
||||
if (stats.edit_files != null) statParts.push(`编辑 ${stats.edit_files} 次`);
|
||||
if (stats.searches != null) statParts.push(`搜索 ${stats.searches} 次`);
|
||||
if (stats.web_pages != null) statParts.push(`网页 ${stats.web_pages} 个`);
|
||||
if (stats.commands != null) statParts.push(`命令 ${stats.commands} 个`);
|
||||
if (statParts.length > 0) {
|
||||
html += `<span>${escapeHtml(statParts.join(' | '))}</span>`;
|
||||
}
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
html += `<pre>${escapeHtml(String(itemMessage))}</pre>`;
|
||||
}
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
}
|
||||
} else if (mode === 'wait_runcommand_id') {
|
||||
const nested = result?.result;
|
||||
if (nested && typeof nested === 'object') {
|
||||
const output = nested.output || nested.stdout || '';
|
||||
const exitCode = nested.exit_code !== undefined ? nested.exit_code : nested.returncode;
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">后台命令输出:</div>';
|
||||
if (exitCode !== undefined) {
|
||||
html += `<div><strong>退出码:</strong>${escapeHtml(String(exitCode))}</div>`;
|
||||
}
|
||||
if (output) {
|
||||
html += `<pre>${escapeHtml(String(output))}</pre>`;
|
||||
} else {
|
||||
html += '<div class="tool-result-empty">[无输出]</div>';
|
||||
}
|
||||
html += '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
@ -1371,42 +1478,6 @@ function renderSendMessageToSubAgent(result: any, args: any): string {
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAskSubAgent(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回答', '✗ 获取失败');
|
||||
const agentId = args.agent_id ?? '';
|
||||
const question = args.question ?? '';
|
||||
const answer =
|
||||
result?.answer ?? result?.content ?? result?.message ?? result?.output ?? result?.result ?? '';
|
||||
|
||||
let html = '<div class="tool-result-meta">';
|
||||
html += `<div><strong>状态:</strong>${status}</div>`;
|
||||
if (agentId !== '') {
|
||||
html += `<div><strong>子智能体 ID:</strong>${escapeHtml(String(agentId))}</div>`;
|
||||
}
|
||||
if (question) {
|
||||
const preview = String(question).slice(0, 200);
|
||||
const suffix = String(question).length > 200 ? '…' : '';
|
||||
html += `<div><strong>问题:</strong>${escapeHtml(preview + suffix)}</div>`;
|
||||
}
|
||||
if (!result?.success && result?.error) {
|
||||
html += `<div><strong>错误:</strong>${escapeHtml(String(result.error))}</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
if (answer && typeof answer === 'string') {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="content-label">回答</div>';
|
||||
html += `<pre>${escapeHtml(answer)}</pre>`;
|
||||
html += '</div>';
|
||||
} else if (result?.success) {
|
||||
html += '<div class="tool-result-content scrollable">';
|
||||
html += '<div class="tool-result-empty">子智能体未返回答复内容。</div>';
|
||||
html += '</div>';
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderAnswerSubAgentQuestion(result: any, args: any): string {
|
||||
const status = formatToolStatusLabel(result, '✓ 已回复', '✗ 回复失败');
|
||||
const questionId = args.question_id ?? result.question_id ?? '';
|
||||
|
||||
@ -170,7 +170,7 @@
|
||||
>目标模式就绪</span
|
||||
>
|
||||
<button
|
||||
v-if="userQuestionMinimized && pendingUserQuestionCount > 0"
|
||||
v-if="pendingUserQuestionCount > 0"
|
||||
type="button"
|
||||
class="floating-project-status__notice floating-project-status__notice--button"
|
||||
@click.stop="$emit('restore-user-question')"
|
||||
@ -828,33 +828,43 @@ const runtimeQueuedMessagesForRender = computed(() => {
|
||||
});
|
||||
|
||||
const projectGitSummaryForRender = computed(() => {
|
||||
if (personalizationStore?.form?.show_git_status_bar === false) return null;
|
||||
if (personalizationStore?.form?.show_git_status_bar === false) {
|
||||
return null;
|
||||
}
|
||||
const summary = props.projectGitSummary;
|
||||
if (!summary?.has_git) return null;
|
||||
if (!summary?.has_git) {
|
||||
return null;
|
||||
}
|
||||
const projectName = String(summary.project_name || '').trim();
|
||||
const branch = String(summary.branch || '').trim();
|
||||
if (!projectName || !branch) return null;
|
||||
return {
|
||||
if (!projectName || !branch) {
|
||||
return null;
|
||||
}
|
||||
const result = {
|
||||
projectName,
|
||||
projectPath: String(summary.project_path || '').trim(),
|
||||
branch,
|
||||
additions: Math.max(0, Number(summary.additions || 0)),
|
||||
deletions: Math.max(0, Number(summary.deletions || 0))
|
||||
};
|
||||
return result;
|
||||
});
|
||||
|
||||
const floatingStatusVisible = computed(() => {
|
||||
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) return false;
|
||||
// 消息队列(预输入/引导消息)存在时隐藏git状态栏,与 / 菜单出现时逻辑相同
|
||||
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance)
|
||||
if (skillSlashMenuOpen.value || fileAtOpen.value || props.quickMenuOpen) {
|
||||
return false;
|
||||
return (
|
||||
}
|
||||
// 消息队列(预输入/引导消息)存在时隐藏git状态栏,与 / 菜单出现时逻辑相同
|
||||
if (runtimeQueuedMessagesForRender.value.length > 0 || props.hasPendingRuntimeGuidance) {
|
||||
return false;
|
||||
}
|
||||
const visible =
|
||||
!!projectGitSummaryForRender.value ||
|
||||
!!props.goalRunning ||
|
||||
!!props.goalModeArmed ||
|
||||
(!!props.userQuestionMinimized && Number(props.pendingUserQuestionCount || 0) > 0) ||
|
||||
(Number(props.activeSubAgentCount || 0) > 0)
|
||||
);
|
||||
(Number(props.pendingUserQuestionCount || 0) > 0) ||
|
||||
(Number(props.activeSubAgentCount || 0) > 0);
|
||||
return visible;
|
||||
});
|
||||
|
||||
const showComposerAvatar = computed(() => {
|
||||
|
||||
@ -104,10 +104,6 @@ const buildText = (entry: ActivityEntry) => {
|
||||
const skillName = args.skill_name || '';
|
||||
return `阅读技能 ${skillName}`;
|
||||
}
|
||||
if (tool === 'search_workspace') {
|
||||
const query = args.query || args.keyword || '';
|
||||
return `在工作区搜索 ${query}`;
|
||||
}
|
||||
if (tool === 'web_search') {
|
||||
const query = args.query || args.q || '';
|
||||
return `在互联网中搜索 ${query}`;
|
||||
|
||||
@ -213,6 +213,7 @@ function submit() {
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 20px 22px 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-question-kicker-row {
|
||||
@ -240,6 +241,9 @@ function submit() {
|
||||
line-height: 1.55;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.01em;
|
||||
white-space: pre-wrap;
|
||||
max-height: min(180px, 30vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-question-nav {
|
||||
@ -297,6 +301,9 @@ function submit() {
|
||||
color: var(--claude-text-secondary);
|
||||
line-height: 1.6;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
max-height: min(160px, 25vh);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-question-options {
|
||||
@ -335,6 +342,7 @@ function submit() {
|
||||
color: var(--claude-text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.42;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.user-question-textarea-wrap {
|
||||
|
||||
@ -2289,20 +2289,64 @@ const updateFloatingMenuPosition = async () => {
|
||||
const button = document.querySelector<HTMLElement>(
|
||||
'.settings-select-wrap.open .settings-select-button'
|
||||
);
|
||||
const menu = document.querySelector<HTMLElement>(
|
||||
'.settings-select-wrap.open .settings-floating-menu'
|
||||
);
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
const rect = button.getBoundingClientRect();
|
||||
const menuWidth = Math.min(300, Math.max(240, window.innerWidth - 32));
|
||||
const left = Math.max(16, Math.min(rect.right - menuWidth, window.innerWidth - menuWidth - 16));
|
||||
const top = Math.max(16, Math.min(rect.bottom + 10, window.innerHeight - 80));
|
||||
|
||||
const padding = 16;
|
||||
const gap = 10;
|
||||
|
||||
// 测量单个选项高度,计算 4 选项固定 max-height
|
||||
const optionEl = menu?.querySelector<HTMLElement>('.settings-menu-option');
|
||||
const optionHeight = optionEl ? optionEl.getBoundingClientRect().height : 44;
|
||||
const menuPadding = 12; // menu padding 6px top + 6px bottom
|
||||
const fixedMaxHeight = Math.round(optionHeight * 4) + menuPadding;
|
||||
|
||||
// 先限制菜单高度为固定值,再次获取实际高度
|
||||
if (menu) {
|
||||
menu.style.maxHeight = `${fixedMaxHeight}px`;
|
||||
}
|
||||
// 重新读取受限后的菜单高度
|
||||
const menuHeight = menu?.getBoundingClientRect().height || fixedMaxHeight;
|
||||
|
||||
const spaceBelow = window.innerHeight - rect.bottom - padding;
|
||||
const spaceAbove = rect.top - padding;
|
||||
|
||||
let top: number;
|
||||
let maxHeight: number | undefined;
|
||||
|
||||
if (spaceBelow >= menuHeight) {
|
||||
// 下方空间足够完整显示
|
||||
top = rect.bottom + gap;
|
||||
} else if (spaceAbove >= menuHeight) {
|
||||
// 上方空间足够完整显示,翻转到上方
|
||||
top = rect.top - menuHeight - gap;
|
||||
} else if (spaceBelow >= spaceAbove) {
|
||||
// 上下都不够,下方空间相对更大:限制高度到可用空间,贴按钮向下
|
||||
const availableHeight = Math.max(80, spaceBelow - gap);
|
||||
maxHeight = availableHeight;
|
||||
top = rect.bottom + gap;
|
||||
} else {
|
||||
// 上方空间相对更大:限制高度到可用空间,翻转到上方
|
||||
const availableHeight = Math.max(80, spaceAbove - gap);
|
||||
maxHeight = availableHeight;
|
||||
top = Math.max(padding, rect.top - availableHeight - gap);
|
||||
}
|
||||
|
||||
floatingMenuStyle.value = {
|
||||
position: 'fixed',
|
||||
top: `${Math.round(top)}px`,
|
||||
left: `${Math.round(left)}px`,
|
||||
right: 'auto',
|
||||
width: `${Math.round(menuWidth)}px`,
|
||||
zIndex: '2147483647'
|
||||
zIndex: '2147483647',
|
||||
...(maxHeight !== undefined ? { maxHeight: `${Math.round(maxHeight)}px` } : {})
|
||||
};
|
||||
};
|
||||
|
||||
@ -3689,8 +3733,9 @@ const applyDefaultHideWorkspaceOption = async (hidden: boolean) => {
|
||||
right: auto;
|
||||
top: auto;
|
||||
width: 300px;
|
||||
max-height: min(420px, 60vh);
|
||||
overflow: auto;
|
||||
max-height: calc(44px * 4 + 12px);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 6px;
|
||||
border-radius: 20px;
|
||||
background: var(--settings-floating-menu-bg);
|
||||
|
||||
@ -217,13 +217,7 @@
|
||||
>
|
||||
<div class="workspace-group-children-inner">
|
||||
<div
|
||||
v-if="getWorkspaceGroup(String(ws.workspace_id || ''))?.loading && !getWorkspaceGroup(String(ws.workspace_id || ''))?.conversations?.length"
|
||||
class="workspace-conversations-loading"
|
||||
>
|
||||
正在加载...
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!getWorkspaceGroup(String(ws.workspace_id || ''))?.conversations?.length"
|
||||
v-if="!getWorkspaceGroup(String(ws.workspace_id || ''))?.conversations?.length"
|
||||
class="workspace-no-conversations"
|
||||
>
|
||||
暂无对话
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { useUiStore } from './ui';
|
||||
import { useConversationStore } from './conversation';
|
||||
|
||||
interface SubAgent {
|
||||
task_id: string;
|
||||
@ -49,13 +50,22 @@ export const useSubAgentStore = defineStore('subAgent', {
|
||||
actions: {
|
||||
async fetchSubAgents() {
|
||||
try {
|
||||
const resp = await fetch('/api/sub_agents');
|
||||
const conversationStore = useConversationStore();
|
||||
let resp;
|
||||
if (conversationStore.multiAgentMode && conversationStore.currentConversationId) {
|
||||
resp = await fetch(`/api/multiagent/active_sub_agents?conversation_id=${encodeURIComponent(conversationStore.currentConversationId)}`);
|
||||
} else {
|
||||
resp = await fetch('/api/sub_agents');
|
||||
}
|
||||
if (!resp.ok) {
|
||||
throw new Error(await resp.text());
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (data.success) {
|
||||
this.subAgents = Array.isArray(data.data) ? data.data : [];
|
||||
const agents = conversationStore.multiAgentMode
|
||||
? (Array.isArray(data.agents) ? data.agents : [])
|
||||
: (Array.isArray(data.data) ? data.data : []);
|
||||
this.subAgents = agents;
|
||||
const activeTaskId = this.activeAgent?.task_id;
|
||||
if (activeTaskId) {
|
||||
const latest = this.subAgents.find((item) => item.task_id === activeTaskId);
|
||||
|
||||
@ -4,6 +4,10 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Terminal Monitor - 实时终端查看器</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/astrion-avatar.svg">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
|
||||
|
||||
<!-- xterm.js 终端模拟器 -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
|
||||
|
||||
@ -40,6 +40,10 @@ class ServerRefactorSmokeTest(unittest.TestCase):
|
||||
import server.chat_flow_runner_helpers as helpers
|
||||
|
||||
self.assertEqual(helpers.extract_intent_from_partial('{"intent": "fix"}'), "fix")
|
||||
self.assertEqual(
|
||||
helpers.extract_intent_from_partial('{"intent": "\\u7b49\\u5f85\\u4e0b\\u8f7d\\u5b8c\\u6210"}'),
|
||||
"等待下载完成",
|
||||
)
|
||||
self.assertEqual(helpers.resolve_monitor_path({"file_path": " a.txt "}), "a.txt")
|
||||
self.assertEqual(helpers.resolve_monitor_memory([1, 2, 3], entry_limit=2), ["1", "2"])
|
||||
|
||||
|
||||
@ -185,63 +185,6 @@ def test_write_file_and_edit():
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def test_search_workspace():
|
||||
"""测试 search_workspace 走 run_command rg 回退。"""
|
||||
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
|
||||
try:
|
||||
manager = SubAgentManager(str(tmp), str(tmp / "data"))
|
||||
terminal = FakeTerminal(tmp)
|
||||
manager.set_terminal(terminal)
|
||||
|
||||
(tmp / "findme.txt").write_text("hello world unique string", encoding="utf-8")
|
||||
|
||||
async def fake_search(project_path, terminal_, arguments):
|
||||
query = arguments["query"]
|
||||
if arguments.get("mode") == "file":
|
||||
matches = [p.name for p in project_path.rglob("*") if p.is_file() and query in p.name]
|
||||
return {"success": True, "mode": "file", "matches": matches}
|
||||
matches = []
|
||||
for p in project_path.rglob("*.txt"):
|
||||
text = p.read_text(encoding="utf-8")
|
||||
if query in text:
|
||||
matches.append({"file": str(p.relative_to(project_path)), "matches": [{"line": 1, "snippet": text}]})
|
||||
return {"success": True, "mode": "content", "results": matches}
|
||||
|
||||
import modules.sub_agent.tools as sat
|
||||
original_search = sat.handle_search_workspace
|
||||
sat.handle_search_workspace = fake_search
|
||||
|
||||
responses = [
|
||||
[make_tool_chunk([{
|
||||
"name": "search_workspace",
|
||||
"args": json.dumps({"mode": "content", "query": "unique string"})
|
||||
}])],
|
||||
[make_tool_chunk([{
|
||||
"name": "finish_task",
|
||||
"args": json.dumps({"success": True, "summary": "搜索完成"})
|
||||
}])],
|
||||
]
|
||||
_, original_model = install_fake_model(responses)
|
||||
try:
|
||||
result = manager.create_sub_agent(
|
||||
agent_id=2,
|
||||
summary="搜索测试",
|
||||
task="测试 search_workspace",
|
||||
deliverables_dir="deliverables2",
|
||||
timeout_seconds=30,
|
||||
conversation_id="conv-test-2",
|
||||
thinking_mode="fast",
|
||||
)
|
||||
assert result["success"], result
|
||||
final = manager.wait_for_completion(task_id=result["task_id"], timeout_seconds=10)
|
||||
assert final["status"] == "completed", final
|
||||
finally:
|
||||
restore_model(original_model)
|
||||
sat.handle_search_workspace = original_search
|
||||
finally:
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
def test_read_mediafile():
|
||||
"""测试 read_mediafile 返回 base64。"""
|
||||
tmp = Path(tempfile.mkdtemp(prefix="sub_agent_test_"))
|
||||
@ -341,7 +284,6 @@ def run_all():
|
||||
print("=" * 60)
|
||||
tests = [
|
||||
("write_file + edit_file", test_write_file_and_edit),
|
||||
("search_workspace", test_search_workspace),
|
||||
("read_mediafile", test_read_mediafile),
|
||||
("terminate", test_terminate),
|
||||
]
|
||||
|
||||
@ -100,7 +100,6 @@ class DeepSeekClientChatMixin:
|
||||
final_messages = self._merge_system_messages(messages)
|
||||
final_messages = self._sanitize_messages_for_model_capability(final_messages)
|
||||
final_messages = self._sanitize_message_fields_for_api(final_messages)
|
||||
|
||||
payload = {
|
||||
"model": api_config["model_id"],
|
||||
"messages": final_messages,
|
||||
|
||||
@ -144,7 +144,13 @@ class TokenMixin:
|
||||
return False
|
||||
|
||||
try:
|
||||
success = self.conversation_manager.update_token_statistics(
|
||||
# 路由到正确的 conversation_manager(与 get_conversation_token_statistics 一致)
|
||||
# 多智能体对话 ID 存储在 multi_agent_conversation_manager 中,
|
||||
# 直接使用 self.conversation_manager 会找不到对话导致统计写入失败
|
||||
target_manager = getattr(
|
||||
self, "_get_conversation_manager_for_id", lambda _: self.conversation_manager
|
||||
)(self.current_conversation_id)
|
||||
success = target_manager.update_token_statistics(
|
||||
self.current_conversation_id,
|
||||
prompt_tokens,
|
||||
completion_tokens,
|
||||
|
||||
@ -267,13 +267,14 @@ def _format_send_message_to_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
return "消息已发送。"
|
||||
|
||||
|
||||
def _format_ask_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
def _format_stop_sub_agent(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("ask_sub_agent", result_data)
|
||||
answer = result_data.get("answer")
|
||||
if answer is None:
|
||||
return "子智能体未返回答复。"
|
||||
return f"子智能体的回答:\n{answer}"
|
||||
return _format_failure("stop_sub_agent", result_data)
|
||||
agent_id = result_data.get("agent_id")
|
||||
message = result_data.get("message") or "子智能体已暂停。"
|
||||
if agent_id is not None:
|
||||
return f"已暂停子智能体 #{agent_id}。{message}"
|
||||
return message
|
||||
|
||||
|
||||
def _format_answer_sub_agent_question(result_data: Dict[str, Any]) -> str:
|
||||
|
||||
@ -38,7 +38,7 @@ from utils.tool_result_formatter.agent_context import (
|
||||
_format_get_sub_agent_status,
|
||||
_format_terminate_sub_agent,
|
||||
_format_send_message_to_sub_agent,
|
||||
_format_ask_sub_agent,
|
||||
_format_stop_sub_agent,
|
||||
_format_answer_sub_agent_question,
|
||||
_format_create_custom_agent,
|
||||
_format_list_agents,
|
||||
@ -131,7 +131,7 @@ TOOL_FORMATTERS = {
|
||||
"close_sub_agent": _format_close_sub_agent,
|
||||
"terminate_sub_agent": _format_terminate_sub_agent,
|
||||
"send_message_to_sub_agent": _format_send_message_to_sub_agent,
|
||||
"ask_sub_agent": _format_ask_sub_agent,
|
||||
"stop_sub_agent": _format_stop_sub_agent,
|
||||
"answer_sub_agent_question": _format_answer_sub_agent_question,
|
||||
"create_custom_agent": _format_create_custom_agent,
|
||||
"list_agents": _format_list_agents,
|
||||
|
||||
@ -142,6 +142,13 @@ def _format_sleep(result_data: Dict[str, Any]) -> str:
|
||||
if not result_data.get("success"):
|
||||
return _format_failure("sleep", result_data)
|
||||
mode = result_data.get("mode")
|
||||
if mode == "wait_sub_agent_output":
|
||||
message = result_data.get("message") or ""
|
||||
agent_id = result_data.get("agent_id")
|
||||
header = f"已收到子智能体 {agent_id} 的输出"
|
||||
if message:
|
||||
return f"{header}\n\n{message}"
|
||||
return header
|
||||
if mode == "wait_sub_agent_ids":
|
||||
results = result_data.get("results") or []
|
||||
header = result_data.get("message") or f"已等待 {len(results)} 个子智能体结束"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user