fix: improve terminal timeout messaging

This commit is contained in:
JOJO 2026-02-12 11:55:15 +08:00
parent 7472028997
commit a013abb3c4
3 changed files with 43 additions and 18 deletions

View File

@ -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"]

View File

@ -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():

View File

@ -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]"