diff --git a/src-tauri/src/services/provider.rs b/src-tauri/src/services/provider.rs index 40cbfb84..4528a685 100644 --- a/src-tauri/src/services/provider.rs +++ b/src-tauri/src/services/provider.rs @@ -1378,6 +1378,14 @@ impl ProviderService { } /// 切换供应商 + /// + /// 切换流程: + /// 1. 验证目标供应商存在 + /// 2. **回填机制**:将当前 live 配置回填到当前供应商,保护用户手动修改 + /// 3. 设置新的当前供应商 + /// 4. 将目标供应商配置写入 live 文件 + /// 5. Gemini 额外处理安全标志 + /// 6. 同步 MCP 配置 pub fn switch(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> { // Check if provider exists let providers = state.db.get_all_providers(app_type.as_str())?; @@ -1385,12 +1393,36 @@ impl ProviderService { .get(id) .ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?; + // Backfill: 将当前 live 配置回填到当前供应商 + if let Some(current_id) = state.db.get_current_provider(app_type.as_str())? { + if current_id != id { + // 只有在切换到不同供应商时才回填 + if let Ok(live_config) = Self::read_live_settings(app_type.clone()) { + if let Some(mut current_provider) = providers.get(¤t_id).cloned() { + current_provider.settings_config = live_config; + // 忽略回填失败,不影响切换流程 + let _ = state.db.save_provider(app_type.as_str(), ¤t_provider); + } + } + } + } + // Set current state.db.set_current_provider(app_type.as_str(), id)?; // Sync to live Self::write_live_snapshot(&app_type, provider)?; + // Gemini 需要额外处理安全标志(PackyCode 或 Google OAuth) + if matches!(app_type, AppType::Gemini) { + let auth_type = Self::detect_gemini_auth_type(provider); + match auth_type { + GeminiAuthType::GoogleOfficial => Self::ensure_google_oauth_security_flag(provider)?, + GeminiAuthType::Packycode => Self::ensure_packycode_security_flag(provider)?, + GeminiAuthType::Generic => {} + } + } + // Sync MCP use crate::services::mcp::McpService; McpService::sync_all_enabled(state)?; diff --git a/src-tauri/tests/provider_commands.rs b/src-tauri/tests/provider_commands.rs index 4588f9b3..dec36c81 100644 --- a/src-tauri/tests/provider_commands.rs +++ b/src-tauri/tests/provider_commands.rs @@ -139,11 +139,11 @@ command = "say" .and_then(|v| v.get("OPENAI_API_KEY")) .and_then(|v| v.as_str()) .unwrap_or(""); - // 注意:v3.7.0+ 的 switch 实现不再 backfill 旧供应商 - // 旧供应商保持其原始配置不变 + // 回填机制:切换前会将 live 配置回填到当前供应商 + // 这保护了用户在 live 文件中的手动修改 assert_eq!( - legacy_auth_value, "stale", - "previous provider should retain its original auth (no backfill in v3.7.0+)" + legacy_auth_value, "legacy-key", + "previous provider should be backfilled with live auth" ); } @@ -251,16 +251,11 @@ fn switch_provider_updates_claude_live_and_state() { let legacy_provider = providers .get("old-provider") .expect("legacy provider still exists"); - // 注意:v3.7.0+ 的 switch 实现不再 backfill 旧供应商 - // 旧供应商保持其原始配置不变 + // 回填机制:切换前会将 live 配置回填到当前供应商 + // 这保护了用户在 live 文件中的手动修改 assert_eq!( - legacy_provider - .settings_config - .get("env") - .and_then(|env| env.get("ANTHROPIC_API_KEY")) - .and_then(|key| key.as_str()), - Some("stale-key"), - "previous provider should retain its original config (no backfill in v3.7.0+)" + legacy_provider.settings_config, legacy_live, + "previous provider should be backfilled with live config" ); let new_provider = providers diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index ac2cc650..22715a9a 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -2,7 +2,7 @@ use serde_json::json; use cc_switch_lib::{ get_claude_settings_path, read_json_file, write_codex_live_atomic, AppError, AppType, - MultiAppConfig, Provider, ProviderMeta, ProviderService, + McpApps, McpServer, MultiAppConfig, Provider, ProviderMeta, ProviderService, }; #[path = "support.rs"] @@ -68,16 +68,27 @@ command = "say" ); } - initial_config.mcp.codex.servers.insert( + // 使用新的统一 MCP 结构(v3.7.0+) + let servers = initial_config.mcp.servers.get_or_insert_with(Default::default); + servers.insert( "echo-server".into(), - json!({ - "id": "echo-server", - "enabled": true, - "server": { + McpServer { + id: "echo-server".into(), + name: "Echo Server".into(), + server: json!({ "type": "stdio", "command": "echo" - } - }), + }), + apps: McpApps { + claude: false, + codex: true, + gemini: false, + }, + description: None, + homepage: None, + docs: None, + tags: Vec::new(), + }, ); let state = create_test_state_with_config(&initial_config).expect("create test state"); @@ -115,9 +126,15 @@ command = "say" .get("config") .and_then(|v| v.as_str()) .unwrap_or_default(); - assert_eq!( - new_config_text, config_text, - "provider config snapshot should match live file" + // provider 存储的是原始配置,不包含 MCP 同步后的内容 + assert!( + new_config_text.contains("mcp_servers.latest"), + "provider config should contain original MCP servers" + ); + // live 文件额外包含同步的 MCP 服务器 + assert!( + config_text.contains("mcp_servers.echo-server"), + "live config should include synced MCP servers" ); let legacy = providers @@ -570,10 +587,8 @@ fn provider_service_delete_claude_removes_provider_files() { !providers.contains_key("delete"), "claude provider should be removed" ); - assert!( - !by_name.exists() && !by_id.exists(), - "provider config files should be deleted" - ); + // v3.7.0+ 不再使用供应商特定文件(如 settings-*.json) + // 删除供应商只影响数据库记录,不清理这些旧格式文件 } #[test]