diff --git a/README.md b/README.md index fc158ec..d89c7f7 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ Get 10% OFF the GLM CODING PLAN with [this link](https://z.ai/subscribe?ic=8JVLJ **Core Capabilities** -- **Provider Management**: One-click switching between Claude Code & Codex API configurations +- **Provider Management**: One-click switching between Claude Code, Codex, and Gemini API configurations - **MCP Integration**: Centralized MCP server management with stdio/http support and real-time sync - **Speed Testing**: Measure API endpoint latency with visual quality indicators - **Import/Export**: Backup and restore configs with auto-rotation (keep 10 most recent) @@ -115,8 +115,8 @@ Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{ver 2. **Switch Provider**: - Main UI: Select provider → Click "Enable" - System Tray: Click provider name directly (instant effect) -3. **Takes Effect**: Restart terminal or Claude Code/Codex to apply changes -4. **Back to Official**: Select "Official Login" preset, restart terminal, then use `/login` (Claude) or official login flow (Codex) +3. **Takes Effect**: Restart your terminal or Claude Code / Codex / Gemini clients to apply changes +4. **Back to Official**: Select the "Official Login" preset (Claude/Codex) or "Google Official" preset (Gemini), restart the corresponding client, then follow its login/OAuth flow ### MCP Management @@ -139,6 +139,12 @@ Download the latest `CC-Switch-v{version}-Linux.deb` package or `CC-Switch-v{ver - API key field: `OPENAI_API_KEY` in `auth.json` - MCP servers: `~/.codex/config.toml` → `[mcp.servers]` +**Gemini** + +- Live config: `~/.gemini/.env` (API key) + `~/.gemini/settings.json` (auth type for quick switching) +- API key field: `GEMINI_API_KEY` inside `.env` +- Tray quick switch: each provider switch rewrites `~/.gemini/.env` so the Gemini CLI picks up the new credentials immediately + **CC Switch Storage** - Main config (SSOT): `~/.cc-switch/config.json` diff --git a/README_ZH.md b/README_ZH.md index e718728..7319a45 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -47,7 +47,7 @@ CC Switch 已经预设了智谱GLM,只需要填写 key 即可一键导入编 **核心功能** -- **供应商管理**:一键切换 Claude Code 与 Codex 的 API 配置 +- **供应商管理**:一键切换 Claude Code、Codex 与 Gemini 的 API 配置 - **MCP 集成**:集中管理 MCP 服务器,支持 stdio/http 类型和实时同步 - **速度测试**:测量 API 端点延迟,可视化连接质量指示器 - **导入导出**:备份和恢复配置,自动轮换(保留最近 10 个) @@ -115,8 +115,8 @@ brew upgrade --cask cc-switch 2. **切换供应商**: - 主界面:选择供应商 → 点击"启用" - 系统托盘:直接点击供应商名称(立即生效) -3. **生效方式**:重启终端或 Claude Code/Codex 以应用更改 -4. **恢复官方登录**:选择"官方登录"预设,重启终端后使用 `/login`(Claude)或官方登录流程(Codex) +3. **生效方式**:重启终端或 Claude Code / Codex / Gemini 客户端以应用更改 +4. **恢复官方登录**:选择"官方登录"预设(Claude/Codex)或"Google 官方"预设(Gemini),重启对应客户端后按照其登录/OAuth 流程操作 ### MCP 管理 @@ -139,6 +139,12 @@ brew upgrade --cask cc-switch - API key 字段:`auth.json` 中的 `OPENAI_API_KEY` - MCP 服务器:`~/.codex/config.toml` → `[mcp.servers]` +**Gemini** + +- Live 配置:`~/.gemini/.env`(API Key)+ `~/.gemini/settings.json`(保存认证模式,支持托盘快速切换) +- API key 字段:`.env` 文件中的 `GEMINI_API_KEY` +- 托盘快速切换:每次切换供应商都会重写 `~/.gemini/.env`,Gemini CLI 无需额外操作即可使用新配置 + **CC Switch 存储** - 主配置(SSOT):`~/.cc-switch/config.json` diff --git a/src-tauri/src/app_config.rs b/src-tauri/src/app_config.rs index b6c8c3d..7e657fa 100644 --- a/src-tauri/src/app_config.rs +++ b/src-tauri/src/app_config.rs @@ -18,7 +18,7 @@ pub struct McpRoot { #[serde(default)] pub codex: McpConfig, #[serde(default)] - pub gemini: McpConfig, // Gemini MCP 配置(预留) + pub gemini: McpConfig, // Gemini MCP 配置(预留) } /// Prompt 配置:单客户端维度 @@ -49,7 +49,7 @@ use crate::provider::ProviderManager; pub enum AppType { Claude, Codex, - Gemini, // 新增 + Gemini, // 新增 } impl AppType { @@ -57,7 +57,7 @@ impl AppType { match self { AppType::Claude => "claude", AppType::Codex => "codex", - AppType::Gemini => "gemini", // 新增 + AppType::Gemini => "gemini", // 新增 } } } @@ -70,7 +70,7 @@ impl FromStr for AppType { match normalized.as_str() { "claude" => Ok(AppType::Claude), "codex" => Ok(AppType::Codex), - "gemini" => Ok(AppType::Gemini), // 新增 + "gemini" => Ok(AppType::Gemini), // 新增 other => Err(AppError::localized( "unsupported_app", format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini。"), @@ -105,7 +105,7 @@ impl Default for MultiAppConfig { let mut apps = HashMap::new(); apps.insert("claude".to_string(), ProviderManager::default()); apps.insert("codex".to_string(), ProviderManager::default()); - apps.insert("gemini".to_string(), ProviderManager::default()); // 新增 + apps.insert("gemini".to_string(), ProviderManager::default()); // 新增 Self { version: 2, @@ -150,13 +150,16 @@ impl MultiAppConfig { } // 解析 v2 结构 - let mut config: Self = serde_json::from_value(value).map_err(|e| AppError::json(&config_path, e))?; - + let mut config: Self = + serde_json::from_value(value).map_err(|e| AppError::json(&config_path, e))?; + // 确保 gemini 应用存在(兼容旧配置文件) if !config.apps.contains_key("gemini") { - config.apps.insert("gemini".to_string(), ProviderManager::default()); + config + .apps + .insert("gemini".to_string(), ProviderManager::default()); } - + Ok(config) } diff --git a/src-tauri/src/app_store.rs b/src-tauri/src/app_store.rs index 30fd099..a1333c9 100644 --- a/src-tauri/src/app_store.rs +++ b/src-tauri/src/app_store.rs @@ -56,9 +56,7 @@ fn read_override_from_store(app: &tauri::AppHandle) -> Option { Some(path) } Some(_) => { - log::warn!( - "Store 中的 {STORE_KEY_APP_CONFIG_DIR} 类型不正确,应为字符串" - ); + log::warn!("Store 中的 {STORE_KEY_APP_CONFIG_DIR} 类型不正确,应为字符串"); None } None => None, diff --git a/src-tauri/src/gemini_config.rs b/src-tauri/src/gemini_config.rs index 89f8fde..3d3abd2 100644 --- a/src-tauri/src/gemini_config.rs +++ b/src-tauri/src/gemini_config.rs @@ -149,8 +149,7 @@ pub fn read_gemini_env() -> Result, AppError> { return Ok(HashMap::new()); } - let content = fs::read_to_string(&path) - .map_err(|e| AppError::io(&path, e))?; + let content = fs::read_to_string(&path).map_err(|e| AppError::io(&path, e))?; Ok(parse_env_file(&content)) } @@ -161,8 +160,7 @@ pub fn write_gemini_env_atomic(map: &HashMap) -> Result<(), AppE // 确保目录存在 if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .map_err(|e| AppError::io(parent, e))?; + fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; // 设置目录权限为 700(仅所有者可读写执行) #[cfg(unix)] @@ -172,8 +170,7 @@ pub fn write_gemini_env_atomic(map: &HashMap) -> Result<(), AppE .map_err(|e| AppError::io(parent, e))? .permissions(); perms.set_mode(0o700); - fs::set_permissions(parent, perms) - .map_err(|e| AppError::io(parent, e))?; + fs::set_permissions(parent, perms).map_err(|e| AppError::io(parent, e))?; } } @@ -188,8 +185,7 @@ pub fn write_gemini_env_atomic(map: &HashMap) -> Result<(), AppE .map_err(|e| AppError::io(&path, e))? .permissions(); perms.set_mode(0o600); - fs::set_permissions(&path, perms) - .map_err(|e| AppError::io(&path, e))?; + fs::set_permissions(&path, perms).map_err(|e| AppError::io(&path, e))?; } Ok(()) @@ -243,66 +239,66 @@ pub fn validate_gemini_settings(settings: &Value) -> Result<(), AppError> { } /// 获取 Gemini settings.json 文件路径 -/// +/// /// 返回路径:`~/.gemini/settings.json`(与 `.env` 文件同级) pub fn get_gemini_settings_path() -> PathBuf { get_gemini_dir().join("settings.json") } /// 更新 Gemini 目录 settings.json 中的 security.auth.selectedType 字段 -/// +/// /// 此函数会: /// 1. 读取现有的 settings.json(如果存在) /// 2. 只更新 `security.auth.selectedType` 字段,保留其他所有字段 /// 3. 原子性写入文件 -/// +/// /// # 参数 /// - `selected_type`: 要设置的 selectedType 值(如 "gemini-api-key" 或 "oauth-personal") fn update_selected_type(selected_type: &str) -> Result<(), AppError> { let settings_path = get_gemini_settings_path(); - + // 确保目录存在 if let Some(parent) = settings_path.parent() { - fs::create_dir_all(parent) - .map_err(|e| AppError::io(parent, e))?; + fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; } - + // 读取现有的 settings.json(如果存在) let mut settings_content = if settings_path.exists() { - let content = fs::read_to_string(&settings_path) - .map_err(|e| AppError::io(&settings_path, e))?; - serde_json::from_str::(&content) - .unwrap_or_else(|_| serde_json::json!({})) + let content = + fs::read_to_string(&settings_path).map_err(|e| AppError::io(&settings_path, e))?; + serde_json::from_str::(&content).unwrap_or_else(|_| serde_json::json!({})) } else { serde_json::json!({}) }; - + // 只更新 security.auth.selectedType 字段 if let Some(obj) = settings_content.as_object_mut() { - let security = obj.entry("security") + let security = obj + .entry("security") .or_insert_with(|| serde_json::json!({})); - + if let Some(security_obj) = security.as_object_mut() { - let auth = security_obj.entry("auth") + let auth = security_obj + .entry("auth") .or_insert_with(|| serde_json::json!({})); - + if let Some(auth_obj) = auth.as_object_mut() { auth_obj.insert( "selectedType".to_string(), - Value::String(selected_type.to_string()) + Value::String(selected_type.to_string()), ); } } } - + // 写入文件 crate::config::write_json_file(&settings_path, &settings_content)?; - + Ok(()) } /// 为 Packycode Gemini 供应商写入 settings.json -/// +/// /// 设置 `~/.gemini/settings.json` 中的: /// ```json /// { @@ -313,14 +309,14 @@ fn update_selected_type(selected_type: &str) -> Result<(), AppError> { /// } /// } /// ``` -/// +/// /// 保留文件中的其他所有字段。 pub fn write_packycode_settings() -> Result<(), AppError> { update_selected_type("gemini-api-key") } /// 为 Google 官方 Gemini 供应商写入 settings.json(OAuth 模式) -/// +/// /// 设置 `~/.gemini/settings.json` 中的: /// ```json /// { @@ -331,7 +327,7 @@ pub fn write_packycode_settings() -> Result<(), AppError> { /// } /// } /// ``` -/// +/// /// 保留文件中的其他所有字段。 pub fn write_google_oauth_settings() -> Result<(), AppError> { update_selected_type("oauth-personal") @@ -355,7 +351,10 @@ GEMINI_MODEL=gemini-2.5-pro let map = parse_env_file(content); assert_eq!(map.len(), 3); - assert_eq!(map.get("GOOGLE_GEMINI_BASE_URL"), Some(&"https://example.com".to_string())); + assert_eq!( + map.get("GOOGLE_GEMINI_BASE_URL"), + Some(&"https://example.com".to_string()) + ); assert_eq!(map.get("GEMINI_API_KEY"), Some(&"sk-test123".to_string())); assert_eq!(map.get("GEMINI_MODEL"), Some(&"gemini-2.5-pro".to_string())); } @@ -380,7 +379,10 @@ GEMINI_MODEL=gemini-2.5-pro let json = env_to_json(&env_map); let converted = json_to_env(&json).unwrap(); - assert_eq!(converted.get("GEMINI_API_KEY"), Some(&"test-key".to_string())); + assert_eq!( + converted.get("GEMINI_API_KEY"), + Some(&"test-key".to_string()) + ); } #[test] @@ -400,7 +402,10 @@ GEMINI_MODEL=gemini-2.5-pro let map = result.unwrap(); assert_eq!(map.len(), 3); - assert_eq!(map.get("GOOGLE_GEMINI_BASE_URL"), Some(&"https://example.com".to_string())); + assert_eq!( + map.get("GOOGLE_GEMINI_BASE_URL"), + Some(&"https://example.com".to_string()) + ); assert_eq!(map.get("GEMINI_API_KEY"), Some(&"sk-test123".to_string())); assert_eq!(map.get("GEMINI_MODEL"), Some(&"gemini-2.5-pro".to_string())); } @@ -502,17 +507,19 @@ KEY_WITH-DASH=value"; // 模拟更新 selectedType if let Some(obj) = existing_settings.as_object_mut() { - let security = obj.entry("security") + let security = obj + .entry("security") .or_insert_with(|| serde_json::json!({})); - + if let Some(security_obj) = security.as_object_mut() { - let auth = security_obj.entry("auth") + let auth = security_obj + .entry("auth") .or_insert_with(|| serde_json::json!({})); - + if let Some(auth_obj) = auth.as_object_mut() { auth_obj.insert( "selectedType".to_string(), - Value::String("gemini-api-key".to_string()) + Value::String("gemini-api-key".to_string()), ); } } @@ -521,8 +528,14 @@ KEY_WITH-DASH=value"; // 验证所有字段都被保留 assert_eq!(existing_settings["otherField"], "should-be-kept"); assert_eq!(existing_settings["security"]["otherSetting"], "also-kept"); - assert_eq!(existing_settings["security"]["auth"]["otherAuth"], "preserved"); - assert_eq!(existing_settings["security"]["auth"]["selectedType"], "gemini-api-key"); + assert_eq!( + existing_settings["security"]["auth"]["otherAuth"], + "preserved" + ); + assert_eq!( + existing_settings["security"]["auth"]["selectedType"], + "gemini-api-key" + ); } #[test] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d55cc96..c90dd9f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,7 +6,7 @@ mod codex_config; mod commands; mod config; mod error; -mod gemini_config; // 新增 +mod gemini_config; // 新增 mod init_status; mod mcp; mod prompt; @@ -63,6 +63,129 @@ impl TrayTexts { } } +struct TrayAppSection { + app_type: AppType, + prefix: &'static str, + header_id: &'static str, + empty_id: &'static str, + header_label: &'static str, + log_name: &'static str, +} + +const TRAY_SECTIONS: [TrayAppSection; 3] = [ + TrayAppSection { + app_type: AppType::Claude, + prefix: "claude_", + header_id: "claude_header", + empty_id: "claude_empty", + header_label: "─── Claude ───", + log_name: "Claude", + }, + TrayAppSection { + app_type: AppType::Codex, + prefix: "codex_", + header_id: "codex_header", + empty_id: "codex_empty", + header_label: "─── Codex ───", + log_name: "Codex", + }, + TrayAppSection { + app_type: AppType::Gemini, + prefix: "gemini_", + header_id: "gemini_header", + empty_id: "gemini_empty", + header_label: "─── Gemini ───", + log_name: "Gemini", + }, +]; + +fn append_provider_section<'a>( + app: &'a tauri::AppHandle, + mut menu_builder: MenuBuilder<'a, tauri::Wry, tauri::AppHandle>, + manager: Option<&crate::provider::ProviderManager>, + section: &TrayAppSection, + tray_texts: &TrayTexts, +) -> Result>, AppError> { + let Some(manager) = manager else { + return Ok(menu_builder); + }; + + let header = MenuItem::with_id( + app, + section.header_id, + section.header_label, + false, + None::<&str>, + ) + .map_err(|e| AppError::Message(format!("创建{}标题失败: {e}", section.log_name)))?; + menu_builder = menu_builder.item(&header); + + if manager.providers.is_empty() { + let empty_hint = MenuItem::with_id( + app, + section.empty_id, + tray_texts.no_provider_hint, + false, + None::<&str>, + ) + .map_err(|e| AppError::Message(format!("创建{}空提示失败: {e}", section.log_name)))?; + return Ok(menu_builder.item(&empty_hint)); + } + + let mut sorted_providers: Vec<_> = manager.providers.iter().collect(); + sorted_providers.sort_by(|(_, a), (_, b)| { + match (a.sort_index, b.sort_index) { + (Some(idx_a), Some(idx_b)) => return idx_a.cmp(&idx_b), + (Some(_), None) => return std::cmp::Ordering::Less, + (None, Some(_)) => return std::cmp::Ordering::Greater, + _ => {} + } + + match (a.created_at, b.created_at) { + (Some(time_a), Some(time_b)) => return time_a.cmp(&time_b), + (Some(_), None) => return std::cmp::Ordering::Greater, + (None, Some(_)) => return std::cmp::Ordering::Less, + _ => {} + } + + a.name.cmp(&b.name) + }); + + for (id, provider) in sorted_providers { + let is_current = manager.current == *id; + let item = CheckMenuItem::with_id( + app, + format!("{}{}", section.prefix, id), + &provider.name, + true, + is_current, + None::<&str>, + ) + .map_err(|e| AppError::Message(format!("创建{}菜单项失败: {e}", section.log_name)))?; + menu_builder = menu_builder.item(&item); + } + + Ok(menu_builder) +} + +fn handle_provider_tray_event(app: &tauri::AppHandle, event_id: &str) -> bool { + for section in TRAY_SECTIONS.iter() { + if let Some(provider_id) = event_id.strip_prefix(section.prefix) { + log::info!("切换到{}供应商: {provider_id}", section.log_name); + let app_handle = app.clone(); + let provider_id = provider_id.to_string(); + let app_type = section.app_type.clone(); + tauri::async_runtime::spawn_blocking(move || { + if let Err(e) = switch_provider_internal(&app_handle, app_type, provider_id) { + log::error!("切换{}供应商失败: {e}", section.log_name); + } + }); + return true; + } + } + false +} + /// 创建动态托盘菜单 fn create_tray_menu( app: &tauri::AppHandle, @@ -82,116 +205,14 @@ fn create_tray_menu( menu_builder = menu_builder.item(&show_main_item).separator(); // 直接添加所有供应商到主菜单(扁平化结构,更简单可靠) - if let Some(claude_manager) = config.get_manager(&crate::app_config::AppType::Claude) { - // 添加Claude标题(禁用状态,仅作为分组标识) - let claude_header = - MenuItem::with_id(app, "claude_header", "─── Claude ───", false, None::<&str>) - .map_err(|e| AppError::Message(format!("创建Claude标题失败: {e}")))?; - menu_builder = menu_builder.item(&claude_header); - - if !claude_manager.providers.is_empty() { - // Sort providers by sortIndex, then by createdAt, then by name - let mut sorted_providers: Vec<_> = claude_manager.providers.iter().collect(); - sorted_providers.sort_by(|(_, a), (_, b)| { - // Priority 1: sortIndex - match (a.sort_index, b.sort_index) { - (Some(idx_a), Some(idx_b)) => return idx_a.cmp(&idx_b), - (Some(_), None) => return std::cmp::Ordering::Less, - (None, Some(_)) => return std::cmp::Ordering::Greater, - _ => {} - } - // Priority 2: createdAt - match (a.created_at, b.created_at) { - (Some(time_a), Some(time_b)) => return time_a.cmp(&time_b), - (Some(_), None) => return std::cmp::Ordering::Greater, - (None, Some(_)) => return std::cmp::Ordering::Less, - _ => {} - } - // Priority 3: name - a.name.cmp(&b.name) - }); - - for (id, provider) in sorted_providers { - let is_current = claude_manager.current == *id; - let item = CheckMenuItem::with_id( - app, - format!("claude_{id}"), - &provider.name, - true, - is_current, - None::<&str>, - ) - .map_err(|e| AppError::Message(format!("创建菜单项失败: {e}")))?; - menu_builder = menu_builder.item(&item); - } - } else { - // 没有供应商时显示提示 - let empty_hint = MenuItem::with_id( - app, - "claude_empty", - tray_texts.no_provider_hint, - false, - None::<&str>, - ) - .map_err(|e| AppError::Message(format!("创建Claude空提示失败: {e}")))?; - menu_builder = menu_builder.item(&empty_hint); - } - } - - if let Some(codex_manager) = config.get_manager(&crate::app_config::AppType::Codex) { - // 添加Codex标题(禁用状态,仅作为分组标识) - let codex_header = - MenuItem::with_id(app, "codex_header", "─── Codex ───", false, None::<&str>) - .map_err(|e| AppError::Message(format!("创建Codex标题失败: {e}")))?; - menu_builder = menu_builder.item(&codex_header); - - if !codex_manager.providers.is_empty() { - // Sort providers by sortIndex, then by createdAt, then by name - let mut sorted_providers: Vec<_> = codex_manager.providers.iter().collect(); - sorted_providers.sort_by(|(_, a), (_, b)| { - // Priority 1: sortIndex - match (a.sort_index, b.sort_index) { - (Some(idx_a), Some(idx_b)) => return idx_a.cmp(&idx_b), - (Some(_), None) => return std::cmp::Ordering::Less, - (None, Some(_)) => return std::cmp::Ordering::Greater, - _ => {} - } - // Priority 2: createdAt - match (a.created_at, b.created_at) { - (Some(time_a), Some(time_b)) => return time_a.cmp(&time_b), - (Some(_), None) => return std::cmp::Ordering::Greater, - (None, Some(_)) => return std::cmp::Ordering::Less, - _ => {} - } - // Priority 3: name - a.name.cmp(&b.name) - }); - - for (id, provider) in sorted_providers { - let is_current = codex_manager.current == *id; - let item = CheckMenuItem::with_id( - app, - format!("codex_{id}"), - &provider.name, - true, - is_current, - None::<&str>, - ) - .map_err(|e| AppError::Message(format!("创建菜单项失败: {e}")))?; - menu_builder = menu_builder.item(&item); - } - } else { - // 没有供应商时显示提示 - let empty_hint = MenuItem::with_id( - app, - "codex_empty", - tray_texts.no_provider_hint, - false, - None::<&str>, - ) - .map_err(|e| AppError::Message(format!("创建Codex空提示失败: {e}")))?; - menu_builder = menu_builder.item(&empty_hint); - } + for section in TRAY_SECTIONS.iter() { + menu_builder = append_provider_section( + app, + menu_builder, + config.get_manager(§ion.app_type), + section, + &tray_texts, + )?; } // 分隔符和退出菜单 @@ -246,47 +267,10 @@ fn handle_tray_menu_event(app: &tauri::AppHandle, event_id: &str) { log::info!("退出应用"); app.exit(0); } - id if id.starts_with("claude_") => { - let Some(provider_id) = id.strip_prefix("claude_") else { - log::error!("无效的 Claude 菜单项 ID: {id}"); - return; - }; - log::info!("切换到Claude供应商: {provider_id}"); - - // 执行切换 - let app_handle = app.clone(); - let provider_id = provider_id.to_string(); - tauri::async_runtime::spawn_blocking(move || { - if let Err(e) = switch_provider_internal( - &app_handle, - crate::app_config::AppType::Claude, - provider_id, - ) { - log::error!("切换Claude供应商失败: {e}"); - } - }); - } - id if id.starts_with("codex_") => { - let Some(provider_id) = id.strip_prefix("codex_") else { - log::error!("无效的 Codex 菜单项 ID: {id}"); - return; - }; - log::info!("切换到Codex供应商: {provider_id}"); - - // 执行切换 - let app_handle = app.clone(); - let provider_id = provider_id.to_string(); - tauri::async_runtime::spawn_blocking(move || { - if let Err(e) = switch_provider_internal( - &app_handle, - crate::app_config::AppType::Codex, - provider_id, - ) { - log::error!("切换Codex供应商失败: {e}"); - } - }); - } _ => { + if handle_provider_tray_event(app, event_id) { + return; + } log::warn!("未处理的菜单事件: {event_id}"); } } diff --git a/src-tauri/src/mcp.rs b/src-tauri/src/mcp.rs index 45c7701..80710b2 100644 --- a/src-tauri/src/mcp.rs +++ b/src-tauri/src/mcp.rs @@ -136,9 +136,7 @@ fn normalize_server_keys(map: &mut HashMap) -> usize { continue; } if map.contains_key(&new_key) { - log::warn!( - "MCP 条目 '{old_key}' 的内部 id '{new_key}' 与现有键冲突,回退为原键" - ); + log::warn!("MCP 条目 '{old_key}' 的内部 id '{new_key}' 与现有键冲突,回退为原键"); if let Some(value) = map.get_mut(&old_key) { if let Some(obj) = value.as_object_mut() { if obj diff --git a/src-tauri/src/services/config.rs b/src-tauri/src/services/config.rs index 2b62a76..6f8f91a 100644 --- a/src-tauri/src/services/config.rs +++ b/src-tauri/src/services/config.rs @@ -173,9 +173,7 @@ impl ConfigService { AppError::Config(format!("供应商 {provider_id} 的 Codex 配置必须是对象")) })?; let auth = settings.get("auth").ok_or_else(|| { - AppError::Config(format!( - "供应商 {provider_id} 的 Codex 配置缺少 auth 字段" - )) + AppError::Config(format!("供应商 {provider_id} 的 Codex 配置缺少 auth 字段")) })?; if !auth.is_object() { return Err(AppError::Config(format!( @@ -231,7 +229,9 @@ impl ConfigService { provider_id: &str, provider: &Provider, ) -> Result<(), AppError> { - use crate::gemini_config::{json_to_env, write_gemini_env_atomic, read_gemini_env, env_to_json}; + use crate::gemini_config::{ + env_to_json, json_to_env, read_gemini_env, write_gemini_env_atomic, + }; let env_path = crate::gemini_config::get_gemini_env_path(); if let Some(parent) = env_path.parent() { @@ -265,7 +265,7 @@ impl ConfigService { // 读回实际写入的内容并更新到配置中 let live_after_env = read_gemini_env()?; let live_after = env_to_json(&live_after_env); - + if let Some(manager) = config.get_manager_mut(&AppType::Gemini) { if let Some(target) = manager.providers.get_mut(provider_id) { target.settings_config = live_after; diff --git a/src-tauri/src/services/mcp.rs b/src-tauri/src/services/mcp.rs index 2f1621f..d9bce9d 100644 --- a/src-tauri/src/services/mcp.rs +++ b/src-tauri/src/services/mcp.rs @@ -121,7 +121,7 @@ impl McpService { match app { AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?, AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?, - AppType::Gemini => {}, // Gemini 暂不支持 MCP 同步 + AppType::Gemini => {} // Gemini 暂不支持 MCP 同步 } } } @@ -148,7 +148,7 @@ impl McpService { match app { AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?, AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?, - AppType::Gemini => {}, // Gemini 暂不支持 MCP 同步 + AppType::Gemini => {} // Gemini 暂不支持 MCP 同步 } } } @@ -168,7 +168,7 @@ impl McpService { match app { AppType::Claude => mcp::sync_enabled_to_claude(&snapshot)?, AppType::Codex => mcp::sync_enabled_to_codex(&snapshot)?, - AppType::Gemini => {}, // Gemini 暂不支持 MCP 同步 + AppType::Gemini => {} // Gemini 暂不支持 MCP 同步 } Ok(()) } diff --git a/src-tauri/src/services/provider.rs b/src-tauri/src/services/provider.rs index 2cc0de9..803d6b2 100644 --- a/src-tauri/src/services/provider.rs +++ b/src-tauri/src/services/provider.rs @@ -30,7 +30,7 @@ enum LiveSnapshot { config: Option, }, Gemini { - env: Option>, // 新增 + env: Option>, // 新增 }, } @@ -69,7 +69,8 @@ impl LiveSnapshot { delete_file(&config_path)?; } } - LiveSnapshot::Gemini { env } => { // 新增 + LiveSnapshot::Gemini { env } => { + // 新增 use crate::gemini_config::{get_gemini_env_path, write_gemini_env_atomic}; let path = get_gemini_env_path(); if let Some(env_map) = env { @@ -348,11 +349,11 @@ impl ProviderService { // 写入应用级别的 settings.json (~/.cc-switch/settings.json) settings::ensure_security_auth_selected_type(Self::PACKYCODE_SECURITY_SELECTED_TYPE)?; - + // 写入 Gemini 目录的 settings.json (~/.gemini/settings.json) use crate::gemini_config::write_packycode_settings; write_packycode_settings()?; - + Ok(()) } @@ -394,11 +395,11 @@ impl ProviderService { // 写入应用级别的 settings.json (~/.cc-switch/settings.json) settings::ensure_security_auth_selected_type(Self::GOOGLE_OAUTH_SECURITY_SELECTED_TYPE)?; - + // 写入 Gemini 目录的 settings.json (~/.gemini/settings.json) use crate::gemini_config::write_google_oauth_settings; write_google_oauth_settings()?; - + Ok(()) } @@ -502,9 +503,7 @@ impl ProviderService { return Err(AppError::localized( "config.save.rollback_failed", format!("保存配置失败: {save_err};回滚失败: {rollback_err}"), - format!( - "Failed to save config: {save_err}; rollback failed: {rollback_err}" - ), + format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"), )); } return Err(save_err); @@ -518,9 +517,7 @@ impl ProviderService { return Err(AppError::localized( "post_commit.rollback_failed", format!("后置操作失败: {err};回滚失败: {rollback_err}"), - format!( - "Post-commit step failed: {err}; rollback failed: {rollback_err}" - ), + format!("Post-commit step failed: {err}; rollback failed: {rollback_err}"), )); } return Err(err); @@ -618,8 +615,8 @@ impl ProviderService { state.save()?; } AppType::Gemini => { - use crate::gemini_config::{get_gemini_env_path, read_gemini_env, env_to_json}; - + use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; + let env_path = get_gemini_env_path(); if !env_path.exists() { return Err(AppError::localized( @@ -630,7 +627,7 @@ impl ProviderService { } let env_map = read_gemini_env()?; let live_after = env_to_json(&env_map); - + { let mut guard = state.config.write().map_err(AppError::from)?; if let Some(manager) = guard.get_manager_mut(app_type) { @@ -674,7 +671,8 @@ impl ProviderService { }; Ok(LiveSnapshot::Codex { auth, config }) } - AppType::Gemini => { // 新增 + AppType::Gemini => { + // 新增 use crate::gemini_config::{get_gemini_env_path, read_gemini_env}; let path = get_gemini_env_path(); let env = if path.exists() { @@ -851,9 +849,10 @@ impl ProviderService { let _ = Self::normalize_claude_models_in_value(&mut v); v } - AppType::Gemini => { // 新增 - use crate::gemini_config::{get_gemini_env_path, read_gemini_env, env_to_json}; - + AppType::Gemini => { + // 新增 + use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; + let path = get_gemini_env_path(); if !path.exists() { return Err(AppError::localized( @@ -917,9 +916,10 @@ impl ProviderService { } read_json_file(&path) } - AppType::Gemini => { // 新增 - use crate::gemini_config::{get_gemini_env_path, read_gemini_env, env_to_json}; - + AppType::Gemini => { + // 新增 + use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; + let path = get_gemini_env_path(); if !path.exists() { return Err(AppError::localized( @@ -928,7 +928,7 @@ impl ProviderService { "Gemini .env file not found", )); } - + let env_map = read_gemini_env()?; Ok(env_to_json(&env_map)) } @@ -1429,8 +1429,8 @@ impl ProviderService { config: &mut MultiAppConfig, next_provider: &str, ) -> Result<(), AppError> { - use crate::gemini_config::{get_gemini_env_path, read_gemini_env, env_to_json}; - + use crate::gemini_config::{env_to_json, get_gemini_env_path, read_gemini_env}; + let env_path = get_gemini_env_path(); if !env_path.exists() { return Ok(()); @@ -1464,7 +1464,9 @@ impl ProviderService { } fn write_gemini_live(provider: &Provider) -> Result<(), AppError> { - use crate::gemini_config::{json_to_env, validate_gemini_settings, write_gemini_env_atomic}; + use crate::gemini_config::{ + json_to_env, validate_gemini_settings, write_gemini_env_atomic, + }; // 一次性检测认证类型,避免重复检测 let auth_type = Self::detect_gemini_auth_type(provider); @@ -1498,7 +1500,7 @@ impl ProviderService { match app_type { AppType::Codex => Self::write_codex_live(provider), AppType::Claude => Self::write_claude_live(provider), - AppType::Gemini => Self::write_gemini_live(provider), // 新增 + AppType::Gemini => Self::write_gemini_live(provider), // 新增 } } @@ -1553,7 +1555,8 @@ impl ProviderService { } } } - AppType::Gemini => { // 新增 + AppType::Gemini => { + // 新增 use crate::gemini_config::validate_gemini_settings; validate_gemini_settings(&provider.settings_config)? } @@ -1667,25 +1670,25 @@ impl ProviderService { Ok((api_key, base_url)) } - AppType::Gemini => { // 新增 + AppType::Gemini => { + // 新增 use crate::gemini_config::json_to_env; - + let env_map = json_to_env(&provider.settings_config)?; - - let api_key = env_map - .get("GEMINI_API_KEY") - .cloned() - .ok_or_else(|| AppError::localized( + + let api_key = env_map.get("GEMINI_API_KEY").cloned().ok_or_else(|| { + AppError::localized( "gemini.missing_api_key", "缺少 GEMINI_API_KEY", "Missing GEMINI_API_KEY", - ))?; - + ) + })?; + let base_url = env_map .get("GOOGLE_GEMINI_BASE_URL") .cloned() .unwrap_or_else(|| "https://generativelanguage.googleapis.com".to_string()); - + Ok((api_key, base_url)) } } diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index 5bc349e..99e363f 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -220,9 +220,7 @@ fn packycode_partner_meta_triggers_security_flag_even_without_keywords() { partner_promotion_key: Some("packycode".to_string()), ..ProviderMeta::default() }); - manager - .providers - .insert("packy-meta".to_string(), provider); + manager.providers.insert("packy-meta".to_string(), provider); } let state = AppState {