From 110c249815e3c3136f01d929642f8dc256dfa44b Mon Sep 17 00:00:00 2001 From: duanyongcheng <1171998654@qq.com> Date: Thu, 15 Jan 2026 23:32:39 +0800 Subject: [PATCH] =?UTF-8?q?refactor(tool):=20=E9=87=8D=E6=9E=84=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E7=A7=8D=E5=AE=89=E8=A3=85=E6=96=B9=E5=BC=8F?= =?UTF-8?q?=20Refactor=20tool=20update=20logic=20to=20support=20multiple?= =?UTF-8?q?=20installation=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 updateToolInstance 替代废弃的 updateTool 命令 Replace deprecated updateTool with updateToolInstance command - Gemini CLI 新增 Homebrew 安装方式检测和更新支持 Add Homebrew installation detection and update support for Gemini CLI - 根据安装方式智能选择更新策略(Npm/Brew 使用 InstallerService,Official 使用 Detector) Smart update strategy based on install method (InstallerService for Npm/Brew, Detector for Official) - 安装器扫描根据工具路径智能判断优先安装方式 Installer scanner intelligently prioritizes install method based on tool path - 工具状态查询时自动检测缺失的 Local 实例 Auto-detect missing Local instances when querying tool status - 修复 Dashboard 工具卡片 Badge 换行问题 Fix Badge wrapping issue in Dashboard tool cards --- .../services/tool/detectors/claude_code.rs | 5 +- .../src/services/tool/detectors/gemini_cli.rs | 59 +++++++++++++++-- src-tauri/src/services/tool/registry/query.rs | 60 +++++++++++++----- .../src/services/tool/registry/version_ops.rs | 63 +++++++++++++++++-- src-tauri/src/utils/installer_scanner.rs | 26 ++++++-- src/lib/tauri-commands/tool.ts | 8 --- .../components/DashboardToolCard.tsx | 8 +-- src/pages/DashboardPage/hooks/useDashboard.ts | 56 +++++++++++++++-- src/pages/DashboardPage/index.tsx | 7 +-- .../components/ActiveProfileCard.tsx | 4 +- 10 files changed, 238 insertions(+), 58 deletions(-) diff --git a/src-tauri/src/services/tool/detectors/claude_code.rs b/src-tauri/src/services/tool/detectors/claude_code.rs index 243d013..e0307a6 100644 --- a/src-tauri/src/services/tool/detectors/claude_code.rs +++ b/src-tauri/src/services/tool/detectors/claude_code.rs @@ -121,14 +121,15 @@ impl ToolDetector for ClaudeCodeDetector { } } - async fn update(&self, executor: &CommandExecutor, force: bool) -> Result<()> { + async fn update(&self, executor: &CommandExecutor, _force: bool) -> Result<()> { // 检测当前安装方法 let method = self.detect_install_method(executor).await; match method { Some(InstallMethod::Official) => { // 官方安装:重新执行安装脚本即可更新 - self.install_official(executor, force).await + // 更新时跳过镜像检查(force=true),因为用户已主动点击更新 + self.install_official(executor, true).await } Some(InstallMethod::Npm) => { // npm 安装:使用 npm update diff --git a/src-tauri/src/services/tool/detectors/gemini_cli.rs b/src-tauri/src/services/tool/detectors/gemini_cli.rs index 4ad990d..35f2c71 100644 --- a/src-tauri/src/services/tool/detectors/gemini_cli.rs +++ b/src-tauri/src/services/tool/detectors/gemini_cli.rs @@ -68,7 +68,19 @@ impl ToolDetector for GeminiCLIDetector { // ==================== 检测逻辑 ==================== async fn detect_install_method(&self, executor: &CommandExecutor) -> Option { - // Gemini CLI 仅支持 npm 安装 + // 检查 Homebrew 安装(macOS) + #[cfg(target_os = "macos")] + { + if executor.command_exists_async("brew").await { + let cmd = "brew list --formula | grep -q '^gemini-cli$'"; + let result = executor.execute_async(cmd).await; + if result.success { + return Some(InstallMethod::Brew); + } + } + } + + // 检查 npm 全局安装 if executor.command_exists_async("npm").await { let stderr_redirect = if cfg!(windows) { "2>nul" @@ -77,12 +89,13 @@ impl ToolDetector for GeminiCLIDetector { }; let cmd = format!("npm list -g @google/gemini-cli {stderr_redirect}"); let result = executor.execute_async(&cmd).await; - if result.success { + if result.success && !result.stdout.contains("(empty)") { return Some(InstallMethod::Npm); } } - Some(InstallMethod::Npm) + // 默认返回 Other(无法确定安装方式) + Some(InstallMethod::Other) } // ==================== 安装逻辑 ==================== @@ -95,14 +108,20 @@ impl ToolDetector for GeminiCLIDetector { ) -> Result<()> { match method { InstallMethod::Npm => self.install_npm(executor, force).await, - InstallMethod::Official | InstallMethod::Brew | InstallMethod::Other => { - anyhow::bail!("Gemini CLI 仅支持 npm 安装") + InstallMethod::Brew => self.install_brew(executor).await, + InstallMethod::Official | InstallMethod::Other => { + anyhow::bail!("Gemini CLI 支持 npm 或 brew 安装") } } } async fn update(&self, executor: &CommandExecutor, _force: bool) -> Result<()> { - self.update_npm(executor).await + // 根据当前安装方式选择更新命令 + let method = self.detect_install_method(executor).await; + match method { + Some(InstallMethod::Brew) => self.update_brew(executor).await, + _ => self.update_npm(executor).await, + } } // ==================== 配置管理 ==================== @@ -172,6 +191,34 @@ impl GeminiCLIDetector { } } + /// 使用 Homebrew 安装(macOS) + async fn install_brew(&self, executor: &CommandExecutor) -> Result<()> { + if !executor.command_exists_async("brew").await { + anyhow::bail!("Homebrew 未安装"); + } + + let command = "brew install gemini-cli"; + let result = executor.execute_async(command).await; + + if result.success { + Ok(()) + } else { + anyhow::bail!("❌ Homebrew 安装失败\n\n{}", result.stderr) + } + } + + /// 使用 Homebrew 更新(macOS) + async fn update_brew(&self, executor: &CommandExecutor) -> Result<()> { + let command = "brew upgrade gemini-cli"; + let result = executor.execute_async(command).await; + + if result.success { + Ok(()) + } else { + anyhow::bail!("❌ Homebrew 更新失败\n\n{}", result.stderr) + } + } + /// 转换为旧版 Tool 结构 fn to_legacy_tool(&self) -> crate::models::Tool { crate::models::Tool::gemini_cli() diff --git a/src-tauri/src/services/tool/registry/query.rs b/src-tauri/src/services/tool/registry/query.rs index 2a4f1a2..c6cf81a 100644 --- a/src-tauri/src/services/tool/registry/query.rs +++ b/src-tauri/src/services/tool/registry/query.rs @@ -56,11 +56,11 @@ impl ToolRegistry { } /// 获取本地工具的轻量级状态(供 Dashboard 使用) - /// 优先从数据库读取,如果数据库为空则执行检测并持久化 + /// 优先从数据库读取,如果某个工具没有 Local 实例则自动检测并持久化 pub async fn get_local_tool_status(&self) -> Result> { tracing::debug!("获取本地工具轻量级状态"); - // 从数据库读取所有实例(不主动检测) + // 从数据库读取所有实例 let grouped = self.get_all_grouped().await?; // 转换为轻量级 ToolStatus @@ -83,22 +83,50 @@ impl ToolRegistry { version: local_instance.version.clone(), }); } else { - // 没有本地实例,返回未安装状态 - statuses.push(crate::models::ToolStatus { - id: tool_id.to_string(), - name: tool_name.to_string(), - installed: false, - version: None, - }); + // 没有本地实例,自动检测并持久化 + tracing::info!("工具 {} 没有 Local 实例,自动检测", tool_id); + match self.detect_and_persist_single_tool(tool_id).await { + Ok(instance) => { + statuses.push(crate::models::ToolStatus { + id: tool_id.to_string(), + name: tool_name.to_string(), + installed: instance.installed, + version: instance.version.clone(), + }); + } + Err(e) => { + tracing::warn!("自动检测工具 {} 失败: {}", tool_id, e); + statuses.push(crate::models::ToolStatus { + id: tool_id.to_string(), + name: tool_name.to_string(), + installed: false, + version: None, + }); + } + } } } else { - // 数据库中没有该工具的任何实例 - statuses.push(crate::models::ToolStatus { - id: tool_id.to_string(), - name: tool_name.to_string(), - installed: false, - version: None, - }); + // 数据库中没有该工具的任何实例,自动检测 + tracing::info!("工具 {} 不在数据库中,自动检测", tool_id); + match self.detect_and_persist_single_tool(tool_id).await { + Ok(instance) => { + statuses.push(crate::models::ToolStatus { + id: tool_id.to_string(), + name: tool_name.to_string(), + installed: instance.installed, + version: instance.version.clone(), + }); + } + Err(e) => { + tracing::warn!("自动检测工具 {} 失败: {}", tool_id, e); + statuses.push(crate::models::ToolStatus { + id: tool_id.to_string(), + name: tool_name.to_string(), + installed: false, + version: None, + }); + } + } } } diff --git a/src-tauri/src/services/tool/registry/version_ops.rs b/src-tauri/src/services/tool/registry/version_ops.rs index 2e80dc0..ff44e8f 100644 --- a/src-tauri/src/services/tool/registry/version_ops.rs +++ b/src-tauri/src/services/tool/registry/version_ops.rs @@ -10,7 +10,11 @@ use anyhow::Result; use std::collections::HashMap; impl ToolRegistry { - /// 更新工具实例(使用配置的安装器) + /// 更新工具实例(智能选择更新方式) + /// + /// # 更新策略 + /// - Npm/Brew: 使用 InstallerService.update_instance_by_installer(基于配置的安装器路径) + /// - Official/Other: 使用 Detector.update 方法(内置更新逻辑) /// /// # 参数 /// - instance_id: 实例ID @@ -30,11 +34,58 @@ impl ToolRegistry { .find(|inst| inst.instance_id == instance_id && inst.tool_type == ToolType::Local) .ok_or_else(|| anyhow::anyhow!("未找到实例: {}", instance_id))?; - // 2. 使用 InstallerService 执行更新 - let installer = InstallerService::new(); - let result = installer - .update_instance_by_installer(instance, force) - .await?; + // 2. 根据安装方法选择更新方式 + let install_method = instance.install_method.clone(); + + let result = match install_method { + Some(InstallMethod::Npm) | Some(InstallMethod::Brew) => { + // Npm/Brew: 使用 InstallerService 执行更新 + let installer = InstallerService::new(); + installer + .update_instance_by_installer(instance, force) + .await? + } + Some(InstallMethod::Official) | Some(InstallMethod::Other) | None => { + // Official/Other/None: 使用 Detector 的 update 方法 + let detector = self + .detector_registry + .get(&instance.base_id) + .ok_or_else(|| anyhow::anyhow!("未找到工具 {} 的检测器", instance.base_id))?; + + tracing::info!( + "使用 Detector 更新工具 {} (安装方式: {:?})", + instance.tool_name, + install_method + ); + + // 执行 Detector 的 update 方法 + detector.update(&self.command_executor, force).await?; + + // 更新成功,获取新版本 + let new_version = if let Some(path) = &instance.install_path { + let version_cmd = format!("{} --version", path); + let version_result = self.command_executor.execute_async(&version_cmd).await; + if version_result.success { + Some(parse_version_string(version_result.stdout.trim())) + } else { + None + } + } else { + detector.get_version(&self.command_executor).await + }; + + UpdateResult { + success: true, + message: "✅ 更新成功!".to_string(), + has_update: false, + current_version: new_version.clone(), + latest_version: new_version, + mirror_version: None, + mirror_is_stale: None, + tool_id: Some(instance.base_id.clone()), + } + } + }; // 3. 如果更新成功,更新数据库中的版本号 if result.success { diff --git a/src-tauri/src/utils/installer_scanner.rs b/src-tauri/src/utils/installer_scanner.rs index 0262bb7..6e8adc8 100644 --- a/src-tauri/src/utils/installer_scanner.rs +++ b/src-tauri/src/utils/installer_scanner.rs @@ -94,12 +94,28 @@ pub fn scan_installer_paths(tool_path: &str) -> Vec { } } - // 5. 排序:同级 npm > 上级 npm > 同级 brew > 上级 brew + // 5. 智能排序:根据工具路径推断最可能的安装方式 + // - 如果工具在 Homebrew 目录下,优先选择 brew + // - 否则优先选择 npm + let is_homebrew_path = tool_path.contains("/opt/homebrew/") + || tool_path.contains("/usr/local/Cellar/") + || tool_path.contains("/home/linuxbrew/"); + candidates.sort_by_key(|c| { - let type_priority = match c.installer_type { - InstallMethod::Npm => 1, - InstallMethod::Brew => 2, - _ => 3, + let type_priority = if is_homebrew_path { + // Homebrew 路径:优先选择 brew + match c.installer_type { + InstallMethod::Brew => 1, + InstallMethod::Npm => 2, + _ => 3, + } + } else { + // 其他路径:优先选择 npm + match c.installer_type { + InstallMethod::Npm => 1, + InstallMethod::Brew => 2, + _ => 3, + } }; (c.level, type_priority) }); diff --git a/src/lib/tauri-commands/tool.ts b/src/lib/tauri-commands/tool.ts index 03c3f44..6f76364 100644 --- a/src/lib/tauri-commands/tool.ts +++ b/src/lib/tauri-commands/tool.ts @@ -95,14 +95,6 @@ export async function updateToolInstance( return await invoke('update_tool_instance', { instanceId, force }); } -/** - * 更新工具(旧版本,已废弃) - * @deprecated 请使用 updateToolInstance - */ -export async function updateTool(tool: string, force?: boolean): Promise { - return await invoke('update_tool', { tool, force }); -} - /** * 获取所有工具实例(按工具ID分组) * @returns 按工具ID分组的实例集合 diff --git a/src/pages/DashboardPage/components/DashboardToolCard.tsx b/src/pages/DashboardPage/components/DashboardToolCard.tsx index 6d6daed..b95bcc7 100644 --- a/src/pages/DashboardPage/components/DashboardToolCard.tsx +++ b/src/pages/DashboardPage/components/DashboardToolCard.tsx @@ -72,18 +72,18 @@ export function DashboardToolCard({ {tool.hasUpdate && ( - + 有更新 )} {isLatest && ( - + 最新版 )} diff --git a/src/pages/DashboardPage/hooks/useDashboard.ts b/src/pages/DashboardPage/hooks/useDashboard.ts index 5783558..12f08ad 100644 --- a/src/pages/DashboardPage/hooks/useDashboard.ts +++ b/src/pages/DashboardPage/hooks/useDashboard.ts @@ -3,7 +3,8 @@ import { checkAllUpdates, checkUpdate, type ToolStatus, - updateTool as updateToolCommand, + updateToolInstance, + getToolInstances, } from '@/lib/tauri-commands'; export function useDashboard(initialTools: ToolStatus[]) { @@ -123,17 +124,62 @@ export function useDashboard(initialTools: ToolStatus[]) { try { setUpdating(toolId); - await updateToolCommand(toolId); + + // 获取工具实例,找到 Local 类型的实例 ID + const instances = await getToolInstances(); + const toolInstances = instances[toolId] || []; + const localInstance = toolInstances.find((inst) => inst.tool_type === 'Local'); + + if (!localInstance) { + return { + success: false, + message: '未找到本地工具实例', + }; + } + + const result = await updateToolInstance(localInstance.instance_id); + + if (result.success) { + // 更新成功后,刷新该工具的状态 + setTools((prevTools) => + prevTools.map((tool) => { + if (tool.id === toolId) { + return { + ...tool, + version: result.latest_version || tool.latestVersion || tool.version, + hasUpdate: false, + latestVersion: result.latest_version || null, + }; + } + return tool; + }), + ); + } return { - success: true, - message: '已更新到最新版本', + success: result.success, + message: result.success ? '已更新到最新版本' : result.message || '更新失败', }; } catch (error) { console.error('Failed to update ' + toolId, error); + // 提取错误信息:处理 Error 对象、字符串和 Tauri 错误对象 + let errorMessage = '更新失败'; + if (error instanceof Error) { + errorMessage = error.message; + } else if (typeof error === 'string') { + errorMessage = error; + } else if (error && typeof error === 'object') { + // Tauri 错误可能是 { message: string } 或其他格式 + const errObj = error as Record; + if (typeof errObj.message === 'string') { + errorMessage = errObj.message; + } else { + errorMessage = JSON.stringify(error); + } + } return { success: false, - message: String(error), + message: errorMessage, }; } finally { setUpdating(null); diff --git a/src/pages/DashboardPage/index.tsx b/src/pages/DashboardPage/index.tsx index fb824fc..9071564 100644 --- a/src/pages/DashboardPage/index.tsx +++ b/src/pages/DashboardPage/index.tsx @@ -166,10 +166,9 @@ export function DashboardPage({ tools: toolsProp, loading: loadingProp }: Dashbo title: '更新成功', description: `${getToolDisplayName(toolId)} ${result.message}`, }); - // 更新成功后重新检测工具状态(而不是仅读数据库) - await handleRefreshToolStatus(); - // 更新成功后自动检测工具更新状态,显示「最新版」标识 - await checkSingleToolUpdate(toolId); + // 更新成功后,handleUpdate 已经设置了 hasUpdate: false + // 不需要再调用 handleRefreshToolStatus 和 checkSingleToolUpdate + // 因为这会导致状态竞态问题 } else { toast({ title: '更新失败', diff --git a/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx b/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx index 01d5f48..b936caf 100644 --- a/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx +++ b/src/pages/ProfileManagementPage/components/ActiveProfileCard.tsx @@ -6,7 +6,7 @@ import { useState, useEffect } from 'react'; import { ChevronDown, ChevronUp, Loader2 } from 'lucide-react'; import type { ProfileGroup } from '@/types/profile'; import type { ToolInstance, ToolType } from '@/types/tool-management'; -import { getToolInstances, checkUpdate, updateTool } from '@/lib/tauri-commands'; +import { getToolInstances, checkUpdate, updateToolInstance } from '@/lib/tauri-commands'; import { useToast } from '@/hooks/use-toast'; import { Select, @@ -136,7 +136,7 @@ export function ActiveProfileCard({ group, proxyRunning }: ActiveProfileCardProp description: `正在更新 ${group.tool_name}...`, }); - const result = await updateTool(group.tool_id); + const result = await updateToolInstance(selectedInstance.instance_id); if (result.success) { setHasUpdate(false);