|
|
@@ -1,5 +1,7 @@
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
use std::process::Command;
|
|
|
+use regex::Regex;
|
|
|
+use super::config::get_git_mirror_config;
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
|
|
pub struct InstalledInfo {
|
|
|
@@ -75,12 +77,49 @@ async fn check_single_software(software: &str) -> InstalledInfo {
|
|
|
_ => return InstalledInfo { installed: false, version: None },
|
|
|
};
|
|
|
|
|
|
- match Command::new(cmd).args(&args).output() {
|
|
|
+ // 在 Windows 上使用 cmd /c 来执行命令,确保能正确加载 PATH 环境变量
|
|
|
+ #[cfg(target_os = "windows")]
|
|
|
+ let output = {
|
|
|
+ let full_cmd = format!("{} {}", cmd, args.join(" "));
|
|
|
+ Command::new("cmd")
|
|
|
+ .args(["/c", &full_cmd])
|
|
|
+ .output()
|
|
|
+ };
|
|
|
+
|
|
|
+ #[cfg(not(target_os = "windows"))]
|
|
|
+ let output = Command::new(cmd).args(&args).output();
|
|
|
+
|
|
|
+ match output {
|
|
|
Ok(output) if output.status.success() => {
|
|
|
- let version = String::from_utf8_lossy(&output.stdout)
|
|
|
- .lines()
|
|
|
- .next()
|
|
|
- .map(|s| s.trim().replace("v", "").to_string());
|
|
|
+ let raw_output = String::from_utf8_lossy(&output.stdout);
|
|
|
+ let first_line = raw_output.lines().next().unwrap_or("").trim();
|
|
|
+
|
|
|
+ // 根据软件类型解析版本号
|
|
|
+ let version = match software {
|
|
|
+ "git" => {
|
|
|
+ // git --version 输出格式: "git version 2.51.1.windows.1"
|
|
|
+ // 提取版本号部分
|
|
|
+ if let Some(caps) = Regex::new(r"(\d+\.\d+\.\d+)").ok().and_then(|re| re.captures(first_line)) {
|
|
|
+ caps.get(1).map(|m| m.as_str().to_string())
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ }
|
|
|
+ }
|
|
|
+ "claudeCode" => {
|
|
|
+ // claude --version 输出格式: "2.0.60 (Claude Code)"
|
|
|
+ // 提取版本号部分
|
|
|
+ if let Some(caps) = Regex::new(r"(\d+\.\d+\.\d+)").ok().and_then(|re| re.captures(first_line)) {
|
|
|
+ caps.get(1).map(|m| m.as_str().to_string())
|
|
|
+ } else {
|
|
|
+ None
|
|
|
+ }
|
|
|
+ }
|
|
|
+ _ => {
|
|
|
+ // 其他软件:去掉开头的 'v' 前缀
|
|
|
+ Some(first_line.trim_start_matches('v').to_string())
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
InstalledInfo {
|
|
|
installed: true,
|
|
|
version,
|
|
|
@@ -155,41 +194,208 @@ async fn get_nodejs_versions() -> Result<VersionResult, String> {
|
|
|
}
|
|
|
|
|
|
async fn get_git_versions() -> Result<VersionResult, String> {
|
|
|
- // Git 版本通常从 GitHub releases 获取
|
|
|
+ // 获取当前镜像配置
|
|
|
+ let mirror_config = get_git_mirror_config().await;
|
|
|
+ let mirror = mirror_config.mirror.as_str();
|
|
|
+
|
|
|
let client = reqwest::Client::new();
|
|
|
- let response = client
|
|
|
- .get("https://api.github.com/repos/git-for-windows/git/releases")
|
|
|
- .header("User-Agent", "Claude-AI-Installer")
|
|
|
- .timeout(std::time::Duration::from_secs(10))
|
|
|
- .send()
|
|
|
- .await
|
|
|
- .map_err(|e| e.to_string())?;
|
|
|
|
|
|
- let releases: Vec<serde_json::Value> = response.json().await.map_err(|e| e.to_string())?;
|
|
|
+ // 根据镜像类型选择不同的获取方式
|
|
|
+ let (versions, mirror_name) = match mirror {
|
|
|
+ "huaweicloud" => {
|
|
|
+ let versions = get_git_versions_from_huaweicloud(&client).await;
|
|
|
+ (versions, "华为云镜像")
|
|
|
+ }
|
|
|
+ _ => {
|
|
|
+ // 默认使用 GitHub
|
|
|
+ let versions = get_git_versions_from_github(&client).await;
|
|
|
+ (versions, "GitHub 官方")
|
|
|
+ }
|
|
|
+ };
|
|
|
|
|
|
- let version_items: Vec<VersionItem> = releases
|
|
|
- .iter()
|
|
|
- .take(10)
|
|
|
- .filter_map(|r| {
|
|
|
- let tag = r.get("tag_name")?.as_str()?.to_string();
|
|
|
- let version = tag.trim_start_matches('v').to_string();
|
|
|
+ // 如果获取失败,使用备用版本列表
|
|
|
+ if versions.is_empty() {
|
|
|
+ return Ok(VersionResult {
|
|
|
+ versions: get_fallback_git_versions(),
|
|
|
+ warning: Some(format!("无法从 {} 获取版本列表, 显示预设版本", mirror_name)),
|
|
|
+ });
|
|
|
+ }
|
|
|
|
|
|
- Some(VersionItem {
|
|
|
- value: version.clone(),
|
|
|
- label: version,
|
|
|
- lts: None,
|
|
|
- disabled: None,
|
|
|
- separator: None,
|
|
|
- })
|
|
|
+ // 构建版本列表
|
|
|
+ let version_items: Vec<VersionItem> = versions
|
|
|
+ .into_iter()
|
|
|
+ .take(15)
|
|
|
+ .map(|version| VersionItem {
|
|
|
+ value: version.clone(),
|
|
|
+ label: format!("Git {}", version),
|
|
|
+ lts: None,
|
|
|
+ disabled: None,
|
|
|
+ separator: None,
|
|
|
})
|
|
|
.collect();
|
|
|
|
|
|
Ok(VersionResult {
|
|
|
versions: version_items,
|
|
|
- warning: None,
|
|
|
+ warning: Some(format!("下载源: {}", mirror_name)),
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+/// 从 GitHub API 获取 Git 版本列表
|
|
|
+async fn get_git_versions_from_github(client: &reqwest::Client) -> Vec<String> {
|
|
|
+ let response = match client
|
|
|
+ .get("https://api.github.com/repos/git-for-windows/git/releases")
|
|
|
+ .header("User-Agent", "Claude-AI-Installer")
|
|
|
+ .header("Accept", "application/vnd.github.v3+json")
|
|
|
+ .timeout(std::time::Duration::from_secs(10))
|
|
|
+ .send()
|
|
|
+ .await
|
|
|
+ {
|
|
|
+ Ok(resp) => resp,
|
|
|
+ Err(e) => {
|
|
|
+ eprintln!("GitHub API 请求失败: {}", e);
|
|
|
+ return vec![];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if !response.status().is_success() {
|
|
|
+ eprintln!("GitHub API 返回错误状态: {}", response.status());
|
|
|
+ return vec![];
|
|
|
+ }
|
|
|
+
|
|
|
+ let releases: Vec<serde_json::Value> = match response.json().await {
|
|
|
+ Ok(r) => r,
|
|
|
+ Err(e) => {
|
|
|
+ eprintln!("解析 GitHub 响应失败: {}", e);
|
|
|
+ return vec![];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ let mut versions: Vec<String> = vec![];
|
|
|
+ for release in releases {
|
|
|
+ if let Some(tag) = release.get("tag_name").and_then(|t| t.as_str()) {
|
|
|
+ // 跳过预发布版本
|
|
|
+ if release.get("prerelease").and_then(|p| p.as_bool()).unwrap_or(false) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ // 提取版本号: v2.47.1.windows.1 -> 2.47.1
|
|
|
+ if let Some(caps) = Regex::new(r"^v?(\d+\.\d+\.\d+)").ok().and_then(|re| re.captures(tag)) {
|
|
|
+ if let Some(version) = caps.get(1) {
|
|
|
+ let v = version.as_str().to_string();
|
|
|
+ if !versions.contains(&v) {
|
|
|
+ versions.push(v);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ versions
|
|
|
+}
|
|
|
+
|
|
|
+/// 从华为云镜像获取 Git 版本列表
|
|
|
+async fn get_git_versions_from_huaweicloud(client: &reqwest::Client) -> Vec<String> {
|
|
|
+ let response = match client
|
|
|
+ .get("https://mirrors.huaweicloud.com/git-for-windows/")
|
|
|
+ .header("User-Agent", "Claude-AI-Installer")
|
|
|
+ .timeout(std::time::Duration::from_secs(10))
|
|
|
+ .send()
|
|
|
+ .await
|
|
|
+ {
|
|
|
+ Ok(resp) => resp,
|
|
|
+ Err(e) => {
|
|
|
+ eprintln!("华为云镜像请求失败: {}", e);
|
|
|
+ return vec![];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ if !response.status().is_success() {
|
|
|
+ eprintln!("华为云镜像返回错误状态: {}", response.status());
|
|
|
+ return vec![];
|
|
|
+ }
|
|
|
+
|
|
|
+ let html = match response.text().await {
|
|
|
+ Ok(h) => h,
|
|
|
+ Err(e) => {
|
|
|
+ eprintln!("读取华为云响应失败: {}", e);
|
|
|
+ return vec![];
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 解析 HTML 页面,查找版本目录链接
|
|
|
+ // 格式: <a href="v2.47.1.windows.1/">v2.47.1.windows.1/</a>
|
|
|
+ let re = match Regex::new(r#"href="v(\d+\.\d+\.\d+)\.windows\.\d+/""#) {
|
|
|
+ Ok(r) => r,
|
|
|
+ Err(_) => return vec![],
|
|
|
+ };
|
|
|
+
|
|
|
+ let mut versions: Vec<String> = vec![];
|
|
|
+ for caps in re.captures_iter(&html) {
|
|
|
+ if let Some(version) = caps.get(1) {
|
|
|
+ let v = version.as_str().to_string();
|
|
|
+ if !versions.contains(&v) {
|
|
|
+ versions.push(v);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 按版本号降序排序
|
|
|
+ versions.sort_by(|a, b| {
|
|
|
+ let a_parts: Vec<u32> = a.split('.').filter_map(|s| s.parse().ok()).collect();
|
|
|
+ let b_parts: Vec<u32> = b.split('.').filter_map(|s| s.parse().ok()).collect();
|
|
|
+ for i in 0..3 {
|
|
|
+ let a_val = a_parts.get(i).unwrap_or(&0);
|
|
|
+ let b_val = b_parts.get(i).unwrap_or(&0);
|
|
|
+ if a_val != b_val {
|
|
|
+ return b_val.cmp(a_val);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ std::cmp::Ordering::Equal
|
|
|
+ });
|
|
|
+
|
|
|
+ versions
|
|
|
+}
|
|
|
+
|
|
|
+/// 获取预设的 Git 版本列表(当 API 不可用时使用)
|
|
|
+fn get_fallback_git_versions() -> Vec<VersionItem> {
|
|
|
+ vec![
|
|
|
+ VersionItem {
|
|
|
+ value: "2.47.1.windows.1".to_string(),
|
|
|
+ label: "2.47.1.windows.1".to_string(),
|
|
|
+ lts: None,
|
|
|
+ disabled: None,
|
|
|
+ separator: None,
|
|
|
+ },
|
|
|
+ VersionItem {
|
|
|
+ value: "2.47.0.windows.2".to_string(),
|
|
|
+ label: "2.47.0.windows.2".to_string(),
|
|
|
+ lts: None,
|
|
|
+ disabled: None,
|
|
|
+ separator: None,
|
|
|
+ },
|
|
|
+ VersionItem {
|
|
|
+ value: "2.46.2.windows.1".to_string(),
|
|
|
+ label: "2.46.2.windows.1".to_string(),
|
|
|
+ lts: None,
|
|
|
+ disabled: None,
|
|
|
+ separator: None,
|
|
|
+ },
|
|
|
+ VersionItem {
|
|
|
+ value: "2.46.1.windows.1".to_string(),
|
|
|
+ label: "2.46.1.windows.1".to_string(),
|
|
|
+ lts: None,
|
|
|
+ disabled: None,
|
|
|
+ separator: None,
|
|
|
+ },
|
|
|
+ VersionItem {
|
|
|
+ value: "2.46.0.windows.1".to_string(),
|
|
|
+ label: "2.46.0.windows.1".to_string(),
|
|
|
+ lts: None,
|
|
|
+ disabled: None,
|
|
|
+ separator: None,
|
|
|
+ },
|
|
|
+ ]
|
|
|
+}
|
|
|
+
|
|
|
async fn get_vscode_versions() -> Result<VersionResult, String> {
|
|
|
// VS Code 通常只提供最新版本
|
|
|
Ok(VersionResult {
|