Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions apps/codex-plus-launcher/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ async fn launcher_main(args: Vec<String>, 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 {
Expand Down Expand Up @@ -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<Option<codex_plus_core::ports::LoopbackPortGuard>> {
Expand Down
27 changes: 25 additions & 2 deletions apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1450,9 +1450,11 @@ pub fn find_desktop_codex_cli() -> CommandResult<Value> {
) 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 }),
);
};
Expand All @@ -1462,6 +1464,27 @@ pub fn find_desktop_codex_cli() -> CommandResult<Value> {
)
}

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<codex_plus_core::connect::WeixinConnectStatus> {
Expand Down
11 changes: 10 additions & 1 deletion apps/codex-plus-manager/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down Expand Up @@ -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<R: tauri::Runtime>(app: tauri::AppHandle<R>) {
APP_EXITING.store(true, Ordering::SeqCst);
Expand Down Expand Up @@ -513,7 +520,9 @@ fn acquire_single_instance_guard() -> Option<codex_plus_core::ports::LoopbackPor
"guard_port": codex_plus_core::ports::manager_guard_port()
}),
);
focus_existing_manager_window();
if !startup_is_background() {
focus_existing_manager_window();
}
None
}
Err(error) => {
Expand Down
56 changes: 56 additions & 0 deletions crates/codex-plus-core/src/app_paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,62 @@ pub fn find_standalone_codex_app_dir() -> Option<PathBuf> {
None
}

/// Finds the CLI shipped by the standalone Codex installer.
pub fn find_standalone_codex_cli() -> Option<PathBuf> {
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<PathBuf> {
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<PathBuf> {
["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>,
Expand Down
Loading