feat: sync current providers to live files after config import

Core Improvements:
- Add sync_current_providers_live command to synchronize in-memory provider
  settings to corresponding live files (~/.claude/settings.json or ~/.codex/auth.json)
- Introduce partial-success state to distinguish between 'import succeeded
  but sync failed' scenario, providing clear user feedback
- Remove unused skip_live_backfill parameter from switch_provider command
- Separate responsibilities: backend handles import/backup, frontend handles
  sync/error presentation

Technical Details:
- Codex: sync auth.json + config.toml with MCP configuration
- Claude: sync settings.json
- Bidirectional sync: read back after write to update in-memory settings_config
- Full i18n support (English and Chinese)
- Graceful handling when no current provider is active

Affected Files:
- Backend: import_export.rs, commands.rs, lib.rs
- Frontend: useImportExport.ts, ImportExportSection.tsx, settings.ts
- i18n: en.json, zh.json

This ensures SSOT (Single Source of Truth) consistency between config.json
and live configuration files after import operations.
This commit is contained in:
Jason
2025-10-27 13:20:59 +08:00
parent 76a8d1760b
commit d064cb8555
8 changed files with 207 additions and 45 deletions
+128
View File
@@ -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(&current_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, &current_id, &provider)?,
AppType::Claude => sync_claude_live(config, &current_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::<serde_json::Value>(&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<Value, String> {
@@ -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<Value, String> {
{
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<R: tauri::Runtime>(