From 971b4376d1cd056eb8d6b9e229fbed0924ab395e Mon Sep 17 00:00:00 2001 From: shy3130 <415333856@qq.com> Date: Mon, 24 Aug 2026 17:14:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai):=20Codex=20CLI=20=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E5=AE=9E=E8=B7=91=20--version=20=E5=B9=B6?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E8=80=81=E7=89=88=E6=9C=AC=E4=B8=8D=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=9A=84=20--ephemeral?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 状态检测原为浅检查, 误报已连接; 改为 codex --version 实测。 --ephemeral 在 codex 0.58 等老版本不存在(unexpected argument), 隔离已由临时 CODEX_HOME 保证, 移除该参数。 --- backend/app/services/ai_provider.py | 21 ++++++-- backend/tests/test_ai_provider.py | 76 +++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/backend/app/services/ai_provider.py b/backend/app/services/ai_provider.py index 01bbf82..1fa1e78 100644 --- a/backend/app/services/ai_provider.py +++ b/backend/app/services/ai_provider.py @@ -249,11 +249,25 @@ def normalize_openai_base_url(url: str) -> str: def codex_cli_available() -> bool: + """Codex CLI 是否真正可用: 解析命令后实跑一次 --version。 + + 仅 which 到二进制不够 — npm 壳缺平台原生二进制时同样存在于 PATH, + 但一运行就报错(如 "Codex CLI not available"), 状态页会误报已连接。 + """ try: - _codex_base_command() - return True + base = _codex_base_command() except RuntimeError: return False + try: + proc = subprocess.run( + [*base, "--version"], + capture_output=True, + timeout=15, + check=False, + ) + except (OSError, subprocess.TimeoutExpired): + return False + return proc.returncode == 0 def ai_configured(provider: str | None = None) -> bool: @@ -589,10 +603,11 @@ async def _run_codex_cli( output_path = codex_home_path / "last-message.txt" _prepare_codex_home(codex_home_path) + # 不传 --ephemeral: 老版本 codex(如 0.58)无此参数, 传了直接报 + # unexpected argument; 会话隔离已由一次性临时 CODEX_HOME 保证(跑完即删)。 args = [ *_codex_base_command(), "exec", - "--ephemeral", "--sandbox", "read-only", "--skip-git-repo-check", diff --git a/backend/tests/test_ai_provider.py b/backend/tests/test_ai_provider.py index fcdc0b8..65d9eeb 100644 --- a/backend/tests/test_ai_provider.py +++ b/backend/tests/test_ai_provider.py @@ -487,3 +487,79 @@ def test_codex_config_preserves_remote_provider_without_docker_rewrite(monkeypat assert provider["base_url"] == "https://custom.example/v1" assert provider["wire_api"] == "responses" assert provider["requires_openai_auth"] is True + + +# ---- Codex CLI 可用性检测 (实跑 --version, 不再仅 which) ---- + + +class _FakeCompleted: + def __init__(self, returncode: int): + self.returncode = returncode + self.stdout = b"" + self.stderr = b"" + + +def test_codex_cli_available_runs_version_check(monkeypatch): + """实跑 --version: 能发现 npm 壳存在但原生二进制跑不起来的情况。""" + calls: list[list[str]] = [] + + def fake_run(args, **kwargs): + calls.append(list(args)) + return _FakeCompleted(0) + + monkeypatch.setattr(ai_provider, "_codex_base_command", lambda: ["codex"]) + monkeypatch.setattr(ai_provider.subprocess, "run", fake_run) + assert ai_provider.codex_cli_available() is True + assert calls == [["codex", "--version"]] + + +def test_codex_cli_available_false_when_version_fails(monkeypatch): + import subprocess + + monkeypatch.setattr(ai_provider, "_codex_base_command", lambda: ["codex"]) + monkeypatch.setattr( + ai_provider.subprocess, "run", lambda a, **k: _FakeCompleted(1) + ) + assert ai_provider.codex_cli_available() is False + # 壳报 "Codex CLI not available" 这类非零退出同样判定不可用 + monkeypatch.setattr( + ai_provider.subprocess, + "run", + lambda a, **k: (_ for _ in ()).throw(subprocess.TimeoutExpired("codex", 1)), + ) + assert ai_provider.codex_cli_available() is False + + +def test_codex_cli_available_false_when_command_missing(monkeypatch): + def raise_not_found(): + raise RuntimeError("未找到 Codex CLI 命令: codex") + + monkeypatch.setattr(ai_provider, "_codex_base_command", raise_not_found) + assert ai_provider.codex_cli_available() is False + + +@pytest.mark.asyncio +async def test_codex_exec_args_exclude_ephemeral(monkeypatch): + """exec 参数不含 --ephemeral: 老版本 codex(如 0.58)无此参数, 传了直接报错。""" + captured: dict = {} + + def fake_run_process(args, prompt, env, timeout): + captured["args"] = list(args) + return 0, b"ok", b"" + + monkeypatch.setattr(ai_provider, "_codex_base_command", lambda: ["codex"]) + monkeypatch.setattr(ai_provider, "_prepare_codex_home", lambda p: None) + monkeypatch.setattr(ai_provider, "_codex_process_env", lambda p: {}) + monkeypatch.setattr(ai_provider, "_run_codex_process", fake_run_process) + monkeypatch.setattr(ai_provider, "_read_output_file", lambda p: "ok") + monkeypatch.setattr(ai_provider, "_remove_tree_best_effort", lambda p: None) + monkeypatch.setattr(ai_provider, "current_ai_model", lambda: "gpt-5.6-sol") + + out = await ai_provider._run_codex_cli( + [{"role": "user", "content": "hi"}], max_tokens=None, timeout=1.0, + ) + assert out == "ok" + args = captured["args"] + assert "--ephemeral" not in args + assert "exec" in args and "--skip-git-repo-check" in args + assert args[args.index("--model") + 1] == "gpt-5.6-sol"