From 8fe06753bbbeb743a4c1a272bd3b5e4745cc7346 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Mon, 15 Dec 2025 15:15:03 +0800 Subject: [PATCH] fix: stabilize terminal tool timeouts --- core/main_terminal.py | 30 +++- modules/persistent_terminal.py | 94 ++++++++--- modules/terminal_manager.py | 27 ++- modules/terminal_ops.py | 197 +++++++++++++++++----- static/src/app.ts | 9 +- static/src/composables/useLegacySocket.ts | 4 + utils/tool_result_formatter.py | 68 +++++--- 7 files changed, 328 insertions(+), 101 deletions(-) diff --git a/core/main_terminal.py b/core/main_terminal.py index 50cc869..4a9da2f 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -1303,7 +1303,7 @@ class MainTerminal: "type": "function", "function": { "name": "terminal_input", - "description": "向活动终端发送命令或输入。禁止启动会占用终端界面的程序(python/node/nano/vim 等);如遇卡死请结合 terminal_snapshot 并使用 terminal_reset 恢复。", + "description": "向活动终端发送命令或输入。禁止启动会占用终端界面的程序(python/node/nano/vim 等);如遇卡死请结合 terminal_snapshot 并使用 terminal_reset 恢复。默认等待输出120秒,最长300秒,超时会尝试中断并返回已捕获输出。", "parameters": { "type": "object", "properties": { @@ -1321,7 +1321,7 @@ class MainTerminal: }, "timeout": { "type": "number", - "description": "等待输出的最长秒数,默认使用配置项 TERMINAL_OUTPUT_WAIT" + "description": "等待输出的最长秒数,默认120,最大300" } }, "required": ["command"] @@ -1446,11 +1446,15 @@ class MainTerminal: "type": "function", "function": { "name": "run_python", - "description": "执行一次性 Python 脚本,可用于处理二进制或非 UTF-8 文件(如 Excel、Word、PDF、图片),或进行数据分析与验证。请在脚本内显式读取文件并输出结果,避免长时间阻塞。", + "description": "执行一次性 Python 脚本,可用于处理二进制或非 UTF-8 文件(如 Excel、Word、PDF、图片),或进行数据分析与验证。默认超时20秒,最长60秒;超时会尝试中断并返回已捕获输出。", "parameters": { "type": "object", "properties": { - "code": {"type": "string", "description": "Python代码"} + "code": {"type": "string", "description": "Python代码"}, + "timeout": { + "type": "number", + "description": "超时时长(秒),默认20,最大60" + } }, "required": ["code"] } @@ -1460,11 +1464,15 @@ class MainTerminal: "type": "function", "function": { "name": "run_command", - "description": "执行一次性终端命令,适合查看文件信息(file/ls/stat/iconv 等)、转换编码或调用 CLI 工具。禁止启动交互式程序;对已聚焦文件仅允许使用 grep -n 等定位命令。输出超过10000字符将被拒绝,可先限制返回体量。", + "description": "执行一次性终端命令,适合查看文件信息(file/ls/stat/iconv 等)、转换编码或调用 CLI 工具。禁止启动交互式程序;对已聚焦文件仅允许使用 grep -n 等定位命令。默认超时10秒,最长30秒,超时会尝试中断并返回已捕获输出;输出超过10000字符将被截断或拒绝。", "parameters": { "type": "object", "properties": { - "command": {"type": "string", "description": "终端命令"} + "command": {"type": "string", "description": "终端命令"}, + "timeout": { + "type": "number", + "description": "超时时长(秒),默认10,最大30" + } }, "required": ["command"] } @@ -2079,10 +2087,16 @@ class MainTerminal: } elif tool_name == "run_python": - result = await self.terminal_ops.run_python_code(arguments["code"]) + result = await self.terminal_ops.run_python_code( + arguments["code"], + timeout=arguments.get("timeout") + ) elif tool_name == "run_command": - result = await self.terminal_ops.run_command(arguments["command"]) + result = await self.terminal_ops.run_command( + arguments["command"], + timeout=arguments.get("timeout") + ) # 字符数检查 if result.get("success") and "output" in result: diff --git a/modules/persistent_terminal.py b/modules/persistent_terminal.py index 455781b..3149d2d 100644 --- a/modules/persistent_terminal.py +++ b/modules/persistent_terminal.py @@ -5,8 +5,9 @@ import subprocess import os import sys import time +import signal from pathlib import Path -from typing import Optional, Callable, Dict, List +from typing import Optional, Callable, Dict, List, Tuple from datetime import datetime import threading import queue @@ -507,16 +508,23 @@ class PersistentTerminal: def _process_output(self, output: str): """处理输出行""" - # 添加到缓冲区 - self.output_buffer.append(output) - self.total_output_size += len(output) now = time.time() - self.last_output_time = now - - # 记录输出事件 - self._output_event_counter += 1 - self.output_history.append((self._output_event_counter, now, output)) - self._append_io_event('output', output, timestamp=now) + noisy_markers = ( + "bash: cannot set terminal process group", + "bash: no job control in this shell", + ) + for line in output.splitlines(keepends=True): + if any(noise in line for noise in noisy_markers): + continue + self.output_buffer.append(line) + self.total_output_size += len(line) + now = time.time() + self.last_output_time = now + + # 记录输出事件 + self._output_event_counter += 1 + self.output_history.append((self._output_event_counter, now, line)) + self._append_io_event('output', line, timestamp=now) # 控制输出历史长度 if len(self.output_history) > 2000: @@ -622,11 +630,18 @@ class PersistentTerminal: if not self.is_running or not self.process: return { "success": False, - "error": "终端未运行", + "error": "终端未运行,请先打开终端会话。", "session": self.session_name } try: + # 清空残留输出,防止上一条命令的输出干扰 + try: + while True: + self.output_queue.get_nowait() + except queue.Empty: + pass + marker = self._capture_history_marker() if timeout is None: timeout = TERMINAL_OUTPUT_WAIT @@ -674,12 +689,19 @@ class PersistentTerminal: else: command_bytes = to_send.encode('utf-8', errors='replace') - self.process.stdin.write(command_bytes) - self.process.stdin.flush() + try: + self.process.stdin.write(command_bytes) + self.process.stdin.flush() + except Exception: + return { + "success": False, + "error": "终端已不可用或输入失败,请重新打开终端会话。", + "session": self.session_name + } # 如果需要等待输出 if wait_for_output: - output = self._wait_for_output(timeout=timeout) + output, timed_out = self._wait_for_output(timeout=timeout) recent_output = self._get_output_since_marker(marker) if recent_output: output = recent_output @@ -690,7 +712,9 @@ class PersistentTerminal: output_clean = output.strip() has_output = bool(output_clean) status = "completed" - if not has_output: + if timed_out: + status = "timeout" + elif not has_output: if self.echo_loop_detected: status = "echo_loop" elif self.is_interactive: @@ -701,23 +725,27 @@ class PersistentTerminal: if self.echo_loop_detected: status = "output_with_echo" message_map = { - "completed": "命令执行完成,已捕获终端输出", - "no_output": "未捕获任何输出,命令可能未产生可见结果或终端已卡死需要重制", - "awaiting_input": "命令已发送,终端正在等待进一步输入或进程仍在运行", + "completed": "命令执行完成", + "no_output": "未捕获输出,命令可能未产生结果", + "awaiting_input": "命令已发送,终端等待进一步输入或仍在运行", "echo_loop": "检测到终端正在回显输入,命令可能未成功执行", - "output_with_echo": "命令产生输出,但终端疑似重复回显,请检查是否卡住" + "output_with_echo": "命令产生输出,但终端疑似重复回显", + "timeout": f"命令超时({int(timeout)}秒)" } message = message_map.get(status, "命令执行完成") if output_truncated: message += f"(输出已截断,保留末尾{TERMINAL_INPUT_MAX_CHARS}字符)" + elapsed_ms = int((time.time() - start_time) * 1000) return { - "success": True, + "success": status in {"completed", "output_with_echo"}, "session": self.session_name, "command": command_text, "output": output, "message": message, "status": status, - "truncated": output_truncated + "truncated": output_truncated, + "elapsed_ms": elapsed_ms, + "timeout": timeout } else: return { @@ -727,7 +755,9 @@ class PersistentTerminal: "output": "", "message": "命令已发送至终端,后续输出将实时流式返回", "status": "pending", - "truncated": False + "truncated": False, + "elapsed_ms": int((time.time() - start_time) * 1000), + "timeout": timeout } except Exception as e: @@ -739,11 +769,15 @@ class PersistentTerminal: "session": self.session_name } - def _wait_for_output(self, timeout: float = 5) -> str: - """等待并收集输出""" + def _wait_for_output(self, timeout: float = 5) -> Tuple[str, bool]: + """等待并收集输出,返回 (output, timed_out)。 + + 策略:若尚未收到任何输出,则一直等到超时;一旦收到输出,若后续空闲>0.3s则视为命令结束。 + """ collected_output = [] start_time = time.time() last_output_time = time.time() + output_seen = False if timeout is None or timeout <= 0: timeout = 0 @@ -754,29 +788,33 @@ class PersistentTerminal: output = self.output_queue.get_nowait() collected_output.append(output) except queue.Empty: - return ''.join(collected_output) + return ''.join(collected_output), False + idle_threshold = 1.5 while time.time() - start_time < timeout: try: remaining = max(0.05, min(0.5, timeout - (time.time() - start_time))) output = self.output_queue.get(timeout=remaining) collected_output.append(output) last_output_time = time.time() + output_seen = True # 快速收集剩余输出,直到短暂空闲 while True: try: output = self.output_queue.get(timeout=0.1) collected_output.append(output) last_output_time = time.time() + output_seen = True except queue.Empty: break except queue.Empty: - if collected_output and time.time() - last_output_time > 0.3: + if output_seen and (time.time() - last_output_time > idle_threshold): break if timeout == 0: break - - return ''.join(collected_output) + + timed_out = (time.time() - start_time) >= timeout and timeout > 0 + return ''.join(collected_output), timed_out def get_output(self, last_n_lines: int = 50) -> str: """ diff --git a/modules/terminal_manager.py b/modules/terminal_manager.py index a3a5851..72e409a 100644 --- a/modules/terminal_manager.py +++ b/modules/terminal_manager.py @@ -491,7 +491,32 @@ class TerminalManager: # 发送命令 terminal = self.terminals[target_session] - result = terminal.send_command(command, wait_for_output, timeout=timeout) + if timeout is None or timeout <= 0: + timeout = 120 + timeout = min(timeout, 300) + + wrapped_command = command + wait_timeout = timeout + base_timeout = timeout + if wait_for_output and timeout > 0: + wrapped_command = f"timeout -k 2 {int(timeout)}s {command}" + wait_timeout = timeout + 3 # 留出终止与收尾输出 + + result = terminal.send_command(wrapped_command, wait_for_output, timeout=wait_timeout) + result["timeout"] = base_timeout + + # 若超时阈值已到且状态仍是 completed,则视为超时(避免误判) + elapsed_ms = result.get("elapsed_ms") + if ( + wait_for_output + and base_timeout + and elapsed_ms is not None + and elapsed_ms >= int(base_timeout * 1000) + and result.get("status") == "completed" + ): + result["status"] = "timeout" + result["success"] = False + result["message"] = result.get("message") or f"命令超时({int(base_timeout)}秒)" return result diff --git a/modules/terminal_ops.py b/modules/terminal_ops.py index f35a7e3..eb3e005 100644 --- a/modules/terminal_ops.py +++ b/modules/terminal_ops.py @@ -5,6 +5,8 @@ import sys import asyncio import subprocess import shutil +import time +import signal from pathlib import Path from typing import Dict, Optional, Tuple, TYPE_CHECKING try: @@ -128,6 +130,13 @@ class TerminalOperator: return False, f"检测到危险命令模式: {pattern}" return True, "" + + @staticmethod + def _clamp_timeout(requested: Optional[int], default: int, max_limit: int) -> int: + """对timeout进行默认化与上限夹紧。""" + if not requested or requested <= 0: + return default + return min(int(requested), max_limit) async def run_command( self, @@ -146,7 +155,7 @@ class TerminalOperator: Returns: 执行结果字典 """ - # 每次执行前重置工具容器,防止上一条命令的输出/状态干扰 + # 每次执行前重置工具容器(保持隔离),但下面改用一次性子进程执行,仍保留重置以兼容后续逻辑 self._reset_toolbox() # 替换命令中的python3为实际可用的命令 if "python3" in command and self.python_cmd != "python3": @@ -176,17 +185,15 @@ class TerminalOperator: "return_code": -1 } - timeout = timeout or TERMINAL_COMMAND_TIMEOUT + # 默认10s,上限30s + timeout = self._clamp_timeout(timeout, default=10, max_limit=TERMINAL_COMMAND_TIMEOUT) print(f"{OUTPUT_FORMATS['terminal']} 执行命令: {command}") print(f"{OUTPUT_FORMATS['info']} 工作目录: {work_path}") - try: - toolbox_raw = await self._get_toolbox().run(command, work_path, timeout=timeout) - result_payload = self._format_toolbox_output(toolbox_raw) - except Exception as exc: - print(f"{OUTPUT_FORMATS['warning']} 工具容器执行失败,回退到本地子进程: {exc}") - result_payload = await self._run_command_subprocess(command, work_path, timeout) + start_ts = time.time() + # 改为一次性子进程执行,确保等待到超时或命令结束 + result_payload = await self._run_command_subprocess(command, work_path, timeout) # 字符数检查 if result_payload.get("success") and "output" in result_payload: @@ -200,6 +207,9 @@ class TerminalOperator: "command": command } + result_payload.setdefault("status", "completed" if result_payload.get("success") else "error") + result_payload["timeout"] = timeout + result_payload["elapsed_ms"] = int((time.time() - start_ts) * 1000) return result_payload def _resolve_work_path(self, working_dir: Optional[str]) -> Path: @@ -212,6 +222,17 @@ class TerminalOperator: def _format_toolbox_output(self, payload: Dict) -> Dict: success = bool(payload.get("success")) output_text = payload.get("output", "") or "" + # 去掉常见的交互式shell警告 + noisy_lines = ( + "bash: cannot set terminal process group", + "bash: no job control in this shell", + ) + filtered = [] + for line in output_text.splitlines(): + if any(noise in line for noise in noisy_lines): + continue + filtered.append(line) + output_text = "\n".join(filtered) result = { "success": success, "output": output_text, @@ -228,67 +249,157 @@ class TerminalOperator: return result async def _run_command_subprocess(self, command: str, work_path: Path, timeout: int) -> Dict: + start_ts = time.time() try: - process = await asyncio.create_subprocess_shell( - command, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - cwd=str(work_path), - shell=True - ) - - try: - stdout, stderr = await asyncio.wait_for( - process.communicate(), - timeout=timeout + process = None + exec_cmd = None + use_shell = True + + # 如果存在容器会话且模式为docker,则在容器内执行 + if self.container_session and getattr(self.container_session, "mode", None) == "docker": + container_name = getattr(self.container_session, "container_name", None) + mount_path = getattr(self.container_session, "mount_path", "/workspace") or "/workspace" + docker_bin = shutil.which("docker") or "docker" + try: + relative = work_path.relative_to(self.project_path).as_posix() + except ValueError: + relative = "" + container_workdir = mount_path.rstrip("/") + if relative: + container_workdir = f"{container_workdir}/{relative}" + exec_cmd = [ + docker_bin, + "exec", + "-w", + container_workdir, + container_name, + "/bin/bash", + "-lc", + command, + ] + use_shell = False + + # 统一环境,确保 Python 输出无缓冲 + env = os.environ.copy() + env.setdefault("PYTHONUNBUFFERED", "1") + + if use_shell: + process = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=str(work_path), + shell=True, + env=env, + start_new_session=True, ) + else: + process = await asyncio.create_subprocess_exec( + *exec_cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + start_new_session=True, + ) + + stdout_buf: list[bytes] = [] + stderr_buf: list[bytes] = [] + + async def _read_stream(stream, collector): + try: + async for chunk in stream: + if chunk: + collector.append(chunk) + except asyncio.CancelledError: + pass + except Exception: + pass + + stdout_task = asyncio.create_task(_read_stream(process.stdout, stdout_buf)) + stderr_task = asyncio.create_task(_read_stream(process.stderr, stderr_buf)) + + timed_out = False + try: + await asyncio.wait_for(process.wait(), timeout=timeout) except asyncio.TimeoutError: - process.kill() - await process.wait() - return { - "success": False, - "error": f"命令执行超时 ({timeout}秒)", - "output": "", - "return_code": -1 - } - + timed_out = True + try: + os.killpg(process.pid, signal.SIGINT) + except Exception: + pass + try: + await asyncio.wait_for(process.wait(), timeout=2) + except asyncio.TimeoutError: + try: + os.killpg(process.pid, signal.SIGKILL) + except Exception: + process.kill() + await process.wait() + + # 确保读取协程结束 + await asyncio.gather(stdout_task, stderr_task, return_exceptions=True) + + # 兜底再读一次,防止剩余缓冲未被读取 + try: + remaining_out = await process.stdout.read() + if remaining_out: + stdout_buf.append(remaining_out) + except Exception: + pass + try: + remaining_err = await process.stderr.read() + if remaining_err: + stderr_buf.append(remaining_err) + except Exception: + pass + + stdout = b"".join(stdout_buf) + stderr = b"".join(stderr_buf) + stdout_text = stdout.decode('utf-8', errors='replace') if stdout else "" stderr_text = stderr.decode('utf-8', errors='replace') if stderr else "" - - success = process.returncode == 0 - if success: - print(f"{OUTPUT_FORMATS['success']} 命令执行成功") - else: - print(f"{OUTPUT_FORMATS['error']} 命令执行失败 (返回码: {process.returncode})") - + + success = (process.returncode == 0) and not timed_out + status = "completed" if success else ("timeout" if timed_out else "error") + output_parts = [] if stdout_text: output_parts.append(stdout_text) if stderr_text: - output_parts.append(f"[stderr]\n{stderr_text}") + output_parts.append(stderr_text) combined_output = "\n".join(output_parts) - + truncated = False if MAX_RUN_COMMAND_CHARS and len(combined_output) > MAX_RUN_COMMAND_CHARS: truncated = True combined_output = combined_output[-MAX_RUN_COMMAND_CHARS:] - + response = { "success": success, + "status": status, "command": command, "output": combined_output, "return_code": process.returncode, - "truncated": truncated + "truncated": truncated, + "timeout": timeout, + "elapsed_ms": int((time.time() - start_ts) * 1000) } + if not success and timed_out: + response["message"] = f"命令执行超时 ({timeout}秒)" + elif not success and process.returncode is not None: + response["message"] = f"命令执行失败 (返回码: {process.returncode})" if stderr_text: response["stderr"] = stderr_text return response except Exception as exc: return { "success": False, + "status": "error", "error": f"执行失败: {str(exc)}", "output": "", - "return_code": -1 + "return_code": -1, + "timeout": timeout, + "elapsed_ms": int((time.time() - start_ts) * 1000) } async def run_python_code( @@ -306,7 +417,7 @@ class TerminalOperator: Returns: 执行结果字典 """ - timeout = timeout or CODE_EXECUTION_TIMEOUT + timeout = self._clamp_timeout(timeout, default=20, max_limit=CODE_EXECUTION_TIMEOUT) # 强制重置工具容器,避免上一段代码仍在运行时输出混入 self._reset_toolbox() @@ -328,7 +439,7 @@ class TerminalOperator: # 使用检测到的Python命令执行文件(相对路径可兼容容器挂载路径) result = await self.run_command( - f'{self.python_cmd} "{relative_temp}"', + f'{self.python_cmd} -u "{relative_temp}"', timeout=timeout ) diff --git a/static/src/app.ts b/static/src/app.ts index 5c46f5f..fe264f8 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -118,6 +118,8 @@ const appOptions = { activeTools: new Map(), toolActionIndex: new Map(), toolStacks: new Map(), + // 当前任务是否仍在进行中(用于保持输入区的“停止”状态) + taskInProgress: false, // ========================================== // 对话管理相关状态 @@ -302,7 +304,7 @@ const appOptions = { }, composerBusy() { const monitorLock = this.monitorIsLocked && this.chatDisplayMode === 'monitor'; - return this.streamingUi || monitorLock || this.stopRequested; + return this.streamingUi || this.taskInProgress || monitorLock || this.stopRequested; } }, @@ -1038,6 +1040,7 @@ const appOptions = { this.streamingMessage = false; this.currentMessageIndex = -1; this.stopRequested = false; + this.taskInProgress = false; this.dropToolEvents = false; // 清理工具状态 @@ -1957,6 +1960,8 @@ const appOptions = { return; } + // 标记任务进行中,直到任务完成或用户手动停止 + this.taskInProgress = true; this.chatAddUserMessage(message); this.socket.emit('send_message', { message: message, conversation_id: this.currentConversationId }); if (typeof this.monitorShowPendingReply === 'function') { @@ -1995,6 +2000,7 @@ const appOptions = { // 立即清理前端状态,避免出现“不可输入也不可停止”的卡死状态 this.clearPendingTools('user_stop'); this.streamingMessage = false; + this.taskInProgress = false; this.forceUnlockMonitor('user_stop'); }, @@ -2035,6 +2041,7 @@ const appOptions = { this.toolActionIndex.clear(); this.toolStacks.clear(); this.stopRequested = false; + this.taskInProgress = false; }, async clearChat() { diff --git a/static/src/composables/useLegacySocket.ts b/static/src/composables/useLegacySocket.ts index 83699c9..302c7e7 100644 --- a/static/src/composables/useLegacySocket.ts +++ b/static/src/composables/useLegacySocket.ts @@ -690,6 +690,7 @@ export async function initializeLegacySocket(ctx: any) { resetStreamingBuffer(); ctx.monitorResetSpeech(); ctx.cleanupStaleToolActions(); + ctx.taskInProgress = true; ctx.chatStartAssistantMessage(); streamingState.activeMessageIndex = typeof ctx.currentMessageIndex === 'number' ? ctx.currentMessageIndex : null; @@ -1114,12 +1115,14 @@ export async function initializeLegacySocket(ctx: any) { // 任务停止 ctx.socket.on('task_stopped', (data) => { socketLog('任务已停止:', data.message); + ctx.taskInProgress = false; ctx.scheduleResetAfterTask('socket:task_stopped', { preserveMonitorWindows: true }); }); // 任务完成(重点:更新Token统计) ctx.socket.on('task_complete', (data) => { socketLog('任务完成', data); + ctx.taskInProgress = false; ctx.scheduleResetAfterTask('socket:task_complete', { preserveMonitorWindows: true }); resetPendingToolEvents(); @@ -1167,6 +1170,7 @@ export async function initializeLegacySocket(ctx: any) { // 仅标记当前流结束,避免状态错乱 ctx.streamingMessage = false; ctx.stopRequested = false; + ctx.taskInProgress = false; }); // 命令结果 diff --git a/utils/tool_result_formatter.py b/utils/tool_result_formatter.py index 07373cd..0aa5949 100644 --- a/utils/tool_result_formatter.py +++ b/utils/tool_result_formatter.py @@ -301,20 +301,45 @@ def _format_terminal_session(result_data: Dict[str, Any]) -> str: return result_data.get("message") or f"{tag} 操作已完成。" +def _plain_command_output(result_data: Dict[str, Any]) -> str: + """生成纯文本输出,按需要加状态前缀。""" + output = result_data.get("output") or "" + status = (result_data.get("status") or "").lower() + timeout = result_data.get("timeout") + return_code = result_data.get("return_code") + truncated = result_data.get("truncated") + + prefixes = [] + if status in {"timeout"} and timeout: + prefixes.append(f"[timeout after {int(timeout)}s]") + elif status in {"timeout"}: + prefixes.append("[timeout]") + elif status in {"killed"}: + prefixes.append("[killed]") + elif status in {"awaiting_input"}: + prefixes.append("[awaiting_input]") + elif status in {"no_output"} and not output: + prefixes.append("[no_output]") + elif status in {"error"} and return_code is not None: + prefixes.append(f"[error rc={return_code}]") + elif status in {"error"}: + prefixes.append("[error]") + + if truncated: + prefixes.append("[truncated]") + + prefix_text = "".join(prefixes) + if prefix_text and output: + return f"{prefix_text}\n{output}" + if prefix_text: + return prefix_text + if not output: + return "[no_output]" + return output + + def _format_terminal_input(result_data: Dict[str, Any]) -> str: - if not result_data.get("success"): - return _format_failure("terminal_input", result_data) - session = result_data.get("session") or result_data.get("session_name") or "default" - command = result_data.get("command") or "(命令缺失)" - status = result_data.get("status") or "completed" - message = result_data.get("message") or "" - lines = [ - f"terminal_input: 在 {session} 执行 `{command}`,状态 {status}", - ] - if message: - lines.append(message) - lines.append(_summarize_output_block(result_data.get("output"), result_data.get("truncated"))) - return "\n".join(lines) + return _plain_command_output(result_data) def _format_sleep(result_data: Dict[str, Any]) -> str: @@ -332,16 +357,19 @@ def _format_sleep(result_data: Dict[str, Any]) -> str: def _format_run_command(result_data: Dict[str, Any]) -> str: - return _format_command_result("run_command", result_data) + text = _plain_command_output(result_data) + if (result_data.get("status") or "").lower() == "timeout": + suggestion = "建议:在持久终端中直接运行该命令(terminal_session + terminal_input),或缩短命令执行时间。" + text = f"{text}\n{suggestion}" if text else suggestion + return text def _format_run_python(result_data: Dict[str, Any]) -> str: - base = _format_command_result("run_python", result_data) - code = result_data.get("code") - if not isinstance(code, str): - return base - header = f"run_python: 执行临时代码({len(code)} 字符)" - return "\n".join([header, base]) + text = _plain_command_output(result_data) + if (result_data.get("status") or "").lower() == "timeout": + suggestion = "建议:将代码保存为脚本后,在持久终端中执行(terminal_session + terminal_input),或拆分/优化代码以缩短运行时间。" + text = f"{text}\n{suggestion}" if text else suggestion + return text def _format_todo_create(result_data: Dict[str, Any]) -> str: