diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs index e0ba84d09..707e5e29a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs @@ -203,12 +203,51 @@ pub(in crate::agent) fn game_creator_codex_cli_version_at( Ok(version.to_string()) } +/// 开发态允许从宿主 PATH 里找到 Codex,但宿主必须持有可锚定的绝对文件: +/// 裸命令名按 PATH 解析成真实路径,否则 `bind_codex_executor` 的 canonicalize 会按 CWD 解析并失败。 +/// 发行构建不走这段,候选顺序、校验与返回值都与原先一致(打包环境用内置侧车/npm 绝对路径)。 +#[cfg(debug_assertions)] +fn anchor_codex_cli_executable_candidate( + candidate: &Path, + path: Option<&std::ffi::OsStr>, +) -> Option { + let is_bare_command_name = candidate + .parent() + .is_some_and(|parent| parent.as_os_str().is_empty()); + if !is_bare_command_name { + return candidate.is_file().then(|| candidate.to_path_buf()); + } + let name = candidate.as_os_str(); + for directory in path.into_iter().flat_map(std::env::split_paths) { + if directory.as_os_str().is_empty() { + continue; + } + let target = directory.join(name); + if target.is_file() { + // 第一个实际命中的项就是 OS 会执行的项;锚定失败时不再从 PATH 里换另一个。 + return target.canonicalize().ok(); + } + } + None +} + pub(crate) fn game_creator_codex_cli_executable_path() -> Result { let mut last_error = None; let mut seen = std::collections::HashSet::new(); let bundled = game_creator_bundled_codex_cli_path(game_creator_bundled_resource_dir().as_deref()); for candidate in game_creator_codex_cli_executable_candidates() { + #[cfg(debug_assertions)] + let candidate = match anchor_codex_cli_executable_candidate( + &candidate, + std::env::var_os("PATH").as_deref(), + ) { + Some(candidate) => candidate, + None => { + last_error = Some("候选执行器不是可锚定的文件".to_string()); + continue; + } + }; let identity = candidate.to_string_lossy().to_ascii_lowercase(); if !seen.insert(identity) { continue;