diff --git a/core/main_terminal.py b/core/main_terminal.py index c463713..1ef3d71 100644 --- a/core/main_terminal.py +++ b/core/main_terminal.py @@ -1544,7 +1544,7 @@ class MainTerminal: "type": "function", "function": { "name": "terminal_input", - "description": "向活动终端发送命令或输入。禁止启动会占用终端界面的程序(python/node/nano/vim 等);如遇卡死请结合 terminal_snapshot 并使用 terminal_reset 恢复。timeout 可填秒数(最大300,超时会强制打断命令)或填 never(不封装超时、不杀进程,可能无输出,无法仅靠快照判断是否成功,需要用 curl/ps 等主动检查)。若不确定上一条命令是否结束,先用 terminal_snapshot 确认后再继续输入。", + "description": "向活动终端发送命令或输入。禁止启动会占用终端界面的程序(python/node/nano/vim 等);如遇卡死请结合 terminal_snapshot 并使用 terminal_reset 恢复。timeout 必填:\n1) 传入数字(秒,最大300)会对命令进行硬超时封装。系统终端执行环境若存在 timeout/gtimeout,会采用类似 timeout -k 2 {秒}s sh -c '命令; echo __CMD_DONE__...__' 的封装;若没有 timeout/gtimeout(少见情况),则退化为外层 sh -c 的 sleep/kill 包装,例如:sh -c '运行的指令 & CMD_PID=$!; (sleep 300 && kill -s INT $CMD_PID >/dev/null 2>&1 && sleep 2 && kill -s KILL $CMD_PID >/dev/null 2>&1) & WAITER=$!; wait $CMD_PID; CMD_STATUS=$?; kill $WAITER >/dev/null 2>&1; echo __CMD_DONE__1770826047975__; exit $CMD_STATUS'。超时后会先 INT 再 KILL,进程会被不可恢复地打断(可能留下半写文件、锁或残留子进程)。\n2) 传入 never 表示不封装、不杀进程,命令原样进入终端并维护状态;此时快照可能无法判断完成情况,需要用 curl/ps/ls 等主动验证。\n适合 timeout=never 的场景示例:启动常驻服务/开发服务器(npm run dev、python web_server.py、uvicorn ...)、开启后台进程后在另一个终端测试、在后台运行时间极长的任务同时做其他事情、持续输出/长时间任务(tail -f 日志、长时间编译/训练/备份/大下载)、需要维持会话状态的操作(例如登录远程服务器后连续执行多条命令)。适合用数字超时的示例:ls/rg/pytest/短脚本等快速命令。\n若不确定上一条命令是否结束,先用 terminal_snapshot 确认后再继续输入。", "parameters": { "type": "object", "properties": self._inject_intent({ @@ -1558,7 +1558,7 @@ class MainTerminal: }, "timeout": { "type": ["number", "string"], - "description": "等待输出的最长秒数,必填,最大300,或填 never 表示不封装超时且不中断进程" + "description": "等待输出的最长秒数,必填,最大300;填 never 表示不封装超时且不中断进程(数字超时会触发外层封装)" } }), "required": ["command", "timeout"] diff --git a/server/socket_handlers.py b/server/socket_handlers.py index d5dd466..3e1b3e6 100644 --- a/server/socket_handlers.py +++ b/server/socket_handlers.py @@ -76,8 +76,16 @@ def handle_disconnect(): """客户端断开""" print(f"[WebSocket] 客户端断开: {request.sid}") username = connection_users.pop(request.sid, None) + # 若同一用户仍有其他活跃连接,不因断开而停止任务 + has_other_connection = False + if username: + for sid, user in connection_users.items(): + if user == username: + has_other_connection = True + break + task_info = get_stop_flag(request.sid, username) - if isinstance(task_info, dict): + if isinstance(task_info, dict) and not has_other_connection: task_info['stop'] = True pending_task = task_info.get('task') if pending_task and not pending_task.done(): diff --git a/utils/tool_result_formatter.py b/utils/tool_result_formatter.py index f9ef7d0..3e46137 100644 --- a/utils/tool_result_formatter.py +++ b/utils/tool_result_formatter.py @@ -334,25 +334,38 @@ def _plain_command_output(result_data: Dict[str, Any]) -> str: message = result_data.get("message") prefixes = [] + partial_output_note = None + partial_no_output_note = None if status in {"timeout"}: - appended = False - # 1) 优先使用数值型 timeout - if isinstance(timeout, (int, float)) and timeout > 0: - prefixes.append(f"[timeout after {int(timeout)}s]") - appended = True - # 2) 字符串数字 - elif isinstance(timeout, str) and timeout.strip().isdigit(): - prefixes.append(f"[timeout after {int(timeout.strip())}s]") - appended = True - # 3) 未设置超时(never),用 elapsed_ms 近似 - elif (isinstance(timeout, str) and timeout.lower() == "never") or result_data.get("never_timeout"): + never_timeout = ( + (isinstance(timeout, str) and timeout.lower() == "never") + or result_data.get("never_timeout") + ) + if never_timeout: elapsed_ms = result_data.get("elapsed_ms") + secs = None if isinstance(elapsed_ms, (int, float)) and elapsed_ms > 0: secs = max(1, int(round(elapsed_ms / 1000))) - prefixes.append(f"[timeout after ~{secs}s]") + if secs: + prefixes.append(f"[partial_output ~{secs}s]") + partial_output_note = f"已返回约{secs}秒内输出,命令可能仍在运行" + partial_no_output_note = f"已等待约{secs}秒未捕获输出,命令可能仍在运行" + else: + prefixes.append("[partial_output]") + partial_output_note = "已返回当前输出,命令可能仍在运行" + partial_no_output_note = "未捕获输出,命令可能仍在运行" + else: + appended = False + # 1) 优先使用数值型 timeout + if isinstance(timeout, (int, float)) and timeout > 0: + prefixes.append(f"[timeout after {int(timeout)}s]") appended = True - if not appended: - prefixes.append("[timeout]") + # 2) 字符串数字 + elif isinstance(timeout, str) and timeout.strip().isdigit(): + prefixes.append(f"[timeout after {int(timeout.strip())}s]") + appended = True + if not appended: + prefixes.append("[timeout]") elif status in {"killed"}: prefixes.append("[killed]") elif status in {"awaiting_input"}: @@ -369,7 +382,7 @@ def _plain_command_output(result_data: Dict[str, Any]) -> str: # 如果执行失败且没有输出,优先显示错误信息 if not result_data.get("success") and not output: - err_text = error or message + err_text = partial_no_output_note or error or message if err_text: prefix_text = "".join(prefixes) if prefixes else "[error]" return f"{prefix_text} {err_text}" if prefix_text else err_text @@ -377,8 +390,12 @@ def _plain_command_output(result_data: Dict[str, Any]) -> str: prefix_text = "".join(prefixes) if prefix_text and output: + if partial_output_note: + prefix_text = f"{prefix_text} {partial_output_note}" return f"{prefix_text}\n{output}" if prefix_text: + if partial_no_output_note: + prefix_text = f"{prefix_text} {partial_no_output_note}" return prefix_text if not output: return "[no_output]"