From 49dae6cb4a23a293dd743eceaae76130647fe22b Mon Sep 17 00:00:00 2001 From: hcw <1416522360@qq.com> Date: Fri, 18 Sep 2026 19:26:35 +0800 Subject: [PATCH 1/2] fix(weixin): fall back from inaccessible desktop CLI --- .../src-tauri/src/commands.rs | 27 ++++++++- crates/codex-plus-core/src/app_paths.rs | 56 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) 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>, From 586cb71538f70679cd605e0abaa938675b81eeb4 Mon Sep 17 00:00:00 2001 From: hcw <1416522360@qq.com> Date: Sat, 19 Sep 2026 10:34:46 +0800 Subject: [PATCH 2/2] fix(weixin): restore enabled connection when launcher starts --- apps/codex-plus-launcher/src/main.rs | 37 ++++++++++++++++++++ apps/codex-plus-manager/src-tauri/src/lib.rs | 11 +++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/apps/codex-plus-launcher/src/main.rs b/apps/codex-plus-launcher/src/main.rs index e2a400a3b..5795b3c1a 100644 --- a/apps/codex-plus-launcher/src/main.rs +++ b/apps/codex-plus-launcher/src/main.rs @@ -84,6 +84,7 @@ async fn launcher_main(args: Vec, helper_only: bool, options: LaunchOpti hooks.shutdown_helper(options.helper_port).await; return Ok(()); } + ensure_weixin_manager_started(); let Some(_guard) = acquire_single_instance_guard(options.debug_port)? else { activate_existing_codex_app(&options).await?; options.status_store.save_latest(&LaunchStatus { @@ -115,6 +116,42 @@ fn current_timestamp_ms() -> u64 { .as_millis() as u64 } +fn ensure_weixin_manager_started() { + let result = (|| -> anyhow::Result<()> { + let settings = codex_plus_core::settings::SettingsStore::default().load()?; + if should_start_weixin_manager(settings.weixin_connect_enabled, &settings.weixin_connect_token) { + codex_plus_core::install::spawn_companion( + codex_plus_core::install::MANAGER_BINARY, + ["--background"], + )?; + } + Ok(()) + })(); + if let Err(error) = result { + let _ = codex_plus_core::diagnostic_log::append_diagnostic_log( + "launcher.weixin_manager_start_failed", + serde_json::json!({ "error": error.to_string() }), + ); + } +} + +fn should_start_weixin_manager(enabled: bool, token: &str) -> bool { + enabled && !token.trim().is_empty() +} + +#[cfg(test)] +mod weixin_startup_tests { + use super::should_start_weixin_manager; + + #[test] + fn only_enabled_and_authenticated_connections_start_manager() { + assert!(should_start_weixin_manager(true, "test-token")); + assert!(!should_start_weixin_manager(false, "test-token")); + assert!(!should_start_weixin_manager(true, "")); + assert!(!should_start_weixin_manager(true, " ")); + } +} + fn acquire_single_instance_guard( debug_port: u16, ) -> anyhow::Result> { diff --git a/apps/codex-plus-manager/src-tauri/src/lib.rs b/apps/codex-plus-manager/src-tauri/src/lib.rs index d7deb13dd..7e8b68698 100644 --- a/apps/codex-plus-manager/src-tauri/src/lib.rs +++ b/apps/codex-plus-manager/src-tauri/src/lib.rs @@ -58,6 +58,9 @@ pub fn run() { main_window_builder = main_window_builder.icon(icon)?; } let main_window = main_window_builder.build()?; + if startup_is_background() { + main_window.hide()?; + } install_tray(app)?; commands::start_weixin_connect_from_saved_settings(); register_main_window_events(main_window, startup_is_transient()); @@ -353,6 +356,10 @@ fn startup_is_transient() -> bool { std::env::args().any(|arg| arg == "--transient") } +fn startup_is_background() -> bool { + std::env::args().any(|arg| arg == "--background") +} + #[tauri::command] fn manager_exit_app(app: tauri::AppHandle) { APP_EXITING.store(true, Ordering::SeqCst); @@ -513,7 +520,9 @@ fn acquire_single_instance_guard() -> Option {