diff --git a/easyagent/src/batch/index.js b/easyagent/src/batch/index.js index 49ad4c2..388a09c 100644 --- a/easyagent/src/batch/index.js +++ b/easyagent/src/batch/index.js @@ -22,6 +22,7 @@ function parseArgs() { systemPromptFile: null, outputFile: null, statsFile: null, + progressFile: null, agentId: null, modelKey: null, thinkingMode: null, @@ -40,6 +41,8 @@ function parseArgs() { config.outputFile = args[++i]; } else if (arg === '--stats-file' && i + 1 < args.length) { config.statsFile = args[++i]; + } else if (arg === '--progress-file' && i + 1 < args.length) { + config.progressFile = args[++i]; } else if (arg === '--agent-id' && i + 1 < args.length) { config.agentId = args[++i]; } else if (arg === '--model-key' && i + 1 < args.length) { @@ -82,6 +85,19 @@ function updateStats(statsFile, stats) { } } +function appendProgress(progressFile, entry) { + if (!progressFile) return; + try { + const dir = path.dirname(progressFile); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.appendFileSync(progressFile, `${JSON.stringify(entry)}\n`, 'utf8'); + } catch (err) { + console.error(`写入进度失败: ${progressFile}`, err); + } +} + // 主函数 async function main() { const config = parseArgs(); @@ -404,6 +420,21 @@ async function main() { process.exit(0); } + let progressArgs = {}; + try { + progressArgs = JSON.parse(toolCall.function.arguments || '{}'); + } catch (err) { + progressArgs = { _raw: toolCall.function.arguments || '' }; + } + const progressId = toolCall.id || `tool_${Date.now()}_${Math.random().toString(16).slice(2, 8)}`; + appendProgress(config.progressFile, { + id: progressId, + tool: toolName, + status: 'running', + args: progressArgs, + ts: Date.now(), + }); + // 执行其他工具 const result = await executeTool({ workspace: config.workspace, @@ -413,6 +444,14 @@ async function main() { abortSignal: null, }); + appendProgress(config.progressFile, { + id: progressId, + tool: toolName, + status: result && result.success ? 'completed' : 'failed', + args: progressArgs, + ts: Date.now(), + }); + // 更新统计 if (toolName === 'read_file') stats.files_read++; else if (toolName === 'edit_file') stats.edit_files++; diff --git a/modules/sub_agent_manager.py b/modules/sub_agent_manager.py index c31c762..19b03cf 100644 --- a/modules/sub_agent_manager.py +++ b/modules/sub_agent_manager.py @@ -108,6 +108,7 @@ class SubAgentManager: system_prompt_file = task_root / "system_prompt.txt" output_file = task_root / "output.json" stats_file = task_root / "stats.json" + progress_file = task_root / "progress.jsonl" # 构建用户消息 user_message = self._build_user_message(agent_id, summary, task, deliverables_path, timeout_seconds) @@ -127,6 +128,7 @@ class SubAgentManager: "--system-prompt-file", str(system_prompt_file), "--output-file", str(output_file), "--stats-file", str(stats_file), + "--progress-file", str(progress_file), "--agent-id", str(agent_id), "--timeout", str(timeout_seconds), ] @@ -162,6 +164,7 @@ class SubAgentManager: "task_root": str(task_root), "output_file": str(output_file), "stats_file": str(stats_file), + "progress_file": str(progress_file), "pid": process.pid, } self.tasks[task_id] = task_record diff --git a/server/chat_flow_task_main.py b/server/chat_flow_task_main.py index ab25f8b..0fad80b 100644 --- a/server/chat_flow_task_main.py +++ b/server/chat_flow_task_main.py @@ -165,6 +165,12 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, debug_log("[SubAgent] 用户请求停止,终止轮询") break + # 若主对话仍在工具循环中,暂不消费完成事件,避免抢占 system 消息插入 + if getattr(web_terminal, "_tool_loop_active", False): + debug_log("[SubAgent] 主对话工具循环中,延迟后台轮询发送 user 消息") + await asyncio.sleep(1) + continue + updates = manager.poll_updates() debug_log(f"[SubAgent] poll_updates 返回 {len(updates)} 个更新") @@ -189,16 +195,20 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, debug_log(f"[SubAgent] 子智能体{agent_id}完成,状态: {status}") - # 构建 user 消息 - user_message = f"""子智能体{agent_id} ({summary}) 已完成任务。 + # 构建 user 消息(后台完成时才发送) + prefix = "这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成" + user_message = f"""{prefix} + +子智能体{agent_id} ({summary}) 已完成任务。 {result_summary} 交付目录:{deliverables_dir}""" - debug_log(f"[SubAgent] 准备直接调用 process_message_task") - debug_log(f"[SubAgent] 消息内容: {user_message[:100]}...") + debug_log(f"[SubAgent] 准备发送 user_message: {user_message[:100]}...") + has_remaining = False + remaining_count = 0 try: if task_id: web_terminal._announced_sub_agent_tasks.add(task_id) @@ -209,10 +219,30 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, manager._save_state() except Exception as exc: debug_log(f"[SubAgent] 保存通知状态失败: {exc}") - sender('user_message', { - 'message': user_message, - 'conversation_id': conversation_id - }) + + # 计算剩余子智能体状态(用于前端清理等待标记) + if not hasattr(web_terminal, "_announced_sub_agent_tasks"): + web_terminal._announced_sub_agent_tasks = set() + announced = web_terminal._announced_sub_agent_tasks + running_tasks = [ + task for task in manager.tasks.values() + if isinstance(task, dict) + and task.get("status") not in TERMINAL_STATUSES.union({"terminated"}) + and task.get("run_in_background") + and task.get("conversation_id") == conversation_id + ] + pending_notice_tasks = [ + task for task in manager.tasks.values() + if isinstance(task, dict) + and task.get("status") in TERMINAL_STATUSES.union({"terminated"}) + and task.get("run_in_background") + and task.get("conversation_id") == conversation_id + and task.get("task_id") not in announced + and not task.get("notified") + ] + remaining_count = len(running_tasks) + len(pending_notice_tasks) + has_remaining = remaining_count > 0 + # 注册为后台任务,确保刷新后可恢复轮询 from .tasks import task_manager workspace_id = getattr(workspace, "workspace_id", None) or "default" @@ -237,8 +267,23 @@ async def poll_sub_agent_completion(*, web_terminal, workspace, conversation_id, session_data=session_data, ) debug_log(f"[SubAgent] 已创建后台任务: task_id={rec.task_id}") + sender('user_message', { + 'message': user_message, + 'conversation_id': conversation_id, + 'task_id': rec.task_id, + 'sub_agent_notice': True, + 'has_running_sub_agents': has_remaining, + 'remaining_count': remaining_count + }) except Exception as e: debug_log(f"[SubAgent] 创建后台任务失败,回退直接执行: {e}") + sender('user_message', { + 'message': user_message, + 'conversation_id': conversation_id, + 'sub_agent_notice': True, + 'has_running_sub_agents': has_remaining, + 'remaining_count': remaining_count + }) try: task = asyncio.create_task(handle_task_with_sender( terminal=web_terminal, diff --git a/server/chat_flow_task_support.py b/server/chat_flow_task_support.py index 7c0c27b..45eec2d 100644 --- a/server/chat_flow_task_support.py +++ b/server/chat_flow_task_support.py @@ -4,6 +4,8 @@ import asyncio import time from typing import Dict, List, Optional +from modules.sub_agent_manager import TERMINAL_STATUSES + async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = False, after_tool_call_id: Optional[str] = None, web_terminal, sender, debug_log, maybe_mark_failure_from_message): """轮询子智能体任务并通知前端,并把结果插入当前对话上下文。""" @@ -17,11 +19,43 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals try: updates = manager.poll_updates() - debug_log(f"[SubAgent] poll inline={inline} updates={len(updates)}") + debug_log(f"[SubAgent] poll inline={inline} after_tool_call_id={after_tool_call_id} updates={len(updates)}") except Exception as exc: debug_log(f"子智能体状态检查失败: {exc}") return + # 兜底:如果 poll_updates 没命中,但任务已被别处更新为终态且未通知,补发一次 + if not updates: + synthesized = [] + try: + for task_id, task in getattr(manager, "tasks", {}).items(): + if not isinstance(task, dict): + continue + status = task.get("status") + if status not in TERMINAL_STATUSES.union({"terminated"}): + continue + if task.get("notified"): + continue + task_conv_id = task.get("conversation_id") + current_conv_id = getattr(getattr(web_terminal, "context_manager", None), "current_conversation_id", None) + if task_conv_id and current_conv_id and task_conv_id != current_conv_id: + continue + final_result = task.get("final_result") + if not final_result: + try: + final_result = manager._check_task_status(task) + except Exception: + final_result = None + if isinstance(final_result, dict): + synthesized.append(final_result) + except Exception as exc: + debug_log(f"[SubAgent] synthesized updates failed: {exc}") + synthesized = [] + + if synthesized: + updates = synthesized + debug_log(f"[SubAgent] synthesized updates count={len(updates)}") + for update in updates: task_id = update.get("task_id") task_info = manager.tasks.get(task_id) if task_id else None @@ -41,6 +75,7 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals message = update.get("system_message") if not message: + debug_log(f"[SubAgent] update missing system_message: task={task_id} keys={list(update.keys())}") continue debug_log(f"[SubAgent] update task={task_id} inline={inline} msg={message}") @@ -72,16 +107,22 @@ async def process_sub_agent_updates(*, messages: List[Dict], inline: bool = Fals insert_index = idx + 1 break - # 直接插入 user 消息,确保下一轮调用能看到子智能体完成通知 + # 运行中插入 system 消息,避免触发新的 user 轮次;非运行中保持 user 通知 + insert_role = "system" if inline else "user" + if not inline: + prefix = "这是一句系统自动发送的user消息,用于通知你子智能体已经运行完成" + if not message.startswith(prefix): + message = f"{prefix}\n\n{message}" messages.insert(insert_index, { - "role": "user", + "role": insert_role, "content": message, "metadata": {"sub_agent_notice": True, "inline": inline, "task_id": task_id} }) - debug_log(f"[SubAgent] 插入子智能体通知位置: {insert_index}") + debug_log(f"[SubAgent] 插入子智能体通知位置: {insert_index} role={insert_role} after_tool_call_id={after_tool_call_id}") sender('system_message', { 'content': message, - 'inline': inline + 'inline': inline, + 'sub_agent_notice': True }) maybe_mark_failure_from_message(web_terminal, message) diff --git a/server/chat_flow_tool_loop.py b/server/chat_flow_tool_loop.py index bb53d8a..2ed3dcd 100644 --- a/server/chat_flow_tool_loop.py +++ b/server/chat_flow_tool_loop.py @@ -16,6 +16,8 @@ from config import TOOL_CALL_COOLDOWN async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, client_sid: str, username: str, iteration: int, conversation_id: Optional[str], last_tool_call_time: float, process_sub_agent_updates, maybe_mark_failure_from_message, mark_force_thinking, get_stop_flag, clear_stop_flag): + previous_tool_loop_active = getattr(web_terminal, "_tool_loop_active", False) + web_terminal._tool_loop_active = True # 执行每个工具 for tool_call in tool_calls: # 检查停止标志 @@ -51,6 +53,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie 'reason': 'user_stop' }) clear_stop_flag(client_sid, username) + web_terminal._tool_loop_active = previous_tool_loop_active return {"stopped": True, "last_tool_call_time": last_tool_call_time} # 工具调用间隔控制 @@ -290,6 +293,7 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie }) clear_stop_flag(client_sid, username) debug_log("[停止检测] 返回stopped=True") + web_terminal._tool_loop_active = previous_tool_loop_active return {"stopped": True, "last_tool_call_time": last_tool_call_time} else: tool_result = await tool_task @@ -475,8 +479,8 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie "content": tool_message_content }) - if function_name not in {'write_file', 'edit_file'}: - await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message) + debug_log(f"[SubAgent] after tool={function_name} call_id={tool_call_id} -> poll updates") + await process_sub_agent_updates(messages=messages, inline=True, after_tool_call_id=tool_call_id, web_terminal=web_terminal, sender=sender, debug_log=debug_log, maybe_mark_failure_from_message=maybe_mark_failure_from_message) await asyncio.sleep(0.2) @@ -484,5 +488,5 @@ async def execute_tool_calls(*, web_terminal, tool_calls, sender, messages, clie mark_force_thinking(web_terminal, reason=f"{function_name}_failed") + web_terminal._tool_loop_active = previous_tool_loop_active return {"stopped": False, "last_tool_call_time": last_tool_call_time} - diff --git a/server/conversation.py b/server/conversation.py index d801dd2..cdd4aa5 100644 --- a/server/conversation.py +++ b/server/conversation.py @@ -452,20 +452,36 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s if not manager: return jsonify({"success": True, "data": []}) try: + try: + # 防止不同进程创建的子智能体未被当前进程感知 + manager._load_state() + except Exception: + pass conversation_id = terminal.context_manager.current_conversation_id data = manager.get_overview(conversation_id=conversation_id) - debug_log("[SubAgent] /api/sub_agents overview", { - "conversation_id": conversation_id, - "count": len(data), - "tasks": [ - { - "task_id": item.get("task_id"), - "status": item.get("status"), - "run_in_background": item.get("run_in_background"), - "conversation_id": item.get("conversation_id") - } for item in data - ] - }) + if not data: + # 若当前对话暂无数据但存在运行中子智能体,回退为全局运行态,避免面板空白 + all_overview = manager.get_overview(conversation_id=None) + if all_overview: + terminal_statuses = TERMINAL_STATUSES.union({"terminated"}) + running_only = [item for item in all_overview if item.get("status") not in terminal_statuses] + if running_only: + data = running_only + debug_log( + "[SubAgent] /api/sub_agents overview " + + json.dumps({ + "conversation_id": conversation_id, + "count": len(data), + "tasks": [ + { + "task_id": item.get("task_id"), + "status": item.get("status"), + "run_in_background": item.get("run_in_background"), + "conversation_id": item.get("conversation_id"), + } for item in data + ], + }, ensure_ascii=False) + ) if not hasattr(terminal, "_announced_sub_agent_tasks"): terminal._announced_sub_agent_tasks = set() announced = terminal._announced_sub_agent_tasks @@ -498,22 +514,82 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s and (status in TERMINAL_STATUSES or status == "terminated") ) item["notice_pending"] = notice_pending - debug_log("[SubAgent] /api/sub_agents notice_pending computed", { - "conversation_id": conversation_id, - "tasks": [ - { - "task_id": item.get("task_id"), - "status": item.get("status"), - "run_in_background": item.get("run_in_background"), - "notice_pending": item.get("notice_pending") - } for item in data - ] - }) + debug_log( + "[SubAgent] /api/sub_agents notice_pending computed " + + json.dumps({ + "conversation_id": conversation_id, + "tasks": [ + { + "task_id": item.get("task_id"), + "status": item.get("status"), + "run_in_background": item.get("run_in_background"), + "notice_pending": item.get("notice_pending"), + } for item in data + ], + }, ensure_ascii=False) + ) return jsonify({"success": True, "data": data}) except Exception as exc: return jsonify({"success": False, "error": str(exc)}), 500 +@conversation_bp.route('/api/sub_agents//activity', methods=['GET']) +@api_login_required +@with_terminal +def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserWorkspace, username: str): + """返回指定子智能体的活动记录(进度)。""" + manager = getattr(terminal, "sub_agent_manager", None) + if not manager: + return jsonify({"success": False, "error": "子智能体管理器不可用"}), 404 + try: + try: + manager._load_state() + except Exception: + pass + + task = manager.tasks.get(task_id) + if not task: + return jsonify({"success": False, "error": "未找到对应子智能体任务"}), 404 + + progress_file = task.get("progress_file") + if not progress_file: + task_root = task.get("task_root") + if task_root: + progress_file = str(Path(task_root) / "progress.jsonl") + + entries: List[Dict[str, Any]] = [] + limit = request.args.get("limit", "200") + try: + limit_num = max(1, min(int(limit), 1000)) + except Exception: + limit_num = 200 + + if progress_file and Path(progress_file).exists(): + try: + lines = Path(progress_file).read_text(encoding="utf-8").splitlines() + if limit_num: + lines = lines[-limit_num:] + for line in lines: + line = line.strip() + if not line: + continue + try: + entries.append(json.loads(line)) + except Exception: + continue + except Exception as exc: + return jsonify({"success": False, "error": f"读取进度失败: {exc}"}), 500 + + payload = { + "task_id": task_id, + "status": task.get("status"), + "entries": entries, + } + return jsonify({"success": True, "data": payload}) + except Exception as exc: + return jsonify({"success": False, "error": str(exc)}), 500 + + @conversation_bp.route('/api/conversations//duplicate', methods=['POST']) @api_login_required @with_terminal diff --git a/static/src/App.vue b/static/src/App.vue index 90ed59b..fb5a925 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -347,6 +347,7 @@ @confirm="handleConfirmReview" /> +
0; + } + } this.$forceUpdate(); this.conditionalScrollToBottom(); }, diff --git a/static/src/app/methods/tooling.ts b/static/src/app/methods/tooling.ts index 0c20acf..da45ad7 100644 --- a/static/src/app/methods/tooling.ts +++ b/static/src/app/methods/tooling.ts @@ -55,6 +55,43 @@ export const toolingMethods = { this.toolActionIndex.clear(); }, + clearPendingTools(reason = 'unspecified') { + this.messages.forEach(msg => { + if (!msg.actions) { + return; + } + msg.actions.forEach(action => { + if (action.type !== 'tool' || !action.tool) { + return; + } + const status = + typeof action.tool.status === 'string' + ? action.tool.status.toLowerCase() + : ''; + if (!status || ['preparing', 'running', 'pending', 'queued', 'stale'].includes(status)) { + action.tool.status = 'cancelled'; + action.tool.message = action.tool.message || '已停止'; + } + }); + }); + if (this.preparingTools && this.preparingTools.clear) { + this.preparingTools.clear(); + } + if (this.activeTools && this.activeTools.clear) { + this.activeTools.clear(); + } + if (this.toolActionIndex && this.toolActionIndex.clear) { + this.toolActionIndex.clear(); + } + if (this.toolStacks && this.toolStacks.clear) { + this.toolStacks.clear(); + } + if (typeof this.toolResetTracking === 'function') { + this.toolResetTracking(); + } + debugLog('清理待处理工具', { reason }); + }, + hasPendingToolActions() { const mapHasEntries = map => map && typeof map.size === 'number' && map.size > 0; if (mapHasEntries(this.preparingTools) || mapHasEntries(this.activeTools)) { diff --git a/static/src/app/methods/ui.ts b/static/src/app/methods/ui.ts index 25074cd..04cb8d0 100644 --- a/static/src/app/methods/ui.ts +++ b/static/src/app/methods/ui.ts @@ -207,6 +207,11 @@ export const uiMethods = { selectPanelMode(mode) { this.uiSetPanelMode(mode); this.uiSetPanelMenuOpen(false); + if (mode === 'subAgents') { + // 切换到子智能体面板时立即刷新,避免等待轮询间隔 + this.subAgentFetch(); + this.subAgentStartPolling(); + } }, openPersonalPage() { @@ -754,11 +759,18 @@ export const uiMethods = { }, addSystemMessage(content) { + if (this.hideSystemMessages) { + return; + } this.chatAddSystemMessage(content); this.$forceUpdate(); this.conditionalScrollToBottom(); }, + appendSystemAction(content) { + this.addSystemMessage(content); + }, + startTitleTyping(title: string, options: { animate?: boolean } = {}) { if (this.titleTypingTimer) { clearInterval(this.titleTypingTimer); diff --git a/static/src/app/state.ts b/static/src/app/state.ts index 8279f53..b807f38 100644 --- a/static/src/app/state.ts +++ b/static/src/app/state.ts @@ -11,6 +11,8 @@ export function dataState() { usePollingMode: true, // 后台子智能体等待状态 waitingForSubAgent: false, + // 是否在对话区展示 system 消息 + hideSystemMessages: true, // 工具状态跟踪 preparingTools: new Map(), diff --git a/static/src/components/overlay/SubAgentActivityDialog.vue b/static/src/components/overlay/SubAgentActivityDialog.vue new file mode 100644 index 0000000..ec86180 --- /dev/null +++ b/static/src/components/overlay/SubAgentActivityDialog.vue @@ -0,0 +1,119 @@ + + + diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index d591cd1..018497d 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -880,6 +880,22 @@ export async function initializeLegacySocket(ctx: any) { ctx.taskInProgress = true; ctx.streamingMessage = false; ctx.stopRequested = false; + if (data?.sub_agent_notice && data?.task_id && ctx.usePollingMode) { + if (typeof data?.has_running_sub_agents === 'boolean') { + ctx.waitingForSubAgent = data.has_running_sub_agents; + } else if (typeof data?.remaining_count === 'number') { + ctx.waitingForSubAgent = data.remaining_count > 0; + } + (async () => { + try { + const { useTaskStore } = await import('../stores/task'); + const taskStore = useTaskStore(); + taskStore.resumeTask(data.task_id); + } catch (error) { + console.warn('恢复任务轮询失败', error); + } + })(); + } ctx.$forceUpdate(); ctx.conditionalScrollToBottom(); }); diff --git a/static/src/stores/subAgent.ts b/static/src/stores/subAgent.ts index 40440ba..1b662cf 100644 --- a/static/src/stores/subAgent.ts +++ b/static/src/stores/subAgent.ts @@ -10,15 +10,34 @@ interface SubAgent { conversation_id?: string; } +interface SubAgentActivityEntry { + id?: string; + tool?: string; + status?: string; + args?: Record; + ts?: number; + error?: string; +} + interface SubAgentState { subAgents: SubAgent[]; pollTimer: ReturnType | null; + activityTimer: ReturnType | null; + activeAgent: SubAgent | null; + activityEntries: SubAgentActivityEntry[]; + activityLoading: boolean; + activityError: string | null; } export const useSubAgentStore = defineStore('subAgent', { state: (): SubAgentState => ({ subAgents: [], - pollTimer: null + pollTimer: null, + activityTimer: null, + activeAgent: null, + activityEntries: [], + activityLoading: false, + activityError: null }), actions: { async fetchSubAgents() { @@ -56,13 +75,55 @@ export const useSubAgentStore = defineStore('subAgent', { if (!agent || !agent.task_id) { return; } - const base = this.getBaseUrl(); - const parentConv = agent.conversation_id || ''; - const convSegment = this.stripConversationPrefix(parentConv); - const agentLabel = agent.agent_id ? `sub_agent${agent.agent_id}` : agent.task_id; - const pathSuffix = convSegment ? `/${convSegment}+${agentLabel}` : `/sub_agent/${agent.task_id}`; - const url = `${base}${pathSuffix}`; - window.open(url, '_blank'); + this.activeAgent = agent; + this.activityEntries = []; + this.activityError = null; + this.fetchSubAgentActivity(agent.task_id); + this.startActivityPolling(); + }, + closeSubAgent() { + this.stopActivityPolling(); + this.activeAgent = null; + this.activityEntries = []; + this.activityError = null; + this.activityLoading = false; + }, + startActivityPolling() { + if (this.activityTimer) { + return; + } + this.activityTimer = setInterval(() => { + const taskId = this.activeAgent?.task_id; + if (taskId) { + this.fetchSubAgentActivity(taskId); + } + }, 2000); + }, + stopActivityPolling() { + if (this.activityTimer) { + clearInterval(this.activityTimer); + this.activityTimer = null; + } + }, + async fetchSubAgentActivity(taskId: string) { + if (!taskId) return; + this.activityLoading = true; + try { + const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=200`); + if (!resp.ok) { + throw new Error(await resp.text()); + } + const data = await resp.json(); + if (data && data.success && data.data) { + const entries = Array.isArray(data.data.entries) ? data.data.entries : []; + this.activityEntries = entries; + } + } catch (error: any) { + this.activityError = error?.message || String(error); + console.error('获取子智能体活动失败:', error); + } finally { + this.activityLoading = false; + } }, stripConversationPrefix(conversationId: string) { if (!conversationId) return ''; diff --git a/static/src/stores/task.ts b/static/src/stores/task.ts index 35eb207..59c8249 100644 --- a/static/src/stores/task.ts +++ b/static/src/stores/task.ts @@ -157,6 +157,22 @@ export const useTaskStore = defineStore('task', { }, 150); }, + resumeTask(taskId: string, options: { status?: 'running' | 'pending' | 'succeeded' | 'failed' | 'canceled'; resetOffset?: boolean; eventHandler?: (event: any) => void } = {}) { + if (!taskId) { + return; + } + if (this.currentTaskId && this.currentTaskId !== taskId) { + this.stopPolling(); + } + this.currentTaskId = taskId; + this.taskStatus = options.status || 'running'; + if (options.resetOffset !== false) { + this.lastEventIndex = 0; + } + this.pollingError = null; + this.startPolling(options.eventHandler); + }, + stopPolling() { if (this.pollingInterval) { debugLog('[Task] 停止轮询'); diff --git a/static/src/styles/components/overlays/_overlays.scss b/static/src/styles/components/overlays/_overlays.scss index a9ad86e..a096a2c 100644 --- a/static/src/styles/components/overlays/_overlays.scss +++ b/static/src/styles/components/overlays/_overlays.scss @@ -1890,6 +1890,143 @@ color: #fff; } +.subagent-activity-overlay { + position: fixed; + inset: 0; + background: var(--theme-overlay-scrim); + backdrop-filter: blur(10px); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + z-index: 2200; + transition: opacity 0.25s ease, backdrop-filter 0.25s ease; +} + +.subagent-activity-modal { + width: 720px; + height: 70vh; + background: var(--theme-surface-card); + border: 1px solid var(--theme-control-border-strong); + border-radius: 18px; + padding: 20px 22px; + box-shadow: var(--theme-shadow-soft); + display: flex; + flex-direction: column; + gap: 12px; +} + +.subagent-activity-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.subagent-activity-title { + font-size: 16px; + font-weight: 600; +} + +.subagent-activity-close { + border: none; + background: transparent; + color: var(--claude-text); + font-size: 20px; + cursor: pointer; + padding: 4px 8px; + border-radius: 8px; +} + +.subagent-activity-close:hover { + background: var(--theme-control-bg); +} + +.subagent-activity-meta { + display: flex; + align-items: center; + gap: 12px; + font-size: 13px; + color: var(--claude-text-secondary); + flex-wrap: wrap; +} + +.subagent-activity-status { + padding: 2px 8px; + border-radius: 999px; + background: rgba(59, 130, 246, 0.15); + color: #3b82f6; + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.04em; +} + +.subagent-activity-status.completed { + background: rgba(16, 185, 129, 0.15); + color: #10b981; +} + +.subagent-activity-status.failed, +.subagent-activity-status.timeout { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + +.subagent-activity-body { + flex: 1; + overflow-y: auto; + padding: 8px 0; +} + +.subagent-activity-empty, +.subagent-activity-error { + color: var(--claude-text-secondary); + font-size: 13px; +} + +.subagent-activity-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.subagent-activity-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 6px 0; + font-size: 13px; +} + +.subagent-activity-text { + color: var(--claude-text); + line-height: 1.4; + word-break: break-all; +} + +.subagent-activity-state { + flex-shrink: 0; + font-size: 12px; + padding: 2px 8px; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.03em; + background: rgba(59, 130, 246, 0.12); + color: #3b82f6; +} + +.subagent-activity-state.completed { + background: rgba(16, 185, 129, 0.15); + color: #10b981; +} + +.subagent-activity-state.failed { + background: rgba(239, 68, 68, 0.15); + color: #ef4444; +} + + .confirm-dialog-fade-enter-active, .confirm-dialog-fade-leave-active { transition: opacity 0.25s ease, backdrop-filter 0.25s ease; diff --git a/static/src/utils/icons.ts b/static/src/utils/icons.ts index b3e376a..21dfcfb 100644 --- a/static/src/utils/icons.ts +++ b/static/src/utils/icons.ts @@ -42,7 +42,7 @@ export const ICONS = Object.freeze({ export const TOOL_ICON_MAP = Object.freeze({ append_to_file: 'pencil', - close_sub_agent: 'octagon', + close_sub_agent: 'bot', create_file: 'file', create_folder: 'folder', create_sub_agent: 'bot', @@ -66,7 +66,8 @@ export const TOOL_ICON_MAP = Object.freeze({ terminal_snapshot: 'clipboard', unfocus_file: 'eye', update_memory: 'brain', - wait_sub_agent: 'clock', + wait_sub_agent: 'bot', + get_sub_agent_status: 'bot', web_search: 'search', trigger_easter_egg: 'sparkles', view_image: 'camera', diff --git a/utils/api_client.py b/utils/api_client.py index 44141bd..3700e41 100644 --- a/utils/api_client.py +++ b/utils/api_client.py @@ -287,34 +287,29 @@ class DeepSeekClient: def _merge_system_messages(self, messages: List[Dict]) -> List[Dict]: """ - 将多个 system 消息合并为一个(部分模型仅支持单条 system)。 - 保留原有顺序,把合并后的 system 放在第一条 system 的位置。 + 仅合并最开头连续的 system 消息(系统提示),后续插入的 system 消息保持原样。 """ if not messages: return messages + merged_contents: List[str] = [] - new_messages: List[Dict] = [] - first_system_index: Optional[int] = None - for msg in messages: - if msg.get("role") == "system": - if first_system_index is None: - first_system_index = len(new_messages) - content = msg.get("content", "") - if isinstance(content, str): - merged_contents.append(content) - else: - merged_contents.append(json.dumps(content, ensure_ascii=False)) + idx = 0 + while idx < len(messages) and messages[idx].get("role") == "system": + content = messages[idx].get("content", "") + if isinstance(content, str): + merged_contents.append(content) else: - new_messages.append(msg) + merged_contents.append(json.dumps(content, ensure_ascii=False)) + idx += 1 + if not merged_contents: return messages + merged = { "role": "system", "content": "\n\n".join(c for c in merged_contents if c) } - insert_at = first_system_index if first_system_index is not None else 0 - new_messages.insert(insert_at, merged) - return new_messages + return [merged] + messages[idx:] def set_deep_thinking_mode(self, enabled: bool): """配置深度思考模式(持续使用思考模型)。"""