Files
cc-switch/src-tauri/src/app_config.rs
YoVinchen 8a05e7bd3d feat(gemini): add Gemini provider integration (#202)
* feat(gemini): add Gemini provider integration

- Add gemini_config.rs module for .env file parsing
- Extend AppType enum to support Gemini
- Implement GeminiConfigEditor and GeminiFormFields components
- Add GeminiIcon with standardized 1024x1024 viewBox
- Add Gemini provider presets configuration
- Update i18n translations for Gemini support
- Extend ProviderService and McpService for Gemini

* fix(gemini): resolve TypeScript errors, add i18n support, and fix MCP logic

**Critical Fixes:**
- Fix TS2741 errors in tests/msw/state.ts by adding missing Gemini type definitions
- Fix ProviderCard.extractApiUrl to support GOOGLE_GEMINI_BASE_URL display
- Add missing apps.gemini i18n keys (zh/en) for proper app name display
- Fix MCP service Gemini cross-app duplication logic to prevent self-copy

**Technical Details:**
- tests/msw/state.ts: Add gemini default providers, current ID, and MCP config
- ProviderCard.tsx: Check both ANTHROPIC_BASE_URL and GOOGLE_GEMINI_BASE_URL
- services/mcp.rs: Skip Gemini in sync_other_side logic with unreachable!() guards
- Run pnpm format to auto-fix code style issues

**Verification:**
-  pnpm typecheck passes
-  pnpm format completed

* feat(gemini): enhance authentication and config parsing

- Add strict and lenient .env parsing modes
- Implement PackyCode partner authentication detection
- Support Google OAuth official authentication
- Auto-configure security.auth.selectedType for PackyCode
- Add comprehensive test coverage for all auth types
- Update i18n for OAuth hints and Gemini config

---------

Co-authored-by: Jason <farion1231@gmail.com>
2025-11-12 10:47:34 +08:00

192 lines
7.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
/// MCP 配置单客户端维度claude 或 codex 下的一组服务器)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct McpConfig {
/// 以 id 为键的服务器定义(宽松 JSON 对象,包含 enabled/source 等 UI 辅助字段)
#[serde(default)]
pub servers: HashMap<String, serde_json::Value>,
}
/// MCP 根:按客户端分开维护(无历史兼容压力,直接以 v2 结构落地)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct McpRoot {
#[serde(default)]
pub claude: McpConfig,
#[serde(default)]
pub codex: McpConfig,
#[serde(default)]
pub gemini: McpConfig, // Gemini MCP 配置(预留)
}
use crate::config::{copy_file, get_app_config_dir, get_app_config_path, write_json_file};
use crate::error::AppError;
use crate::provider::ProviderManager;
/// 应用类型
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AppType {
Claude,
Codex,
Gemini, // 新增
}
impl AppType {
pub fn as_str(&self) -> &str {
match self {
AppType::Claude => "claude",
AppType::Codex => "codex",
AppType::Gemini => "gemini", // 新增
}
}
}
impl FromStr for AppType {
type Err = AppError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let normalized = s.trim().to_lowercase();
match normalized.as_str() {
"claude" => Ok(AppType::Claude),
"codex" => Ok(AppType::Codex),
"gemini" => Ok(AppType::Gemini), // 新增
other => Err(AppError::localized(
"unsupported_app",
format!("不支持的应用标识: '{other}'。可选值: claude, codex, gemini。"),
format!("Unsupported app id: '{other}'. Allowed: claude, codex, gemini."),
)),
}
}
}
/// 多应用配置结构(向后兼容)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MultiAppConfig {
#[serde(default = "default_version")]
pub version: u32,
/// 应用管理器claude/codex
#[serde(flatten)]
pub apps: HashMap<String, ProviderManager>,
/// MCP 配置(按客户端分治)
#[serde(default)]
pub mcp: McpRoot,
}
fn default_version() -> u32 {
2
}
impl Default for MultiAppConfig {
fn default() -> Self {
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()); // 新增
Self {
version: 2,
apps,
mcp: McpRoot::default(),
}
}
}
impl MultiAppConfig {
/// 从文件加载配置(仅支持 v2 结构)
pub fn load() -> Result<Self, AppError> {
let config_path = get_app_config_path();
if !config_path.exists() {
log::info!("配置文件不存在,创建新的多应用配置");
return Ok(Self::default());
}
// 尝试读取文件
let content =
std::fs::read_to_string(&config_path).map_err(|e| AppError::io(&config_path, e))?;
// 先解析为 Value以便严格判定是否为 v1 结构;
// 满足:顶层同时包含 providers(object) + current(string),且不包含 version/apps/mcp 关键键,即视为 v1
let value: serde_json::Value =
serde_json::from_str(&content).map_err(|e| AppError::json(&config_path, e))?;
let is_v1 = value.as_object().is_some_and(|map| {
let has_providers = map.get("providers").map(|v| v.is_object()).unwrap_or(false);
let has_current = map.get("current").map(|v| v.is_string()).unwrap_or(false);
// v1 的充分必要条件:有 providers 和 current且 apps 不存在version/mcp 可能存在但不作为 v2 判据)
let has_apps = map.contains_key("apps");
has_providers && has_current && !has_apps
});
if is_v1 {
return Err(AppError::localized(
"config.unsupported_v1",
"检测到旧版 v1 配置格式。当前版本已不再支持运行时自动迁移。\n\n解决方案:\n1. 安装 v3.2.x 版本进行一次性自动迁移\n2. 或手动编辑 ~/.cc-switch/config.json将顶层结构调整为\n {\"version\": 2, \"claude\": {...}, \"codex\": {...}, \"mcp\": {...}}\n\n",
"Detected legacy v1 config. Runtime auto-migration is no longer supported.\n\nSolutions:\n1. Install v3.2.x for one-time auto-migration\n2. Or manually edit ~/.cc-switch/config.json to adjust the top-level structure:\n {\"version\": 2, \"claude\": {...}, \"codex\": {...}, \"mcp\": {...}}\n\n",
));
}
// 解析 v2 结构
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());
}
Ok(config)
}
/// 保存配置到文件
pub fn save(&self) -> Result<(), AppError> {
let config_path = get_app_config_path();
// 先备份旧版(若存在)到 ~/.cc-switch/config.json.bak再写入新内容
if config_path.exists() {
let backup_path = get_app_config_dir().join("config.json.bak");
if let Err(e) = copy_file(&config_path, &backup_path) {
log::warn!("备份 config.json 到 .bak 失败: {e}");
}
}
write_json_file(&config_path, self)?;
Ok(())
}
/// 获取指定应用的管理器
pub fn get_manager(&self, app: &AppType) -> Option<&ProviderManager> {
self.apps.get(app.as_str())
}
/// 获取指定应用的管理器(可变引用)
pub fn get_manager_mut(&mut self, app: &AppType) -> Option<&mut ProviderManager> {
self.apps.get_mut(app.as_str())
}
/// 确保应用存在
pub fn ensure_app(&mut self, app: &AppType) {
if !self.apps.contains_key(app.as_str()) {
self.apps
.insert(app.as_str().to_string(), ProviderManager::default());
}
}
/// 获取指定客户端的 MCP 配置(不可变引用)
pub fn mcp_for(&self, app: &AppType) -> &McpConfig {
match app {
AppType::Claude => &self.mcp.claude,
AppType::Codex => &self.mcp.codex,
AppType::Gemini => &self.mcp.gemini,
}
}
/// 获取指定客户端的 MCP 配置(可变引用)
pub fn mcp_for_mut(&mut self, app: &AppType) -> &mut McpConfig {
match app {
AppType::Claude => &mut self.mcp.claude,
AppType::Codex => &mut self.mcp.codex,
AppType::Gemini => &mut self.mcp.gemini,
}
}
}