diff --git a/src-tauri/src/commands/misc.rs b/src-tauri/src/commands/misc.rs index dc59acabe..6fac6c6c6 100644 --- a/src-tauri/src/commands/misc.rs +++ b/src-tauri/src/commands/misc.rs @@ -58,3 +58,116 @@ pub async fn get_init_error() -> Result, String> { pub async fn get_migration_result() -> Result { Ok(crate::init_status::take_migration_success()) } + +#[derive(serde::Serialize)] +pub struct ToolVersion { + name: String, + version: Option, + latest_version: Option, // 新增字段:最新版本 + error: Option, +} + +#[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(); + + // 用于获取远程版本的 client + let client = reqwest::Client::builder() + .user_agent("cc-switch/1.0") + .build() + .map_err(|e| e.to_string())?; + + for tool in tools { + // 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() + }; + + 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())), + } + }; + + // 2. 获取远程最新版本 + let latest_version = match tool { + "claude" => fetch_npm_latest_version(&client, "@anthropic-ai/claude-code").await, + "codex" => fetch_github_latest_release(&client, "openai/openai-python").await, + "gemini" => fetch_npm_latest_version(&client, "@google/gemini-cli").await, // 修正:使用 npm 官方包 @google/gemini-cli + _ => None, + }; + + results.push(ToolVersion { + name: tool.to_string(), + version: local_version, + latest_version, + error: local_error, + }); + } + + Ok(results) +} + +/// Helper function to fetch latest version from GitHub Release +async fn fetch_github_latest_release(client: &reqwest::Client, repo: &str) -> Option { + let url = format!("https://api.github.com/repos/{repo}/releases/latest"); + // GitHub API 需要 user-agent + match client.get(&url).send().await { + Ok(resp) => { + if let Ok(json) = resp.json::().await { + json.get("tag_name") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } else { + None + } + } + Err(_) => None, + } +} + +/// Helper function to fetch latest version from npm registry +async fn fetch_npm_latest_version(client: &reqwest::Client, package: &str) -> Option { + let url = format!("https://registry.npmjs.org/{package}"); + match client.get(&url).send().await { + Ok(resp) => { + if let Ok(json) = resp.json::().await { + json.get("dist-tags") + .and_then(|tags| tags.get("latest")) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + } else { + None + } + } + Err(_) => None, + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4d6a36ab4..d6a9eabfd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -676,6 +676,7 @@ pub fn run() { commands::save_model_test_config, commands::get_model_test_logs, commands::cleanup_model_test_logs, + commands::get_tool_versions, ]); let app = builder diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx new file mode 100644 index 000000000..122877712 --- /dev/null +++ b/src/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }