黄中银 2 недель назад
Родитель
Сommit
c73473eecd

+ 39 - 61
build-win.bat

@@ -4,6 +4,7 @@ chcp 65001 >nul 2>&1
 :: ============================================
 :: Claude AI Installer Windows 构建脚本
 :: Build script for Windows using Tauri
+:: 此脚本作为 scripts/build.js 的简单入口
 :: ============================================
 
 :: 切换到脚本所在目录
@@ -15,16 +16,16 @@ if /i "%~1"=="-h" goto :show_help
 
 echo.
 echo   ========================================
-echo   Claude AI Installer - Windows Build
-echo   Using Tauri 2.0
+echo   Claude AI Installer - Windows 构建
+echo   使用 Tauri 2.0
 echo   ========================================
 echo.
 
 :: 检查 Rust 是否安装
 where rustc >nul 2>&1
 if errorlevel 1 (
-    echo   [91mx[0m Rust is not installed. Please install Rust first.
-    echo   [94mi[0m Visit: https://rustup.rs/
+    echo   [91mx[0m 未找到 Rust,请先安装 Rust
+    echo   [94mi[0m 访问: https://rustup.rs/
     pause
     exit /b 1
 )
@@ -35,7 +36,7 @@ for /f "tokens=*" %%i in ('rustc --version') do echo   [92m√[0m Rust: %%i
 :: 检查 Node.js
 where node >nul 2>&1
 if errorlevel 1 (
-    echo   [91mx[0m Node.js is not installed. Please install Node.js first.
+    echo   [91mx[0m 未找到 Node.js,请先安装 Node.js
     pause
     exit /b 1
 )
@@ -46,69 +47,42 @@ for /f "tokens=*" %%i in ('node --version') do echo   [92m√[0m Node.js: %%i
 :: 检查 node_modules
 if not exist "node_modules" (
     echo.
-    echo   [93m![0m node_modules not found, installing dependencies...
+    echo   [93m![0m 未找到 node_modules,正在安装依赖...
     call npm install
     if errorlevel 1 (
-        echo   [91mx[0m Failed to install dependencies
+        echo   [91mx[0m 安装依赖失败
         pause
         exit /b 1
     )
 )
 
-:: 解析参数
-set "BUILD_MODE=release"
-set "SKIP_FRONTEND="
+:: 解析参数并转换为 Node.js 脚本参数
+set "NODE_ARGS="
 
 :parse_args
 if "%~1"=="" goto :run_build
-if /i "%~1"=="--debug" set "BUILD_MODE=debug" & shift & goto :parse_args
-if /i "%~1"=="-d" set "BUILD_MODE=debug" & shift & goto :parse_args
-if /i "%~1"=="--skip-frontend" set "SKIP_FRONTEND=1" & shift & goto :parse_args
-if /i "%~1"=="-s" set "SKIP_FRONTEND=1" & shift & goto :parse_args
+if /i "%~1"=="--debug" set "NODE_ARGS=%NODE_ARGS% --debug" & shift & goto :parse_args
+if /i "%~1"=="-d" set "NODE_ARGS=%NODE_ARGS% --debug" & shift & goto :parse_args
+if /i "%~1"=="--skip-frontend" set "NODE_ARGS=%NODE_ARGS% --skip-frontend" & shift & goto :parse_args
+if /i "%~1"=="-s" set "NODE_ARGS=%NODE_ARGS% --skip-frontend" & shift & goto :parse_args
 shift
 goto :parse_args
 
 :run_build
 echo.
+echo   [96m^> 调用 Node.js 构建脚本...[0m
+echo   [94mi[0m 执行: node scripts/build.js %NODE_ARGS%
+echo.
 
-:: 构建前端(如果没有跳过)
-if not defined SKIP_FRONTEND (
-    echo   [96m^> Building frontend...[0m
-    call npm run build:frontend
-    if errorlevel 1 (
-        echo   [91mx[0m Failed to build frontend
-        pause
-        exit /b 1
-    )
-    echo   [92m√[0m Frontend built successfully
-    echo.
-)
-
-:: 构建 Tauri 应用
-echo   [96m^> Building Tauri application (%BUILD_MODE% mode)...[0m
-
-if "%BUILD_MODE%"=="debug" (
-    call npm run tauri build -- --debug
-) else (
-    call npm run tauri build
-)
-
+call node scripts/build.js %NODE_ARGS%
 if errorlevel 1 (
     echo.
-    echo   [91mx[0m Build failed!
+    echo   [91mx[0m 构建失败!
     echo.
     pause
     exit /b 1
 )
 
-echo.
-echo   ========================================
-echo   [92m√[0m Build completed successfully!
-echo   ========================================
-echo.
-echo   Output location:
-echo     src-tauri\target\release\bundle\nsis\
-echo     src-tauri\target\release\bundle\msi\
 echo.
 pause
 goto :eof
@@ -118,28 +92,32 @@ goto :eof
 :: ============================================
 :show_help
 echo.
-echo   Claude AI Installer Windows Build Script (Tauri)
+echo   Claude AI Installer Windows 构建脚本 (Tauri)
 echo.
-echo   Usage:
-echo     build-win.bat [options]
+echo   用法:
+echo     build-win.bat [参数]
 echo.
-echo   Options:
-echo     --debug, -d          Build in debug mode
-echo     --skip-frontend, -s  Skip frontend build
-echo     --help, -h           Show this help message
+echo   参数:
+echo     --debug, -d          以调试模式构建
+echo     --skip-frontend, -s  跳过前端构建
+echo     --help, -h           显示此帮助信息
 echo.
-echo   Examples:
-echo     build-win.bat                    Full release build
-echo     build-win.bat --debug            Debug build
-echo     build-win.bat -s                 Skip frontend, only build Tauri
+echo   示例:
+echo     build-win.bat                    完整发布构建
+echo     build-win.bat --debug            调试构建
+echo     build-win.bat -s                 跳过前端,仅构建 Tauri
 echo.
-echo   Output:
-echo     src-tauri\target\release\bundle\nsis\*.exe   (NSIS installer)
-echo     src-tauri\target\release\bundle\msi\*.msi    (MSI installer)
+echo   输出:
+echo     release\*-nsis.exe                              (NSIS 安装程序)
+echo     release\*-portable.exe                          (便携版)
+echo     release\Claude-AI-Installer-x.x.x-portable\     (带启动器的便携版)
+echo     release\Claude-AI-Installer-x.x.x-portable.zip  (便携版压缩包)
 echo.
-echo   Prerequisites:
+echo   前置要求:
 echo     - Rust (https://rustup.rs/)
 echo     - Node.js (https://nodejs.org/)
-echo     - Visual Studio Build Tools (for Windows)
+echo     - Visual Studio Build Tools (Windows 平台)
+echo.
+echo   注意: 此脚本调用 scripts/build.js 执行实际构建
 echo.
 goto :eof

+ 158 - 191
scripts/build.js

@@ -1,24 +1,25 @@
 #!/usr/bin/env node
 
 /**
- * 多平台构建脚本
- * Multi-platform build script for Claude AI Installer
+ * Tauri 构建脚本
+ * Build script for Claude AI Installer (Tauri)
  *
  * 用法 / Usage:
  *   node scripts/build.js [options]
  *
  * 选项 / Options:
- *   --platform, -p    目标平台: win, mac, linux, all (默认: 当前平台)
- *   --arch, -a        架构: x64, arm64, all (默认: 当前架构)
- *   --publish, -P     发布模式: always, onTag, never (默认: never)
- *   --skip-build      跳过前端构建,仅打包
- *   --skip-typecheck  跳过 TypeScript 类型检查
+ *   --debug, -d       调试模式构建
+ *   --skip-frontend   跳过前端构建
  *   --help, -h        显示帮助信息
  */
 
-const { execSync } = require('child_process');
-const path = require('path');
-const fs = require('fs');
+import { execSync } from 'child_process';
+import path from 'path';
+import fs from 'fs';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
 
 // 颜色输出
 const colors = {
@@ -39,39 +40,31 @@ const log = {
   step: (msg) => console.log(`\n${colors.cyan}${colors.bright}▶ ${msg}${colors.reset}`),
 };
 
+// 项目路径
+const PROJECT_ROOT = path.resolve(__dirname, '..');
+const TAURI_DIR = path.join(PROJECT_ROOT, 'src-tauri');
+const RELEASE_DIR = path.join(PROJECT_ROOT, 'release');
+const LAUNCHER_DIR = path.join(PROJECT_ROOT, 'launcher');
+
 // 解析命令行参数
 function parseArgs() {
   const args = process.argv.slice(2);
   const options = {
-    platform: process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux',
-    arch: process.arch,
-    publish: 'never',
-    skipBuild: false,
-    skipTypecheck: false,
+    debug: false,
+    skipFrontend: false,
     help: false,
   };
 
   for (let i = 0; i < args.length; i++) {
     const arg = args[i];
     switch (arg) {
-      case '--platform':
-      case '-p':
-        options.platform = args[++i];
-        break;
-      case '--arch':
-      case '-a':
-        options.arch = args[++i];
+      case '--debug':
+      case '-d':
+        options.debug = true;
         break;
-      case '--publish':
-      case '-P':
-        options.publish = args[++i];
-        break;
-      case '--skip-build':
+      case '--skip-frontend':
       case '-s':
-        options.skipBuild = true;
-        break;
-      case '--skip-typecheck':
-        options.skipTypecheck = true;
+        options.skipFrontend = true;
         break;
       case '--help':
       case '-h':
@@ -86,42 +79,27 @@ function parseArgs() {
 // 显示帮助信息
 function showHelp() {
   console.log(`
-${colors.bright}Claude AI Installer 构建脚本${colors.reset}
+${colors.bright}Claude AI Installer 构建脚本 (Tauri)${colors.reset}
 
 ${colors.cyan}用法:${colors.reset}
   node scripts/build.js [options]
 
 ${colors.cyan}选项:${colors.reset}
-  --platform, -p <platform>   目标平台
-                              win    - Windows (NSIS 安装包)
-                              mac    - macOS (DMG)
-                              linux  - Linux (AppImage)
-                              all    - 所有平台
-                              默认: 当前平台
-
-  --arch, -a <arch>           目标架构
-                              x64    - 64位 Intel/AMD
-                              arm64  - ARM64 (Apple Silicon, etc.)
-                              all    - 所有架构
-                              默认: 当前架构
-
-  --publish, -P <mode>        发布模式
-                              always - 总是发布
-                              onTag  - 仅在 Git tag 时发布
-                              never  - 不发布 (默认)
-
-  --skip-build, -s            跳过前端构建,仅执行打包
-
-  --skip-typecheck            跳过 TypeScript 类型检查
-
-  --help, -h                  显示此帮助信息
+  --debug, -d           以调试模式构建
+  --skip-frontend, -s   跳过前端构建
+  --help, -h            显示此帮助信息
 
 ${colors.cyan}示例:${colors.reset}
-  node scripts/build.js                          # 构建当前平台
-  node scripts/build.js -p win                   # 构建 Windows 版本
-  node scripts/build.js -p mac -a arm64          # 构建 macOS ARM64 版本
-  node scripts/build.js -p all                   # 构建所有平台
-  node scripts/build.js -p win -P always         # 构建并发布 Windows 版本
+  node scripts/build.js                # 完整发布构建
+  node scripts/build.js --debug        # 调试构建
+  node scripts/build.js -s             # 跳过前端,仅构建 Tauri
+
+${colors.cyan}输出:${colors.reset}
+  release/                                          # 最终发布目录
+    ├── *-nsis.exe                                  # NSIS 安装程序
+    ├── *-portable.exe                              # 便携版
+    └── Claude-AI-Installer-x.x.x-portable/         # 带启动器的便携版
+        └── Claude-AI-Installer-x.x.x-portable.zip  # 便携版压缩包
 `);
 }
 
@@ -131,7 +109,7 @@ function exec(command, options = {}) {
   try {
     execSync(command, {
       stdio: 'inherit',
-      cwd: path.resolve(__dirname, '..'),
+      cwd: PROJECT_ROOT,
       ...options,
     });
     return true;
@@ -141,137 +119,94 @@ function exec(command, options = {}) {
   }
 }
 
-// 获取 electron-builder 平台参数
-function getPlatformArgs(platform, arch) {
-  const platformMap = {
-    win: '--win',
-    mac: '--mac',
-    linux: '--linux',
-  };
-
-  const archMap = {
-    x64: '--x64',
-    arm64: '--arm64',
-    all: '--x64 --arm64',
-  };
-
-  let args = [];
-
-  if (platform === 'all') {
-    args.push('--win', '--mac', '--linux');
-  } else {
-    args.push(platformMap[platform] || platformMap.win);
-  }
-
-  if (arch && arch !== 'all') {
-    args.push(archMap[arch] || '');
-  } else if (arch === 'all') {
-    args.push('--x64', '--arm64');
-  }
+// 获取版本号
+function getVersion() {
+  const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
+  const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
+  return tauriConf.version;
+}
 
-  return args.join(' ');
+// 获取产品名称
+function getProductName() {
+  const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
+  const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
+  return tauriConf.productName || 'Claude AI Installer';
 }
 
 // 清理旧的构建文件
 function cleanBuild() {
   log.step('清理旧的构建文件...');
 
-  const dirsToClean = ['dist', 'dist-electron'];
-  const projectRoot = path.resolve(__dirname, '..');
-
-  for (const dir of dirsToClean) {
-    const fullPath = path.join(projectRoot, dir);
-    if (fs.existsSync(fullPath)) {
-      fs.rmSync(fullPath, { recursive: true, force: true });
-      log.info(`已删除: ${dir}`);
-    }
-  }
-
-  // 清空 release 目录内容,但保留目录本身
-  const releaseDir = path.join(projectRoot, 'release');
-  if (fs.existsSync(releaseDir)) {
-    const files = fs.readdirSync(releaseDir);
-    for (const file of files) {
-      const filePath = path.join(releaseDir, file);
-      fs.rmSync(filePath, { recursive: true, force: true });
-    }
-    log.info('已清空: release');
+  // 清空 release 目录
+  if (fs.existsSync(RELEASE_DIR)) {
+    fs.rmSync(RELEASE_DIR, { recursive: true, force: true });
   }
+  fs.mkdirSync(RELEASE_DIR, { recursive: true });
 
   log.success('清理完成');
 }
 
-// 类型检查
-function typeCheck() {
-  log.step('执行类型检查...');
-  if (!exec('npx vue-tsc --noEmit')) {
-    throw new Error('类型检查失败');
-  }
-  log.success('类型检查通过');
-}
+// 构建 Tauri 应用
+function buildTauri(options) {
+  log.step(`构建 Tauri 应用 (${options.debug ? '调试' : '发布'}模式)...`);
 
-// 构建前端
-function buildFrontend() {
-  log.step('构建前端资源...');
-  if (!exec('npx vite build')) {
-    throw new Error('前端构建失败');
+  let command = 'npm run tauri build';
+  if (options.debug) {
+    command += ' -- --debug';
   }
-  log.success('前端构建完成');
-}
-
-// 打包 Electron 应用
-function packageApp(options) {
-  log.step(`打包应用 (平台: ${options.platform}, 架构: ${options.arch})...`);
-
-  const platformArgs = getPlatformArgs(options.platform, options.arch);
-  const publishArg = options.publish !== 'never' ? `--publish ${options.publish}` : '';
-
-  const command = `npx electron-builder ${platformArgs} ${publishArg}`.trim();
 
   if (!exec(command)) {
-    throw new Error('应用打包失败');
+    throw new Error('Tauri 构建失败');
   }
 
-  log.success('应用打包完成');
+  log.success('Tauri 构建完成');
 }
 
-// 显示构建结果
-function showResults() {
-  log.step('构建结果:');
-
-  const releaseDir = path.resolve(__dirname, '..', 'release');
+// 获取 Tauri 生成的 exe 文件名(小写,空格转连字符)
+function getTauriExeName() {
+  const productName = getProductName();
+  // Tauri 将产品名称转换为小写并用连字符替换空格
+  return productName.toLowerCase().replace(/\s+/g, '-') + '.exe';
+}
 
-  if (!fs.existsSync(releaseDir)) {
-    log.warn('release 目录不存在');
-    return;
-  }
+// 复制构建产物到 release 目录
+function copyArtifacts(options) {
+  log.step('复制构建产物到 release 目录...');
 
-  const files = fs.readdirSync(releaseDir);
-  const packages = files.filter(f => {
-    const ext = path.extname(f).toLowerCase();
-    return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
-  });
+  const mode = options.debug ? 'debug' : 'release';
+  const bundleDir = path.join(TAURI_DIR, 'target', mode, 'bundle');
+  const nsisDir = path.join(bundleDir, 'nsis');
 
-  if (packages.length === 0) {
-    log.warn('未找到打包文件');
-    return;
+  // 复制 NSIS 安装程序
+  if (fs.existsSync(nsisDir)) {
+    const files = fs.readdirSync(nsisDir);
+    for (const file of files) {
+      if (file.endsWith('.exe')) {
+        const src = path.join(nsisDir, file);
+        const dest = path.join(RELEASE_DIR, file);
+        fs.copyFileSync(src, dest);
+        log.info(`复制: ${file}`);
+      }
+    }
   }
 
-  console.log('');
-  for (const pkg of packages) {
-    const filePath = path.join(releaseDir, pkg);
-    const stats = fs.statSync(filePath);
-    const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
-    console.log(`  ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`);
+  // 复制独立 exe (portable)
+  const exeName = getTauriExeName();
+  const portableExe = path.join(TAURI_DIR, 'target', mode, exeName);
+  if (fs.existsSync(portableExe)) {
+    const version = getVersion();
+    const portableName = `Claude-AI-Installer_${version}_x64-portable.exe`;
+    const dest = path.join(RELEASE_DIR, portableName);
+    fs.copyFileSync(portableExe, dest);
+    log.info(`复制便携版: ${portableName}`);
+  } else {
+    log.warn(`未找到便携版 exe: ${portableExe}`);
   }
-  console.log('');
-  log.info(`输出目录: ${releaseDir}`);
-}
 
-// ==================== Portable 版本构建 ====================
+  log.success('构建产物复制完成');
+}
 
-const LAUNCHER_DIR = path.resolve(__dirname, '..', 'launcher');
-const RELEASE_DIR = path.resolve(__dirname, '..', 'release');
+// ==================== Portable 版本构建(带启动器)====================
 
 /**
  * 查找 portable exe 文件
@@ -287,6 +222,13 @@ function findPortableExe() {
 function buildLauncher() {
   log.step('构建启动器...');
 
+  // 检查 launcher.js 是否存在
+  const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js');
+  if (!fs.existsSync(launcherScript)) {
+    log.warn('launcher.js 不存在,跳过启动器构建');
+    return null;
+  }
+
   // 检查是否安装了 pkg
   try {
     execSync('npx pkg --version', { stdio: 'ignore' });
@@ -295,13 +237,12 @@ function buildLauncher() {
     execSync('npm install -g pkg', { stdio: 'inherit' });
   }
 
-  const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js');
   const outputPath = path.join(RELEASE_DIR, 'launcher.exe');
 
   // 使用 pkg 打包
   execSync(`npx pkg "${launcherScript}" --target node18-win-x64 --output "${outputPath}"`, {
     stdio: 'inherit',
-    cwd: path.resolve(__dirname, '..'),
+    cwd: PROJECT_ROOT,
   });
 
   log.success(`启动器构建完成: ${outputPath}`);
@@ -312,8 +253,7 @@ function buildLauncher() {
  * 创建 Portable 目录结构
  */
 function createPortableStructure(portableExe, launcherExe) {
-  const packageJson = require(path.resolve(__dirname, '..', 'package.json'));
-  const version = packageJson.version;
+  const version = getVersion();
   const portableDirName = `Claude-AI-Installer-${version}-portable`;
   const portableDir = path.join(RELEASE_DIR, portableDirName);
   const appDir = path.join(portableDir, 'app');
@@ -394,19 +334,58 @@ function buildPortableVersion() {
   // 查找 portable exe
   const portableExe = findPortableExe();
   if (!portableExe) {
-    throw new Error('未找到 portable exe 文件');
+    log.warn('未找到 portable exe 文件,跳过带启动器的 Portable 版本构建');
+    return;
   }
   log.info(`找到 Portable 文件: ${portableExe}`);
 
   // 构建启动器
   const launcherExe = buildLauncher();
+  if (!launcherExe) {
+    log.warn('启动器构建失败,跳过带启动器的 Portable 版本构建');
+    return;
+  }
 
   // 创建 Portable 目录结构
   createPortableStructure(portableExe, launcherExe);
 
+  // 清理临时的 launcher.exe
+  fs.unlinkSync(launcherExe);
+
   log.success('Portable 版本构建完成');
 }
 
+// 显示构建结果
+function showResults() {
+  log.step('构建结果:');
+
+  if (!fs.existsSync(RELEASE_DIR)) {
+    log.warn('release 目录不存在');
+    return;
+  }
+
+  const files = fs.readdirSync(RELEASE_DIR);
+  const packages = files.filter(f => {
+    const ext = path.extname(f).toLowerCase();
+    return ['.exe', '.zip'].includes(ext);
+  });
+
+  if (packages.length === 0) {
+    log.warn('未找到打包文件');
+    return;
+  }
+
+  console.log('');
+  for (const pkg of packages) {
+    const filePath = path.join(RELEASE_DIR, pkg);
+    const stats = fs.statSync(filePath);
+    const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
+    console.log(`  ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`);
+  }
+  console.log('');
+  log.info(`输出目录: ${RELEASE_DIR}`);
+}
+
 // 主函数
 async function main() {
   const options = parseArgs();
@@ -419,14 +398,12 @@ async function main() {
   console.log(`
 ${colors.bright}╔════════════════════════════════════════════╗
 ║     Claude AI Installer 构建脚本           ║
+║              (Tauri 2.0)                   ║
 ╚════════════════════════════════════════════╝${colors.reset}
 `);
 
-  log.info(`目标平台: ${options.platform}`);
-  log.info(`目标架构: ${options.arch}`);
-  log.info(`发布模式: ${options.publish}`);
-  log.info(`跳过构建: ${options.skipBuild}`);
-  log.info(`跳过类型检查: ${options.skipTypecheck}`);
+  log.info(`构建模式: ${options.debug ? '调试' : '发布'}`);
+  log.info(`跳过前端: ${options.skipFrontend}`);
 
   const startTime = Date.now();
 
@@ -434,26 +411,16 @@ ${colors.bright}╔════════════════════
     // 清理旧的构建文件
     cleanBuild();
 
-    if (!options.skipBuild) {
-      // 类型检查
-      if (!options.skipTypecheck) {
-        typeCheck();
-      } else {
-        log.warn('跳过类型检查');
-      }
-      // 构建前端
-      buildFrontend();
-    } else {
-      log.warn('跳过前端构建');
-    }
+    // 构建 Tauri 应用
+    buildTauri(options);
 
-    packageApp(options);
+    // 复制构建产物
+    copyArtifacts(options);
 
-    // Windows 平台自动构建带启动器的 Portable 版本
-    if (options.platform === 'win' || options.platform === 'all') {
-      buildPortableVersion();
-    }
+    // 构建带启动器的 Portable 版本
+    buildPortableVersion();
 
+    // 显示结果
     showResults();
 
     const duration = ((Date.now() - startTime) / 1000).toFixed(1);

+ 24 - 35
src-tauri/src/commands/claude_code.rs

@@ -1,5 +1,5 @@
+use crate::utils::shell::{run_shell_hidden, spawn_process, CommandOptions};
 use serde::{Deserialize, Serialize};
-use std::process::Command;
 
 #[derive(Serialize, Deserialize)]
 pub struct ClaudeCodeStatus {
@@ -16,14 +16,8 @@ pub struct CommandResult {
 /// 检查 Claude Code 是否已安装
 #[tauri::command]
 pub async fn check_claude_code() -> ClaudeCodeStatus {
-    // 在 Windows 上使用 cmd /c 来执行命令,确保能正确加载 PATH 环境变量
-    #[cfg(target_os = "windows")]
-    let output = Command::new("cmd")
-        .args(["/c", "claude --version"])
-        .output();
-
-    #[cfg(not(target_os = "windows"))]
-    let output = Command::new("claude").args(["--version"]).output();
+    // 使用 shell 执行以获取最新的 PATH 环境变量
+    let output = run_shell_hidden("claude --version");
 
     match output {
         Ok(output) if output.status.success() => {
@@ -46,9 +40,8 @@ pub async fn check_claude_code() -> ClaudeCodeStatus {
 /// 安装 Claude Code
 #[tauri::command]
 pub async fn install_claude_code() -> CommandResult {
-    let output = Command::new("npm")
-        .args(["install", "-g", "@anthropic-ai/claude-code"])
-        .output();
+    // 使用 shell 执行以获取最新的 PATH(确保能找到 npm)
+    let output = run_shell_hidden("npm install -g @anthropic-ai/claude-code");
 
     match output {
         Ok(out) if out.status.success() => CommandResult {
@@ -61,7 +54,7 @@ pub async fn install_claude_code() -> CommandResult {
         },
         Err(e) => CommandResult {
             success: false,
-            error: Some(e.to_string()),
+            error: Some(e),
         },
     }
 }
@@ -72,18 +65,21 @@ pub async fn launch_claude_code() -> CommandResult {
     #[cfg(target_os = "windows")]
     {
         // 在 Windows 上,尝试在新的终端窗口中启动
-        let output = Command::new("cmd")
-            .args(["/c", "start", "cmd", "/k", "claude"])
-            .spawn();
-
-        match output {
+        // 使用 visible 选项因为需要显示终端窗口
+        let result = spawn_process(
+            "cmd",
+            &["/c", "start", "cmd", "/k", "claude"],
+            CommandOptions::visible(),
+        );
+
+        match result {
             Ok(_) => CommandResult {
                 success: true,
                 error: None,
             },
             Err(e) => CommandResult {
                 success: false,
-                error: Some(e.to_string()),
+                error: Some(e),
             },
         }
     }
@@ -96,18 +92,16 @@ pub async fn launch_claude_code() -> CommandResult {
             activate
         end tell"#;
 
-        let output = Command::new("osascript")
-            .args(["-e", script])
-            .spawn();
+        let result = spawn_process("osascript", &["-e", script], CommandOptions::visible());
 
-        match output {
+        match result {
             Ok(_) => CommandResult {
                 success: true,
                 error: None,
             },
             Err(e) => CommandResult {
                 success: false,
-                error: Some(e.to_string()),
+                error: Some(e),
             },
         }
     }
@@ -119,19 +113,14 @@ pub async fn launch_claude_code() -> CommandResult {
 
         for terminal in terminals {
             if which::which(terminal).is_ok() {
-                let output = match terminal {
-                    "gnome-terminal" => Command::new(terminal)
-                        .args(["--", "claude"])
-                        .spawn(),
-                    "konsole" => Command::new(terminal)
-                        .args(["-e", "claude"])
-                        .spawn(),
-                    _ => Command::new(terminal)
-                        .args(["-e", "claude"])
-                        .spawn(),
+                let args: &[&str] = match terminal {
+                    "gnome-terminal" => &["--", "claude"],
+                    _ => &["-e", "claude"],
                 };
 
-                if output.is_ok() {
+                let result = spawn_process(terminal, args, CommandOptions::visible());
+
+                if result.is_ok() {
                     return CommandResult {
                         success: true,
                         error: None,

+ 33 - 49
src-tauri/src/commands/install.rs

@@ -1,6 +1,6 @@
 use crate::commands::AppState;
+use crate::utils::shell::{run_program, run_shell_hidden, CommandOptions};
 use serde::{Deserialize, Serialize};
-use std::process::Command;
 use tauri::{Emitter, State};
 
 #[derive(Serialize, Deserialize, Clone)]
@@ -99,18 +99,14 @@ pub async fn uninstall_software(software: String) -> Result<bool, String> {
         match software.as_str() {
             "nodejs" => {
                 // 通过控制面板卸载
-                let _ = Command::new("powershell")
-                    .args([
-                        "-Command",
-                        "Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -like '*Node*' } | ForEach-Object { $_.Uninstall() }"
-                    ])
-                    .output();
+                let _ = run_shell_hidden(
+                    "powershell -Command \"Get-WmiObject -Class Win32_Product | Where-Object { $_.Name -like '*Node*' } | ForEach-Object { $_.Uninstall() }\""
+                );
                 Ok(true)
             }
             "pnpm" => {
-                let output = Command::new("npm")
-                    .args(["uninstall", "-g", "pnpm"])
-                    .output()
+                // 使用 shell 执行以获取最新的 PATH
+                let output = run_shell_hidden("npm uninstall -g pnpm")
                     .map_err(|e| e.to_string())?;
                 Ok(output.status.success())
             }
@@ -122,9 +118,8 @@ pub async fn uninstall_software(software: String) -> Result<bool, String> {
     {
         match software.as_str() {
             "pnpm" => {
-                let output = Command::new("npm")
-                    .args(["uninstall", "-g", "pnpm"])
-                    .output()
+                // 使用 shell 执行以获取最新的 PATH
+                let output = run_shell_hidden("npm uninstall -g pnpm")
                     .map_err(|e| e.to_string())?;
                 Ok(output.status.success())
             }
@@ -200,9 +195,8 @@ where
             args.push(path);
         }
 
-        let output = Command::new("msiexec")
-            .args(&args)
-            .output()
+        let args_str: Vec<&str> = args.iter().map(|s| &**s).collect();
+        let output = run_program("msiexec", &args_str, CommandOptions::hidden())
             .map_err(|e| e.to_string())?;
 
         // 清理临时文件
@@ -224,9 +218,10 @@ where
     {
         emit_status("Installing Node.js via Homebrew...", 10.0, Some("install.installing"));
 
-        let output = Command::new("brew")
-            .args(["install", &format!("node@{}", version.trim_start_matches('v').split('.').next().unwrap_or("22"))])
-            .output()
+        // 使用 shell 执行以获取最新的 PATH(包括 Homebrew 路径)
+        let major_version = version.trim_start_matches('v').split('.').next().unwrap_or("22");
+        let cmd = format!("brew install node@{}", major_version);
+        let output = run_shell_hidden(&cmd)
             .map_err(|e| e.to_string())?;
 
         if output.status.success() {
@@ -248,18 +243,16 @@ where
         // 使用 NodeSource 安装
         let major_version = version.trim_start_matches('v').split('.').next().unwrap_or("22");
 
-        let setup_output = Command::new("bash")
-            .args(["-c", &format!("curl -fsSL https://deb.nodesource.com/setup_{}.x | sudo -E bash -", major_version)])
-            .output()
+        // 使用 shell 执行以获取最新的 PATH
+        let setup_cmd = format!("curl -fsSL https://deb.nodesource.com/setup_{}.x | sudo -E bash -", major_version);
+        let setup_output = run_shell_hidden(&setup_cmd)
             .map_err(|e| e.to_string())?;
 
         if !setup_output.status.success() {
             return Err(String::from_utf8_lossy(&setup_output.stderr).to_string());
         }
 
-        let install_output = Command::new("sudo")
-            .args(["apt-get", "install", "-y", "nodejs"])
-            .output()
+        let install_output = run_shell_hidden("sudo apt-get install -y nodejs")
             .map_err(|e| e.to_string())?;
 
         if install_output.status.success() {
@@ -296,9 +289,8 @@ where
         });
     }
 
-    let output = Command::new("npm")
-        .args(["install", "-g", "pnpm"])
-        .output()
+    // 使用 shell 执行以获取最新的 PATH(确保能找到刚安装的 npm)
+    let output = run_shell_hidden("npm install -g pnpm")
         .map_err(|e| e.to_string())?;
 
     if output.status.success() {
@@ -363,9 +355,7 @@ where
             args.push(path);
         }
 
-        let output = Command::new(&exe_path)
-            .args(&args)
-            .output()
+        let output = run_program(exe_path.to_str().unwrap(), &args, CommandOptions::hidden())
             .map_err(|e| e.to_string())?;
 
         let _ = std::fs::remove_file(&exe_path);
@@ -386,9 +376,8 @@ where
     {
         emit_status("Installing VS Code via Homebrew...", 10.0, Some("install.installing"));
 
-        let output = Command::new("brew")
-            .args(["install", "--cask", "visual-studio-code"])
-            .output()
+        // 使用 shell 执行以获取最新的 PATH(包括 Homebrew 路径)
+        let output = run_shell_hidden("brew install --cask visual-studio-code")
             .map_err(|e| e.to_string())?;
 
         if output.status.success() {
@@ -423,9 +412,9 @@ where
         let bytes = response.bytes().await.map_err(|e| e.to_string())?;
         std::fs::write(&deb_path, bytes).map_err(|e| e.to_string())?;
 
-        let output = Command::new("sudo")
-            .args(["dpkg", "-i", deb_path.to_str().unwrap()])
-            .output()
+        // 使用 shell 执行以获取最新的 PATH
+        let cmd = format!("sudo dpkg -i {}", deb_path.to_str().unwrap());
+        let output = run_shell_hidden(&cmd)
             .map_err(|e| e.to_string())?;
 
         let _ = std::fs::remove_file(&deb_path);
@@ -515,9 +504,7 @@ where
             args.push(path);
         }
 
-        let output = Command::new(&exe_path)
-            .args(&args)
-            .output()
+        let output = run_program(exe_path.to_str().unwrap(), &args, CommandOptions::hidden())
             .map_err(|e| e.to_string())?;
 
         let _ = std::fs::remove_file(&exe_path);
@@ -538,9 +525,8 @@ where
     {
         emit_status("Installing Git via Homebrew...", 10.0, Some("install.installing"));
 
-        let output = Command::new("brew")
-            .args(["install", "git"])
-            .output()
+        // 使用 shell 执行以获取最新的 PATH(包括 Homebrew 路径)
+        let output = run_shell_hidden("brew install git")
             .map_err(|e| e.to_string())?;
 
         if output.status.success() {
@@ -559,9 +545,8 @@ where
     {
         emit_status("Installing Git...", 10.0, Some("install.installing"));
 
-        let output = Command::new("sudo")
-            .args(["apt-get", "install", "-y", "git"])
-            .output()
+        // 使用 shell 执行以获取最新的 PATH
+        let output = run_shell_hidden("sudo apt-get install -y git")
             .map_err(|e| e.to_string())?;
 
         if output.status.success() {
@@ -598,9 +583,8 @@ where
         });
     }
 
-    let output = Command::new("npm")
-        .args(["install", "-g", "@anthropic-ai/claude-code"])
-        .output()
+    // 使用 shell 执行以获取最新的 PATH(确保能找到刚安装的 npm)
+    let output = run_shell_hidden("npm install -g @anthropic-ai/claude-code")
         .map_err(|e| e.to_string())?;
 
     if output.status.success() {

+ 0 - 25
src-tauri/src/commands/logs.rs

@@ -130,28 +130,3 @@ pub async fn get_install_history(limit: Option<usize>) -> Vec<InstallHistoryItem
     vec![]
 }
 
-/// 添加安装历史记录 (内部使用,保留以供将来使用)
-#[allow(dead_code)]
-pub fn add_install_history(item: InstallHistoryItem) -> Result<(), String> {
-    let _ = ensure_log_dir().map_err(|e| e.to_string())?;
-    let path = get_history_path();
-
-    let mut history: Vec<InstallHistoryItem> = if let Ok(content) = fs::read_to_string(&path) {
-        serde_json::from_str(&content).unwrap_or_default()
-    } else {
-        vec![]
-    };
-
-    history.push(item);
-
-    // 只保留最近 100 条记录
-    if history.len() > 100 {
-        let skip_count = history.len() - 100;
-        history = history.into_iter().skip(skip_count).collect();
-    }
-
-    let content = serde_json::to_string_pretty(&history).map_err(|e| e.to_string())?;
-    fs::write(&path, content).map_err(|e| e.to_string())?;
-
-    Ok(())
-}

+ 0 - 18
src-tauri/src/commands/mod.rs

@@ -9,28 +9,10 @@ pub mod window;
 pub mod updater;
 
 use std::sync::Mutex;
-use std::collections::HashMap;
-use tokio::sync::broadcast;
 
 /// 安装进程状态
 #[derive(Default)]
 pub struct AppState {
     /// 当前安装进程的取消标志
     pub cancel_flag: Mutex<bool>,
-    /// 安装状态广播通道(保留以供将来使用)
-    #[allow(dead_code)]
-    pub install_status_tx: Mutex<Option<broadcast::Sender<InstallStatusEvent>>>,
-    /// 配置缓存(保留以供将来使用)
-    #[allow(dead_code)]
-    pub config_cache: Mutex<HashMap<String, serde_json::Value>>,
-}
-
-/// 安装状态事件
-#[derive(Clone, serde::Serialize)]
-pub struct InstallStatusEvent {
-    pub software: String,
-    pub message: String,
-    pub progress: f64,
-    pub i18n_key: Option<String>,
-    pub i18n_params: Option<HashMap<String, String>>,
 }

+ 9 - 18
src-tauri/src/commands/software.rs

@@ -1,5 +1,5 @@
+use crate::utils::shell::run_shell_hidden;
 use serde::{Deserialize, Serialize};
-use std::process::Command;
 use regex::Regex;
 use super::config::get_git_mirror_config;
 
@@ -68,26 +68,17 @@ pub async fn check_installed(software: String) -> Result<serde_json::Value, Stri
 }
 
 async fn check_single_software(software: &str) -> InstalledInfo {
-    let (cmd, args) = match software {
-        "nodejs" => ("node", vec!["--version"]),
-        "pnpm" => ("pnpm", vec!["--version"]),
-        "vscode" => ("code", vec!["--version"]),
-        "git" => ("git", vec!["--version"]),
-        "claudeCode" => ("claude", vec!["--version"]),
+    let cmd = match software {
+        "nodejs" => "node --version",
+        "pnpm" => "pnpm --version",
+        "vscode" => "code --version",
+        "git" => "git --version",
+        "claudeCode" => "claude --version",
         _ => return InstalledInfo { installed: false, version: None },
     };
 
-    // 在 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();
+    // 使用 shell 执行以获取最新的 PATH 环境变量
+    let output = run_shell_hidden(cmd);
 
     match output {
         Ok(output) if output.status.success() => {

+ 19 - 18
src-tauri/src/commands/system.rs

@@ -1,5 +1,5 @@
+use crate::utils::shell::run_shell_hidden;
 use serde::{Deserialize, Serialize};
-use std::process::Command;
 
 #[derive(Serialize, Deserialize)]
 pub struct PackageManagerResult {
@@ -42,12 +42,9 @@ pub async fn check_admin() -> bool {
     #[cfg(target_os = "windows")]
     {
         // Windows: 使用 PowerShell 检查管理员权限
-        let output = Command::new("powershell")
-            .args([
-                "-Command",
-                "([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"
-            ])
-            .output();
+        let output = run_shell_hidden(
+            "powershell -Command \"([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)\""
+        );
 
         is_admin = match output {
             Ok(out) => {
@@ -112,8 +109,9 @@ pub async fn check_package_manager() -> PackageManagerResult {
 
     #[cfg(target_os = "macos")]
     {
-        // 检查 Homebrew
-        if which::which("brew").is_ok() {
+        // 使用 shell 检查 Homebrew,确保能获取最新的 PATH
+        let output = run_shell_hidden("brew --version");
+        if output.is_ok() && output.unwrap().status.success() {
             log::info!("check_package_manager: Homebrew found");
             PackageManagerResult {
                 exists: true,
@@ -130,8 +128,14 @@ pub async fn check_package_manager() -> PackageManagerResult {
 
     #[cfg(target_os = "linux")]
     {
-        // 检查 apt
-        if which::which("apt").is_ok() || which::which("apt-get").is_ok() {
+        // 使用 shell 检查 apt,确保能获取最新的 PATH
+        let apt_output = run_shell_hidden("apt --version");
+        let apt_get_output = run_shell_hidden("apt-get --version");
+
+        let apt_exists = apt_output.is_ok() && apt_output.unwrap().status.success();
+        let apt_get_exists = apt_get_output.is_ok() && apt_get_output.unwrap().status.success();
+
+        if apt_exists || apt_get_exists {
             log::info!("check_package_manager: APT found");
             PackageManagerResult {
                 exists: true,
@@ -163,13 +167,10 @@ pub async fn install_package_manager(manager: String) -> Result<(), String> {
         "brew" => {
             #[cfg(target_os = "macos")]
             {
-                let output = Command::new("bash")
-                    .args([
-                        "-c",
-                        r#"/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)""#
-                    ])
-                    .output()
-                    .map_err(|e| e.to_string())?;
+                // 使用 shell 执行以获取最新的 PATH
+                let output = run_shell_hidden(
+                    r#"/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)""#
+                ).map_err(|e| e.to_string())?;
 
                 if output.status.success() {
                     Ok(())

+ 6 - 21
src-tauri/src/commands/vscode.rs

@@ -1,5 +1,5 @@
+use crate::utils::shell::run_shell_hidden;
 use serde::{Deserialize, Serialize};
-use std::process::Command;
 
 #[derive(Serialize, Deserialize)]
 pub struct ExtensionStatus {
@@ -16,16 +16,8 @@ pub struct CommandResult {
 /// 检查 VS Code 扩展是否已安装
 #[tauri::command]
 pub async fn check_vscode_extension(extension_id: String) -> ExtensionStatus {
-    // 在 Windows 上,code 实际上是 code.cmd,需要通过 cmd 调用
-    #[cfg(target_os = "windows")]
-    let output = Command::new("cmd")
-        .args(["/c", "code", "--list-extensions", "--show-versions"])
-        .output();
-
-    #[cfg(not(target_os = "windows"))]
-    let output = Command::new("code")
-        .args(["--list-extensions", "--show-versions"])
-        .output();
+    // 使用 shell 执行以获取最新的 PATH 环境变量
+    let output = run_shell_hidden("code --list-extensions --show-versions");
 
     match output {
         Ok(out) if out.status.success() => {
@@ -60,16 +52,9 @@ pub async fn check_vscode_extension(extension_id: String) -> ExtensionStatus {
 /// 安装 VS Code 扩展
 #[tauri::command]
 pub async fn install_vscode_extension(extension_id: String) -> CommandResult {
-    // 在 Windows 上,code 实际上是 code.cmd,需要通过 cmd 调用
-    #[cfg(target_os = "windows")]
-    let output = Command::new("cmd")
-        .args(["/c", "code", "--install-extension", &extension_id, "--force"])
-        .output();
-
-    #[cfg(not(target_os = "windows"))]
-    let output = Command::new("code")
-        .args(["--install-extension", &extension_id, "--force"])
-        .output();
+    // 使用 shell 执行以获取最新的 PATH 环境变量
+    let cmd = format!("code --install-extension {} --force", extension_id);
+    let output = run_shell_hidden(&cmd);
 
     match output {
         Ok(out) if out.status.success() => CommandResult {

+ 7 - 13
src-tauri/src/commands/window.rs

@@ -71,22 +71,16 @@ pub async fn select_directory(_default_path: Option<String>) -> DirectoryResult
 
     #[cfg(target_os = "windows")]
     {
-        use std::process::Command;
+        use crate::utils::shell::{run_shell, CommandOptions};
 
         // 使用 PowerShell 打开文件夹选择对话框
-        let script = r#"
-            Add-Type -AssemblyName System.Windows.Forms
-            $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
-            $dialog.Description = "Select a folder"
-            $dialog.ShowNewFolderButton = $true
-            if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
-                $dialog.SelectedPath
-            }
-        "#;
+        // 需要显示窗口因为这是一个 GUI 对话框
+        let script = r#"Add-Type -AssemblyName System.Windows.Forms; $dialog = New-Object System.Windows.Forms.FolderBrowserDialog; $dialog.Description = 'Select a folder'; $dialog.ShowNewFolderButton = $true; if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) { $dialog.SelectedPath }"#;
 
-        let output = Command::new("powershell")
-            .args(["-Command", script])
-            .output();
+        let output = run_shell(
+            &format!("powershell -Command \"{}\"", script),
+            CommandOptions::visible(),  // 需要显示对话框
+        );
 
         match output {
             Ok(out) if out.status.success() => {

+ 0 - 131
src-tauri/src/utils/download.rs

@@ -1,131 +0,0 @@
-use futures_util::StreamExt;
-use std::path::Path;
-use tokio::fs::File;
-use tokio::io::AsyncWriteExt;
-
-/// 下载文件到指定路径
-#[allow(dead_code)]
-pub async fn download_file(
-    url: &str,
-    dest: &Path,
-    progress_callback: Option<impl Fn(u64, u64)>,
-) -> Result<(), String> {
-    let client = reqwest::Client::new();
-
-    let response = client
-        .get(url)
-        .send()
-        .await
-        .map_err(|e| format!("Failed to send request: {}", e))?;
-
-    let total_size = response.content_length().unwrap_or(0);
-
-    let mut file = File::create(dest)
-        .await
-        .map_err(|e| format!("Failed to create file: {}", e))?;
-
-    let mut downloaded: u64 = 0;
-    let mut stream = response.bytes_stream();
-
-    while let Some(chunk) = stream.next().await {
-        let chunk = chunk.map_err(|e| format!("Failed to read chunk: {}", e))?;
-
-        file.write_all(&chunk)
-            .await
-            .map_err(|e| format!("Failed to write chunk: {}", e))?;
-
-        downloaded += chunk.len() as u64;
-
-        if let Some(ref callback) = progress_callback {
-            callback(downloaded, total_size);
-        }
-    }
-
-    file.flush()
-        .await
-        .map_err(|e| format!("Failed to flush file: {}", e))?;
-
-    Ok(())
-}
-
-/// 带断点续传的下载
-#[allow(dead_code)]
-pub async fn download_file_resumable(
-    url: &str,
-    dest: &Path,
-    progress_callback: Option<impl Fn(u64, u64)>,
-) -> Result<(), String> {
-    let client = reqwest::Client::new();
-
-    // 检查已下载的大小
-    let existing_size = if dest.exists() {
-        tokio::fs::metadata(dest)
-            .await
-            .map(|m| m.len())
-            .unwrap_or(0)
-    } else {
-        0
-    };
-
-    // 发送 HEAD 请求获取文件大小
-    let head_response = client
-        .head(url)
-        .send()
-        .await
-        .map_err(|e| format!("Failed to send HEAD request: {}", e))?;
-
-    let total_size = head_response.content_length().unwrap_or(0);
-
-    // 如果文件已完整下载,直接返回
-    if existing_size >= total_size && total_size > 0 {
-        return Ok(());
-    }
-
-    // 发送带 Range 头的请求
-    let mut request = client.get(url);
-
-    if existing_size > 0 {
-        request = request.header("Range", format!("bytes={}-", existing_size));
-    }
-
-    let response = request
-        .send()
-        .await
-        .map_err(|e| format!("Failed to send request: {}", e))?;
-
-    // 打开文件(追加模式或创建新文件)
-    let mut file = if existing_size > 0 {
-        tokio::fs::OpenOptions::new()
-            .append(true)
-            .open(dest)
-            .await
-            .map_err(|e| format!("Failed to open file: {}", e))?
-    } else {
-        File::create(dest)
-            .await
-            .map_err(|e| format!("Failed to create file: {}", e))?
-    };
-
-    let mut downloaded = existing_size;
-    let mut stream = response.bytes_stream();
-
-    while let Some(chunk) = stream.next().await {
-        let chunk = chunk.map_err(|e| format!("Failed to read chunk: {}", e))?;
-
-        file.write_all(&chunk)
-            .await
-            .map_err(|e| format!("Failed to write chunk: {}", e))?;
-
-        downloaded += chunk.len() as u64;
-
-        if let Some(ref callback) = progress_callback {
-            callback(downloaded, total_size);
-        }
-    }
-
-    file.flush()
-        .await
-        .map_err(|e| format!("Failed to flush file: {}", e))?;
-
-    Ok(())
-}

+ 0 - 4
src-tauri/src/utils/mod.rs

@@ -1,8 +1,4 @@
-pub mod download;
 pub mod shell;
 
-// 这些工具函数目前未被使用,但保留以供将来使用
-#[allow(unused_imports)]
-pub use download::*;
 #[allow(unused_imports)]
 pub use shell::*;

+ 370 - 97
src-tauri/src/utils/shell.rs

@@ -1,123 +1,396 @@
+//! 统一的命令执行工具模块
+//!
+//! 提供跨平台的命令执行功能,支持:
+//! - 获取最新的环境变量(不仅是 PATH)
+//! - 控制窗口是否可见(Windows)
+//! - 通过 shell 或直接执行命令
+
+use std::collections::HashMap;
 use std::process::{Command, Output, Stdio};
 
-// 以下工具函数保留以供将来使用
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
 
-/// 执行命令并返回输出
-#[allow(dead_code)]
-pub fn run_command(cmd: &str, args: &[&str]) -> Result<Output, String> {
-    Command::new(cmd)
-        .args(args)
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .output()
-        .map_err(|e| format!("Failed to execute command: {}", e))
-}
+#[cfg(target_os = "windows")]
+const CREATE_NO_WINDOW: u32 = 0x08000000;
 
-/// 执行命令并返回标准输出
-#[allow(dead_code)]
-pub fn run_command_output(cmd: &str, args: &[&str]) -> Result<String, String> {
-    let output = run_command(cmd, args)?;
+// ==================== 命令执行选项 ====================
 
-    if output.status.success() {
-        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
-    } else {
-        Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
+/// 命令执行选项
+#[derive(Clone)]
+pub struct CommandOptions {
+    /// 是否显示窗口(仅 Windows 有效)
+    /// - true: 显示窗口(用于需要用户交互的命令)
+    /// - false: 隐藏窗口(用于后台执行的命令,默认)
+    pub show_window: bool,
+
+    /// 是否使用最新的环境变量(默认 true)
+    pub use_fresh_env: bool,
+
+    /// 需要刷新的环境变量名称列表
+    /// PATH 会自动包含,无需手动添加
+    pub env_names: Vec<String>,
+
+    /// 工作目录
+    pub working_dir: Option<String>,
+}
+
+impl Default for CommandOptions {
+    fn default() -> Self {
+        Self {
+            show_window: false,
+            use_fresh_env: true,
+            env_names: vec![],
+            working_dir: None,
+        }
     }
 }
 
-/// 检查命令是否存在
-#[allow(dead_code)]
-pub fn command_exists(cmd: &str) -> bool {
-    which::which(cmd).is_ok()
+impl CommandOptions {
+    /// 创建显示窗口的选项(用于需要用户交互的场景)
+    pub fn visible() -> Self {
+        Self {
+            show_window: true,
+            ..Self::default()
+        }
+    }
+
+    /// 创建隐藏窗口的选项(用于后台执行)
+    pub fn hidden() -> Self {
+        Self::default()
+    }
 }
 
-/// 获取命令版本
-#[allow(dead_code)]
-pub fn get_command_version(cmd: &str) -> Option<String> {
-    let output = Command::new(cmd)
-        .arg("--version")
-        .stdout(Stdio::piped())
-        .stderr(Stdio::piped())
-        .output()
-        .ok()?;
+// ==================== 环境变量获取 ====================
 
-    if output.status.success() {
-        let version = String::from_utf8_lossy(&output.stdout);
-        version.lines().next().map(|s| s.trim().to_string())
-    } else {
-        None
+/// 获取系统最新的环境变量
+///
+/// 在 Windows 上,从注册表读取 Machine 和 User 的环境变量
+/// 在 macOS/Linux 上,通过启动一个新的 login shell 来获取
+///
+/// # 参数
+/// - `env_names`: 需要获取的环境变量名称列表
+///
+/// # 返回
+/// 环境变量名称到值的映射
+pub fn get_fresh_env(env_names: &[&str]) -> HashMap<String, String> {
+    let mut env_map = HashMap::new();
+
+    if env_names.is_empty() {
+        return env_map;
+    }
+
+    #[cfg(target_os = "windows")]
+    {
+        for name in env_names {
+            let script = format!(
+                "[Environment]::GetEnvironmentVariable('{}', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('{}', 'User')",
+                name, name
+            );
+
+            let output = Command::new("powershell")
+                .args(["-Command", &script])
+                .creation_flags(CREATE_NO_WINDOW)
+                .stdout(Stdio::piped())
+                .stderr(Stdio::piped())
+                .output();
+
+            if let Ok(out) = output {
+                if out.status.success() {
+                    let value = String::from_utf8_lossy(&out.stdout).trim().to_string();
+                    // 清理空值和重复的分号
+                    let cleaned: String = value
+                        .split(';')
+                        .filter(|s| !s.is_empty())
+                        .collect::<Vec<_>>()
+                        .join(";");
+                    if !cleaned.is_empty() {
+                        env_map.insert(name.to_string(), cleaned);
+                    }
+                }
+            }
+        }
     }
+
+    #[cfg(not(target_os = "windows"))]
+    {
+        // 在 macOS/Linux 上,通过 login shell 获取环境变量
+        let env_echo = env_names
+            .iter()
+            .map(|n| format!("echo \"__ENV_{}=${}\"", n, n))
+            .collect::<Vec<_>>()
+            .join(" && ");
+
+        let shell = if cfg!(target_os = "macos") {
+            "zsh"
+        } else {
+            "bash"
+        };
+
+        let output = Command::new(shell)
+            .args(["-l", "-c", &env_echo])
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped())
+            .output();
+
+        if let Ok(out) = output {
+            if out.status.success() {
+                let stdout = String::from_utf8_lossy(&out.stdout);
+                for line in stdout.lines() {
+                    if line.starts_with("__ENV_") {
+                        if let Some((key, value)) = line.strip_prefix("__ENV_").and_then(|s| s.split_once('=')) {
+                            if !value.is_empty() {
+                                env_map.insert(key.to_string(), value.to_string());
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    env_map
 }
 
-/// 在 Windows 上刷新 PATH 环境变量
-#[allow(dead_code)]
-#[cfg(target_os = "windows")]
-pub fn refresh_path() -> Result<String, String> {
-    let output = Command::new("powershell")
-        .args([
-            "-Command",
-            "[Environment]::GetEnvironmentVariable('Path', 'Machine') + ';' + [Environment]::GetEnvironmentVariable('Path', 'User')"
-        ])
-        .output()
-        .map_err(|e| e.to_string())?;
-
-    if output.status.success() {
-        Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
+// ==================== 核心命令执行函数 ====================
+
+/// 通过 shell 执行命令字符串
+///
+/// 这是最常用的命令执行方法,适用于大多数场景。
+///
+/// 在 Windows 上使用 `cmd /c`
+/// 在 macOS 上使用 `zsh -l -c`
+/// 在 Linux 上使用 `bash -l -c`
+///
+/// # 参数
+/// - `command`: 要执行的命令字符串
+/// - `options`: 执行选项
+///
+/// # 示例
+/// ```rust
+/// // 隐藏窗口执行(默认)
+/// let output = run_shell("node --version", CommandOptions::new())?;
+///
+/// // 显示窗口执行
+/// let output = run_shell("npm install", CommandOptions::visible())?;
+///
+/// // 添加额外的环境变量
+/// let output = run_shell("echo $HOME", CommandOptions::new().with_env("HOME"))?;
+/// ```
+pub fn run_shell(command: &str, options: CommandOptions) -> Result<Output, String> {
+    // 获取需要刷新的环境变量
+    let fresh_env = if options.use_fresh_env {
+        let mut env_names: Vec<&str> = vec!["PATH"];
+        for name in &options.env_names {
+            env_names.push(name);
+        }
+        get_fresh_env(&env_names)
     } else {
-        Err(String::from_utf8_lossy(&output.stderr).to_string())
+        HashMap::new()
+    };
+
+    #[cfg(target_os = "windows")]
+    {
+        let mut cmd = Command::new("cmd");
+        cmd.args(["/c", command])
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped());
+
+        // 设置工作目录
+        if let Some(dir) = &options.working_dir {
+            cmd.current_dir(dir);
+        }
+
+        // 设置环境变量
+        for (key, value) in &fresh_env {
+            cmd.env(key, value);
+        }
+
+        // 设置窗口显示
+        if !options.show_window {
+            cmd.creation_flags(CREATE_NO_WINDOW);
+        }
+
+        cmd.output()
+            .map_err(|e| format!("执行命令失败 '{}': {}", command, e))
     }
-}
 
-#[allow(dead_code)]
-#[cfg(not(target_os = "windows"))]
-pub fn refresh_path() -> Result<String, String> {
-    std::env::var("PATH").map_err(|e| e.to_string())
+    #[cfg(target_os = "macos")]
+    {
+        let mut cmd = Command::new("zsh");
+        cmd.args(["-l", "-c", command])
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped());
+
+        if let Some(dir) = &options.working_dir {
+            cmd.current_dir(dir);
+        }
+
+        // macOS 使用 login shell 会自动加载最新环境变量
+        // 但我们仍然设置获取到的环境变量以确保一致性
+        for (key, value) in &fresh_env {
+            cmd.env(key, value);
+        }
+
+        cmd.output()
+            .or_else(|_| {
+                // 如果 zsh 失败,尝试 bash
+                let mut fallback = Command::new("bash");
+                fallback
+                    .args(["-l", "-c", command])
+                    .stdout(Stdio::piped())
+                    .stderr(Stdio::piped());
+
+                if let Some(dir) = &options.working_dir {
+                    fallback.current_dir(dir);
+                }
+
+                for (key, value) in &fresh_env {
+                    fallback.env(key, value);
+                }
+
+                fallback.output()
+            })
+            .map_err(|e| format!("执行命令失败 '{}': {}", command, e))
+    }
+
+    #[cfg(target_os = "linux")]
+    {
+        let mut cmd = Command::new("bash");
+        cmd.args(["-l", "-c", command])
+            .stdout(Stdio::piped())
+            .stderr(Stdio::piped());
+
+        if let Some(dir) = &options.working_dir {
+            cmd.current_dir(dir);
+        }
+
+        for (key, value) in &fresh_env {
+            cmd.env(key, value);
+        }
+
+        cmd.output()
+            .or_else(|_| {
+                // 如果 bash 失败,尝试 sh
+                let mut fallback = Command::new("sh");
+                fallback
+                    .args(["-l", "-c", command])
+                    .stdout(Stdio::piped())
+                    .stderr(Stdio::piped());
+
+                if let Some(dir) = &options.working_dir {
+                    fallback.current_dir(dir);
+                }
+
+                for (key, value) in &fresh_env {
+                    fallback.env(key, value);
+                }
+
+                fallback.output()
+            })
+            .map_err(|e| format!("执行命令失败 '{}': {}", command, e))
+    }
 }
 
-/// 以管理员权限执行命令 (Windows)
-#[allow(dead_code)]
-#[cfg(target_os = "windows")]
-pub fn run_as_admin(cmd: &str, args: &[&str]) -> Result<(), String> {
-    use std::os::windows::process::CommandExt;
-
-    const CREATE_NO_WINDOW: u32 = 0x08000000;
-
-    let args_str = args.join(" ");
-
-    let output = Command::new("powershell")
-        .creation_flags(CREATE_NO_WINDOW)
-        .args([
-            "-Command",
-            &format!(
-                "Start-Process -FilePath '{}' -ArgumentList '{}' -Verb RunAs -Wait",
-                cmd, args_str
-            ),
-        ])
-        .output()
-        .map_err(|e| e.to_string())?;
-
-    if output.status.success() {
-        Ok(())
-    } else {
-        Err(String::from_utf8_lossy(&output.stderr).to_string())
+/// 直接执行程序(不通过 shell)
+///
+/// 适用于需要直接调用可执行文件的场景,如安装程序。
+///
+/// # 参数
+/// - `program`: 要执行的程序路径
+/// - `args`: 命令参数
+/// - `options`: 执行选项
+///
+/// # 示例
+/// ```rust
+/// let output = run_program("msiexec", &["/i", "setup.msi", "/qn"], CommandOptions::hidden())?;
+/// ```
+pub fn run_program(program: &str, args: &[&str], options: CommandOptions) -> Result<Output, String> {
+    let mut cmd = Command::new(program);
+    cmd.args(args)
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped());
+
+    // 设置工作目录
+    if let Some(dir) = &options.working_dir {
+        cmd.current_dir(dir);
     }
+
+    // 设置环境变量
+    if options.use_fresh_env {
+        let mut env_names: Vec<&str> = vec!["PATH"];
+        for name in &options.env_names {
+            env_names.push(name);
+        }
+
+        let fresh_env = get_fresh_env(&env_names);
+        for (key, value) in fresh_env {
+            cmd.env(&key, &value);
+        }
+    }
+
+    // Windows 特定:设置窗口显示
+    #[cfg(target_os = "windows")]
+    {
+        if !options.show_window {
+            cmd.creation_flags(CREATE_NO_WINDOW);
+        }
+    }
+
+    cmd.output()
+        .map_err(|e| format!("执行程序失败 '{}': {}", program, e))
 }
 
-#[allow(dead_code)]
-#[cfg(not(target_os = "windows"))]
-pub fn run_as_admin(cmd: &str, args: &[&str]) -> Result<(), String> {
-    // 在 Unix 系统上使用 sudo
-    let mut full_args = vec![cmd];
-    full_args.extend(args);
+/// 启动一个进程(不等待完成)
+///
+/// 用于启动外部程序,如打开终端窗口。
+///
+/// # 参数
+/// - `program`: 要启动的程序
+/// - `args`: 程序参数
+/// - `options`: 执行选项
+pub fn spawn_process(program: &str, args: &[&str], options: CommandOptions) -> Result<(), String> {
+    let mut cmd = Command::new(program);
+    cmd.args(args);
+
+    if let Some(dir) = &options.working_dir {
+        cmd.current_dir(dir);
+    }
 
-    let output = Command::new("sudo")
-        .args(&full_args)
-        .output()
-        .map_err(|e| e.to_string())?;
+    // 设置环境变量
+    if options.use_fresh_env {
+        let mut env_names: Vec<&str> = vec!["PATH"];
+        for name in &options.env_names {
+            env_names.push(name);
+        }
 
-    if output.status.success() {
-        Ok(())
-    } else {
-        Err(String::from_utf8_lossy(&output.stderr).to_string())
+        let fresh_env = get_fresh_env(&env_names);
+        for (key, value) in fresh_env {
+            cmd.env(&key, &value);
+        }
     }
+
+    #[cfg(target_os = "windows")]
+    {
+        if !options.show_window {
+            cmd.creation_flags(CREATE_NO_WINDOW);
+        }
+    }
+
+    cmd.spawn()
+        .map_err(|e| format!("启动进程失败 '{}': {}", program, e))?;
+
+    Ok(())
 }
+
+// ==================== 便捷方法 ====================
+
+/// 通过 shell 执行命令(隐藏窗口,使用最新环境变量)
+///
+/// 这是最常用的便捷方法,适用于大多数后台命令执行场景。
+#[inline]
+pub fn run_shell_hidden(command: &str) -> Result<Output, String> {
+    run_shell(command, CommandOptions::hidden())
+}
+
+