fix: stabilize terminal tool timeouts
This commit is contained in:
parent
28383722cc
commit
8fe06753bb
@ -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:
|
||||
|
||||
@ -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
|
||||
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, output))
|
||||
self._append_io_event('output', output, timestamp=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:
|
||||
"""
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
@ -129,6 +131,13 @@ class TerminalOperator:
|
||||
|
||||
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,
|
||||
command: str,
|
||||
@ -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,44 +249,124 @@ 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
|
||||
)
|
||||
process = None
|
||||
exec_cmd = None
|
||||
use_shell = True
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(),
|
||||
timeout=timeout
|
||||
# 如果存在容器会话且模式为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
|
||||
@ -275,20 +376,30 @@ class TerminalOperator:
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
|
||||
// 命令结果
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user