Files
clawpanel/src-tauri/src/utils.rs
2026-05-22 20:42:16 +08:00

366 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
#[cfg(target_os = "windows")]
fn push_unique_candidate(
candidates: &mut Vec<std::path::PathBuf>,
seen: &mut std::collections::HashSet<String>,
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<std::path::PathBuf>,
seen: &mut std::collections::HashSet<String>,
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<std::path::PathBuf> {
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<std::path::PathBuf> {
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<std::path::PathBuf> {
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<std::path::PathBuf> {
// 优先使用用户绑定的路径
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<std::path::PathBuf> {
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<String> {
// 优先使用用户绑定的路径
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
}
}