#[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; #[cfg(target_os = "windows")] fn push_unique_candidate( candidates: &mut Vec, seen: &mut std::collections::HashSet, path: std::path::PathBuf, ) { let key = path.to_string_lossy().replace('/', "\\").to_lowercase(); if seen.insert(key) { candidates.push(path); } } #[cfg(target_os = "windows")] fn push_windows_cli_files( candidates: &mut Vec, seen: &mut std::collections::HashSet, base: std::path::PathBuf, ) { push_unique_candidate(candidates, seen, base.join("openclaw.cmd")); push_unique_candidate(candidates, seen, base.join("openclaw.exe")); push_unique_candidate(candidates, seen, base.join("openclaw")); push_unique_candidate( candidates, seen, base.join("node_modules") .join("@qingchencloud") .join("openclaw-zh") .join("bin") .join("openclaw.js"), ); push_unique_candidate( candidates, seen, base.join("node_modules") .join("openclaw") .join("bin") .join("openclaw.js"), ); } #[cfg(target_os = "windows")] fn common_windows_cli_candidates() -> Vec { let mut candidates = Vec::new(); let mut seen = std::collections::HashSet::new(); // 先按 enhanced PATH 顺序找,保持与用户命令行优先级一致。 for dir in crate::commands::enhanced_path().split(';') { let dir = dir.trim(); if dir.is_empty() { continue; } push_windows_cli_files(&mut candidates, &mut seen, std::path::PathBuf::from(dir)); } if let Ok(appdata) = std::env::var("APPDATA") { push_windows_cli_files( &mut candidates, &mut seen, std::path::PathBuf::from(appdata).join("npm"), ); } if let Some(prefix) = crate::commands::windows_npm_global_prefix() { push_windows_cli_files(&mut candidates, &mut seen, std::path::PathBuf::from(prefix)); } for sa_dir in crate::commands::config::all_standalone_dirs() { push_windows_cli_files(&mut candidates, &mut seen, sa_dir); } if let Ok(localappdata) = std::env::var("LOCALAPPDATA") { let localappdata = std::path::PathBuf::from(localappdata); push_windows_cli_files( &mut candidates, &mut seen, localappdata.join("Programs").join("OpenClaw"), ); push_windows_cli_files(&mut candidates, &mut seen, localappdata.join("OpenClaw")); push_windows_cli_files( &mut candidates, &mut seen, localappdata.join("Programs").join("nodejs"), ); } if let Ok(program_files) = std::env::var("ProgramFiles") { let program_files = std::path::PathBuf::from(program_files); push_windows_cli_files(&mut candidates, &mut seen, program_files.join("nodejs")); push_windows_cli_files(&mut candidates, &mut seen, program_files.join("OpenClaw")); } if let Ok(program_files_x86) = std::env::var("ProgramFiles(x86)") { push_windows_cli_files( &mut candidates, &mut seen, std::path::PathBuf::from(program_files_x86).join("nodejs"), ); } if let Ok(profile) = std::env::var("USERPROFILE") { push_windows_cli_files( &mut candidates, &mut seen, std::path::PathBuf::from(profile).join(".openclaw-bin"), ); } for drive in ["C", "D", "E", "F", "G"] { push_windows_cli_files( &mut candidates, &mut seen, std::path::PathBuf::from(format!(r"{drive}:\OpenClaw")), ); push_windows_cli_files( &mut candidates, &mut seen, std::path::PathBuf::from(format!(r"{drive}:\AI\OpenClaw")), ); } const CREATE_NO_WINDOW: u32 = 0x08000000; let mut where_cmd = std::process::Command::new("where"); where_cmd.arg("openclaw"); where_cmd.env("PATH", crate::commands::enhanced_path()); where_cmd.creation_flags(CREATE_NO_WINDOW); if let Ok(output) = where_cmd.output() { if output.status.success() { for line in String::from_utf8_lossy(&output.stdout).lines() { let trimmed = line.trim(); if !trimmed.is_empty() { push_unique_candidate( &mut candidates, &mut seen, std::path::PathBuf::from(trimmed), ); } } } } candidates } pub fn is_rejected_cli_path(cli_path: &str) -> bool { let lower = cli_path.replace('\\', "/").to_lowercase(); lower.contains("/.cherrystudio/") || lower.contains("cherry-studio") } /// 读取 clawpanel.json 中用户绑定的 CLI 路径 fn bound_cli_path() -> Option { let config = crate::commands::read_panel_config_value()?; let raw = config.get("openclawCliPath")?.as_str()?; if raw.is_empty() { return None; } let p = std::path::PathBuf::from(raw); crate::commands::config::resolve_openclaw_cli_input_path(&p) } fn apply_openclaw_dir_env(cmd: &mut std::process::Command) { let openclaw_dir = crate::commands::openclaw_dir(); let config_path = openclaw_dir.join("openclaw.json"); cmd.env("OPENCLAW_HOME", &openclaw_dir); cmd.env("OPENCLAW_STATE_DIR", &openclaw_dir); cmd.env("OPENCLAW_CONFIG_PATH", &config_path); } fn apply_openclaw_dir_env_tokio(cmd: &mut tokio::process::Command) { let openclaw_dir = crate::commands::openclaw_dir(); let config_path = openclaw_dir.join("openclaw.json"); cmd.env("OPENCLAW_HOME", &openclaw_dir); cmd.env("OPENCLAW_STATE_DIR", &openclaw_dir); cmd.env("OPENCLAW_CONFIG_PATH", &config_path); } fn configured_cli_candidates() -> Vec { crate::commands::openclaw_search_paths() .into_iter() .filter_map(|p| crate::commands::config::resolve_openclaw_cli_input_path(&p)) .filter(|p| !is_rejected_cli_path(&p.to_string_lossy())) .collect() } /// Windows: 在 PATH 和常见安装目录中查找 openclaw CLI 的完整路径 /// 避免通过 `cmd /c openclaw` 调用时 npm .cmd shim 中的引号导致 /// "\"node\"" is not recognized 错误 #[cfg(target_os = "windows")] fn find_openclaw_cmd() -> Option { // 优先使用用户绑定的路径 if let Some(bound) = bound_cli_path() { return Some(bound); } for candidate in configured_cli_candidates() { if candidate.exists() { return Some(candidate); } } common_windows_cli_candidates() .into_iter() .find(|candidate| candidate.exists() && !is_rejected_cli_path(&candidate.to_string_lossy())) } #[cfg(not(target_os = "windows"))] fn common_non_windows_cli_candidates() -> Vec { let mut candidates = Vec::new(); // standalone 安装目录(集中管理,避免多处硬编码) for sa_dir in crate::commands::config::all_standalone_dirs() { candidates.push(sa_dir.join("openclaw")); } // 其他标准路径 if let Some(home) = dirs::home_dir() { candidates.push(home.join(".local").join("bin").join("openclaw")); } candidates.push(std::path::PathBuf::from("/opt/homebrew/bin/openclaw")); candidates.push(std::path::PathBuf::from("/usr/local/bin/openclaw")); candidates.push(std::path::PathBuf::from("/usr/bin/openclaw")); candidates } /// 解析当前实际使用的 openclaw CLI 完整路径(跨平台) pub fn resolve_openclaw_cli_path() -> Option { // 优先使用用户绑定的路径 if let Some(bound) = bound_cli_path() { return Some(bound.to_string_lossy().to_string()); } for candidate in configured_cli_candidates() { if candidate.exists() { return Some(candidate.to_string_lossy().to_string()); } } #[cfg(target_os = "windows")] { find_openclaw_cmd().map(|p| p.to_string_lossy().to_string()) } #[cfg(not(target_os = "windows"))] { // Fix #219: 优先通过 enhanced_path 搜索:其中 nvm/volta 等版本管理器路径排在 Homebrew 前面, // 与 `which openclaw` 的优先级一致,避免残留的 Homebrew 旧版本被优先检测到 let path = crate::commands::enhanced_path(); let sep = ':'; for dir in path.split(sep) { let candidate = std::path::Path::new(dir).join("openclaw"); if candidate.exists() { return Some(candidate.to_string_lossy().to_string()); } } // 兜底:检查 enhanced_path 可能未覆盖到的固定路径(如 GUI 环境 PATH 受限时) for candidate in common_non_windows_cli_candidates() { if candidate.exists() { return Some(candidate.to_string_lossy().to_string()); } } None } } /// 根据 CLI 路径判断安装来源 pub fn classify_cli_source(cli_path: &str) -> String { let lower = cli_path.replace('\\', "/").to_lowercase(); // standalone 安装 if lower.contains("/programs/openclaw/") || lower.contains("/openclaw-bin/") || lower.contains("/opt/openclaw/") { return "standalone".into(); } // npm 汉化版 if lower.contains("openclaw-zh") || lower.contains("@qingchencloud") { return "npm-zh".into(); } // npm 全局(大概率官方版) if lower.contains("/npm/") || lower.contains("/node_modules/") { return "npm-official".into(); } // Homebrew if lower.contains("/homebrew/") || lower.contains("/usr/local/bin") { return "npm-global".into(); } "unknown".into() } /// 跨平台获取 openclaw 命令的方法(同步版本) #[allow(dead_code)] pub fn openclaw_command() -> std::process::Command { #[cfg(target_os = "windows")] { const CREATE_NO_WINDOW: u32 = 0x08000000; let enhanced = crate::commands::enhanced_path(); // 优先:找到 openclaw.cmd 完整路径,用 cmd /c "完整路径" 避免引号问题 if let Some(cmd_path) = find_openclaw_cmd() { let mut cmd = std::process::Command::new("cmd"); if cmd_path .extension() .and_then(|s| s.to_str()) .is_some_and(|ext| ext.eq_ignore_ascii_case("js")) { cmd.arg("/c").arg("node").arg(cmd_path); } else { cmd.arg("/c").arg(cmd_path); } cmd.env("PATH", &enhanced); apply_openclaw_dir_env(&mut cmd); crate::commands::apply_proxy_env(&mut cmd); cmd.creation_flags(CREATE_NO_WINDOW); return cmd; } // 兜底:直接用 cmd /c openclaw let mut cmd = std::process::Command::new("cmd"); cmd.arg("/c").arg("openclaw"); cmd.env("PATH", &enhanced); apply_openclaw_dir_env(&mut cmd); crate::commands::apply_proxy_env(&mut cmd); cmd.creation_flags(CREATE_NO_WINDOW); cmd } #[cfg(not(target_os = "windows"))] { let bin = resolve_openclaw_cli_path().unwrap_or_else(|| "openclaw".into()); let mut cmd = std::process::Command::new(bin); cmd.env("PATH", crate::commands::enhanced_path()); apply_openclaw_dir_env(&mut cmd); crate::commands::apply_proxy_env(&mut cmd); cmd } } /// 异步版本的 openclaw 命令(推荐使用,避免阻塞 UI) pub fn openclaw_command_async() -> tokio::process::Command { #[cfg(target_os = "windows")] { const CREATE_NO_WINDOW: u32 = 0x08000000; let enhanced = crate::commands::enhanced_path(); // 优先:找到 openclaw.cmd 完整路径 if let Some(cmd_path) = find_openclaw_cmd() { let mut cmd = tokio::process::Command::new("cmd"); if cmd_path .extension() .and_then(|s| s.to_str()) .is_some_and(|ext| ext.eq_ignore_ascii_case("js")) { cmd.arg("/c").arg("node").arg(cmd_path); } else { cmd.arg("/c").arg(cmd_path); } cmd.env("PATH", &enhanced); apply_openclaw_dir_env_tokio(&mut cmd); crate::commands::apply_proxy_env_tokio(&mut cmd); cmd.creation_flags(CREATE_NO_WINDOW); return cmd; } // 兜底 let mut cmd = tokio::process::Command::new("cmd"); cmd.arg("/c").arg("openclaw"); cmd.env("PATH", &enhanced); apply_openclaw_dir_env_tokio(&mut cmd); crate::commands::apply_proxy_env_tokio(&mut cmd); cmd.creation_flags(CREATE_NO_WINDOW); cmd } #[cfg(not(target_os = "windows"))] { let bin = resolve_openclaw_cli_path().unwrap_or_else(|| "openclaw".into()); let mut cmd = tokio::process::Command::new(bin); cmd.env("PATH", crate::commands::enhanced_path()); apply_openclaw_dir_env_tokio(&mut cmd); crate::commands::apply_proxy_env_tokio(&mut cmd); cmd } }