diff --git a/apps/codex-plus-manager/src-tauri/src/commands.rs b/apps/codex-plus-manager/src-tauri/src/commands.rs index 1f17a6456..251378c9d 100644 --- a/apps/codex-plus-manager/src-tauri/src/commands.rs +++ b/apps/codex-plus-manager/src-tauri/src/commands.rs @@ -1450,9 +1450,11 @@ pub fn find_desktop_codex_cli() -> CommandResult { ) else { return failed("未找到 Codex Desktop 应用。", json!({ "path": null })); }; - let Some(path) = codex_plus_core::app_paths::find_bundled_codex_cli(&app_dir) else { + let bundled = codex_plus_core::app_paths::find_bundled_codex_cli(&app_dir); + let standalone = codex_plus_core::app_paths::find_standalone_codex_cli(); + let Some(path) = [bundled, standalone].into_iter().flatten().find(|candidate| codex_cli_can_start(candidate)) else { return failed( - "已找到 Codex Desktop,但包内没有可用的 Codex CLI。", + "已找到 Codex Desktop,但没有可用的 Codex CLI;请安装或指定用户目录中的 Codex CLI。", json!({ "path": null }), ); }; @@ -1462,6 +1464,27 @@ pub fn find_desktop_codex_cli() -> CommandResult { ) } +fn codex_cli_can_start(path: &std::path::Path) -> bool { + use std::process::{Command, Stdio}; + use std::time::{Duration, Instant}; + let mut command = Command::new(path); + command.arg("--version").stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + command.creation_flags(codex_plus_core::windows_create_no_window()); + } + let Ok(mut child) = command.spawn() else { return false; }; + let deadline = Instant::now() + Duration::from_secs(5); + loop { + match child.try_wait() { + Ok(Some(status)) => return status.success(), + Ok(None) if Instant::now() < deadline => std::thread::sleep(Duration::from_millis(25)), + _ => { let _ = child.kill(); let _ = child.wait(); return false; } + } + } +} + fn spawn_weixin_connect( settings: BackendSettings, ) -> anyhow::Result { diff --git a/crates/codex-plus-core/src/app_paths.rs b/crates/codex-plus-core/src/app_paths.rs index 4d9726e1b..a1d5a69d8 100644 --- a/crates/codex-plus-core/src/app_paths.rs +++ b/crates/codex-plus-core/src/app_paths.rs @@ -410,6 +410,62 @@ pub fn find_standalone_codex_app_dir() -> Option { None } +/// Finds the CLI shipped by the standalone Codex installer. +pub fn find_standalone_codex_cli() -> Option { + let local_appdata = std::env::var_os("LOCALAPPDATA")?; + find_standalone_codex_cli_in(&PathBuf::from(local_appdata).join("OpenAI").join("Codex").join("bin")) +} + +fn find_standalone_codex_cli_in(bin_dir: &Path) -> Option { + let mut candidates = Vec::new(); + if let Some(path) = standalone_cli_in_dir(bin_dir) { candidates.push(path); } + if let Ok(entries) = std::fs::read_dir(bin_dir) { + for entry in entries.flatten() { + if entry.path().is_dir() { + if let Some(path) = standalone_cli_in_dir(&entry.path()) { candidates.push(path); } + } + } + } + candidates.sort_by_key(|path| std::fs::metadata(path).and_then(|metadata| metadata.modified()).ok()); + candidates.pop() +} + +fn standalone_cli_in_dir(dir: &Path) -> Option { + ["codex.exe", "codex", "Codex.exe", "Codex"].iter().map(|name| dir.join(name)).find(|path| path.is_file()) +} + +#[cfg(test)] +mod standalone_cli_tests { + use super::find_standalone_codex_cli_in; + + #[test] + fn standalone_cli_finds_latest_versioned_binary() { + let temp = tempfile::tempdir().unwrap(); + let old = temp.path().join("old"); + let new = temp.path().join("new"); + std::fs::create_dir_all(&old).unwrap(); + std::fs::create_dir_all(&new).unwrap(); + std::fs::write(old.join("codex.exe"), "old").unwrap(); + std::fs::write(new.join("codex.exe"), "new").unwrap(); + std::fs::File::options().write(true).open(old.join("codex.exe")).unwrap().set_times(std::fs::FileTimes::new().set_modified(std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000))).unwrap(); + assert_eq!(find_standalone_codex_cli_in(temp.path()), Some(new.join("codex.exe"))); + } + + #[test] + fn standalone_cli_returns_none_without_binary() { + let temp = tempfile::tempdir().unwrap(); + assert_eq!(find_standalone_codex_cli_in(temp.path()), None); + } + + #[test] + fn standalone_cli_finds_unversioned_binary() { + let temp = tempfile::tempdir().unwrap(); + let binary = temp.path().join("codex.exe"); + std::fs::write(&binary, "cli").unwrap(); + assert_eq!(find_standalone_codex_cli_in(temp.path()), Some(binary)); + } +} + pub fn resolve_codex_app_dir_with_saved( app_dir: Option<&Path>, saved_app_path: Option<&str>,