mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-05-06 19:26:48 +08:00
refactor(provider): use local settings for current provider selection
Complete the device-level settings separation for cloud sync support. Backend changes: - Modify switch() to update both local settings and database is_current - Modify current() to read from local settings first, fallback to database - Rename sync_current_from_db() to sync_current_to_live() - Update tray menu to read current provider from local settings Frontend changes: - Update Settings interface: remove legacy fields (customEndpoints*, security) - Add currentProviderClaude/Codex/Gemini fields - Update settings schema accordingly Test fixes: - Update Gemini security tests to check ~/.gemini/settings.json instead of ~/.cc-switch/settings.json (security field was never stored in CC Switch settings) This ensures each device maintains its own current provider selection independently when database is synced across devices.
This commit is contained in:
@@ -44,7 +44,7 @@ pub async fn import_config_from_file(
|
||||
|
||||
// 导入后同步当前供应商到各自的 live 配置
|
||||
let app_state = AppState::new(db_for_state);
|
||||
if let Err(err) = ProviderService::sync_current_from_db(&app_state) {
|
||||
if let Err(err) = ProviderService::sync_current_to_live(&app_state) {
|
||||
log::warn!("导入后同步 live 配置失败: {err}");
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ pub async fn sync_current_providers_live(state: State<'_, AppState>) -> Result<V
|
||||
let db = state.db.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let app_state = AppState::new(db);
|
||||
ProviderService::sync_current_from_db(&app_state)?;
|
||||
ProviderService::sync_current_to_live(&app_state)?;
|
||||
Ok::<_, AppError>(json!({
|
||||
"success": true,
|
||||
"message": "Live configuration synchronized"
|
||||
|
||||
+10
-3
@@ -220,9 +220,16 @@ fn create_tray_menu(
|
||||
for section in TRAY_SECTIONS.iter() {
|
||||
let app_type_str = section.app_type.as_str();
|
||||
let providers = app_state.db.get_all_providers(app_type_str)?;
|
||||
let current_id = app_state
|
||||
.db
|
||||
.get_current_provider(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();
|
||||
|
||||
let manager = crate::provider::ProviderManager {
|
||||
|
||||
@@ -124,13 +124,28 @@ pub(crate) fn write_live_snapshot(app_type: &AppType, provider: &Provider) -> Re
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync current provider from database to live configuration
|
||||
pub fn sync_current_from_db(state: &AppState) -> Result<(), AppError> {
|
||||
/// Sync current provider to live configuration
|
||||
///
|
||||
/// 从本地 settings 获取当前供应商 ID,fallback 到数据库的 is_current 字段。
|
||||
/// 这确保了云同步场景下多设备可以独立选择供应商。
|
||||
pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
||||
for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
||||
let current_id = match state.db.get_current_provider(app_type.as_str())? {
|
||||
// Priority: local settings > database is_current
|
||||
let current_id = match crate::settings::get_current_provider(&app_type) {
|
||||
Some(id) => id,
|
||||
None => continue,
|
||||
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,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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)?;
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::settings::CustomEndpoint;
|
||||
use crate::store::AppState;
|
||||
|
||||
// Re-export sub-module functions for external access
|
||||
pub use live::{import_default_config, read_live_settings, sync_current_from_db};
|
||||
pub use live::{import_default_config, read_live_settings, sync_current_to_live};
|
||||
|
||||
// Internal re-exports (pub(crate))
|
||||
pub(crate) use live::write_live_snapshot;
|
||||
@@ -94,7 +94,15 @@ impl ProviderService {
|
||||
}
|
||||
|
||||
/// Get current provider ID
|
||||
///
|
||||
/// 优先从本地 settings 读取当前供应商,fallback 到数据库的 is_current 字段。
|
||||
/// 这确保了云同步场景下多设备可以独立选择供应商。
|
||||
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())
|
||||
@@ -167,9 +175,9 @@ impl ProviderService {
|
||||
/// Switch flow:
|
||||
/// 1. Validate target provider exists
|
||||
/// 2. **Backfill mechanism**: Backfill current live config to current provider, protect user manual modifications
|
||||
/// 3. Set new current provider
|
||||
/// 4. Write target provider config to live files
|
||||
/// 5. Gemini additional security flag handling
|
||||
/// 3. Update local settings current_provider_xxx (device-level)
|
||||
/// 4. Update database is_current (as default for new devices)
|
||||
/// 5. Write target provider config to live files
|
||||
/// 6. Sync MCP configuration
|
||||
pub fn switch(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
||||
// Check if provider exists
|
||||
@@ -179,7 +187,11 @@ impl ProviderService {
|
||||
.ok_or_else(|| AppError::Message(format!("供应商 {id} 不存在")))?;
|
||||
|
||||
// Backfill: Backfill current live config to current provider
|
||||
if let Some(current_id) = state.db.get_current_provider(app_type.as_str())? {
|
||||
// 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());
|
||||
|
||||
if let Some(current_id) = current_id {
|
||||
if current_id != id {
|
||||
// Only backfill when switching to a different provider
|
||||
if let Ok(live_config) = read_live_settings(app_type.clone()) {
|
||||
@@ -192,7 +204,10 @@ impl ProviderService {
|
||||
}
|
||||
}
|
||||
|
||||
// Set current
|
||||
// Update local settings (device-level, takes priority)
|
||||
crate::settings::set_current_provider(&app_type, Some(id))?;
|
||||
|
||||
// Update database is_current (as default for new devices)
|
||||
state.db.set_current_provider(app_type.as_str(), id)?;
|
||||
|
||||
// Sync to live (write_gemini_live handles security flag internally for Gemini)
|
||||
@@ -204,9 +219,9 @@ impl ProviderService {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync current provider from database to live configuration (re-export)
|
||||
pub fn sync_current_from_db(state: &AppState) -> Result<(), AppError> {
|
||||
sync_current_from_db(state)
|
||||
/// Sync current provider to live configuration (re-export)
|
||||
pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
||||
sync_current_to_live(state)
|
||||
}
|
||||
|
||||
/// Import default configuration from live files (re-export)
|
||||
|
||||
@@ -843,21 +843,22 @@ fn sync_gemini_packycode_sets_security_selected_type() {
|
||||
ConfigService::sync_current_providers_to_live(&mut config)
|
||||
.expect("syncing gemini live should succeed");
|
||||
|
||||
let settings_path = home.join(".cc-switch").join("settings.json");
|
||||
// security field is written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json
|
||||
let gemini_settings = home.join(".gemini").join("settings.json");
|
||||
assert!(
|
||||
settings_path.exists(),
|
||||
"settings.json should exist at {}",
|
||||
settings_path.display()
|
||||
gemini_settings.exists(),
|
||||
"Gemini settings.json should exist at {}",
|
||||
gemini_settings.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");
|
||||
let raw = std::fs::read_to_string(&gemini_settings).expect("read gemini settings.json");
|
||||
let value: serde_json::Value = serde_json::from_str(&raw).expect("parse gemini settings.json");
|
||||
assert_eq!(
|
||||
value
|
||||
.pointer("/security/auth/selectedType")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("gemini-api-key"),
|
||||
"syncing PackyCode Gemini should enforce security.auth.selectedType"
|
||||
"syncing PackyCode Gemini should enforce security.auth.selectedType in Gemini settings"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -893,22 +894,7 @@ fn sync_gemini_google_official_sets_oauth_security() {
|
||||
ConfigService::sync_current_providers_to_live(&mut config)
|
||||
.expect("syncing google official gemini should succeed");
|
||||
|
||||
let cc_settings = home.join(".cc-switch").join("settings.json");
|
||||
assert!(
|
||||
cc_settings.exists(),
|
||||
"app settings should exist at {}",
|
||||
cc_settings.display()
|
||||
);
|
||||
let cc_raw = std::fs::read_to_string(&cc_settings).expect("read .cc-switch settings");
|
||||
let cc_value: serde_json::Value = serde_json::from_str(&cc_raw).expect("parse app settings");
|
||||
assert_eq!(
|
||||
cc_value
|
||||
.pointer("/security/auth/selectedType")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("oauth-personal"),
|
||||
"syncing Google official should set oauth-personal in app settings"
|
||||
);
|
||||
|
||||
// security field is written to ~/.gemini/settings.json, not ~/.cc-switch/settings.json
|
||||
let gemini_settings = home.join(".gemini").join("settings.json");
|
||||
assert!(
|
||||
gemini_settings.exists(),
|
||||
@@ -923,7 +909,7 @@ fn sync_gemini_google_official_sets_oauth_security() {
|
||||
.pointer("/security/auth/selectedType")
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("oauth-personal"),
|
||||
"Gemini settings should also record oauth-personal"
|
||||
"Gemini settings should record oauth-personal for Google Official"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,14 +8,22 @@ const directorySchema = z
|
||||
.or(z.literal(""));
|
||||
|
||||
export const settingsSchema = z.object({
|
||||
// 设备级 UI 设置
|
||||
showInTray: z.boolean(),
|
||||
minimizeToTrayOnClose: z.boolean(),
|
||||
enableClaudePluginIntegration: z.boolean().optional(),
|
||||
launchOnStartup: z.boolean().optional(),
|
||||
language: z.enum(["en", "zh"]).optional(),
|
||||
|
||||
// 设备级目录覆盖
|
||||
claudeConfigDir: directorySchema.nullable().optional(),
|
||||
codexConfigDir: directorySchema.nullable().optional(),
|
||||
language: z.enum(["en", "zh"]).optional(),
|
||||
customEndpointsClaude: z.record(z.string(), z.unknown()).optional(),
|
||||
customEndpointsCodex: z.record(z.string(), z.unknown()).optional(),
|
||||
geminiConfigDir: directorySchema.nullable().optional(),
|
||||
|
||||
// 当前供应商 ID(设备级)
|
||||
currentProviderClaude: z.string().optional(),
|
||||
currentProviderCodex: z.string().optional(),
|
||||
currentProviderGemini: z.string().optional(),
|
||||
});
|
||||
|
||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||
|
||||
+16
-14
@@ -97,33 +97,35 @@ export interface ProviderMeta {
|
||||
}
|
||||
|
||||
// 应用设置类型(用于设置对话框与 Tauri API)
|
||||
// 存储在本地 ~/.cc-switch/settings.json,不随数据库同步
|
||||
export interface Settings {
|
||||
// ===== 设备级 UI 设置 =====
|
||||
// 是否在系统托盘(macOS 菜单栏)显示图标
|
||||
showInTray: boolean;
|
||||
// 点击关闭按钮时是否最小化到托盘而不是关闭应用
|
||||
minimizeToTrayOnClose: boolean;
|
||||
// 启用 Claude 插件联动(写入 ~/.claude/config.json 的 primaryApiKey)
|
||||
enableClaudePluginIntegration?: boolean;
|
||||
// 是否开机自启
|
||||
launchOnStartup?: boolean;
|
||||
// 首选语言(可选,默认中文)
|
||||
language?: "en" | "zh";
|
||||
|
||||
// ===== 设备级目录覆盖 =====
|
||||
// 覆盖 Claude Code 配置目录(可选)
|
||||
claudeConfigDir?: string;
|
||||
// 覆盖 Codex 配置目录(可选)
|
||||
codexConfigDir?: string;
|
||||
// 覆盖 Gemini 配置目录(可选)
|
||||
geminiConfigDir?: string;
|
||||
// 首选语言(可选,默认中文)
|
||||
language?: "en" | "zh";
|
||||
// 是否开机自启
|
||||
launchOnStartup?: boolean;
|
||||
// Claude 自定义端点列表
|
||||
customEndpointsClaude?: Record<string, CustomEndpoint>;
|
||||
// Codex 自定义端点列表
|
||||
customEndpointsCodex?: Record<string, CustomEndpoint>;
|
||||
// 安全设置(兼容未来扩展)
|
||||
security?: {
|
||||
auth?: {
|
||||
selectedType?: string;
|
||||
};
|
||||
};
|
||||
|
||||
// ===== 当前供应商 ID(设备级)=====
|
||||
// 当前 Claude 供应商 ID(优先于数据库 is_current)
|
||||
currentProviderClaude?: string;
|
||||
// 当前 Codex 供应商 ID(优先于数据库 is_current)
|
||||
currentProviderCodex?: string;
|
||||
// 当前 Gemini 供应商 ID(优先于数据库 is_current)
|
||||
currentProviderGemini?: string;
|
||||
}
|
||||
|
||||
// MCP 服务器连接参数(宽松:允许扩展字段)
|
||||
|
||||
Reference in New Issue
Block a user