Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src-tauri/src/services/tool/detectors/claude_code.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 53 additions & 6 deletions src-tauri/src/services/tool/detectors/gemini_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,19 @@ impl ToolDetector for GeminiCLIDetector {
// ==================== 检测逻辑 ====================

async fn detect_install_method(&self, executor: &CommandExecutor) -> Option<InstallMethod> {
// 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"
Expand All @@ -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)
}

// ==================== 安装逻辑 ====================
Expand All @@ -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,
}
}

// ==================== 配置管理 ====================
Expand Down Expand Up @@ -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()
Expand Down
60 changes: 44 additions & 16 deletions src-tauri/src/services/tool/registry/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,11 @@ impl ToolRegistry {
}

/// 获取本地工具的轻量级状态(供 Dashboard 使用)
/// 优先从数据库读取,如果数据库为空则执行检测并持久化
/// 优先从数据库读取,如果某个工具没有 Local 实例则自动检测并持久化
pub async fn get_local_tool_status(&self) -> Result<Vec<crate::models::ToolStatus>> {
tracing::debug!("获取本地工具轻量级状态");

// 从数据库读取所有实例(不主动检测)
// 从数据库读取所有实例
let grouped = self.get_all_grouped().await?;

// 转换为轻量级 ToolStatus
Expand All @@ -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,
});
}
}
}
}

Expand Down
63 changes: 57 additions & 6 deletions src-tauri/src/services/tool/registry/version_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
26 changes: 21 additions & 5 deletions src-tauri/src/utils/installer_scanner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,28 @@ pub fn scan_installer_paths(tool_path: &str) -> Vec<InstallerCandidate> {
}
}

// 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)
});
Expand Down
8 changes: 0 additions & 8 deletions src/lib/tauri-commands/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,6 @@ export async function updateToolInstance(
return await invoke<UpdateResult>('update_tool_instance', { instanceId, force });
}

/**
* 更新工具(旧版本,已废弃)
* @deprecated 请使用 updateToolInstance
*/
export async function updateTool(tool: string, force?: boolean): Promise<UpdateResult> {
return await invoke<UpdateResult>('update_tool', { tool, force });
}

/**
* 获取所有工具实例(按工具ID分组)
* @returns 按工具ID分组的实例集合
Expand Down
8 changes: 4 additions & 4 deletions src/pages/DashboardPage/components/DashboardToolCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,18 @@ export function DashboardToolCard({
{tool.hasUpdate && (
<Badge
variant="secondary"
className="gap-1 bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200"
className="gap-1 whitespace-nowrap bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200"
>
<RefreshCw className="h-3 w-3" />
<RefreshCw className="h-3 w-3 flex-shrink-0" />
有更新
</Badge>
)}
{isLatest && (
<Badge
variant="secondary"
className="gap-1 bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
className="gap-1 whitespace-nowrap bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
>
<CheckCircle2 className="h-3 w-3" />
<CheckCircle2 className="h-3 w-3 flex-shrink-0" />
最新版
</Badge>
)}
Expand Down
Loading