mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
7b1a68ee4e
Replace Mutex with RwLock for AppState.config to enable concurrent reads, improving performance for tray menu building and query operations that previously blocked each other unnecessarily. Key changes: - Migrate AppState.config from Mutex<MultiAppConfig> to RwLock<MultiAppConfig> - Distinguish read-only operations (read()) from mutations (write()) across all command handlers and service layers - Offload blocking file I/O in import/export commands to spawn_blocking threads, minimizing lock hold time and preventing main thread blocking - Extract load_config_for_import() to separate I/O logic from state updates - Update all integration tests to use RwLock semantics Performance impact: - Concurrent reads: Multiple threads can now query config simultaneously (tray menu, provider list, MCP config) - Reduced contention: Write locks only acquired during actual mutations - Non-blocking I/O: Config import/export no longer freezes UI thread All existing tests pass with new locking semantics.
324 lines
9.5 KiB
Rust
324 lines
9.5 KiB
Rust
use crate::app_config::{AppType, MultiAppConfig};
|
|
use crate::error::AppError;
|
|
use crate::provider::Provider;
|
|
use chrono::Utc;
|
|
use serde_json::{json, Value};
|
|
use std::fs;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
// 默认仅保留最近 10 份备份,避免目录无限膨胀
|
|
const MAX_BACKUPS: usize = 10;
|
|
|
|
/// 创建配置文件备份
|
|
pub fn create_backup(config_path: &PathBuf) -> Result<String, AppError> {
|
|
if !config_path.exists() {
|
|
return Ok(String::new());
|
|
}
|
|
|
|
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
|
|
let backup_id = format!("backup_{}", timestamp);
|
|
|
|
let backup_dir = config_path
|
|
.parent()
|
|
.ok_or_else(|| AppError::Config("Invalid config path".into()))?
|
|
.join("backups");
|
|
|
|
// 创建备份目录
|
|
fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
|
|
|
|
let backup_path = backup_dir.join(format!("{}.json", backup_id));
|
|
|
|
let contents = fs::read(config_path).map_err(|e| AppError::io(config_path, e))?;
|
|
fs::write(&backup_path, contents).map_err(|e| AppError::io(&backup_path, e))?;
|
|
|
|
// 备份完成后清理旧的备份文件(仅保留最近 MAX_BACKUPS 份)
|
|
cleanup_old_backups(&backup_dir, MAX_BACKUPS)?;
|
|
|
|
Ok(backup_id)
|
|
}
|
|
|
|
fn cleanup_old_backups(backup_dir: &PathBuf, retain: usize) -> Result<(), AppError> {
|
|
if retain == 0 {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut entries: Vec<_> = match fs::read_dir(backup_dir) {
|
|
Ok(iter) => iter
|
|
.filter_map(|entry| entry.ok())
|
|
.filter(|entry| {
|
|
entry
|
|
.path()
|
|
.extension()
|
|
.map(|ext| ext == "json")
|
|
.unwrap_or(false)
|
|
})
|
|
.collect(),
|
|
Err(_) => return Ok(()),
|
|
};
|
|
|
|
if entries.len() <= retain {
|
|
return Ok(());
|
|
}
|
|
|
|
let remove_count = entries.len().saturating_sub(retain);
|
|
|
|
entries.sort_by(|a, b| {
|
|
let a_time = a.metadata().and_then(|m| m.modified()).ok();
|
|
let b_time = b.metadata().and_then(|m| m.modified()).ok();
|
|
a_time.cmp(&b_time)
|
|
});
|
|
|
|
for entry in entries.into_iter().take(remove_count) {
|
|
if let Err(err) = fs::remove_file(entry.path()) {
|
|
log::warn!(
|
|
"Failed to remove old backup {}: {}",
|
|
entry.path().display(),
|
|
err
|
|
);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> {
|
|
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<(), AppError> {
|
|
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<(), AppError> {
|
|
use serde_json::Value;
|
|
|
|
let settings = provider.settings_config.as_object().ok_or_else(|| {
|
|
AppError::Config(format!("供应商 {} 的 Codex 配置必须是对象", provider_id))
|
|
})?;
|
|
let auth = settings.get("auth").ok_or_else(|| {
|
|
AppError::Config(format!(
|
|
"供应商 {} 的 Codex 配置缺少 auth 字段",
|
|
provider_id
|
|
))
|
|
})?;
|
|
if !auth.is_object() {
|
|
return Err(AppError::Config(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<(), AppError> {
|
|
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| AppError::io(parent, 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> {
|
|
tauri::async_runtime::spawn_blocking(move || {
|
|
let config_path = crate::config::get_app_config_path();
|
|
let config_content =
|
|
fs::read_to_string(&config_path).map_err(|e| AppError::io(&config_path, e))?;
|
|
|
|
let target_path = PathBuf::from(&file_path);
|
|
fs::write(&target_path, &config_content).map_err(|e| AppError::io(&target_path, e))?;
|
|
|
|
Ok::<_, AppError>(json!({
|
|
"success": true,
|
|
"message": "Configuration exported successfully",
|
|
"filePath": file_path
|
|
}))
|
|
})
|
|
.await
|
|
.map_err(|e| format!("导出配置失败: {}", e))?
|
|
.map_err(|e: AppError| e.to_string())
|
|
}
|
|
|
|
/// 从文件导入配置
|
|
#[tauri::command]
|
|
pub async fn import_config_from_file(
|
|
file_path: String,
|
|
state: tauri::State<'_, crate::store::AppState>,
|
|
) -> Result<Value, String> {
|
|
let path_buf = PathBuf::from(&file_path);
|
|
let (new_config, backup_id) =
|
|
tauri::async_runtime::spawn_blocking(move || load_config_for_import(&path_buf))
|
|
.await
|
|
.map_err(|e| format!("导入配置失败: {}", e))?
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
{
|
|
let mut guard = state
|
|
.config
|
|
.write()
|
|
.map_err(|e| AppError::from(e).to_string())?;
|
|
*guard = new_config;
|
|
}
|
|
|
|
Ok(json!({
|
|
"success": true,
|
|
"message": "Configuration imported successfully",
|
|
"backupId": backup_id
|
|
}))
|
|
}
|
|
|
|
/// 从文件导入配置的核心逻辑,供命令及测试复用。
|
|
pub fn import_config_from_path(
|
|
file_path: &Path,
|
|
state: &crate::store::AppState,
|
|
) -> Result<String, AppError> {
|
|
let (new_config, backup_id) = load_config_for_import(file_path)?;
|
|
|
|
{
|
|
let mut guard = state.config.write().map_err(AppError::from)?;
|
|
*guard = new_config;
|
|
}
|
|
|
|
Ok(backup_id)
|
|
}
|
|
|
|
fn load_config_for_import(file_path: &Path) -> Result<(MultiAppConfig, String), AppError> {
|
|
let import_content = fs::read_to_string(file_path).map_err(|e| AppError::io(file_path, e))?;
|
|
|
|
let new_config: crate::app_config::MultiAppConfig =
|
|
serde_json::from_str(&import_content).map_err(|e| AppError::json(file_path, e))?;
|
|
|
|
let config_path = crate::config::get_app_config_path();
|
|
let backup_id = create_backup(&config_path)?;
|
|
|
|
fs::write(&config_path, &import_content).map_err(|e| AppError::io(&config_path, e))?;
|
|
|
|
Ok((new_config, backup_id))
|
|
}
|
|
|
|
/// 同步当前供应商配置到对应的 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
|
|
.write()
|
|
.map_err(|e| AppError::from(e).to_string())?;
|
|
sync_current_providers_to_live(&mut config_state).map_err(|e| e.to_string())?;
|
|
}
|
|
|
|
Ok(json!({
|
|
"success": true,
|
|
"message": "Live configuration synchronized"
|
|
}))
|
|
}
|
|
|
|
/// 保存文件对话框
|
|
#[tauri::command]
|
|
pub async fn save_file_dialog<R: tauri::Runtime>(
|
|
app: tauri::AppHandle<R>,
|
|
default_name: String,
|
|
) -> Result<Option<String>, String> {
|
|
use tauri_plugin_dialog::DialogExt;
|
|
|
|
let dialog = app.dialog();
|
|
let result = dialog
|
|
.file()
|
|
.add_filter("JSON", &["json"])
|
|
.set_file_name(&default_name)
|
|
.blocking_save_file();
|
|
|
|
Ok(result.map(|p| p.to_string()))
|
|
}
|
|
|
|
/// 打开文件对话框
|
|
#[tauri::command]
|
|
pub async fn open_file_dialog<R: tauri::Runtime>(
|
|
app: tauri::AppHandle<R>,
|
|
) -> Result<Option<String>, String> {
|
|
use tauri_plugin_dialog::DialogExt;
|
|
|
|
let dialog = app.dialog();
|
|
let result = dialog
|
|
.file()
|
|
.add_filter("JSON", &["json"])
|
|
.blocking_pick_file();
|
|
|
|
Ok(result.map(|p| p.to_string()))
|
|
}
|