From 2d41366f04a18f1202c757bf09d96ab83d577bee Mon Sep 17 00:00:00 2001 From: xiaoxing0135 <706015750@qq.com> Date: Fri, 5 Jun 2026 01:51:47 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20resolve=20known=20gaps=20=E2=80=94=20sys?= =?UTF-8?q?.argv=20safety,=20output=20capture,=20VERSION=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/check_version.py | 12 ++++++---- install.py | 14 ++++++++---- runtime/cli/interactive.py | 18 ++++++++++++--- runtime/cli/slash_commands.py | 43 ++++++++++++++++------------------- 4 files changed, 51 insertions(+), 36 deletions(-) diff --git a/config/check_version.py b/config/check_version.py index c76f409..dd0f044 100644 --- a/config/check_version.py +++ b/config/check_version.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """Check if Test-Agent update is available. Called by Claude Code Stop hook. -Reads .version from project root, fetches remote VERSION via HTTP. +Reads VERSION from project root, fetches remote VERSION via HTTP. Prints notification only when newer version available. -Rate-limited: checks at most once per 24h via .version_last_check timestamp. +Rate-limited: checks at most once per 24h via VERSION_last_check timestamp. """ import os import time @@ -16,13 +16,17 @@ def main(): project_root = os.getcwd() - version_file = os.path.join(project_root, ".version") + version_file = os.path.join(project_root, "VERSION") + legacy_file = os.path.join(project_root, ".version") + # Migration: rename legacy .version to VERSION if VERSION is missing + if not os.path.isfile(version_file) and os.path.isfile(legacy_file): + os.rename(legacy_file, version_file) if not os.path.isfile(version_file): return # Not a Test-Agent project, skip silently # Rate limit: check at most once per CHECK_INTERVAL - last_check_file = os.path.join(project_root, ".version_last_check") + last_check_file = os.path.join(project_root, "VERSION_last_check") now = time.time() if os.path.isfile(last_check_file): try: diff --git a/install.py b/install.py index 52cf57e..59b0250 100644 --- a/install.py +++ b/install.py @@ -606,8 +606,8 @@ def _read_template_version(template_dir): def _write_local_version(project_root, version): - """写入 .version 文件供后续更新检测。""" - vf = os.path.join(project_root, ".version") + """写入 VERSION 文件供后续更新检测。""" + vf = os.path.join(project_root, "VERSION") with open(vf, "w", encoding="utf-8") as f: f.write(version + "\n") @@ -654,9 +654,13 @@ def _update_deps(project_root): def do_update(): """轻量更新:克隆最新模板 → 比较版本 → 拷贝文件 → 更新依赖 → 保留用户数据。""" - version_file = os.path.join(PROJECT_ROOT, ".version") + version_file = os.path.join(PROJECT_ROOT, "VERSION") + legacy_file = os.path.join(PROJECT_ROOT, ".version") + # Migration: rename legacy .version to VERSION if VERSION is missing + if not os.path.isfile(version_file) and os.path.isfile(legacy_file): + os.rename(legacy_file, version_file) if not os.path.isfile(version_file): - print(f"❌ 未找到 .version 文件") + print(f"❌ 未找到 VERSION 文件") print(f" 当前目录: {os.getcwd()}") print(f" 查找路径: {version_file}") print(f" 请先执行完整安装:python install.py <目录>") @@ -787,7 +791,7 @@ def main(): # 10. 恢复用户数据 restore_user_data(PROJECT_ROOT, backed) - # 11. 写入 .version 供后续更新检测 + # 11. 写入 VERSION 供后续更新检测 version = _read_template_version(template_dir) if version: _write_local_version(PROJECT_ROOT, version) diff --git a/runtime/cli/interactive.py b/runtime/cli/interactive.py index c602578..c4f5e80 100644 --- a/runtime/cli/interactive.py +++ b/runtime/cli/interactive.py @@ -150,17 +150,25 @@ def _handle_natural_language(text: str) -> None: console.print(f"[dim]\"{summary}\"[/]") t0 = time.time() + _old_argv = None try: - import sys as _sys + import sys as _sys, contextlib, io + + _old_argv = _sys.argv[:] _sys.argv = ["tagent", "run", context_input] with console.status("[bold green]Routing...", spinner="dots"): from runtime.cli.commands.run import run as _run - _run() + _capture = io.StringIO() + with contextlib.redirect_stdout(_capture): + _run() elapsed = (time.time() - t0) * 1000 + output = _capture.getvalue().strip() + if output: + console.print(output) console.print(f" [dim]Completed in {elapsed:.0f}ms[/]") - mem.add("assistant", f"[Run: {text}]") + mem.add("assistant", output[:500] if output else f"[Run: {text[:100]}]") except SystemExit: pass except KeyboardInterrupt: @@ -169,6 +177,10 @@ def _handle_natural_language(text: str) -> None: except Exception: console.print(f" [red]Error[/] [dim]({(time.time()-t0)*1000:.0f}ms)[/]") mem.add("assistant", "[Error: command failed]") + finally: + if _old_argv is not None: + import sys as _sys + _sys.argv = _old_argv # ── Fuzzy matching (thefuck-style) ───────────────────────────────── diff --git a/runtime/cli/slash_commands.py b/runtime/cli/slash_commands.py index 6b441ae..9e773d1 100644 --- a/runtime/cli/slash_commands.py +++ b/runtime/cli/slash_commands.py @@ -52,6 +52,17 @@ def all_commands() -> list[CommandDef]: return list(COMMAND_REGISTRY) +def _run_with_argv(cmd: list[str], fn) -> None: + """Execute fn with temporary sys.argv, restoring afterward.""" + import sys + old = sys.argv[:] + try: + sys.argv = cmd + fn() + finally: + sys.argv = old + + # ── Short, memorable commands ──────────────────────────────────── @@ -96,10 +107,8 @@ def _cmd_run(args: str) -> None: from runtime.cli._shared import console console.print("[red]Usage: /run [/]") return - import sys as _sys - _sys.argv = ["tagent", "run"] + args.split() from runtime.cli.commands.run import run - run() + _run_with_argv(["tagent", "run"] + args.split(), run) @register("plan", "Plan only", aliases=["p"], args_hint="") @@ -108,55 +117,41 @@ def _cmd_plan(args: str) -> None: from runtime.cli._shared import console console.print("[red]Usage: /plan [/]") return - import sys as _sys - _sys.argv = ["tagent", "plan"] + args.split() from runtime.cli.commands.run import plan - plan() + _run_with_argv(["tagent", "plan"] + args.split(), plan) @register("doctor", "Health check", aliases=["health"], args_hint="[--agents] [--probe]") def _cmd_doctor(args: str) -> None: - import sys as _sys - _sys.argv = ["tagent", "doctor"] + (args.split() if args.strip() else []) from runtime.cli.commands.doctor import doctor - doctor() + _run_with_argv(["tagent", "doctor"] + (args.split() if args.strip() else []), doctor) @register("ls", "List experts + skills", aliases=["list", "catalog"]) def _cmd_ls(args: str) -> None: - import sys as _sys - _sys.argv = ["tagent", "catalog"] from runtime.cli.commands.catalog import catalog - catalog() + _run_with_argv(["tagent", "catalog"], catalog) @register("setup", "Generate config", aliases=["init"], args_hint="[--preset ...]") def _cmd_setup(args: str) -> None: - import sys as _sys - _sys.argv = ["tagent", "init"] + (args.split() if args.strip() else []) from runtime.cli.commands.init import init_project - init_project() + _run_with_argv(["tagent", "init"] + (args.split() if args.strip() else []), init_project) @register("ready", "Release readiness", aliases=["readiness"]) def _cmd_ready(args: str) -> None: - import sys as _sys - _sys.argv = ["tagent", "readiness"] + (args.split() if args.strip() else []) from runtime.cli.commands.readiness import readiness - readiness() + _run_with_argv(["tagent", "readiness"] + (args.split() if args.strip() else []), readiness) @register("check", "Framework self-test", aliases=["selftest"], args_hint="[--e2e] [--strict]") def _cmd_check(args: str) -> None: - import sys as _sys - _sys.argv = ["tagent", "selftest"] + (args.split() if args.strip() else []) from runtime.cli.commands.selftest import selftest - selftest() + _run_with_argv(["tagent", "selftest"] + (args.split() if args.strip() else []), selftest) @register("demo", "Quick demo", args_hint="[--real-llm]") def _cmd_demo(args: str) -> None: - import sys as _sys - _sys.argv = ["tagent", "demo"] + (args.split() if args.strip() else []) from runtime.cli.commands.demo import demo - demo() + _run_with_argv(["tagent", "demo"] + (args.split() if args.strip() else []), demo)