From 55c5f7c633edbeede0c56bf803381f70691993c5 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Dec 2025 17:56:09 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20Clojure=20?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E7=8E=AF=E5=A2=83=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/config.rs | 24 + src-tauri/src/env_commands.rs | 27 +- src-tauri/src/env_manager.rs | 87 +++- src-tauri/src/env_providers/clojure.rs | 639 +++++++++++++++++++++++++ src-tauri/src/env_providers/mod.rs | 2 + src-tauri/src/env_providers/scala.rs | 9 +- src-tauri/src/example.rs | 2 +- src-tauri/src/main.rs | 3 +- src/composables/usePluginConfig.ts | 20 +- 9 files changed, 796 insertions(+), 17 deletions(-) create mode 100644 src-tauri/src/env_providers/clojure.rs diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index e4b54db..6c48998 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -22,6 +22,12 @@ pub struct EditorConfig { pub space_dot_omission: Option, // 是否显示空格省略 } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnvironmentMirrorConfig { + pub enabled: Option, + pub base_url: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { pub log_directory: Option, @@ -30,6 +36,7 @@ pub struct AppConfig { pub theme: Option, pub plugins: Option>, pub editor: Option, + pub environment_mirror: Option, } impl Default for AppConfig { @@ -50,6 +57,10 @@ impl Default for AppConfig { show_function_help: Some(false), space_dot_omission: Some(false), }), + environment_mirror: Some(EnvironmentMirrorConfig { + enabled: Some(true), + base_url: Some("http://cdn.global.devlive.top".to_string()), + }), } } } @@ -113,6 +124,15 @@ impl ConfigManager { println!("读取配置 -> 添加默认 editor 配置"); } + // 检查并设置 environment_mirror 默认配置 + if config.environment_mirror.is_none() { + config.environment_mirror = Some(EnvironmentMirrorConfig { + enabled: Some(true), + base_url: Some("http://cdn.global.devlive.top".to_string()), + }); + println!("读取配置 -> 添加默认 environment_mirror 配置"); + } + Ok(config) } Err(e) => { @@ -218,6 +238,10 @@ impl ConfigManager { show_function_help: Some(false), space_dot_omission: Some(false), }), + environment_mirror: Some(EnvironmentMirrorConfig { + enabled: Some(true), + base_url: Some("http://cdn.global.devlive.top".to_string()), + }), } } diff --git a/src-tauri/src/env_commands.rs b/src-tauri/src/env_commands.rs index fda94f2..f7e12ea 100644 --- a/src-tauri/src/env_commands.rs +++ b/src-tauri/src/env_commands.rs @@ -1,6 +1,6 @@ use crate::env_manager::{EnvironmentInfo, EnvironmentManager}; use log::info; -use tauri::{AppHandle, State}; +use tauri::{AppHandle, Emitter, State}; use tokio::sync::Mutex; pub type EnvironmentManagerState = Mutex; @@ -24,20 +24,37 @@ pub async fn download_and_install_version( ) -> Result { info!("下载并安装 {} 版本 {}", language, version); let manager = env_manager.lock().await; - manager - .download_and_install_version(&language, &version, app_handle) - .await + let result = manager + .download_and_install_version(&language, &version, app_handle.clone()) + .await; + + if result.is_ok() { + // 发送配置更新事件通知前端刷新配置 + app_handle.emit("config-updated", ()).ok(); + info!("已发送配置更新事件"); + } + + result } #[tauri::command] pub async fn switch_environment_version( language: String, version: String, + app_handle: AppHandle, env_manager: State<'_, EnvironmentManagerState>, ) -> Result<(), String> { info!("切换 {} 到版本 {}", language, version); let manager = env_manager.lock().await; - manager.switch_version(&language, &version).await + let result = manager.switch_version(&language, &version).await; + + if result.is_ok() { + // 发送配置更新事件通知前端刷新配置 + app_handle.emit("config-updated", ()).ok(); + info!("已发送配置更新事件"); + } + + result } #[tauri::command] diff --git a/src-tauri/src/env_manager.rs b/src-tauri/src/env_manager.rs index 78f293a..a9fca96 100644 --- a/src-tauri/src/env_manager.rs +++ b/src-tauri/src/env_manager.rs @@ -98,7 +98,7 @@ impl EnvironmentManager { let provider = self .providers .get(language) - .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issus", language))?; + .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issues", language))?; info!("获取 {} 环境信息", language); @@ -126,7 +126,7 @@ impl EnvironmentManager { let provider = self .providers .get(language) - .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issus", language))?; + .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issues", language))?; info!("开始下载并安装 {} 版本 {}", language, version); provider.download_and_install(version, app_handle).await @@ -136,7 +136,7 @@ impl EnvironmentManager { let provider = self .providers .get(language) - .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issus", language))?; + .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issues", language))?; info!("切换 {} 到版本 {}", language, version); provider.switch_version(version).await @@ -175,3 +175,84 @@ pub fn emit_download_progress( error!("发送下载进度事件失败: {}", e); } } + +// 将 GitHub 下载 URL 转换为 CDN 镜像 URL +pub fn convert_to_cdn_url( + original_url: &str, + language: &str, + version: &str, +) -> Result { + use crate::config::get_app_config_internal; + + let config = get_app_config_internal().map_err(|e| format!("获取配置失败: {}", e))?; + + let mirror_config = config.environment_mirror.as_ref(); + let enabled = mirror_config.and_then(|m| m.enabled).unwrap_or(false); + + if !enabled { + return Ok(original_url.to_string()); + } + + let base_url = mirror_config + .and_then(|m| m.base_url.as_ref()) + .ok_or_else(|| "CDN 基础 URL 未配置".to_string())?; + + // 从原始 URL 中提取文件名 + let file_name = original_url + .split('/') + .last() + .ok_or_else(|| "无效的下载 URL".to_string())?; + + // 构建 CDN URL: {base_url}/{language}/{version}/{filename} + let cdn_url = format!( + "{}/{}/{}/{}", + base_url.trim_end_matches('/'), + language, + version, + file_name + ); + + info!("转换下载 URL: {} -> {}", original_url, cdn_url); + Ok(cdn_url) +} + +// 尝试从 CDN 下载,失败则回退到原始 URL +pub async fn download_with_fallback( + client: &reqwest::Client, + original_url: &str, + language: &str, + version: &str, +) -> Result { + // 首先尝试从 CDN 下载 + match convert_to_cdn_url(original_url, language, version) { + Ok(cdn_url) if cdn_url != original_url => { + info!("尝试从 CDN 下载: {}", cdn_url); + match client.get(&cdn_url).send().await { + Ok(response) if response.status().is_success() => { + info!("CDN 下载成功"); + return Ok(response); + } + Ok(response) => { + info!("CDN 下载失败 (HTTP {}), 回退到原始 URL", response.status()); + } + Err(e) => { + info!("CDN 下载失败 ({}), 回退到原始 URL", e); + } + } + } + Ok(_) => { + info!("CDN 镜像未启用,使用原始 URL"); + } + Err(e) => { + info!("CDN URL 转换失败 ({}), 使用原始 URL", e); + } + } + + // 回退到原始 URL + info!("从原始 URL 下载: {}", original_url); + client + .get(original_url) + .send() + .await + .map_err(|e| format!("下载失败: {}", e)) +} diff --git a/src-tauri/src/env_providers/clojure.rs b/src-tauri/src/env_providers/clojure.rs new file mode 100644 index 0000000..e37fd9d --- /dev/null +++ b/src-tauri/src/env_providers/clojure.rs @@ -0,0 +1,639 @@ +use crate::env_manager::{ + DownloadStatus, EnvironmentProvider, EnvironmentVersion, download_with_fallback, + emit_download_progress, +}; +use log::{error, info, warn}; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use std::time::{Duration, SystemTime}; +use tauri::AppHandle; + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct GithubRelease { + tag_name: String, + name: String, + published_at: String, + assets: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct GithubAsset { + name: String, + browser_download_url: String, + size: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +struct CachedReleases { + releases: Vec, + cached_at: SystemTime, +} + +pub struct ClojureEnvironmentProvider { + install_dir: PathBuf, + cache_file: PathBuf, +} + +impl ClojureEnvironmentProvider { + pub fn new() -> Self { + let install_dir = Self::get_default_install_dir(); + let cache_file = install_dir.join("releases_cache.json"); + + if let Err(e) = std::fs::create_dir_all(&install_dir) { + error!("创建 Clojure 安装目录失败: {}", e); + } + + Self { + install_dir, + cache_file, + } + } + + fn get_default_install_dir() -> PathBuf { + let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")); + home_dir.join(".codeforge").join("clojure") + } + + fn read_cache(&self) -> Option> { + if !self.cache_file.exists() { + return None; + } + + match std::fs::read_to_string(&self.cache_file) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(cached) => { + if let Ok(elapsed) = SystemTime::now().duration_since(cached.cached_at) { + if elapsed < Duration::from_secs(3600) { + info!("使用缓存的 Clojure 版本列表(缓存时间: {:?})", elapsed); + return Some(cached.releases); + } else { + info!("缓存已过期({:?}),将重新获取", elapsed); + } + } + } + Err(e) => { + warn!("解析缓存文件失败: {}", e); + } + }, + Err(e) => { + warn!("读取缓存文件失败: {}", e); + } + } + + None + } + + fn write_cache(&self, releases: &[GithubRelease]) { + let cached = CachedReleases { + releases: releases.to_vec(), + cached_at: SystemTime::now(), + }; + + match serde_json::to_string_pretty(&cached) { + Ok(content) => { + if let Err(e) = std::fs::write(&self.cache_file, content) { + warn!("写入缓存文件失败: {}", e); + } else { + info!("已缓存 Clojure 版本列表"); + } + } + Err(e) => { + warn!("序列化缓存数据失败: {}", e); + } + } + } + + fn get_download_pattern() -> &'static str { + // Clojure 工具包是跨平台的,文件名格式为 clojure-tools-{version}.tar.gz + "clojure-tools-" + } + + async fn fetch_github_releases(&self) -> Result, String> { + if let Some(cached_releases) = self.read_cache() { + return Ok(cached_releases); + } + + let url = "https://api.github.com/repos/clojure/brew-install/releases?per_page=20"; + + info!("从 GitHub API 获取 Clojure 版本列表: {}", url); + + let client = reqwest::Client::builder() + .user_agent("CodeForge") + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let mut request = client.get(url); + + if let Ok(token) = std::env::var("GITHUB_TOKEN") { + info!("使用 GITHUB_TOKEN 进行认证"); + request = request.header("Authorization", format!("token {}", token)); + } + + let response = request.send().await.map_err(|e| { + if let Some(_cached_releases) = self.read_cache_ignore_expiry() { + warn!("GitHub API 请求失败,使用过期缓存: {}", e); + return format!("GitHub API 请求失败,已使用缓存数据: {}", e); + } + format!("请求 GitHub API 失败: {}", e) + })?; + + let status = response.status(); + + if status.as_u16() == 403 || status.as_u16() == 429 { + let error_msg = if let Ok(body) = response.text().await { + if body.contains("rate limit") { + warn!("GitHub API 限流,尝试使用缓存"); + if let Some(cached_releases) = self.read_cache_ignore_expiry() { + return Ok(cached_releases); + } + format!( + "GitHub API 限流已超出。请稍后再试,或设置 GITHUB_TOKEN 环境变量以增加限额。详情: {}", + body + ) + } else { + format!("GitHub API 返回错误 ({}): {}", status, body) + } + } else { + format!("GitHub API 返回错误: {}", status) + }; + return Err(error_msg); + } + + if !status.is_success() { + return Err(format!("GitHub API 返回错误: {}", status)); + } + + let releases: Vec = response + .json() + .await + .map_err(|e| format!("解析 GitHub API 响应失败: {}", e))?; + + info!("成功获取 {} 个 Clojure 版本", releases.len()); + + self.write_cache(&releases); + + Ok(releases) + } + + fn read_cache_ignore_expiry(&self) -> Option> { + if !self.cache_file.exists() { + return None; + } + + match std::fs::read_to_string(&self.cache_file) { + Ok(content) => match serde_json::from_str::(&content) { + Ok(cached) => { + info!("使用缓存的 Clojure 版本列表(忽略过期时间)"); + Some(cached.releases) + } + Err(e) => { + warn!("解析缓存文件失败: {}", e); + None + } + }, + Err(e) => { + warn!("读取缓存文件失败: {}", e); + None + } + } + } + + fn get_version_install_path(&self, version: &str) -> PathBuf { + self.install_dir.join(version) + } + + fn is_version_installed(&self, version: &str) -> bool { + let install_path = self.get_version_install_path(version); + if !install_path.exists() { + return false; + } + + // Clojure 安装后的 bin 目录结构 + let bin_path = install_path.join("bin"); + if !bin_path.exists() { + return false; + } + + // 检查 clojure 和 clj 脚本是否存在 + let clojure_bin = bin_path.join("clojure"); + let clj_bin = bin_path.join("clj"); + + clojure_bin.exists() && clj_bin.exists() + } + + async fn download_file( + &self, + url: &str, + dest: &PathBuf, + app_handle: AppHandle, + version: &str, + ) -> Result<(), String> { + info!("开始下载: {} -> {}", url, dest.display()); + + let client = reqwest::Client::builder() + .user_agent("CodeForge") + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let response = download_with_fallback(&client, url, "clojure", version).await?; + + if !response.status().is_success() { + return Err(format!("下载失败: HTTP {}", response.status())); + } + + let total_size = response.content_length().unwrap_or(0); + info!("文件大小: {} bytes", total_size); + + emit_download_progress( + &app_handle, + "clojure", + version, + 0, + total_size, + DownloadStatus::Downloading, + ); + + let mut file = std::fs::File::create(dest).map_err(|e| format!("创建文件失败: {}", e))?; + + let mut downloaded: u64 = 0; + let mut stream = response.bytes_stream(); + + use futures_util::StreamExt; + use std::io::Write; + + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| format!("下载数据失败: {}", e))?; + file.write_all(&chunk) + .map_err(|e| format!("写入文件失败: {}", e))?; + + downloaded += chunk.len() as u64; + + if downloaded % (1024 * 1024) == 0 || downloaded == total_size { + emit_download_progress( + &app_handle, + "clojure", + version, + downloaded, + total_size, + DownloadStatus::Downloading, + ); + } + } + + info!("下载完成: {}", dest.display()); + Ok(()) + } + + async fn extract_archive( + &self, + archive_path: &PathBuf, + dest_dir: &PathBuf, + app_handle: AppHandle, + version: &str, + ) -> Result<(), String> { + info!( + "开始解压: {} -> {}", + archive_path.display(), + dest_dir.display() + ); + + emit_download_progress( + &app_handle, + "clojure", + version, + 0, + 0, + DownloadStatus::Extracting, + ); + + std::fs::create_dir_all(dest_dir).map_err(|e| format!("创建目录失败: {}", e))?; + + // Clojure 在所有平台都使用 tar.gz 格式 + self.extract_tar_gz(archive_path, dest_dir)?; + + info!("解压完成"); + Ok(()) + } + + fn extract_tar_gz(&self, archive_path: &PathBuf, dest_dir: &PathBuf) -> Result<(), String> { + use flate2::read::GzDecoder; + use tar::Archive; + + let file = + std::fs::File::open(archive_path).map_err(|e| format!("打开压缩文件失败: {}", e))?; + + let gz = GzDecoder::new(file); + let mut archive = Archive::new(gz); + + archive + .unpack(dest_dir) + .map_err(|e| format!("解压 tar.gz 失败: {}", e))?; + + Ok(()) + } + + // 组织 Clojure 安装目录结构 + fn organize_installation( + &self, + temp_dir: &Path, + install_path: &Path, + ) -> Result<(), String> { + std::fs::create_dir_all(install_path).map_err(|e| format!("创建安装目录失败: {}", e))?; + + let tools_dir = temp_dir.join("clojure-tools"); + if !tools_dir.exists() { + return Err("解压后未找到 clojure-tools 目录".to_string()); + } + + // 创建 bin 和 libexec 目录 + let bin_dir = install_path.join("bin"); + let libexec_dir = install_path.join("libexec"); + std::fs::create_dir_all(&bin_dir).map_err(|e| format!("创建 bin 目录失败: {}", e))?; + std::fs::create_dir_all(&libexec_dir) + .map_err(|e| format!("创建 libexec 目录失败: {}", e))?; + + // 移动脚本文件到 bin 目录,并替换 PREFIX 占位符 + for script in &["clojure", "clj"] { + let src = tools_dir.join(script); + let dst = bin_dir.join(script); + if src.exists() { + // 读取脚本内容 + let content = std::fs::read_to_string(&src) + .map_err(|e| format!("读取 {} 失败: {}", script, e))?; + + // 替换 PREFIX 占位符为实际的安装路径 + let install_dir_str = install_path.to_string_lossy(); + let modified_content = content.replace( + "install_dir=PREFIX", + &format!("install_dir={}", install_dir_str), + ); + + // 写入修改后的内容 + std::fs::write(&dst, modified_content) + .map_err(|e| format!("写入 {} 失败: {}", script, e))?; + + // 设置可执行权限 + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&dst, std::fs::Permissions::from_mode(0o755)) + .map_err(|e| format!("设置 {} 权限失败: {}", script, e))?; + } + } + } + + // 移动 jar 文件和配置文件到 libexec 目录 + let entries = std::fs::read_dir(&tools_dir) + .map_err(|e| format!("读取 clojure-tools 目录失败: {}", e))?; + + for entry in entries.flatten() { + let path = entry.path(); + let file_name = path.file_name().unwrap().to_string_lossy().to_string(); + + // 跳过脚本文件和 install.sh + if file_name == "clojure" + || file_name == "clj" + || file_name == "install.sh" + || file_name.ends_with(".1") + { + continue; + } + + // 复制其他文件到 libexec + if path.is_file() { + let dst = libexec_dir.join(&file_name); + std::fs::copy(&path, &dst) + .map_err(|e| format!("复制 {} 失败: {}", file_name, e))?; + } + } + + info!("Clojure 安装目录组织完成: {}", install_path.display()); + Ok(()) + } + + async fn update_plugin_config(&self, version: &str, install_path: &str) -> Result<(), String> { + use crate::config::{get_app_config_internal, update_app_config}; + + info!( + "更新 Clojure 插件配置: 版本={}, 路径={}", + version, install_path + ); + + let mut config = get_app_config_internal().map_err(|e| format!("获取配置失败: {}", e))?; + + if let Some(ref mut plugins) = config.plugins { + if let Some(clojure_plugin) = plugins.iter_mut().find(|p| p.language == "clojure") { + clojure_plugin.execute_home = Some(install_path.to_string()); + clojure_plugin.run_command = Some(String::from("bin/clojure $filename")); + + info!( + "已更新 Clojure 插件配置: execute_home={}, run_command=bin/clojure $filename", + install_path + ); + } + } + + update_app_config(config) + .await + .map_err(|e| format!("保存配置失败: {}", e))?; + + Ok(()) + } +} + +#[async_trait::async_trait] +impl EnvironmentProvider for ClojureEnvironmentProvider { + fn get_language(&self) -> &'static str { + "clojure" + } + + async fn fetch_available_versions(&self) -> Result, String> { + let releases = self.fetch_github_releases().await?; + let pattern = Self::get_download_pattern(); + + let mut versions = Vec::new(); + + for release in releases { + if let Some(asset) = release.assets.iter().find(|a| a.name.contains(pattern)) { + let version = release.tag_name.trim_start_matches('v').to_string(); + let is_installed = self.is_version_installed(&version); + + let install_path = if is_installed { + Some( + self.get_version_install_path(&version) + .to_string_lossy() + .to_string(), + ) + } else { + None + }; + + versions.push(EnvironmentVersion { + version: version.clone(), + download_url: asset.browser_download_url.clone(), + install_path, + is_installed, + size: Some(asset.size), + release_date: Some(release.published_at.clone()), + }); + } + } + + Ok(versions) + } + + async fn get_installed_versions(&self) -> Result, String> { + let mut installed = Vec::new(); + + if !self.install_dir.exists() { + return Ok(installed); + } + + let entries = + std::fs::read_dir(&self.install_dir).map_err(|e| format!("读取安装目录失败: {}", e))?; + + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() { + let version = path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("") + .to_string(); + + if self.is_version_installed(&version) { + installed.push(EnvironmentVersion { + version: version.clone(), + download_url: String::new(), + install_path: Some(path.to_string_lossy().to_string()), + is_installed: true, + size: None, + release_date: None, + }); + } + } + } + + Ok(installed) + } + + async fn download_and_install( + &self, + version: &str, + app_handle: AppHandle, + ) -> Result { + info!("开始下载并安装 Clojure {}", version); + + if self.is_version_installed(version) { + return Err(format!("Clojure {} 已经安装", version)); + } + + emit_download_progress( + &app_handle, + "clojure", + version, + 0, + 0, + DownloadStatus::Downloading, + ); + + let available_versions = self.fetch_available_versions().await?; + let version_info = available_versions + .iter() + .find(|v| v.version == version) + .ok_or_else(|| format!("未找到版本: {}", version))?; + + let download_url = &version_info.download_url; + let file_name = download_url + .split('/') + .last() + .ok_or_else(|| "无效的下载 URL".to_string())?; + let temp_file = std::env::temp_dir().join(file_name); + + self.download_file(download_url, &temp_file, app_handle.clone(), version) + .await?; + + let install_path = self.get_version_install_path(version); + let temp_extract_dir = std::env::temp_dir().join(format!("clojure-tools-{}", version)); + + self.extract_archive(&temp_file, &temp_extract_dir, app_handle.clone(), version) + .await?; + + std::fs::remove_file(&temp_file).ok(); + + emit_download_progress( + &app_handle, + "clojure", + version, + 0, + 0, + DownloadStatus::Installing, + ); + + // 组织安装目录结构 + self.organize_installation(&temp_extract_dir, &install_path)?; + + // 清理临时解压目录 + std::fs::remove_dir_all(&temp_extract_dir).ok(); + + self.update_plugin_config(version, &install_path.to_string_lossy()) + .await?; + + emit_download_progress( + &app_handle, + "clojure", + version, + 0, + 0, + DownloadStatus::Completed, + ); + + info!("Clojure {} 安装成功", version); + Ok(install_path.to_string_lossy().to_string()) + } + + async fn switch_version(&self, version: &str) -> Result<(), String> { + info!("切换 Clojure 版本到 {}", version); + + if !self.is_version_installed(version) { + return Err(format!("版本 {} 未安装", version)); + } + + let install_path = self.get_version_install_path(version); + + self.update_plugin_config(version, &install_path.to_string_lossy()) + .await?; + + info!("成功切换到 Clojure {}", version); + Ok(()) + } + + async fn get_current_version(&self) -> Result, String> { + use crate::config::get_app_config_internal; + + let config = get_app_config_internal().map_err(|e| format!("获取配置失败: {}", e))?; + + if let Some(plugins) = config.plugins { + if let Some(clojure_plugin) = plugins.iter().find(|p| p.language == "clojure") { + if let Some(ref execute_home) = clojure_plugin.execute_home { + let path = PathBuf::from(execute_home); + + if let Ok(relative) = path.strip_prefix(&self.install_dir) { + if let Some(version_component) = relative.components().next() { + if let Some(version) = version_component.as_os_str().to_str() { + info!("当前 Clojure 版本: {}", version); + return Ok(Some(version.to_string())); + } + } + } + } + } + } + + Ok(None) + } + + fn get_install_dir(&self) -> PathBuf { + self.install_dir.clone() + } +} diff --git a/src-tauri/src/env_providers/mod.rs b/src-tauri/src/env_providers/mod.rs index 3d87810..3904cbf 100644 --- a/src-tauri/src/env_providers/mod.rs +++ b/src-tauri/src/env_providers/mod.rs @@ -1,3 +1,5 @@ +pub mod clojure; pub mod scala; +pub use clojure::ClojureEnvironmentProvider; pub use scala::ScalaEnvironmentProvider; diff --git a/src-tauri/src/env_providers/scala.rs b/src-tauri/src/env_providers/scala.rs index d2500ef..846610f 100644 --- a/src-tauri/src/env_providers/scala.rs +++ b/src-tauri/src/env_providers/scala.rs @@ -1,5 +1,6 @@ use crate::env_manager::{ - DownloadStatus, EnvironmentProvider, EnvironmentVersion, emit_download_progress, + DownloadStatus, EnvironmentProvider, EnvironmentVersion, download_with_fallback, + emit_download_progress, }; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; @@ -264,11 +265,7 @@ impl ScalaEnvironmentProvider { .build() .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; - let response = client - .get(url) - .send() - .await - .map_err(|e| format!("下载失败: {}", e))?; + let response = download_with_fallback(&client, url, "scala", version).await?; if !response.status().is_success() { return Err(format!("下载失败: HTTP {}", response.status())); diff --git a/src-tauri/src/example.rs b/src-tauri/src/example.rs index 1ef5106..5c2e5d3 100644 --- a/src-tauri/src/example.rs +++ b/src-tauri/src/example.rs @@ -18,7 +18,7 @@ pub async fn load_example( let manager = plugin_manager.lock().await; let plugin = manager .get_plugin(&language) - .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issus", language))?; + .ok_or_else(|| format!("暂未支持 {} 语言,请前往 github 提供 issues", language))?; // 获取该语言的文件扩展名 let file_extension = plugin.get_file_extension(); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 2dff372..b3a960b 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -22,7 +22,7 @@ use crate::env_commands::{ get_supported_environment_languages, switch_environment_version, }; use crate::env_manager::EnvironmentManager; -use crate::env_providers::ScalaEnvironmentProvider; +use crate::env_providers::{ClojureEnvironmentProvider, ScalaEnvironmentProvider}; use crate::execution::{ ExecutionHistory, PluginManagerState as ExecutionPluginManagerState, clear_execution_history, execute_code, get_execution_history, is_execution_running, stop_execution, @@ -45,6 +45,7 @@ fn main() { // 初始化环境管理器 let mut env_manager = EnvironmentManager::new(); + env_manager.register_provider(Box::new(ClojureEnvironmentProvider::new())); env_manager.register_provider(Box::new(ScalaEnvironmentProvider::new())); tauri::Builder::default() diff --git a/src/composables/usePluginConfig.ts b/src/composables/usePluginConfig.ts index 47df01a..d5b29ff 100644 --- a/src/composables/usePluginConfig.ts +++ b/src/composables/usePluginConfig.ts @@ -1,5 +1,6 @@ -import { ref, watch } from 'vue' +import { ref, watch, onMounted, onUnmounted } from 'vue' import { invoke } from '@tauri-apps/api/core' +import { listen } from '@tauri-apps/api/event' import { debounce } from 'lodash-es' import { open as openDialog } from '@tauri-apps/plugin-dialog' import { useToast } from '../plugins/toast' @@ -30,6 +31,8 @@ export function usePluginConfig(emit?: any) const isInitialLoad = ref(true) const isSaving = ref(false) + let unlistenConfigUpdate: (() => void) | null = null + const pluginConfig = ref({ enabled: true, execute_home: '', @@ -268,6 +271,21 @@ export function usePluginConfig(emit?: any) } }) + // 监听配置更新事件 + onMounted(async () => { + unlistenConfigUpdate = await listen('config-updated', async () => { + console.log('收到配置更新事件,重新加载配置') + await getGlobalConfig() + }) + }) + + // 清理事件监听 + onUnmounted(() => { + if (unlistenConfigUpdate) { + unlistenConfigUpdate() + } + }) + return { // 状态 activePlugin, From 363fe1f3f5fdfd2e6d38f3c5c3851b448a7a5924 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Dec 2025 17:59:37 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=88=87?= =?UTF-8?q?=E6=8D=A2=E7=89=88=E6=9C=AC=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/set_version.sh | 79 +++++++++++++++++++++++++++++++++++++++ package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 5 files changed, 83 insertions(+), 4 deletions(-) create mode 100755 bin/set_version.sh diff --git a/bin/set_version.sh b/bin/set_version.sh new file mode 100755 index 0000000..580c1ab --- /dev/null +++ b/bin/set_version.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +# 版本设置脚本 +# 用法: ./bin/set_version.sh +# 例如: ./bin/set_version.sh 25.0.5 + +set -e + +if [ -z "$1" ]; then + echo "错误: 请提供版本号" + echo "用法: $0 " + echo "例如: $0 25.0.5" + exit 1 +fi + +NEW_VERSION=$1 +CURRENT_BRANCH=$(git branch --show-current) + +echo "==========================================" +echo "设置新版本: $NEW_VERSION" +echo "当前分支: $CURRENT_BRANCH" +echo "==========================================" + +# 1. 更新 package.json +echo "更新 package.json..." +if [ -f "package.json" ]; then + sed -i.bak "s/\"version\": \".*\"/\"version\": \"$NEW_VERSION\"/" package.json + rm -f package.json.bak + echo "✓ package.json 已更新" +else + echo "⚠ package.json 不存在" +fi + +# 2. 更新 src-tauri/Cargo.toml +echo "更新 src-tauri/Cargo.toml..." +if [ -f "src-tauri/Cargo.toml" ]; then + sed -i.bak "s/^version = \".*\"/version = \"$NEW_VERSION\"/" src-tauri/Cargo.toml + rm -f src-tauri/Cargo.toml.bak + echo "✓ src-tauri/Cargo.toml 已更新" +else + echo "⚠ src-tauri/Cargo.toml 不存在" +fi + +# 3. 更新 src-tauri/tauri.conf.json +echo "更新 src-tauri/tauri.conf.json..." +if [ -f "src-tauri/tauri.conf.json" ]; then + sed -i.bak "s/\"version\": \".*\"/\"version\": \"$NEW_VERSION\"/" src-tauri/tauri.conf.json + rm -f src-tauri/tauri.conf.json.bak + echo "✓ src-tauri/tauri.conf.json 已更新" +else + echo "⚠ src-tauri/tauri.conf.json 不存在" +fi + +# 4. 创建新的开发分支 +NEW_BRANCH="dev-$NEW_VERSION" +echo "" +echo "创建新的开发分支: $NEW_BRANCH" +read -p "是否创建新分支并切换? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + git checkout -b "$NEW_BRANCH" + echo "✓ 已创建并切换到分支: $NEW_BRANCH" +else + echo "跳过创建新分支" +fi + +echo "" +echo "==========================================" +echo "版本更新完成!" +echo "新版本: $NEW_VERSION" +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "新分支: $NEW_BRANCH" +fi +echo "==========================================" +echo "" +echo "接下来的步骤:" +echo "1. 检查更改: git diff" +echo "2. 提交更改: git add . && git commit -m 'chore: bump version to $NEW_VERSION'" +echo "3. 推送到远程: git push -u origin $NEW_BRANCH" diff --git a/package.json b/package.json index bcd126a..71932d2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codeforge", "private": true, - "version": "25.0.4", + "version": "25.0.5", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index c7603b9..fcb865c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "CodeForge" -version = "25.0.4" +version = "25.0.5" dependencies = [ "async-trait", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a4946c5..cb41e95 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "CodeForge" -version = "25.0.4" +version = "25.0.5" description = "CodeForge 是一款轻量级、高性能的桌面代码执行器,专为开发者、学生和编程爱好者设计。" authors = ["devlive-community"] edition = "2024" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ded4215..921429e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CodeForge", - "version": "25.0.4", + "version": "25.0.5", "identifier": "org.devlive.codeforge", "build": { "beforeDevCommand": "pnpm dev", From 8a9851d11907288dd7090d5759128cf4233f8d44 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Dec 2025 18:05:38 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E6=A3=80=E6=B5=8B=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 3 ++- src/components/StatusBar.vue | 16 +++++++++++++++- src/composables/useLanguageManager.ts | 1 + 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/App.vue b/src/App.vue index 8a0b501..9e7c588 100644 --- a/src/App.vue +++ b/src/App.vue @@ -52,7 +52,7 @@ - + @@ -115,6 +115,7 @@ const { getCurrentConsoleType, handleLanguageChange, refreshLanguageList, + refreshEnvInfo, initialize } = useLanguageManager(code, clearOutput, toast) diff --git a/src/components/StatusBar.vue b/src/components/StatusBar.vue index 8b91f6c..4f65b42 100644 --- a/src/components/StatusBar.vue +++ b/src/components/StatusBar.vue @@ -7,6 +7,12 @@ :class="[getIconClass(), { 'animate-spin': isLoading }]" class="w-4 h-4"/> {{ getStatusText() }} +
@@ -25,7 +31,7 @@ diff --git a/src/composables/useLanguageManager.ts b/src/composables/useLanguageManager.ts index 2d944d1..9d4b87a 100644 --- a/src/composables/useLanguageManager.ts +++ b/src/composables/useLanguageManager.ts @@ -203,6 +203,7 @@ export function useLanguageManager( getLanguageDisplayName, handleLanguageChange, refreshLanguageList, + refreshEnvInfo, initialize, getCurrentPluginConfig, getCurrentConsoleType From 28e3174c1a9f82d45e7feb744e2a00a9bce165eb Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Dec 2025 18:18:18 +0800 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E9=85=8D=E7=BD=AE=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Settings.vue | 7 + src/components/setting/Network.vue | 203 +++++++++++++++++++++++++++++ src/composables/useSettings.ts | 11 +- 3 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 src/components/setting/Network.vue diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 21a5e96..bfb902b 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -20,6 +20,11 @@ + + + @@ -31,6 +36,7 @@ import Tabs from '../ui/Tabs.vue' import General from './setting/General.vue' import Language from './setting/Language.vue' import Editor from './setting/Editor.vue' +import Network from './setting/Network.vue' import { useSettings } from '../composables/useSettings.ts' const emit = defineEmits<{ @@ -44,6 +50,7 @@ const { tabsData, handleEditorSettingsChanged, handleLanguageSettingsChanged, + handleNetworkSettingsChanged, handleEditorError, closeSettings, initialize diff --git a/src/components/setting/Network.vue b/src/components/setting/Network.vue new file mode 100644 index 0000000..a467605 --- /dev/null +++ b/src/components/setting/Network.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/composables/useSettings.ts b/src/composables/useSettings.ts index 1e4d9f5..91dc8ff 100644 --- a/src/composables/useSettings.ts +++ b/src/composables/useSettings.ts @@ -1,5 +1,5 @@ import { nextTick, ref } from 'vue' -import { BracesIcon, CodeIcon, ShieldIcon } from 'lucide-vue-next' +import { BracesIcon, CodeIcon, Globe, ShieldIcon } from 'lucide-vue-next' export function useSettings(emit: any) { @@ -11,7 +11,8 @@ export function useSettings(emit: any) const tabsData = [ { key: 'general', label: '通用', icon: ShieldIcon }, { key: 'editor', label: '编辑器', icon: CodeIcon }, - { key: 'language', label: '语言', icon: BracesIcon } + { key: 'language', label: '语言', icon: BracesIcon }, + { key: 'network', label: '网络', icon: Globe } ] const handleEditorSettingsChanged = (config: any) => { @@ -24,6 +25,11 @@ export function useSettings(emit: any) emit('settings-changed', config) } + const handleNetworkSettingsChanged = (config: any) => { + console.log('设置模态框接收到网络配置变更:', config) + emit('settings-changed', config) + } + const handleEditorError = (message: string) => { console.error('编辑器设置错误:', message) } @@ -51,6 +57,7 @@ export function useSettings(emit: any) tabsData, handleEditorSettingsChanged, handleLanguageSettingsChanged, + handleNetworkSettingsChanged, handleEditorError, closeSettings, initialize From 68b1138c9a0615d041ee4661fb5c33b619eaa753 Mon Sep 17 00:00:00 2001 From: qianmoQ Date: Fri, 26 Dec 2025 18:47:39 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=E5=9B=9E=E6=BB=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src-tauri/src/config.rs | 4 + src-tauri/src/env_manager.rs | 23 ++- src-tauri/src/env_providers/clojure.rs | 238 +++++++++++++++++++++++-- src-tauri/src/env_providers/scala.rs | 2 + src/components/setting/Network.vue | 52 +++++- 5 files changed, 302 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 6c48998..7af01f3 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -26,6 +26,7 @@ pub struct EditorConfig { pub struct EnvironmentMirrorConfig { pub enabled: Option, pub base_url: Option, + pub fallback_enabled: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -60,6 +61,7 @@ impl Default for AppConfig { environment_mirror: Some(EnvironmentMirrorConfig { enabled: Some(true), base_url: Some("http://cdn.global.devlive.top".to_string()), + fallback_enabled: Some(false), }), } } @@ -129,6 +131,7 @@ impl ConfigManager { config.environment_mirror = Some(EnvironmentMirrorConfig { enabled: Some(true), base_url: Some("http://cdn.global.devlive.top".to_string()), + fallback_enabled: Some(false), }); println!("读取配置 -> 添加默认 environment_mirror 配置"); } @@ -241,6 +244,7 @@ impl ConfigManager { environment_mirror: Some(EnvironmentMirrorConfig { enabled: Some(true), base_url: Some("http://cdn.global.devlive.top".to_string()), + fallback_enabled: Some(false), }), } } diff --git a/src-tauri/src/env_manager.rs b/src-tauri/src/env_manager.rs index a9fca96..dcf8ff6 100644 --- a/src-tauri/src/env_manager.rs +++ b/src-tauri/src/env_manager.rs @@ -10,6 +10,7 @@ use tauri::{AppHandle, Emitter}; pub struct EnvironmentVersion { pub version: String, pub download_url: String, + pub fallback_url: Option, // 备用下载地址(如 GitHub URL) pub install_path: Option, pub is_installed: bool, pub size: Option, @@ -223,6 +224,15 @@ pub async fn download_with_fallback( language: &str, version: &str, ) -> Result { + use crate::config::get_app_config_internal; + + // 检查是否启用自动回退 + let fallback_enabled = get_app_config_internal() + .ok() + .and_then(|config| config.environment_mirror) + .and_then(|mirror| mirror.fallback_enabled) + .unwrap_or(false); + // 首先尝试从 CDN 下载 match convert_to_cdn_url(original_url, language, version) { Ok(cdn_url) if cdn_url != original_url => { @@ -233,10 +243,19 @@ pub async fn download_with_fallback( return Ok(response); } Ok(response) => { - info!("CDN 下载失败 (HTTP {}), 回退到原始 URL", response.status()); + let status = response.status(); + if fallback_enabled { + info!("CDN 下载失败 (HTTP {}), 回退到原始 URL", status); + } else { + return Err(format!("CDN 下载失败 (HTTP {}), 未启用自动回退", status)); + } } Err(e) => { - info!("CDN 下载失败 ({}), 回退到原始 URL", e); + if fallback_enabled { + info!("CDN 下载失败 ({}), 回退到原始 URL", e); + } else { + return Err(format!("CDN 下载失败 ({}), 未启用自动回退", e)); + } } } } diff --git a/src-tauri/src/env_providers/clojure.rs b/src-tauri/src/env_providers/clojure.rs index e37fd9d..872cda9 100644 --- a/src-tauri/src/env_providers/clojure.rs +++ b/src-tauri/src/env_providers/clojure.rs @@ -1,6 +1,5 @@ use crate::env_manager::{ - DownloadStatus, EnvironmentProvider, EnvironmentVersion, download_with_fallback, - emit_download_progress, + DownloadStatus, EnvironmentProvider, EnvironmentVersion, emit_download_progress, }; use log::{error, info, warn}; use serde::{Deserialize, Serialize}; @@ -23,6 +22,25 @@ struct GithubAsset { size: u64, } +// CDN Metadata 结构 +#[derive(Debug, Deserialize, Serialize, Clone)] +struct MetadataRelease { + version: String, // 版本号,如 "1.11.1.1262" + display_name: String, // 显示名称,如 "Clojure 1.11.1.1262" + published_at: String, // 发布时间 + download_url: String, // CDN 下载地址 + github_url: String, // GitHub 官方下载地址(作为备用) + file_name: String, // 文件名,如 "clojure-tools-1.11.1.1262.tar.gz" + size: u64, // 文件大小(字节) + supported_platforms: Vec, // 支持的平台,如 ["macos", "linux", "windows"] +} + +#[derive(Debug, Deserialize, Serialize)] +struct Metadata { + language: String, // 语言名称 "clojure" + releases: Vec, +} + #[derive(Debug, Deserialize, Serialize)] struct CachedReleases { releases: Vec, @@ -108,6 +126,115 @@ impl ClojureEnvironmentProvider { "clojure-tools-" } + // 获取当前系统平台 + fn get_current_platform() -> &'static str { + if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "windows") { + "windows" + } else { + "unknown" + } + } + + // 将 metadata 转换为 EnvironmentVersion 列表 + fn parse_metadata_to_versions( + &self, + metadata: Metadata, + ) -> Result, String> { + let current_platform = Self::get_current_platform(); + let mut versions = Vec::new(); + + for release in metadata.releases { + // 检查是否支持当前平台 + if !release + .supported_platforms + .contains(¤t_platform.to_string()) + { + continue; + } + + let is_installed = self.is_version_installed(&release.version); + let install_path = if is_installed { + Some( + self.get_version_install_path(&release.version) + .to_string_lossy() + .to_string(), + ) + } else { + None + }; + + versions.push(EnvironmentVersion { + version: release.version.clone(), + download_url: release.download_url.clone(), // 直接使用 metadata 中的 CDN 下载地址 + fallback_url: Some(release.github_url.clone()), // 保存 GitHub URL 作为备用 + install_path, + is_installed, + size: Some(release.size), + release_date: Some(release.published_at.clone()), + }); + } + + if versions.is_empty() { + return Err(format!("没有找到支持 {} 平台的版本", current_platform)); + } + + Ok(versions) + } + + // 从 CDN 获取 metadata.json + async fn fetch_metadata_from_cdn(&self) -> Result { + use crate::config::get_app_config_internal; + + let config = get_app_config_internal().map_err(|e| format!("读取配置失败: {}", e))?; + + let cdn_enabled = config + .environment_mirror + .as_ref() + .and_then(|m| m.enabled) + .unwrap_or(false); + + if !cdn_enabled { + return Err("CDN 未启用".to_string()); + } + + let base_url = config + .environment_mirror + .as_ref() + .and_then(|m| m.base_url.as_ref()) + .ok_or("CDN 地址未配置")?; + + let metadata_url = format!("{}/clojure/metadata.json", base_url); + info!("从 CDN 获取 Clojure metadata: {}", metadata_url); + + let client = reqwest::Client::builder() + .user_agent("CodeForge") + .timeout(Duration::from_secs(30)) + .build() + .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; + + let response = client + .get(&metadata_url) + .send() + .await + .map_err(|e| format!("请求 CDN metadata 失败: {}", e))?; + + if !response.status().is_success() { + return Err(format!("CDN 返回错误状态码: {}", response.status())); + } + + let metadata: Metadata = response + .json() + .await + .map_err(|e| format!("解析 metadata.json 失败: {}", e))?; + + info!("成功从 CDN 获取 {} 个版本", metadata.releases.len()); + Ok(metadata) + } + async fn fetch_github_releases(&self) -> Result, String> { if let Some(cached_releases) = self.read_cache() { return Ok(cached_releases); @@ -224,10 +351,13 @@ impl ClojureEnvironmentProvider { async fn download_file( &self, url: &str, + fallback_url: Option<&String>, dest: &PathBuf, app_handle: AppHandle, version: &str, ) -> Result<(), String> { + use crate::config::get_app_config_internal; + info!("开始下载: {} -> {}", url, dest.display()); let client = reqwest::Client::builder() @@ -235,7 +365,64 @@ impl ClojureEnvironmentProvider { .build() .map_err(|e| format!("创建 HTTP 客户端失败: {}", e))?; - let response = download_with_fallback(&client, url, "clojure", version).await?; + // 尝试从主 URL 下载 + let response = match client.get(url).send().await { + Ok(resp) if resp.status().is_success() => { + info!("下载成功"); + resp + } + Ok(resp) => { + let status = resp.status(); + warn!("下载失败: HTTP {}", status); + + // 检查是否启用 fallback 且有 fallback URL + if let Some(fb_url) = fallback_url { + let fallback_enabled = get_app_config_internal() + .ok() + .and_then(|config| config.environment_mirror) + .and_then(|mirror| mirror.fallback_enabled) + .unwrap_or(false); + + if fallback_enabled { + info!("尝试使用备用 URL: {}", fb_url); + client + .get(fb_url) + .send() + .await + .map_err(|e| format!("备用 URL 下载失败: {}", e))? + } else { + return Err(format!("下载失败 (HTTP {}), 未启用自动回退", status)); + } + } else { + return Err(format!("下载失败: HTTP {}", status)); + } + } + Err(e) => { + warn!("下载失败: {}", e); + + // 检查是否启用 fallback 且有 fallback URL + if let Some(fb_url) = fallback_url { + let fallback_enabled = get_app_config_internal() + .ok() + .and_then(|config| config.environment_mirror) + .and_then(|mirror| mirror.fallback_enabled) + .unwrap_or(false); + + if fallback_enabled { + info!("尝试使用备用 URL: {}", fb_url); + client + .get(fb_url) + .send() + .await + .map_err(|e| format!("备用 URL 下载失败: {}", e))? + } else { + return Err(format!("下载失败 ({}), 未启用自动回退", e)); + } + } else { + return Err(format!("下载失败: {}", e)); + } + } + }; if !response.status().is_success() { return Err(format!("下载失败: HTTP {}", response.status())); @@ -333,11 +520,7 @@ impl ClojureEnvironmentProvider { } // 组织 Clojure 安装目录结构 - fn organize_installation( - &self, - temp_dir: &Path, - install_path: &Path, - ) -> Result<(), String> { + fn organize_installation(&self, temp_dir: &Path, install_path: &Path) -> Result<(), String> { std::fs::create_dir_all(install_path).map_err(|e| format!("创建安装目录失败: {}", e))?; let tools_dir = temp_dir.join("clojure-tools"); @@ -448,6 +631,32 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { } async fn fetch_available_versions(&self) -> Result, String> { + use crate::config::get_app_config_internal; + + // 优先尝试从 CDN 获取 metadata.json + match self.fetch_metadata_from_cdn().await { + Ok(metadata) => { + info!("使用 CDN metadata 获取版本列表"); + return self.parse_metadata_to_versions(metadata); + } + Err(e) => { + warn!("CDN metadata 获取失败: {}", e); + + // 检查是否启用 fallback + let fallback_enabled = get_app_config_internal() + .ok() + .and_then(|config| config.environment_mirror) + .and_then(|mirror| mirror.fallback_enabled) + .unwrap_or(false); + + if !fallback_enabled { + return Err(format!("CDN metadata 获取失败,未启用自动回退: {}", e)); + } + + info!("fallback 已启用,回退到 GitHub API"); + } + } + let releases = self.fetch_github_releases().await?; let pattern = Self::get_download_pattern(); @@ -471,6 +680,7 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { versions.push(EnvironmentVersion { version: version.clone(), download_url: asset.browser_download_url.clone(), + fallback_url: None, // GitHub API 获取的版本没有 CDN URL,所以不需要 fallback install_path, is_installed, size: Some(asset.size), @@ -505,6 +715,7 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { installed.push(EnvironmentVersion { version: version.clone(), download_url: String::new(), + fallback_url: None, install_path: Some(path.to_string_lossy().to_string()), is_installed: true, size: None, @@ -544,14 +755,21 @@ impl EnvironmentProvider for ClojureEnvironmentProvider { .ok_or_else(|| format!("未找到版本: {}", version))?; let download_url = &version_info.download_url; + let fallback_url = version_info.fallback_url.as_ref(); let file_name = download_url .split('/') .last() .ok_or_else(|| "无效的下载 URL".to_string())?; let temp_file = std::env::temp_dir().join(file_name); - self.download_file(download_url, &temp_file, app_handle.clone(), version) - .await?; + self.download_file( + download_url, + fallback_url, + &temp_file, + app_handle.clone(), + version, + ) + .await?; let install_path = self.get_version_install_path(version); let temp_extract_dir = std::env::temp_dir().join(format!("clojure-tools-{}", version)); diff --git a/src-tauri/src/env_providers/scala.rs b/src-tauri/src/env_providers/scala.rs index 846610f..555086f 100644 --- a/src-tauri/src/env_providers/scala.rs +++ b/src-tauri/src/env_providers/scala.rs @@ -493,6 +493,7 @@ impl EnvironmentProvider for ScalaEnvironmentProvider { versions.push(EnvironmentVersion { version: version.clone(), download_url: asset.browser_download_url.clone(), + fallback_url: None, install_path, is_installed, size: Some(asset.size), @@ -539,6 +540,7 @@ impl EnvironmentProvider for ScalaEnvironmentProvider { installed.push(EnvironmentVersion { version: version.clone(), download_url: String::new(), + fallback_url: None, install_path: Some(actual_install_path.to_string_lossy().to_string()), is_installed: true, size: None, diff --git a/src/components/setting/Network.vue b/src/components/setting/Network.vue index a467605..e5b2af4 100644 --- a/src/components/setting/Network.vue +++ b/src/components/setting/Network.vue @@ -41,6 +41,33 @@ @input="handleCdnBaseUrlChange"/> +
+
+
+ + +
+
+
+
+ + 已启用 +
+
+ + 未启用 +
+
+
+
@@ -51,7 +78,8 @@
  • URL 格式:{base_url}/{language}/{version}/{filename}
  • 例如:http://cdn.global.devlive.top/clojure/1.12.4.1582/clojure-tools-1.12.4.1582.tar.gz
  • -
  • 如果 CDN 下载失败,系统会自动回退到 GitHub 官方源
  • +
  • 启用自动回退后,CDN 下载失败会自动使用 GitHub 官方源
  • +
  • 关闭自动回退后,CDN 下载失败将直接报错,不会尝试其他源
  • @@ -76,7 +104,7 @@