diff --git a/.gitignore b/.gitignore index b9042c4..12fd92d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ go.mod .Rhistory program .env +*.gz diff --git a/docs/content/release/25.0.5.md b/docs/content/release/25.0.5.md new file mode 100644 index 0000000..7e03ccc --- /dev/null +++ b/docs/content/release/25.0.5.md @@ -0,0 +1,96 @@ +--- +title: 25.0.4 +--- + +CodeForge v25.0.5 正式发布!本次更新带来了 JVM 生态和 Go 语言的完整支持,全面优化了环境管理、网络配置和用户体验。我们新增了 3 种重要编程语言,重构了版本管理系统,让 CodeForge 成为更强大、更易用的代码执行工具。 + +CodeForge v25.0.5 is officially released! This update brings complete support for the JVM ecosystem and Go language, with comprehensive optimizations to environment management, network configuration, and user experience. We've added 3 important programming languages and refactored the version management system, making CodeForge a more powerful and user-friendly code execution tool. + +--- + +## 📦 版本信息 | Release Information + +- **项目地址 | Repository**:https://github.com/devlive-community/codeforge +- **官方网站 | Official Website**:https://codeforge.devlive.org/ +- **版本号 | Version**:v25.0.5 +- **发布日期 | Release Date**:2025年12月28日 | December 28, 2025 + +--- + +## ✨ 新增功能 | New Features + +### 新增语言支持 | New Language Support + +- **🐹 Go** - Google 开发的现代系统编程语言,支持一键环境安装 +- **🔷 Scala** - JVM 平台函数式编程语言,支持版本同步管理 +- **🎯 Clojure** - JVM 平台 Lisp 方言,现代函数式编程,支持版本同步 + +现在 CodeForge 支持 **32+** 种编程语言。 + +Now CodeForge supports **32+** programming languages. + +### 环境管理增强 | Enhanced Environment Management + +- **版本卸载功能** - 支持已安装版本的便捷卸载,管理更灵活 +- **版本切换脚本** - 快速切换不同语言版本,提升开发效率 +- **手动环境检测** - 手动触发环境检测,确保配置准确性 +- **动态配置刷新** - 配置修改即时生效,无需重启应用 +- **目录结构优化** - 统一日志文件、插件安装和缓冲目录 + +**Version Uninstallation** - Convenient uninstallation of installed versions for flexible management +**Version Switching Scripts** - Quickly switch between different language versions for improved efficiency +**Manual Environment Detection** - Manually trigger environment detection to ensure configuration accuracy +**Dynamic Configuration Refresh** - Configuration changes take effect immediately without restart +**Directory Structure Optimization** - Unified log file, plugin installation, and buffer directories + +### 网络与性能优化 | Network & Performance Optimization + +- **网络配置页面** - 全新网络设置界面,支持自定义参数配置 +- **网络回滚机制** - 配置出错可快速回滚,保障系统稳定性 +- **统一 CDN 加速** - 全面启用 CDN,大幅提升下载速度 +- **缓冲管理优化** - 增强缓冲区管理,提升整体性能表现 + +**Network Configuration Page** - Brand new network settings interface with customizable parameters +**Network Rollback Mechanism** - Quick rollback for configuration errors to ensure system stability +**Unified CDN Acceleration** - Full CDN acceleration for significantly faster downloads +**Buffer Management Optimization** - Enhanced buffer management for improved overall performance + +### 用户体验提升 | User Experience Improvements + +- **窗口拖拽调整** - 支持自由拖拽调整窗口大小 +- **滚动条美化** - 重新设计滚动条样式,界面更精致 +- **版本列表优化** - 美化已安装版本和可用版本展示 +- **插件管理** - 支持插件启用/禁用切换,灵活控制功能 +- **显示优化** - 修复内容显示不全问题,完善视觉体验 + +**Resizable Windows** - Support for drag-to-resize windows +**Beautified Scrollbars** - Redesigned scrollbar styles for a more refined interface +**Optimized Version Lists** - Enhanced display for installed and available versions +**Plugin Management** - Support for enabling/disabling plugins for flexible control +**Display Optimization** - Fixed incomplete content display for better visual experience + +--- + +## 🐛 问题修复 | Bug Fixes + +- **修复代码格式化问题** - 解决代码格式化相关异常 +- **修复插件输出异常** - 解决 Scala 和 Clojure 插件无效输出 +- **修复显示问题** - 修复窗口内容显示不全的问题 +- **修复自动保存问题** - 修复打开语言配置时的自动保存异常 +- **修复界面宽度问题** - 修复语言列表宽度显示异常 +- **代码质量优化** - 解决代码警告,提升代码质量 + +**Fixed Code Formatting Issues** - Resolved code formatting related anomalies +**Fixed Plugin Output Issues** - Resolved invalid output from Scala and Clojure plugins +**Fixed Display Issues** - Fixed incomplete window content display +**Fixed Auto-save Issues** - Fixed auto-save anomaly when opening language configuration +**Fixed Interface Width Issues** - Fixed language list width display anomaly +**Code Quality Optimization** - Resolved code warnings and improved code quality + +--- + +## 📥 立即下载 | Download Now + +在 [GitHub Releases](https://github.com/devlive-community/codeforge/releases) 下载最新版本,或访问[官方网站](https://codeforge.devlive.org/)了解更多信息。 + +Download the latest version from [GitHub Releases](https://github.com/devlive-community/codeforge/releases), or visit the [Official Website](https://codeforge.devlive.org/) for more information. \ No newline at end of file diff --git a/docs/pageforge.yaml b/docs/pageforge.yaml index fd28730..2432cc1 100644 --- a/docs/pageforge.yaml +++ b/docs/pageforge.yaml @@ -43,6 +43,7 @@ footer: nav: - 发布日志: + - /release/25.0.5.md - /release/25.0.4.md - /release/25.0.3.md - /release/25.0.2.md diff --git a/scripts/sync-clojure-versions.js b/scripts/sync-clojure-versions.js index e517aba..4716571 100755 --- a/scripts/sync-clojure-versions.js +++ b/scripts/sync-clojure-versions.js @@ -72,6 +72,7 @@ function getConfig() { ossAccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, ossBucket: process.env.OSS_BUCKET, cdnDomain: process.env.CDN_DOMAIN, // 自定义 CDN 域名(可选) + githubToken: process.env.GITHUB_TOKEN, // GitHub Personal Access Token(可选,用于提高 API 速率限制) githubRepo: 'clojure/brew-install', ossPrefix: 'global/plugins/clojure/', tempDir: path.join(__dirname, '.temp-clojure'), @@ -130,6 +131,11 @@ function httpGet(url, isJson = true) { } }; + // 如果是 GitHub API 请求且配置了 Token,添加认证头 + if (url.includes('api.github.com') && CONFIG.githubToken) { + options.headers['Authorization'] = `token ${CONFIG.githubToken}`; + } + client.get(url, options, (res) => { if (res.statusCode === 302 || res.statusCode === 301) { // 处理重定向 diff --git a/scripts/sync-go-versions.js b/scripts/sync-go-versions.js new file mode 100755 index 0000000..17ba814 --- /dev/null +++ b/scripts/sync-go-versions.js @@ -0,0 +1,339 @@ +#!/usr/bin/env node + +/** + * 同步 Go 版本到阿里云 OSS + * + * 功能: + * 1. 从 GitHub API 获取 Go 所有版本 + * 2. 下载版本文件 + * 3. 使用阿里云官方 ali-oss SDK 上传到 OSS /global/plugins/go/ 目录 + * 4. 生成 metadata.json 文件 + * + * 使用方法: + * node scripts/sync-go-versions.js + * + * 环境变量(可以在 .env 文件中配置): + * - OSS_REGION: 阿里云 OSS 区域 + * - OSS_ACCESS_KEY_ID: 阿里云访问密钥 ID + * - OSS_ACCESS_KEY_SECRET: 阿里云访问密钥 Secret + * - OSS_BUCKET: OSS Bucket 名称 + * - CDN_DOMAIN: 自定义 CDN 域名(可选) + */ + +import https from 'https'; +import http from 'http'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; +import { fileURLToPath } from 'url'; +import OSS from 'ali-oss'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function loadEnv() { + const envPath = path.join(__dirname, '..', '.env'); + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf8'); + let loadedCount = 0; + + envContent.split('\n').forEach(line => { + line = line.trim(); + if (!line || line.startsWith('#')) return; + + const match = line.match(/^([^=]+)=(.*)$/); + if (match) { + const key = match[1].trim(); + let value = match[2].trim(); + value = value.replace(/^["']|["']$/g, ''); + if (!process.env[key]) { + process.env[key] = value; + loadedCount++; + } + } + }); + + console.log(`✓ 已从 .env 文件加载 ${loadedCount} 个环境变量\n`); + } else { + console.log('⚠ 未找到 .env 文件,将使用环境变量或默认值\n'); + } +} + +function getConfig() { + return { + ossRegion: process.env.OSS_REGION || 'oss-cn-hangzhou', + ossAccessKeyId: process.env.OSS_ACCESS_KEY_ID, + ossAccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, + ossBucket: process.env.OSS_BUCKET, + cdnDomain: process.env.CDN_DOMAIN, + githubToken: process.env.GITHUB_TOKEN, + githubRepo: 'golang/go', + ossPrefix: 'global/plugins/go/', + tempDir: path.join(__dirname, '.temp-go'), + platformMap: { + 'darwin-arm64': 'macos-aarch64', + 'darwin-amd64': 'macos-x86_64', + 'linux-arm64': 'linux-aarch64', + 'linux-amd64': 'linux-x86_64', + 'windows-amd64': 'windows-x86_64' + } + }; +} + +let CONFIG; + +function validateConfig() { + const missing = []; + if (!CONFIG.ossAccessKeyId) missing.push('OSS_ACCESS_KEY_ID'); + if (!CONFIG.ossAccessKeySecret) missing.push('OSS_ACCESS_KEY_SECRET'); + if (!CONFIG.ossBucket) missing.push('OSS_BUCKET'); + + if (missing.length > 0) { + console.error('错误: 请设置以下环境变量:'); + missing.forEach(key => console.error(` - ${key}`)); + console.error('\n提示: 可以在 .env 文件中配置这些变量'); + console.error('示例: cp .env.example .env'); + process.exit(1); + } + + console.log('配置信息:'); + console.log(` OSS Region: ${CONFIG.ossRegion}`); + console.log(` OSS Bucket: ${CONFIG.ossBucket}`); + console.log(` CDN Domain: ${CONFIG.cdnDomain || '未配置 (使用默认 OSS 域名)'}`); + console.log(''); +} + +function ensureTempDir() { + if (!fs.existsSync(CONFIG.tempDir)) { + fs.mkdirSync(CONFIG.tempDir, { recursive: true }); + } +} + +function cleanupTempDir() { + if (fs.existsSync(CONFIG.tempDir)) { + fs.rmSync(CONFIG.tempDir, { recursive: true, force: true }); + } +} + +function httpGet(url, isJson = true) { + return new Promise((resolve, reject) => { + const client = url.startsWith('https') ? https : http; + const options = { + headers: { + 'User-Agent': 'CodeForge-Sync-Script' + } + }; + + if (url.includes('api.github.com') && CONFIG.githubToken) { + options.headers['Authorization'] = `token ${CONFIG.githubToken}`; + } + + client.get(url, options, (res) => { + if (res.statusCode === 302 || res.statusCode === 301) { + return httpGet(res.headers.location, isJson).then(resolve).catch(reject); + } + + if (res.statusCode !== 200) { + reject(new Error(`HTTP ${res.statusCode}: ${url}`)); + return; + } + + const chunks = []; + res.on('data', chunk => chunks.push(chunk)); + res.on('end', () => { + const data = Buffer.concat(chunks); + if (isJson) { + try { + resolve(JSON.parse(data.toString())); + } catch (e) { + reject(new Error(`JSON 解析失败: ${e.message}`)); + } + } else { + resolve(data); + } + }); + }).on('error', reject); + }); +} + +async function downloadFile(url, destPath) { + console.log(` 下载: ${url}`); + const data = await httpGet(url, false); + fs.writeFileSync(destPath, data); + return destPath; +} + +async function getGoReleases() { + console.log('正在获取 Go 版本列表...'); + const url = 'https://go.dev/dl/?mode=json&include=all'; + return await httpGet(url); +} + +function calculateMD5(filePath) { + const buffer = fs.readFileSync(filePath); + return crypto.createHash('md5').update(buffer).digest('hex'); +} + +function getFileSize(filePath) { + const stats = fs.statSync(filePath); + return stats.size; +} + +function createOSSClient() { + return new OSS({ + region: CONFIG.ossRegion, + accessKeyId: CONFIG.ossAccessKeyId, + accessKeySecret: CONFIG.ossAccessKeySecret, + bucket: CONFIG.ossBucket + }); +} + +async function uploadToOSS(client, localPath, ossPath) { + try { + await client.put(ossPath, localPath); + console.log(` ✓ 上传成功: ${ossPath}`); + } catch (error) { + throw new Error(`上传失败: ${error.message}`); + } +} + +async function uploadMetadata(client, metadata) { + try { + const metadataJson = JSON.stringify(metadata, null, 2); + const buffer = Buffer.from(metadataJson, 'utf8'); + const ossPath = `${CONFIG.ossPrefix}metadata.json`; + + await client.put(ossPath, buffer); + console.log(`✓ metadata.json 上传成功`); + } catch (error) { + throw new Error(`上传 metadata 失败: ${error.message}`); + } +} + +async function main() { + try { + console.log('=== Go 版本同步工具 ===\n'); + + loadEnv(); + CONFIG = getConfig(); + validateConfig(); + + const ossClient = createOSSClient(); + ensureTempDir(); + + const releases = await getGoReleases(); + console.log(`找到 ${releases.length} 个版本\n`); + + const metadata = { + language: 'go', + last_updated: new Date().toISOString(), + releases: [] + }; + + const goOsMap = { + 'darwin': ['macos-aarch64', 'macos-x86_64'], + 'linux': ['linux-aarch64', 'linux-x86_64'], + 'windows': ['windows-x86_64'] + }; + + const goArchMap = { + 'arm64': 'aarch64', + 'amd64': 'x86_64' + }; + + for (const release of releases) { + const version = release.version.replace(/^go/, ''); + console.log(`处理版本: ${version}`); + + const archiveFiles = release.files.filter(f => f.kind === 'archive'); + + if (archiveFiles.length === 0) { + console.log(` ⚠ 跳过: 未找到归档文件`); + continue; + } + + let processedCount = 0; + + for (const file of archiveFiles) { + const mappedArch = goArchMap[file.arch] || file.arch; + const osPlatforms = goOsMap[file.os]; + + if (!osPlatforms) continue; + + const platform = osPlatforms.find(p => p.includes(mappedArch)); + if (!platform) continue; + + try { + const fileName = file.filename; + const localPath = path.join(CONFIG.tempDir, fileName); + const goDevUrl = `https://go.dev/dl/${fileName}`; + + console.log(` 下载 ${fileName}...`); + await downloadFile(goDevUrl, localPath); + + const fileSize = getFileSize(localPath); + const md5 = calculateMD5(localPath); + + const ossPath = `${CONFIG.ossPrefix}${version}/${fileName}`; + await uploadToOSS(ossClient, localPath, ossPath); + + const cdnUrl = CONFIG.cdnDomain + ? `${CONFIG.cdnDomain}/${ossPath}` + : `https://${CONFIG.ossBucket}.${CONFIG.ossRegion}.aliyuncs.com/${ossPath}`; + + metadata.releases.push({ + version: version, + display_name: `Go ${release.version}`, + published_at: new Date().toISOString(), + download_url: cdnUrl, + github_url: goDevUrl, + file_name: fileName, + size: fileSize, + md5: md5, + supported_platforms: [platform] + }); + + processedCount++; + fs.unlinkSync(localPath); + } catch (error) { + console.error(` ✗ 处理文件 ${file.filename} 失败: ${error.message}`); + } + } + + if (processedCount > 0) { + console.log(` ✓ 版本 ${release.version} 处理完成 (${processedCount} 个平台)\n`); + } else { + console.log(` ⚠ 版本 ${release.version} 没有可用的平台文件\n`); + } + } + + metadata.releases.sort((a, b) => { + const versionA = a.version.split('.').map(Number); + const versionB = b.version.split('.').map(Number); + for (let i = 0; i < Math.max(versionA.length, versionB.length); i++) { + const numA = versionA[i] || 0; + const numB = versionB[i] || 0; + if (numA !== numB) return numB - numA; + } + return 0; + }); + + console.log('\n上传 metadata.json...'); + await uploadMetadata(ossClient, metadata); + + console.log(`\n✓ 同步完成!共处理 ${metadata.releases.length} 个版本`); + + const metadataUrl = CONFIG.cdnDomain + ? `${CONFIG.cdnDomain}/${CONFIG.ossPrefix}metadata.json` + : `https://${CONFIG.ossBucket}.${CONFIG.ossRegion}.aliyuncs.com/${CONFIG.ossPrefix}metadata.json`; + console.log(`\nmetadata URL: ${metadataUrl}`); + + } catch (error) { + console.error('\n✗ 错误:', error.message); + process.exit(1); + } finally { + cleanupTempDir(); + } +} + +main(); diff --git a/scripts/sync-scala-versions.js b/scripts/sync-scala-versions.js index 9657e20..102c5ee 100755 --- a/scripts/sync-scala-versions.js +++ b/scripts/sync-scala-versions.js @@ -69,6 +69,7 @@ function getConfig() { ossAccessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, ossBucket: process.env.OSS_BUCKET, cdnDomain: process.env.CDN_DOMAIN, + githubToken: process.env.GITHUB_TOKEN, githubRepo: 'lampepfl/dotty', ossPrefix: 'global/plugins/scala/', tempDir: path.join(__dirname, '.temp-scala'), @@ -130,6 +131,10 @@ function httpGet(url, isJson = true) { } }; + if (url.includes('api.github.com') && CONFIG.githubToken) { + options.headers['Authorization'] = `token ${CONFIG.githubToken}`; + } + client.get(url, options, (res) => { if (res.statusCode === 302 || res.statusCode === 301) { return httpGet(res.headers.location, isJson).then(resolve).catch(reject); diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 31cc22e..9734beb 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -29,6 +29,11 @@ pub struct EnvironmentMirrorConfig { pub fallback_enabled: Option, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GithubConfig { + pub token: Option, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppConfig { pub log_directory: Option, @@ -38,6 +43,7 @@ pub struct AppConfig { pub plugins: Option>, pub editor: Option, pub environment_mirror: Option, + pub github: Option, } impl Default for AppConfig { @@ -63,6 +69,7 @@ impl Default for AppConfig { base_url: Some("https://cdn.global.devlive.top".to_string()), fallback_enabled: Some(false), }), + github: Some(GithubConfig { token: None }), } } } @@ -246,6 +253,7 @@ impl ConfigManager { base_url: Some("https://cdn.global.devlive.top".to_string()), fallback_enabled: Some(false), }), + github: Some(GithubConfig { token: None }), } } diff --git a/src-tauri/src/env_providers/clojure.rs b/src-tauri/src/env_providers/clojure.rs index 5d81d0e..1d14a5b 100644 --- a/src-tauri/src/env_providers/clojure.rs +++ b/src-tauri/src/env_providers/clojure.rs @@ -62,6 +62,11 @@ impl ClojureEnvironmentProvider { match std::fs::read_to_string(&self.cache_file) { Ok(content) => match serde_json::from_str::(&content) { Ok(cached) => { + if cached.releases.is_empty() { + warn!("缓存的版本列表为空,将重新获取"); + return None; + } + if let Ok(elapsed) = SystemTime::now().duration_since(cached.cached_at) { if elapsed < Duration::from_secs(3600) { info!("使用缓存的 Clojure 版本列表(缓存时间: {:?})", elapsed); @@ -185,8 +190,20 @@ impl ClojureEnvironmentProvider { let mut request = client.get(url); - if let Ok(token) = std::env::var("GITHUB_TOKEN") { - info!("使用 GITHUB_TOKEN 进行认证"); + // 优先从配置读取 GitHub Token,其次从环境变量读取 + let token = if let Ok(config) = crate::config::get_app_config_internal() { + config + .github + .and_then(|g| g.token) + .filter(|t| !t.is_empty()) + } else { + None + }; + + let token = token.or_else(|| std::env::var("GITHUB_TOKEN").ok()); + + if let Some(token) = token { + info!("使用 GitHub Token 进行认证"); request = request.header("Authorization", format!("token {}", token)); } diff --git a/src-tauri/src/env_providers/go.rs b/src-tauri/src/env_providers/go.rs new file mode 100644 index 0000000..55b64de --- /dev/null +++ b/src-tauri/src/env_providers/go.rs @@ -0,0 +1,629 @@ +use super::metadata::{Metadata, fetch_metadata_from_cdn, is_cdn_enabled, is_fallback_enabled}; +use crate::env_manager::{DownloadStatus, EnvironmentProvider, EnvironmentVersion}; +use futures_util::StreamExt; +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 GoRelease { + version: String, + stable: bool, + files: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct GoFile { + filename: String, + os: String, + arch: String, + version: String, + sha256: String, + size: u64, + kind: String, +} + +#[derive(Debug, Deserialize, Serialize)] +struct CachedReleases { + releases: Vec, + cached_at: SystemTime, +} + +pub struct GoEnvironmentProvider { + install_dir: PathBuf, + cache_file: PathBuf, +} + +impl GoEnvironmentProvider { + 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!("创建 Go 安装目录失败: {}", 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("plugins").join("go") + } + + 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 cached.releases.is_empty() { + warn!("缓存的版本列表为空,将重新获取"); + return None; + } + + if let Ok(elapsed) = SystemTime::now().duration_since(cached.cached_at) { + if elapsed < Duration::from_secs(3600) { + info!("使用缓存的 Go 版本列表(缓存时间: {:?})", elapsed); + return Some(cached.releases); + } else { + info!("缓存已过期({:?}),将重新获取", elapsed); + } + } + } + Err(e) => { + warn!("解析缓存文件失败: {}", e); + } + }, + Err(e) => { + warn!("读取缓存文件失败: {}", e); + } + } + + None + } + + fn write_cache(&self, releases: &[GoRelease]) { + 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!("已缓存 Go 版本列表"); + } + } + Err(e) => { + warn!("序列化缓存数据失败: {}", e); + } + } + } + + fn get_current_platform() -> &'static str { + if cfg!(target_os = "macos") { + if cfg!(target_arch = "aarch64") { + "macos-aarch64" + } else { + "macos-x86_64" + } + } else if cfg!(target_os = "linux") { + if cfg!(target_arch = "aarch64") { + "linux-aarch64" + } else { + "linux-x86_64" + } + } else if cfg!(target_os = "windows") { + "windows-x86_64" + } else { + "unknown" + } + } + + fn get_os_arch() -> (&'static str, &'static str) { + let os = if cfg!(target_os = "macos") { + "darwin" + } else if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "windows") { + "windows" + } else { + "unknown" + }; + + let arch = if cfg!(target_arch = "aarch64") { + "arm64" + } else if cfg!(target_arch = "x86_64") { + "amd64" + } else { + "unknown" + }; + + (os, arch) + } + + fn parse_metadata_to_versions( + &self, + metadata: Metadata, + ) -> Result, String> { + let current_platform = Self::get_current_platform(); + let mut versions = Vec::new(); + let mut seen_versions = std::collections::HashSet::new(); + + for release in metadata.releases { + let is_supported = release + .supported_platforms + .iter() + .any(|p| p == current_platform || p.starts_with(&format!("{}-", current_platform))); + + if !is_supported { + continue; + } + + if !seen_versions.insert(release.version.clone()) { + 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(), + fallback_url: Some(release.github_url.clone()), + 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) + } + + async fn fetch_go_releases(&self) -> Result, String> { + if let Some(cached) = self.read_cache() { + return Ok(cached); + } + + info!("从 Go 官方 API 获取版本列表"); + let url = "https://go.dev/dl/?mode=json&include=all"; + + let client = reqwest::Client::new(); + let request = client.get(url).header("User-Agent", "CodeForge"); + + let response = request + .send() + .await + .map_err(|e| format!("请求 Go API 失败: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Go API 返回错误: {}", response.status())); + } + + let releases: Vec = response + .json() + .await + .map_err(|e| format!("解析 Go API 响应失败: {}", e))?; + + self.write_cache(&releases); + + Ok(releases) + } + + 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); + install_path.join("go").join("bin").exists() + } + + async fn extract_archive(&self, archive_path: &Path, dest_dir: &Path) -> Result<(), String> { + info!("正在解压文件到: {}", dest_dir.display()); + + if archive_path.extension().and_then(|s| s.to_str()) == Some("zip") { + let file = std::fs::File::open(archive_path) + .map_err(|e| format!("打开压缩文件失败: {}", e))?; + let mut archive = + zip::ZipArchive::new(file).map_err(|e| format!("读取 ZIP 文件失败: {}", e))?; + + archive + .extract(dest_dir) + .map_err(|e| format!("解压 ZIP 文件失败: {}", e))?; + } else { + let tar_gz = std::fs::File::open(archive_path) + .map_err(|e| format!("打开压缩文件失败: {}", e))?; + let tar = flate2::read::GzDecoder::new(tar_gz); + let mut archive = tar::Archive::new(tar); + + archive + .unpack(dest_dir) + .map_err(|e| format!("解压 tar.gz 文件失败: {}", e))?; + } + + info!("解压完成"); + Ok(()) + } + + 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 = client + .get(url) + .send() + .await + .map_err(|e| format!("下载失败: {}", e))?; + + if !response.status().is_success() { + return Err(format!("下载失败: HTTP {}", response.status())); + } + + let total_size = response.content_length().unwrap_or(0); + let mut downloaded: u64 = 0; + let mut file = std::fs::File::create(dest).map_err(|e| format!("创建文件失败: {}", e))?; + + let mut stream = response.bytes_stream(); + + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| format!("下载数据失败: {}", e))?; + std::io::Write::write_all(&mut file, &chunk) + .map_err(|e| format!("写入文件失败: {}", e))?; + + downloaded += chunk.len() as u64; + let percentage = if total_size > 0 { + (downloaded as f64 / total_size as f64 * 100.0) as u64 + } else { + 0 + }; + + crate::env_manager::emit_download_progress( + &app_handle, + "go", + version, + downloaded, + total_size, + DownloadStatus::Downloading, + ); + + if percentage % 10 == 0 { + info!("下载进度: {}%", percentage); + } + } + + info!("下载完成"); + Ok(()) + } +} + +#[async_trait::async_trait] +impl EnvironmentProvider for GoEnvironmentProvider { + fn get_language(&self) -> &'static str { + "go" + } + + async fn fetch_available_versions(&self) -> Result, String> { + if is_cdn_enabled() { + match fetch_metadata_from_cdn("go").await { + Ok(metadata) => { + info!("使用 CDN metadata 获取版本列表"); + return self.parse_metadata_to_versions(metadata); + } + Err(e) => { + warn!("CDN metadata 获取失败: {}", e); + + if !is_fallback_enabled() { + return Err(format!("CDN metadata 获取失败,未启用自动回退: {}", e)); + } + + info!("fallback 已启用,回退到 GitHub API"); + } + } + } else { + info!("CDN 未启用,使用 GitHub API"); + } + + let releases = self.fetch_go_releases().await?; + let (target_os, target_arch) = Self::get_os_arch(); + + let mut versions = Vec::new(); + let mut seen_versions = std::collections::HashSet::new(); + + for release in releases { + let version = release.version.trim_start_matches("go").to_string(); + + if !seen_versions.insert(version.clone()) { + continue; + } + + if let Some(file) = release + .files + .iter() + .find(|f| f.os == target_os && f.arch == target_arch && f.kind == "archive") + { + 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 + }; + + let download_url = format!("https://go.dev/dl/{}", file.filename); + + versions.push(EnvironmentVersion { + version: version.clone(), + download_url, + fallback_url: None, + install_path, + is_installed, + size: Some(file.size), + release_date: None, + }); + } + } + + if versions.is_empty() { + return Err("未找到可用的 Go 版本".to_string()); + } + + 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(), + fallback_url: None, + 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!("开始下载并安装 Go {}", version); + + if self.is_version_installed(version) { + return Err(format!("Go {} 已经安装", version)); + } + + crate::env_manager::emit_download_progress( + &app_handle, + "go", + 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); + + crate::env_manager::emit_download_progress( + &app_handle, + "go", + version, + 0, + 0, + DownloadStatus::Extracting, + ); + + self.extract_archive(&temp_file, &install_path).await?; + + std::fs::remove_file(&temp_file).ok(); + + let go_root = install_path.join("go"); + let mut config = crate::config::get_app_config() + .await + .map_err(|e| format!("获取配置失败: {}", e))?; + + if let Some(plugins) = &mut config.plugins { + if let Some(go_plugin) = plugins.iter_mut().find(|p| p.language == "go") { + go_plugin.execute_home = Some(go_root.to_string_lossy().to_string()); + + // 根据操作系统设置 run_command + let run_cmd = if cfg!(target_os = "windows") { + "bin/go.exe run $filename" + } else { + "bin/go run $filename" + }; + go_plugin.run_command = Some(String::from(run_cmd)); + + info!( + "已更新 Go 插件配置: execute_home={}, run_command={}", + go_root.display(), + run_cmd + ); + } + } + + crate::config::update_app_config(config, app_handle.clone()) + .await + .map_err(|e| format!("保存配置失败: {}", e))?; + + crate::env_manager::emit_download_progress( + &app_handle, + "go", + version, + 0, + 0, + DownloadStatus::Completed, + ); + + info!("Go {} 安装成功", version); + Ok(install_path.to_string_lossy().to_string()) + } + + async fn switch_version(&self, version: &str, app_handle: AppHandle) -> Result<(), String> { + info!("切换 Go 版本到 {}", version); + + if !self.is_version_installed(version) { + return Err(format!("版本 {} 未安装", version)); + } + + let install_path = self.get_version_install_path(version); + let go_root = install_path.join("go"); + + let mut config = crate::config::get_app_config() + .await + .map_err(|e| format!("获取配置失败: {}", e))?; + + if let Some(plugins) = &mut config.plugins { + if let Some(go_plugin) = plugins.iter_mut().find(|p| p.language == "go") { + go_plugin.execute_home = Some(go_root.to_string_lossy().to_string()); + + // 根据操作系统设置 run_command + let run_cmd = if cfg!(target_os = "windows") { + "bin/go.exe run $filename" + } else { + "bin/go run $filename" + }; + go_plugin.run_command = Some(String::from(run_cmd)); + + info!( + "已更新 Go 插件配置: execute_home={}, run_command={}", + go_root.display(), + run_cmd + ); + } + } + + crate::config::update_app_config(config, app_handle.clone()) + .await + .map_err(|e| format!("保存配置失败: {}", e))?; + + info!("成功切换到 Go {}", 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(go_plugin) = plugins.iter().find(|p| p.language == "go") { + if let Some(ref execute_home) = go_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!("当前 Go 版本: {}", version); + return Ok(Some(version.to_string())); + } + } + } + } + } + } + + Ok(None) + } + + fn get_install_dir(&self) -> PathBuf { + self.install_dir.clone() + } + + async fn uninstall_version(&self, version: &str) -> Result<(), String> { + let version_dir = self.install_dir.join(version); + + if !version_dir.exists() { + return Err(format!("版本 {} 未安装", version)); + } + + let current_version = self.get_current_version().await.ok().flatten(); + if current_version.as_deref() == Some(version) { + return Err(format!("无法卸载当前正在使用的版本 {}", version)); + } + + std::fs::remove_dir_all(&version_dir).map_err(|e| format!("删除版本目录失败: {}", e))?; + + info!("已卸载 Go 版本 {}", version); + Ok(()) + } +} diff --git a/src-tauri/src/env_providers/mod.rs b/src-tauri/src/env_providers/mod.rs index 78c2f03..6f8fd61 100644 --- a/src-tauri/src/env_providers/mod.rs +++ b/src-tauri/src/env_providers/mod.rs @@ -1,6 +1,8 @@ pub mod clojure; +pub mod go; pub mod metadata; pub mod scala; pub use clojure::ClojureEnvironmentProvider; +pub use go::GoEnvironmentProvider; pub use scala::ScalaEnvironmentProvider; diff --git a/src-tauri/src/env_providers/scala.rs b/src-tauri/src/env_providers/scala.rs index 987eb29..6918404 100644 --- a/src-tauri/src/env_providers/scala.rs +++ b/src-tauri/src/env_providers/scala.rs @@ -65,24 +65,26 @@ impl ScalaEnvironmentProvider { } match std::fs::read_to_string(&self.cache_file) { - Ok(content) => { - match serde_json::from_str::(&content) { - Ok(cached) => { - // 检查缓存是否过期(1小时) - if let Ok(elapsed) = SystemTime::now().duration_since(cached.cached_at) { - if elapsed < Duration::from_secs(3600) { - info!("使用缓存的 Scala 版本列表(缓存时间: {:?})", elapsed); - return Some(cached.releases); - } else { - info!("缓存已过期({:?}),将重新获取", elapsed); - } - } + Ok(content) => match serde_json::from_str::(&content) { + Ok(cached) => { + if cached.releases.is_empty() { + warn!("缓存的版本列表为空,将重新获取"); + return None; } - Err(e) => { - warn!("解析缓存文件失败: {}", e); + + if let Ok(elapsed) = SystemTime::now().duration_since(cached.cached_at) { + if elapsed < Duration::from_secs(3600) { + info!("使用缓存的 Scala 版本列表(缓存时间: {:?})", elapsed); + return Some(cached.releases); + } else { + info!("缓存已过期({:?}),将重新获取", elapsed); + } } } - } + Err(e) => { + warn!("解析缓存文件失败: {}", e); + } + }, Err(e) => { warn!("读取缓存文件失败: {}", e); } @@ -146,9 +148,20 @@ impl ScalaEnvironmentProvider { // 构建请求,如果有 GitHub Token 则添加认证头 let mut request = client.get(url); - // 尝试从环境变量获取 GitHub Token - if let Ok(token) = std::env::var("GITHUB_TOKEN") { - info!("使用 GITHUB_TOKEN 进行认证"); + // 优先从配置读取 GitHub Token,其次从环境变量读取 + let token = if let Ok(config) = crate::config::get_app_config_internal() { + config + .github + .and_then(|g| g.token) + .filter(|t| !t.is_empty()) + } else { + None + }; + + let token = token.or_else(|| std::env::var("GITHUB_TOKEN").ok()); + + if let Some(token) = token { + info!("使用 GitHub Token 进行认证"); request = request.header("Authorization", format!("token {}", token)); } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index d84fe46..fd78ded 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -24,7 +24,9 @@ use crate::env_commands::{ get_supported_environment_languages, switch_environment_version, uninstall_environment_version, }; use crate::env_manager::EnvironmentManager; -use crate::env_providers::{ClojureEnvironmentProvider, ScalaEnvironmentProvider}; +use crate::env_providers::{ + ClojureEnvironmentProvider, GoEnvironmentProvider, ScalaEnvironmentProvider, +}; use crate::execution::{ ExecutionHistory, PluginManagerState as ExecutionPluginManagerState, clear_execution_history, execute_code, get_execution_history, is_execution_running, stop_execution, @@ -48,6 +50,7 @@ fn main() { // 初始化环境管理器 let mut env_manager = EnvironmentManager::new(); env_manager.register_provider(Box::new(ClojureEnvironmentProvider::new())); + env_manager.register_provider(Box::new(GoEnvironmentProvider::new())); env_manager.register_provider(Box::new(ScalaEnvironmentProvider::new())); tauri::Builder::default() diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 7edddef..e14392f 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -30,6 +30,11 @@ + + + @@ -43,6 +48,7 @@ import Language from './setting/Language.vue' import Editor from './setting/Editor.vue' import Network from './setting/Network.vue' import Cache from './setting/Cache.vue' +import Logs from './setting/Logs.vue' import { useSettings } from '../composables/useSettings.ts' const emit = defineEmits<{ diff --git a/src/components/setting/General.vue b/src/components/setting/General.vue index 4f1d7b6..239f2da 100644 --- a/src/components/setting/General.vue +++ b/src/components/setting/General.vue @@ -1,131 +1,126 @@ diff --git a/src/components/setting/Logs.vue b/src/components/setting/Logs.vue new file mode 100644 index 0000000..0da3e96 --- /dev/null +++ b/src/components/setting/Logs.vue @@ -0,0 +1,129 @@ + + + diff --git a/src/components/setting/Network.vue b/src/components/setting/Network.vue index ce2af36..1a7b0b9 100644 --- a/src/components/setting/Network.vue +++ b/src/components/setting/Network.vue @@ -99,6 +99,7 @@ + @@ -118,7 +119,7 @@ const emit = defineEmits<{ const toast = useToast() -// 状态 +// CDN 状态 const cdnEnabled = ref(false) const cdnBaseUrl = ref('') const fallbackEnabled = ref(false) diff --git a/src/composables/useSettings.ts b/src/composables/useSettings.ts index 866eee6..3f1e1bd 100644 --- a/src/composables/useSettings.ts +++ b/src/composables/useSettings.ts @@ -1,5 +1,5 @@ import { nextTick, ref } from 'vue' -import { BracesIcon, CodeIcon, Database, Globe, ShieldIcon } from 'lucide-vue-next' +import { BracesIcon, CodeIcon, Database, FileText, Globe, ShieldIcon } from 'lucide-vue-next' export function useSettings(emit: any) { @@ -13,7 +13,8 @@ export function useSettings(emit: any) { key: 'editor', label: '编辑器', icon: CodeIcon }, { key: 'language', label: '语言', icon: BracesIcon }, { key: 'network', label: '网络', icon: Globe }, - { key: 'cache', label: '缓存', icon: Database } + { key: 'cache', label: '缓存', icon: Database }, + { key: 'logs', label: '日志', icon: FileText } ] const handleEditorSettingsChanged = (config: any) => {