From 2766a8ab571733c0e53a499bf00c5403f6bc99c7 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Fri, 19 Jun 2026 20:23:44 +0800 Subject: [PATCH] =?UTF-8?q?fix(sub-agent):=20=E4=BF=AE=E5=A4=8D=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E5=8F=97=E9=99=90=E6=97=B6=E6=97=A0=E6=B3=95=E5=88=9B?= =?UTF-8?q?=E5=BB=BA=E5=AD=90=E6=99=BA=E8=83=BD=E4=BD=93=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=AF=A6=E6=83=85=E9=A1=B5=E5=8F=AA=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E6=9C=80=E8=BF=91=E4=B8=80=E6=AC=A1=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/sub_agent.py | 6 +++ easyagent/src/batch/index.js | 7 ++- modules/sub_agent_manager.py | 27 ++++++++-- server/conversation.py | 7 +-- .../overlay/SubAgentActivityDialog.vue | 52 +++++++++++++------ static/src/stores/subAgent.ts | 2 +- 6 files changed, 76 insertions(+), 25 deletions(-) diff --git a/config/sub_agent.py b/config/sub_agent.py index c52893c..085bfd8 100644 --- a/config/sub_agent.py +++ b/config/sub_agent.py @@ -14,6 +14,11 @@ SUB_AGENT_SERVICE_BASE_URL = os.environ.get("SUB_AGENT_SERVICE_URL", "http://127 SUB_AGENT_DEFAULT_TIMEOUT = int(os.environ.get("SUB_AGENT_DEFAULT_TIMEOUT", "180")) # 秒 SUB_AGENT_STATUS_POLL_INTERVAL = float(os.environ.get("SUB_AGENT_STATUS_POLL_INTERVAL", "2.0")) +# 子智能体进程自身的网络权限(与工具级 HOST_SANDBOX_NETWORK_PERMISSION 独立)。 +# 子智能体需要调用模型 API,因此默认给予完整网络;工具级网络限制仍由 +# HOST_SANDBOX_NETWORK_PERMISSION 控制,并由主进程对命令单独加沙箱。 +SUB_AGENT_HOST_NETWORK_PERMISSION = os.environ.get("SUB_AGENT_HOST_NETWORK_PERMISSION", "full") + # 存储与并发限制 # 子智能体任务目录与状态文件属于运行态数据,跟随 DATA_DIR(默认 ~/.agents//data)。 SUB_AGENT_TASKS_BASE_DIR = _resolve_repo_path(os.environ.get("SUB_AGENT_TASKS_BASE_DIR", ""), f"{DATA_DIR}/sub_agent_tasks") @@ -34,4 +39,5 @@ __all__ = [ "SUB_AGENT_STATE_FILE", "SUB_AGENT_MAX_ACTIVE", "SUB_AGENT_MODELS_CONFIG_FILE", + "SUB_AGENT_HOST_NETWORK_PERMISSION", ] diff --git a/easyagent/src/batch/index.js b/easyagent/src/batch/index.js index 9adc687..b8a7925 100644 --- a/easyagent/src/batch/index.js +++ b/easyagent/src/batch/index.js @@ -17,7 +17,12 @@ const { applyUsage, normalizeUsagePayload } = require('../utils/token_usage'); // ── stdout 输出 ── function emit(type, data) { const line = JSON.stringify({ type, ...data }); - process.stdout.write(line + '\n'); + try { + // 使用同步写并刷新,避免管道缓冲导致主进程不能实时读到进度 + fs.writeSync(1, line + '\n'); + } catch (err) { + process.stdout.write(line + '\n'); + } } // ── 解析命令行参数 ── diff --git a/modules/sub_agent_manager.py b/modules/sub_agent_manager.py index 923d076..e8b3953 100644 --- a/modules/sub_agent_manager.py +++ b/modules/sub_agent_manager.py @@ -16,6 +16,7 @@ from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING from config import ( OUTPUT_FORMATS, SUB_AGENT_DEFAULT_TIMEOUT, + SUB_AGENT_HOST_NETWORK_PERMISSION, SUB_AGENT_MAX_ACTIVE, SUB_AGENT_MODELS_CONFIG_FILE, SUB_AGENT_STATE_FILE, @@ -188,7 +189,10 @@ class SubAgentManager: return {"success": False, "error": "宿主机子智能体执行被拒绝:HOST_SANDBOX_ENABLED=0"} try: command_str = self._join_shell_words(cmd) - network_permission = os.environ.get("HOST_SANDBOX_NETWORK_PERMISSION", "restricted") + # 子智能体进程需要调用模型 API,使用独立的网络权限(默认 full)。 + # 其内部执行的 run_command 等命令仍通过 HOST_SANDBOX_NETWORK_PERMISSION + # 保持工具级网络限制。 + network_permission = SUB_AGENT_HOST_NETWORK_PERMISSION plan = build_host_sandbox_plan( command_str, self.project_path, env, network_permission=network_permission ) @@ -813,15 +817,32 @@ class SubAgentManager: if self.state_file.exists(): try: data = json.loads(self.state_file.read_text(encoding="utf-8")) - self.tasks = data.get("tasks", {}) - self.conversation_agents = data.get("conversation_agents", {}) + loaded_tasks = data.get("tasks", {}) + loaded_agents = data.get("conversation_agents", {}) except json.JSONDecodeError: logger.warning("子智能体状态文件损坏,已忽略。") self.tasks = {} self.conversation_agents = {} + return else: self.tasks = {} self.conversation_agents = {} + return + + # 与已加载的运行态任务合并,避免覆盖仅存在于内存中的状态 + # (例如 _stdout_lines、notified 等运行时标记)。 + runtime_only_keys = {"_stdout_lines"} + merged_tasks: Dict[str, Dict] = {} + for task_id, task in loaded_tasks.items(): + existing = self.tasks.get(task_id) + if existing: + for key in runtime_only_keys: + if key in existing: + task[key] = existing[key] + merged_tasks[task_id] = task + self.tasks = merged_tasks + self.conversation_agents = loaded_agents + if self.tasks: migrated = False for task in self.tasks.values(): diff --git a/server/conversation.py b/server/conversation.py index 1d7e88d..d6dddd9 100644 --- a/server/conversation.py +++ b/server/conversation.py @@ -1457,11 +1457,12 @@ def get_sub_agent_activity(task_id: str, terminal: WebTerminal, workspace: UserW progress_file = str(Path(task_root) / "progress.jsonl") entries: List[Dict[str, Any]] = [] - limit = request.args.get("limit", "200") + # 默认返回足够多的记录,确保历史工具调用完整展示;同时保留上限防止异常大文件拖垮响应。 + limit = request.args.get("limit", "100000") try: - limit_num = max(1, min(int(limit), 1000)) + limit_num = max(1, min(int(limit), 100000)) except Exception: - limit_num = 200 + limit_num = 100000 if progress_file and Path(progress_file).exists(): try: diff --git a/static/src/components/overlay/SubAgentActivityDialog.vue b/static/src/components/overlay/SubAgentActivityDialog.vue index f2fdcc5..97198ac 100644 --- a/static/src/components/overlay/SubAgentActivityDialog.vue +++ b/static/src/components/overlay/SubAgentActivityDialog.vue @@ -111,11 +111,6 @@ const buildText = (entry: ActivityEntry) => { return `${tool || '工具'}`; }; -const isTerminalStatus = (status?: string) => { - const normalized = (status || '').toString().toLowerCase(); - return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized); -}; - const canStop = computed(() => { if (!activeAgent.value?.task_id) return false; return !isTerminalStatus(activeAgent.value.status); @@ -137,26 +132,49 @@ const handleStop = async () => { } }; +const isTerminalStatus = (status?: string) => { + const normalized = (status || '').toString().toLowerCase(); + return ['completed', 'failed', 'timeout', 'terminated', 'cancelled'].includes(normalized); +}; + const displayItems = computed(() => { - const order: string[] = []; - const map = new Map(); - for (const entry of activityEntries.value || []) { + const entries = activityEntries.value || []; + const groups: { key: string; entry: ActivityEntry }[] = []; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; if (!entry) continue; - const key = entry.id || `${entry.tool || 'tool'}-${entry.ts || order.length}`; - if (!map.has(key)) { - map.set(key, { ...entry }); - order.push(key); - } else { - map.set(key, { ...map.get(key), ...entry }); + + const baseKey = entry.id || `${entry.tool || 'tool'}-${entry.ts || i}`; + const lastGroup = groups[groups.length - 1]; + + // 同一个 id 的相邻进度更新(running -> completed)合并为一次工具调用。 + // 如果上一个同 id 的组已经进入终态,则后续同 id 的记录视为新的工具调用。 + if ( + lastGroup && + (lastGroup.entry.id === entry.id || lastGroup.key === baseKey) && + !isTerminalStatus(lastGroup.entry.status) + ) { + lastGroup.entry = { ...lastGroup.entry, ...entry }; + continue; } + + // 生成唯一 key:id 重复时加上序号 + let key = baseKey; + let suffix = 0; + while (groups.some((g) => g.key === key)) { + suffix++; + key = `${baseKey}--${suffix}`; + } + groups.push({ key, entry: { ...entry } }); } - return order.map((key) => { - const item = map.get(key) || {}; + return groups.map((group) => { + const item = group.entry; const state = normalizeStatus(item.status); const stateLabel = state === 'completed' ? '完成' : state === 'failed' ? '失败' : '进行中'; return { - key, + key: group.key, state, stateLabel, text: buildText(item) diff --git a/static/src/stores/subAgent.ts b/static/src/stores/subAgent.ts index d8967da..8c5b57c 100644 --- a/static/src/stores/subAgent.ts +++ b/static/src/stores/subAgent.ts @@ -121,7 +121,7 @@ export const useSubAgentStore = defineStore('subAgent', { if (!taskId) return; this.activityLoading = true; try { - const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=200`); + const resp = await fetch(`/api/sub_agents/${taskId}/activity?limit=100000`); if (!resp.ok) { throw new Error(await resp.text()); }