diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 734846d..c294217 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -369,7 +369,6 @@ pub async fn switch_provider( AppType::Codex => { use serde_json::Value; - // 回填:读取 live(auth.json + config.toml)写回当前供应商 settings_config if !{ let cur = config .get_manager_mut(&app_type) @@ -424,7 +423,6 @@ pub async fn switch_provider( let settings_path = get_claude_settings_path(); - // 回填:读取 live settings.json 写回当前供应商 settings_config if settings_path.exists() { let cur_id = { let m = config @@ -816,9 +814,7 @@ pub async fn query_provider_usage( use crate::provider::{UsageData, UsageResult}; // 解析参数 - let provider_id = provider_id - .or(providerId) - .ok_or("缺少 providerId 参数")?; + let provider_id = provider_id.or(providerId).ok_or("缺少 providerId 参数")?; let app_type = app_type .or_else(|| app.as_deref().map(|s| s.into())) @@ -832,14 +828,9 @@ pub async fn query_provider_usage( .lock() .map_err(|e| format!("获取锁失败: {}", e))?; - let manager = config - .get_manager(&app_type) - .ok_or("应用类型不存在")?; + let manager = config.get_manager(&app_type).ok_or("应用类型不存在")?; - let provider = manager - .providers - .get(&provider_id) - .ok_or("供应商不存在")?; + let provider = manager.providers.get(&provider_id).ok_or("供应商不存在")?; // 2. 检查脚本配置 let usage_script = provider @@ -864,13 +855,9 @@ pub async fn query_provider_usage( }; // 5. 执行脚本 - let result = crate::usage_script::execute_usage_script( - &usage_script_code, - &api_key, - &base_url, - timeout, - ) - .await; + let result = + crate::usage_script::execute_usage_script(&usage_script_code, &api_key, &base_url, timeout) + .await; // 6. 构建结果(支持单对象或数组) match result { @@ -878,12 +865,11 @@ pub async fn query_provider_usage( // 尝试解析为数组 let usage_list: Vec = if data.is_array() { // 直接解析为数组 - serde_json::from_value(data) - .map_err(|e| format!("数据格式错误: {}", e))? + serde_json::from_value(data).map_err(|e| format!("数据格式错误: {}", e))? } else { // 单对象包装为数组(向后兼容) - let single: UsageData = serde_json::from_value(data) - .map_err(|e| format!("数据格式错误: {}", e))?; + let single: UsageData = + serde_json::from_value(data).map_err(|e| format!("数据格式错误: {}", e))?; vec![single] }; @@ -893,13 +879,11 @@ pub async fn query_provider_usage( error: None, }) } - Err(e) => { - Ok(UsageResult { - success: false, - data: None, - error: Some(e), - }) - } + Err(e) => Ok(UsageResult { + success: false, + data: None, + error: Some(e), + }), } } @@ -1539,4 +1523,4 @@ pub async fn update_providers_sort_order( state.save()?; Ok(true) -} \ No newline at end of file +} diff --git a/src-tauri/src/import_export.rs b/src-tauri/src/import_export.rs index 6f500b3..10976c3 100644 --- a/src-tauri/src/import_export.rs +++ b/src-tauri/src/import_export.rs @@ -1,3 +1,5 @@ +use crate::app_config::{AppType, MultiAppConfig}; +use crate::provider::Provider; use chrono::Utc; use serde_json::{json, Value}; use std::fs; @@ -79,6 +81,113 @@ fn cleanup_old_backups(backup_dir: &PathBuf, retain: usize) -> Result<(), String Ok(()) } +fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), String> { + sync_current_provider_for_app(config, &AppType::Claude)?; + sync_current_provider_for_app(config, &AppType::Codex)?; + Ok(()) +} + +fn sync_current_provider_for_app( + config: &mut MultiAppConfig, + app_type: &AppType, +) -> Result<(), String> { + let (current_id, provider) = { + let manager = match config.get_manager(app_type) { + Some(manager) => manager, + None => return Ok(()), + }; + + if manager.current.is_empty() { + return Ok(()); + } + + let current_id = manager.current.clone(); + let provider = match manager.providers.get(¤t_id) { + Some(provider) => provider.clone(), + None => { + log::warn!( + "当前应用 {:?} 的供应商 {} 不存在,跳过 live 同步", + app_type, + current_id + ); + return Ok(()); + } + }; + (current_id, provider) + }; + + match app_type { + AppType::Codex => sync_codex_live(config, ¤t_id, &provider)?, + AppType::Claude => sync_claude_live(config, ¤t_id, &provider)?, + } + + Ok(()) +} + +fn sync_codex_live( + config: &mut MultiAppConfig, + provider_id: &str, + provider: &Provider, +) -> Result<(), String> { + use serde_json::Value; + + let settings = provider + .settings_config + .as_object() + .ok_or_else(|| format!("供应商 {} 的 Codex 配置必须是对象", provider_id))?; + let auth = settings + .get("auth") + .ok_or_else(|| format!("供应商 {} 的 Codex 配置缺少 auth 字段", provider_id))?; + if !auth.is_object() { + return Err(format!( + "供应商 {} 的 Codex auth 配置必须是 JSON 对象", + provider_id + )); + } + let cfg_text = settings.get("config").and_then(Value::as_str); + + crate::codex_config::write_codex_live_atomic(auth, cfg_text)?; + crate::mcp::sync_enabled_to_codex(config)?; + + let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?; + if let Some(manager) = config.get_manager_mut(&AppType::Codex) { + if let Some(target) = manager.providers.get_mut(provider_id) { + if let Some(obj) = target.settings_config.as_object_mut() { + obj.insert( + "config".to_string(), + serde_json::Value::String(cfg_text_after), + ); + } + } + } + + Ok(()) +} + +fn sync_claude_live( + config: &mut MultiAppConfig, + provider_id: &str, + provider: &Provider, +) -> Result<(), String> { + use crate::config::{read_json_file, write_json_file}; + + let settings_path = crate::config::get_claude_settings_path(); + if let Some(parent) = settings_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| format!("创建 Claude 配置目录失败: {}", e))?; + } + + write_json_file(&settings_path, &provider.settings_config)?; + + let live_after = read_json_file::(&settings_path)?; + if let Some(manager) = config.get_manager_mut(&AppType::Claude) { + if let Some(target) = manager.providers.get_mut(provider_id) { + target.settings_config = live_after; + } + } + + Ok(()) +} + /// 导出配置文件 #[tauri::command] pub async fn export_config_to_file(file_path: String) -> Result { @@ -135,6 +244,25 @@ pub async fn import_config_from_file( })) } +/// 同步当前供应商配置到对应的 live 文件 +#[tauri::command] +pub async fn sync_current_providers_live( + state: tauri::State<'_, crate::store::AppState>, +) -> Result { + { + let mut config_state = state + .config + .lock() + .map_err(|e| format!("Failed to lock config: {}", e))?; + sync_current_providers_to_live(&mut config_state)?; + } + + Ok(json!({ + "success": true, + "message": "Live configuration synchronized" + })) +} + /// 保存文件对话框 #[tauri::command] pub async fn save_file_dialog( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1150b38..6db0d96 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -11,8 +11,8 @@ mod migration; mod provider; mod settings; mod speedtest; -mod usage_script; mod store; +mod usage_script; use store::AppState; use tauri::{ @@ -509,6 +509,7 @@ pub fn run() { import_export::import_config_from_file, import_export::save_file_dialog, import_export::open_file_dialog, + import_export::sync_current_providers_live, update_tray_menu, ]); @@ -537,4 +538,4 @@ pub fn run() { let _ = (app_handle, event); } }); -} \ No newline at end of file +} diff --git a/src/components/settings/ImportExportSection.tsx b/src/components/settings/ImportExportSection.tsx index 2818926..f9c195f 100644 --- a/src/components/settings/ImportExportSection.tsx +++ b/src/components/settings/ImportExportSection.tsx @@ -167,6 +167,20 @@ function ImportStatusMessage({ ); } + if (status === "partial-success") { + return ( +
+ +
+

{t("settings.importPartialSuccess")}

+

{t("settings.importPartialHint")}

+
+
+ ); + } + const message = errorMessage || t("settings.importFailed"); return ( diff --git a/src/hooks/useImportExport.ts b/src/hooks/useImportExport.ts index cacdb40..9ee568d 100644 --- a/src/hooks/useImportExport.ts +++ b/src/hooks/useImportExport.ts @@ -3,7 +3,12 @@ import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { settingsApi } from "@/lib/api"; -export type ImportStatus = "idle" | "importing" | "success" | "error"; +export type ImportStatus = + | "idle" + | "importing" + | "success" + | "partial-success" + | "error"; export interface UseImportExportOptions { onImportSuccess?: () => void | Promise; @@ -86,8 +91,22 @@ export function useImportExport( try { const result = await settingsApi.importConfigFromFile(selectedFile); - if (result.success) { - setBackupId(result.backupId ?? null); + if (!result.success) { + setStatus("error"); + const message = + result.message || + t("settings.configCorrupted", { + defaultValue: "配置文件已损坏或格式不正确", + }); + setErrorMessage(message); + toast.error(message); + return; + } + + setBackupId(result.backupId ?? null); + + try { + await settingsApi.syncCurrentProvidersLive(); setStatus("success"); toast.success( t("settings.importSuccess", { @@ -98,15 +117,15 @@ export function useImportExport( successTimerRef.current = window.setTimeout(() => { void onImportSuccess?.(); }, 1500); - } else { - setStatus("error"); - const message = - result.message || - t("settings.configCorrupted", { - defaultValue: "配置文件已损坏或格式不正确", - }); - setErrorMessage(message); - toast.error(message); + } catch (error) { + console.error("[useImportExport] Failed to sync live config", error); + setStatus("partial-success"); + toast.warning( + t("settings.importPartialSuccess", { + defaultValue: + "配置已导入,但同步到当前供应商失败。请手动重新选择一次供应商。", + }), + ); } } catch (error) { console.error("[useImportExport] Failed to import config", error); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index e8fc4ee..a8d5fa5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -123,6 +123,9 @@ "importing": "Importing...", "importSuccess": "Import Successful!", "importFailed": "Import Failed", + "syncLiveFailed": "Imported, but failed to sync to the current provider. Please reselect the provider manually.", + "importPartialSuccess": "Config imported, but failed to sync to the current provider.", + "importPartialHint": "Please manually reselect the provider to refresh the live configuration.", "configExported": "Config exported to:", "exportFailed": "Export failed", "selectFileFailed": "Failed to select file", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 2dd6176..3c74648 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -123,6 +123,9 @@ "importing": "导入中...", "importSuccess": "导入成功!", "importFailed": "导入失败", + "syncLiveFailed": "已导入,但同步到当前供应商失败,请手动重新选择一次供应商。", + "importPartialSuccess": "配置已导入,但同步到当前供应商失败。", + "importPartialHint": "请手动重新选择一次供应商以刷新对应配置。", "configExported": "配置已导出到:", "exportFailed": "导出失败", "selectFileFailed": "选择文件失败", diff --git a/src/lib/api/settings.ts b/src/lib/api/settings.ts index 9934f5a..8bfc255 100644 --- a/src/lib/api/settings.ts +++ b/src/lib/api/settings.ts @@ -97,6 +97,16 @@ export const settingsApi = { }); }, + async syncCurrentProvidersLive(): Promise { + const result = (await invoke("sync_current_providers_live")) as { + success?: boolean; + message?: string; + }; + if (!result?.success) { + throw new Error(result?.message || "Sync current providers failed"); + } + }, + async openExternal(url: string): Promise { try { const u = new URL(url);