"""check_process: 查看/终止 shell·run_python `background=true` 启动的后台进程。 host in-process 工具(不进 CONTAINER_TOOLS):状态文件在宿主侧 user_root/.zcbot_procs, docker 模式的容器探测/回收走宿主 docker CLI —— 两种 backend 一个工具通吃。 """ from __future__ import annotations import subprocess import time from pathlib import Path from typing import Optional from core import procs from .base import Tool, compact_tool_output class CheckProcessTool(Tool): name = "check_process" description = ( "Check status / output of background processes started with background=true " "(shell or run_python), or kill one. " "Call without proc_id to list this task's background processes. " "With proc_id: returns status + tail of the process log. " "If still running: do NOT poll in a tight loop — do other useful work, or end " "the turn and tell the user to ask later ('跑完了吗'). " "action='kill' force-terminates the process." ) parameters = { "type": "object", "properties": { "proc_id": { "type": "string", "description": "后台进程 id(启动时返回的 p 开头短串)。缺省 = 列出本 task 全部后台进程。", }, "action": { "type": "string", "enum": ["status", "kill"], "description": "status(默认)= 查状态+日志尾部;kill = 强制终止该进程。", }, "tail_bytes": { "type": "integer", "description": "返回日志尾部的最大字节数,默认 4000,上限 20000。", }, }, "required": [], } def __init__( self, *, task_id: str, base_dir: Optional[Path] = None, user_root: Optional[Path] = None, ) -> None: super().__init__(base_dir, user_root=user_root) self.task_id = str(task_id) @property def _anchor(self) -> Path: # 常规部署有 user_root;CLI --working-dir 指外部目录时退 base_dir return self.user_root or self.base_dir def execute( self, proc_id: Optional[str] = None, action: str = "status", tail_bytes: int = 4000, ) -> str: if not proc_id: items = procs.list_procs(self._anchor, self.task_id) if not items: return "本 task 没有后台进程。用 shell/run_python 的 background=true 可启动。" return "后台进程列表:\n" + "\n".join(procs.format_proc_line(m) for m in items) d = procs.proc_dir(self._anchor, self.task_id, proc_id.strip()) if d is None or not d.is_dir(): return f"[Error] 后台进程不存在: {proc_id!r}(用 check_process 不带参数列出现有进程)" meta = procs.read_meta(d) if meta is None: return f"[Error] 后台进程元数据损坏: {proc_id!r}" if action == "kill": msg = procs.kill_proc(meta, d) return f"[check_process] {proc_id}: {msg}" st, ec = procs.status_of(meta, d) tail_bytes = max(500, min(int(tail_bytes or 4000), 20_000)) tail = procs.tail_log(d, max_bytes=tail_bytes) # docker 模式:已结束但专用容器还在空转 → 顺手回收(幂等,sweep 也会兜底) if st != "running" and meta.get("backend") == "docker": name = str(meta.get("container") or "") if name: try: subprocess.run(["docker", "rm", "-f", name], capture_output=True, timeout=30) except (OSError, subprocess.TimeoutExpired): pass header = f"[check_process] {proc_id} · {meta.get('kind')} · {meta.get('command', '')[:150]}" if st == "running": elapsed = procs._fmt_elapsed(time.time() - float(meta.get("created_ts") or time.time())) body = ( f"状态: running(已运行 {elapsed},上限 {meta.get('timeout_s')}s)\n" f"--- 日志尾部 ---\n{tail}\n" f"提示:仍在运行。别循环轮询 —— 先做别的工作,或结束回合告知用户稍后询问进度。" ) elif st == "finished": note = " (timeout killed)" if ec == 124 else (" (killed)" if meta.get("killed") else "") body = f"状态: finished, exit_code={ec}{note}\n--- 日志尾部 ---\n{tail}" else: body = ( f"状态: lost(进程已不在,且未留下退出码 —— 可能宿主重启把它带走了)\n" f"--- 日志尾部(截至中断) ---\n{tail}\n" f"如需结果请重新以 background=true 启动。" ) return compact_tool_output(f"{header}\n{body}")