fix(ai): Codex CLI 状态检测实跑 --version 并移除老版本不支持的 --ephemeral

状态检测原为浅检查, 误报已连接; 改为 codex --version 实测。
--ephemeral 在 codex 0.58 等老版本不存在(unexpected argument),
隔离已由临时 CODEX_HOME 保证, 移除该参数。
This commit is contained in:
shy3130
2026-08-24 17:14:39 +08:00
parent 46635074e8
commit 971b4376d1
2 changed files with 94 additions and 3 deletions
+18 -3
View File
@@ -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",
+76
View File
@@ -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"