diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 841c5baf..ddf6055b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -221,16 +221,12 @@ fn create_tray_menu( let app_type_str = section.app_type.as_str(); let providers = app_state.db.get_all_providers(app_type_str)?; - // 优先从本地 settings 读取当前供应商,fallback 到数据库 - let current_id = crate::settings::get_current_provider(§ion.app_type) - .or_else(|| { - app_state - .db - .get_current_provider(app_type_str) - .ok() - .flatten() - }) - .unwrap_or_default(); + // 使用有效的当前供应商 ID(验证存在性,自动清理失效 ID) + let current_id = crate::settings::get_effective_current_provider( + &app_state.db, + §ion.app_type, + )? + .unwrap_or_default(); let manager = crate::provider::ProviderManager { providers, diff --git a/src-tauri/src/services/provider/live.rs b/src-tauri/src/services/provider/live.rs index 529e5e13..2450f764 100644 --- a/src-tauri/src/services/provider/live.rs +++ b/src-tauri/src/services/provider/live.rs @@ -126,36 +126,24 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re /// Sync current provider to live configuration /// -/// 从本地 settings 获取当前供应商 ID,fallback 到数据库的 is_current 字段。 -/// 这确保了云同步场景下多设备可以独立选择供应商。 +/// 使用有效的当前供应商 ID(验证过存在性)。 +/// 优先从本地 settings 读取,验证后 fallback 到数据库的 is_current 字段。 +/// 这确保了配置导入后无效 ID 会自动 fallback 到数据库。 pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> { for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] { - // Priority: local settings > database is_current - let current_id = match crate::settings::get_current_provider(&app_type) { + // Use validated effective current provider + let current_id = match crate::settings::get_effective_current_provider(&state.db, &app_type)? + { Some(id) => id, - None => { - // Fallback: get from database and update local settings - match state.db.get_current_provider(app_type.as_str())? { - Some(id) => { - // Update local settings for future use - let _ = crate::settings::set_current_provider(&app_type, Some(&id)); - id - } - None => continue, - } - } + None => continue, }; let providers = state.db.get_all_providers(app_type.as_str())?; if let Some(provider) = providers.get(¤t_id) { write_live_snapshot(&app_type, provider)?; - } else { - log::warn!( - "无法同步 live 配置: 当前供应商 {} ({}) 未找到", - current_id, - app_type.as_str() - ); } + // Note: get_effective_current_provider already validates existence, + // so providers.get() should always succeed here } // MCP sync diff --git a/src-tauri/src/services/provider/mod.rs b/src-tauri/src/services/provider/mod.rs index 5900bd4f..67ca16e6 100644 --- a/src-tauri/src/services/provider/mod.rs +++ b/src-tauri/src/services/provider/mod.rs @@ -95,17 +95,11 @@ impl ProviderService { /// Get current provider ID /// - /// 优先从本地 settings 读取当前供应商,fallback 到数据库的 is_current 字段。 - /// 这确保了云同步场景下多设备可以独立选择供应商。 + /// 使用有效的当前供应商 ID(验证过存在性)。 + /// 优先从本地 settings 读取,验证后 fallback 到数据库的 is_current 字段。 + /// 这确保了云同步场景下多设备可以独立选择供应商,且返回的 ID 一定有效。 pub fn current(state: &AppState, app_type: AppType) -> Result { - // 优先从本地 settings 读取 - if let Some(id) = crate::settings::get_current_provider(&app_type) { - return Ok(id); - } - // Fallback 到数据库的默认供应商 - state - .db - .get_current_provider(app_type.as_str()) + crate::settings::get_effective_current_provider(&state.db, &app_type) .map(|opt| opt.unwrap_or_default()) } @@ -143,9 +137,10 @@ impl ProviderService { Self::normalize_provider_if_claude(&app_type, &mut provider); Self::validate_provider_settings(&app_type, &provider)?; - // Check if this is current provider - let current_id = state.db.get_current_provider(app_type.as_str())?; - let is_current = current_id.as_deref() == Some(provider.id.as_str()); + // Check if this is current provider (use effective current, not just DB) + let effective_current = + crate::settings::get_effective_current_provider(&state.db, &app_type)?; + let is_current = effective_current.as_deref() == Some(provider.id.as_str()); // Save to database state.db.save_provider(app_type.as_str(), &provider)?; @@ -160,13 +155,19 @@ impl ProviderService { } /// Delete a provider + /// + /// 同时检查本地 settings 和数据库的当前供应商,防止删除任一端正在使用的供应商。 pub fn delete(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> { - let current = state.db.get_current_provider(app_type.as_str())?; - if current.as_deref() == Some(id) { + // Check both local settings and database + let local_current = crate::settings::get_current_provider(&app_type); + let db_current = state.db.get_current_provider(app_type.as_str())?; + + if local_current.as_deref() == Some(id) || db_current.as_deref() == Some(id) { return Err(AppError::Message( "无法删除当前正在使用的供应商".to_string(), )); } + state.db.delete_provider(app_type.as_str(), id) } @@ -187,9 +188,8 @@ impl ProviderService { .ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?; // Backfill: Backfill current live config to current provider - // Use local settings first, fallback to database - let current_id = crate::settings::get_current_provider(&app_type) - .or_else(|| state.db.get_current_provider(app_type.as_str()).ok().flatten()); + // Use effective current provider (validated existence) to ensure backfill targets valid provider + let current_id = crate::settings::get_effective_current_provider(&state.db, &app_type)?; if let Some(current_id) = current_id { if current_id != id { diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index ddb47fa6..b078d83a 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -260,3 +260,38 @@ pub fn set_current_provider(app_type: &AppType, id: Option<&str>) -> Result<(), update_settings(settings) } + +/// 获取有效的当前供应商 ID(验证存在性) +/// +/// 逻辑: +/// 1. 从本地 settings 读取当前供应商 ID +/// 2. 验证该 ID 在数据库中存在 +/// 3. 如果不存在则清理本地 settings,fallback 到数据库的 is_current +/// +/// 这确保了返回的 ID 一定是有效的(在数据库中存在)。 +/// 多设备云同步场景下,配置导入后本地 ID 可能失效,此函数会自动修复。 +pub fn get_effective_current_provider( + db: &crate::database::Database, + app_type: &AppType, +) -> Result, AppError> { + // 1. 从本地 settings 读取 + if let Some(local_id) = get_current_provider(app_type) { + // 2. 验证该 ID 在数据库中存在 + let providers = db.get_all_providers(app_type.as_str())?; + if providers.contains_key(&local_id) { + // 存在,直接返回 + return Ok(Some(local_id)); + } + + // 3. 不存在,清理本地 settings + log::warn!( + "本地 settings 中的供应商 {} ({}) 在数据库中不存在,将清理并 fallback 到数据库", + local_id, + app_type.as_str() + ); + let _ = set_current_provider(app_type, None); + } + + // Fallback 到数据库的 is_current + db.get_current_provider(app_type.as_str()) +} diff --git a/src-tauri/tests/provider_service.rs b/src-tauri/tests/provider_service.rs index 22715a9a..74534e17 100644 --- a/src-tauri/tests/provider_service.rs +++ b/src-tauri/tests/provider_service.rs @@ -185,15 +185,16 @@ fn switch_packycode_gemini_updates_security_selected_type() { ProviderService::switch(&state, AppType::Gemini, "packy-gemini") .expect("switching to PackyCode Gemini should succeed"); - let settings_path = home.join(".cc-switch").join("settings.json"); + // Gemini security settings are written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json + let settings_path = home.join(".gemini").join("settings.json"); assert!( settings_path.exists(), - "settings.json should exist at {}", + "Gemini settings.json should exist at {}", settings_path.display() ); - let raw = std::fs::read_to_string(&settings_path).expect("read settings.json"); + let raw = std::fs::read_to_string(&settings_path).expect("read gemini settings.json"); let value: serde_json::Value = - serde_json::from_str(&raw).expect("parse settings.json after switch"); + serde_json::from_str(&raw).expect("parse gemini settings.json after switch"); assert_eq!( value @@ -239,15 +240,16 @@ fn packycode_partner_meta_triggers_security_flag_even_without_keywords() { ProviderService::switch(&state, AppType::Gemini, "packy-meta") .expect("switching to partner meta provider should succeed"); - let settings_path = home.join(".cc-switch").join("settings.json"); + // Gemini security settings are written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json + let settings_path = home.join(".gemini").join("settings.json"); assert!( settings_path.exists(), - "settings.json should exist at {}", + "Gemini settings.json should exist at {}", settings_path.display() ); - let raw = std::fs::read_to_string(&settings_path).expect("read settings.json"); + let raw = std::fs::read_to_string(&settings_path).expect("read gemini settings.json"); let value: serde_json::Value = - serde_json::from_str(&raw).expect("parse settings.json after switch"); + serde_json::from_str(&raw).expect("parse gemini settings.json after switch"); assert_eq!( value @@ -292,23 +294,7 @@ fn switch_google_official_gemini_sets_oauth_security() { ProviderService::switch(&state, AppType::Gemini, "google-official") .expect("switching to Google official Gemini should succeed"); - let settings_path = home.join(".cc-switch").join("settings.json"); - assert!( - settings_path.exists(), - "settings.json should exist at {}", - settings_path.display() - ); - - let raw = std::fs::read_to_string(&settings_path).expect("read settings.json"); - let value: serde_json::Value = serde_json::from_str(&raw).expect("parse settings.json"); - assert_eq!( - value - .pointer("/security/auth/selectedType") - .and_then(|v| v.as_str()), - Some("oauth-personal"), - "Google official Gemini should set oauth-personal selectedType in app settings" - ); - + // Gemini security settings are written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json let gemini_settings = home.join(".gemini").join("settings.json"); assert!( gemini_settings.exists(), @@ -324,7 +310,7 @@ fn switch_google_official_gemini_sets_oauth_security() { .pointer("/security/auth/selectedType") .and_then(|v| v.as_str()), Some("oauth-personal"), - "Gemini settings json should also reflect oauth-personal" + "Gemini settings json should reflect oauth-personal for Google Official" ); } @@ -593,6 +579,10 @@ fn provider_service_delete_claude_removes_provider_files() { #[test] fn provider_service_delete_current_provider_returns_error() { + let _guard = test_mutex().lock().expect("acquire test mutex"); + reset_test_fs(); + let _home = ensure_test_home(); + let mut config = MultiAppConfig::default(); { let manager = config