diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index d0664c2f3..f5a1b6bcc 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -69,8 +69,6 @@ pub struct ToolVersion { #[tauri::command] pub async fn get_tool_versions() -> Result, String> { - use std::process::Command; - let tools = vec!["claude", "codex", "gemini"]; let mut results = Vec::new(); @@ -81,39 +79,16 @@ pub async fn get_tool_versions() -> Result, String> { .map_err(|e| e.to_string())?; for tool in tools { - // 1. 获取本地版本 (保持不变) + // 1. 获取本地版本 - 先尝试直接执行,失败则扫描常见路径 let (local_version, local_error) = { - let output = if cfg!(target_os = "windows") { - Command::new("cmd") - .args(["/C", &format!("{tool} --version")]) - .output() - } else { - Command::new("sh") - .arg("-c") - .arg(format!("{tool} --version")) - .output() - }; + // 先尝试直接执行 + let direct_result = try_get_version(tool); - match output { - Ok(out) => { - if out.status.success() { - ( - Some(String::from_utf8_lossy(&out.stdout).trim().to_string()), - None, - ) - } else { - let err = String::from_utf8_lossy(&out.stderr).trim().to_string(); - ( - None, - Some(if err.is_empty() { - "未安装或无法执行".to_string() - } else { - err - }), - ) - } - } - Err(e) => (None, Some(e.to_string())), + if direct_result.0.is_some() { + direct_result + } else { + // 扫描常见的 npm 全局安装路径 + scan_cli_version(tool) } }; @@ -153,3 +128,124 @@ async fn fetch_npm_latest_version(client: &reqwest::Client, package: &str) -> Op Err(_) => None, } } + +/// 从版本输出中提取纯版本号 +fn extract_version(raw: &str) -> String { + // 匹配 semver 格式: x.y.z 或 x.y.z-xxx + let re = regex::Regex::new(r"\d+\.\d+\.\d+(-[\w.]+)?").unwrap(); + re.find(raw) + .map(|m| m.as_str().to_string()) + .unwrap_or_else(|| raw.to_string()) +} + +/// 尝试直接执行命令获取版本 +fn try_get_version(tool: &str) -> (Option, Option) { + use std::process::Command; + + let output = if cfg!(target_os = "windows") { + Command::new("cmd") + .args(["/C", &format!("{tool} --version")]) + .output() + } else { + Command::new("sh") + .arg("-c") + .arg(format!("{tool} --version")) + .output() + }; + + match output { + Ok(out) => { + if out.status.success() { + let raw = String::from_utf8_lossy(&out.stdout).trim().to_string(); + (Some(extract_version(&raw)), None) + } else { + let err = String::from_utf8_lossy(&out.stderr).trim().to_string(); + ( + None, + Some(if err.is_empty() { + "未安装或无法执行".to_string() + } else { + err + }), + ) + } + } + Err(e) => (None, Some(e.to_string())), + } +} + +/// 扫描常见路径查找 CLI +fn scan_cli_version(tool: &str) -> (Option, Option) { + use std::process::Command; + + let home = dirs::home_dir().unwrap_or_default(); + + // 常见的 npm 全局安装路径 + let mut search_paths: Vec = vec![ + home.join(".npm-global/bin"), + home.join(".local/bin"), + home.join("n/bin"), // n version manager + ]; + + #[cfg(target_os = "macos")] + { + search_paths.push(std::path::PathBuf::from("/opt/homebrew/bin")); + search_paths.push(std::path::PathBuf::from("/usr/local/bin")); + } + + #[cfg(target_os = "linux")] + { + search_paths.push(std::path::PathBuf::from("/usr/local/bin")); + search_paths.push(std::path::PathBuf::from("/usr/bin")); + } + + #[cfg(target_os = "windows")] + { + if let Some(appdata) = dirs::data_dir() { + search_paths.push(appdata.join("npm")); + } + search_paths.push(std::path::PathBuf::from("C:\\Program Files\\nodejs")); + } + + // 扫描 nvm 目录下的所有 node 版本 + let nvm_base = home.join(".nvm/versions/node"); + if nvm_base.exists() { + if let Ok(entries) = std::fs::read_dir(&nvm_base) { + for entry in entries.flatten() { + let bin_path = entry.path().join("bin"); + if bin_path.exists() { + search_paths.push(bin_path); + } + } + } + } + + // 在每个路径中查找工具 + for path in &search_paths { + let tool_path = if cfg!(target_os = "windows") { + path.join(format!("{tool}.cmd")) + } else { + path.join(tool) + }; + + if tool_path.exists() { + // 构建 PATH 环境变量,确保 node 可被找到 + let current_path = std::env::var("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", path.display(), current_path); + + let output = Command::new(&tool_path) + .arg("--version") + .env("PATH", &new_path) + .output(); + + if let Ok(out) = output { + if out.status.success() { + let raw = String::from_utf8_lossy(&out.stdout).trim().to_string(); + return (Some(extract_version(&raw)), None); + } + } + } + } + + (None, Some("未安装或无法执行".to_string())) +} diff --git a/src/components/providers/forms/EndpointSpeedTest.tsx b/src/components/providers/forms/EndpointSpeedTest.tsx index 3adbf9dbd..ebeb77129 100644 --- a/src/components/providers/forms/EndpointSpeedTest.tsx +++ b/src/components/providers/forms/EndpointSpeedTest.tsx @@ -559,7 +559,7 @@ const EndpointSpeedTest: React.FC = ({
handleSelect(entry.url)} - className={`group flex cursor-pointer items-center justify-between px-3 py-2.5 rounded-lg border transition ${ + className={`group flex cursor-pointer items-center justify-between px-3 py-2.5 rounded-lg border transition text-foreground ${ isSelected ? "border-primary/70 bg-primary/5 shadow-sm" : "border-border-default bg-background hover:bg-muted" @@ -577,7 +577,7 @@ const EndpointSpeedTest: React.FC = ({ {/* 内容 */}
-
+
{entry.url}
diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 5050b0cdf..98c583d82 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -9,7 +9,7 @@ const Input = React.forwardRef(