mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-11 05:46:01 +08:00
fix(provider): validate current provider ID existence before use
Add get_effective_current_provider() to validate local settings ID against database, with automatic cleanup and fallback to DB is_current. This fixes edge cases in multi-device cloud sync scenarios where local settings may contain stale provider IDs: - current(): now returns validated effective provider ID - update(): correctly syncs live config when local ID differs from DB - delete(): checks both local settings and DB to prevent deletion - switch(): backfill logic now targets valid provider - sync_current_to_live(): uses validated ID with auto-fallback - tray menu: displays correct checkmark on startup Also fixes test issues: - Add missing test setup calls (mutex, reset_test_fs, ensure_test_home) - Correct Gemini security settings path to ~/.gemini/settings.json
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String, AppError> {
|
||||
// 优先从本地 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 {
|
||||
|
||||
@@ -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<Option<String>, 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())
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user