diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 859ea98..a736b0d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -8,7 +8,6 @@ mod config; mod error; mod init_status; mod mcp; -mod migration; mod provider; mod services; mod settings; @@ -466,14 +465,9 @@ pub fn run() { log::warn!("迁移 app_config_dir 失败: {}", e); } - // 首次启动迁移:扫描副本文件,合并到 config.json,并归档副本;旧 config.json 先归档 + // 确保配置结构就绪(已移除旧版本的副本迁移逻辑) { let mut config_guard = app_state.config.write().unwrap(); - let migrated = migration::migrate_copies_into_config(&mut config_guard)?; - if migrated { - log::info!("已将副本文件导入到 config.json,并完成归档"); - } - // 确保两个 App 条目存在 config_guard.ensure_app(&app_config::AppType::Claude); config_guard.ensure_app(&app_config::AppType::Codex); } diff --git a/src-tauri/src/migration.rs b/src-tauri/src/migration.rs deleted file mode 100644 index cadf600..0000000 --- a/src-tauri/src/migration.rs +++ /dev/null @@ -1,435 +0,0 @@ -use crate::app_config::{AppType, MultiAppConfig}; -use crate::config::{ - archive_file, delete_file, get_app_config_dir, get_app_config_path, get_claude_config_dir, -}; -use crate::error::AppError; -use serde_json::Value; -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::path::PathBuf; - -fn now_ts() -> u64 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_secs() -} - -fn get_marker_path() -> PathBuf { - get_app_config_dir().join("migrated.copies.v1") -} - -fn sanitized_id(base: &str) -> String { - crate::config::sanitize_provider_name(base) -} - -fn next_unique_id(existing: &HashSet, base: &str) -> String { - let base = sanitized_id(base); - if !existing.contains(&base) { - return base; - } - for i in 2..1000 { - let candidate = format!("{}-{}", base, i); - if !existing.contains(&candidate) { - return candidate; - } - } - format!("{}-dup", base) -} - -fn extract_claude_api_key(value: &Value) -> Option { - value - .get("env") - .and_then(|env| { - env.get("ANTHROPIC_AUTH_TOKEN") - .or_else(|| env.get("ANTHROPIC_API_KEY")) - }) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) -} - -fn extract_codex_api_key(value: &Value) -> Option { - value - .get("auth") - .and_then(|auth| { - auth.get("OPENAI_API_KEY") - .or_else(|| auth.get("openai_api_key")) - }) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()) -} - -fn norm_name(s: &str) -> String { - s.trim().to_lowercase() -} - -// 去重策略:name + 原始 key 直接比较(不做哈希) - -fn scan_claude_copies() -> Vec<(String, PathBuf, Value)> { - let mut items = Vec::new(); - let dir = get_claude_config_dir(); - if !dir.exists() { - return items; - } - if let Ok(rd) = fs::read_dir(&dir) { - for e in rd.flatten() { - let p = e.path(); - let fname = match p.file_name().and_then(|s| s.to_str()) { - Some(s) => s, - None => continue, - }; - if fname == "settings.json" || fname == "claude.json" { - continue; - } - if !fname.starts_with("settings-") || !fname.ends_with(".json") { - continue; - } - let name = fname - .trim_start_matches("settings-") - .trim_end_matches(".json"); - if let Ok(val) = crate::config::read_json_file::(&p) { - items.push((name.to_string(), p, val)); - } - } - } - items -} - -fn scan_codex_copies() -> Vec<(String, Option, Option, Value)> { - let mut by_name: HashMap, Option)> = HashMap::new(); - let dir = crate::codex_config::get_codex_config_dir(); - if !dir.exists() { - return Vec::new(); - } - if let Ok(rd) = fs::read_dir(&dir) { - for e in rd.flatten() { - let p = e.path(); - let fname = match p.file_name().and_then(|s| s.to_str()) { - Some(s) => s, - None => continue, - }; - if fname.starts_with("auth-") && fname.ends_with(".json") { - let name = fname.trim_start_matches("auth-").trim_end_matches(".json"); - let entry = by_name.entry(name.to_string()).or_default(); - entry.0 = Some(p); - } else if fname.starts_with("config-") && fname.ends_with(".toml") { - let name = fname - .trim_start_matches("config-") - .trim_end_matches(".toml"); - let entry = by_name.entry(name.to_string()).or_default(); - entry.1 = Some(p); - } - } - } - - let mut items = Vec::new(); - for (name, (auth_path, config_path)) in by_name { - if let Some(authp) = auth_path { - if let Ok(auth) = crate::config::read_json_file::(&authp) { - let config_str = if let Some(cfgp) = &config_path { - match crate::codex_config::read_and_validate_config_from_path(cfgp) { - Ok(s) => s, - Err(e) => { - log::warn!("跳过无效 Codex config-{}.toml: {}", name, e); - String::new() - } - } - } else { - String::new() - }; - let settings = serde_json::json!({ - "auth": auth, - "config": config_str, - }); - items.push((name, Some(authp), config_path, settings)); - } - } - } - items -} - -pub fn migrate_copies_into_config(config: &mut MultiAppConfig) -> Result { - // 如果已迁移过则跳过;若目录不存在则先创建,避免新装用户写入标记时失败 - let marker = get_marker_path(); - if let Some(parent) = marker.parent() { - std::fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?; - } - if marker.exists() { - return Ok(false); - } - - let claude_items = scan_claude_copies(); - let codex_items = scan_codex_copies(); - if claude_items.is_empty() && codex_items.is_empty() { - // 即便没有可迁移项,也写入标记避免每次扫描 - fs::write(&marker, b"no-copies").map_err(|e| AppError::io(&marker, e))?; - return Ok(false); - } - - // 备份旧的 config.json - let ts = now_ts(); - let app_cfg_path = get_app_config_path(); - if app_cfg_path.exists() { - let _ = archive_file(ts, "cc-switch", &app_cfg_path); - } - - // 读取 live:Claude(settings.json / claude.json) - let live_claude: Option<(String, Value)> = { - let settings_path = crate::config::get_claude_settings_path(); - if settings_path.exists() { - match crate::config::read_json_file::(&settings_path) { - Ok(val) => Some(("default".to_string(), val)), - Err(e) => { - log::warn!("读取 Claude live 配置失败: {}", e); - None - } - } - } else { - None - } - }; - - // 合并:Claude(优先 live,然后副本) - 去重键: name + apiKey(直接比较) - config.ensure_app(&AppType::Claude); - let manager = config.get_manager_mut(&AppType::Claude).unwrap(); - let mut ids: HashSet = manager.providers.keys().cloned().collect(); - let mut live_claude_id: Option = None; - - if let Some((name, value)) = &live_claude { - let cand_key = extract_claude_api_key(value); - let exist_id = manager.providers.iter().find_map(|(id, p)| { - let pk = extract_claude_api_key(&p.settings_config); - if norm_name(&p.name) == norm_name(name) && pk == cand_key { - Some(id.clone()) - } else { - None - } - }); - if let Some(exist_id) = exist_id { - if let Some(prov) = manager.providers.get_mut(&exist_id) { - log::info!("合并到已存在 Claude 供应商 '{}' (by name+key)", name); - prov.settings_config = value.clone(); - live_claude_id = Some(exist_id); - } - } else { - let id = next_unique_id(&ids, name); - ids.insert(id.clone()); - let provider = - crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None); - manager.providers.insert(provider.id.clone(), provider); - live_claude_id = Some(id); - } - } - for (name, path, value) in claude_items.iter() { - let cand_key = extract_claude_api_key(value); - let exist_id = manager.providers.iter().find_map(|(id, p)| { - let pk = extract_claude_api_key(&p.settings_config); - if norm_name(&p.name) == norm_name(name) && pk == cand_key { - Some(id.clone()) - } else { - None - } - }); - if let Some(exist_id) = exist_id { - if let Some(prov) = manager.providers.get_mut(&exist_id) { - log::info!( - "覆盖 Claude 供应商 '{}' 来自 {} (by name+key)", - name, - path.display() - ); - prov.settings_config = value.clone(); - } - } else { - let id = next_unique_id(&ids, name); - ids.insert(id.clone()); - let provider = - crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None); - manager.providers.insert(provider.id.clone(), provider); - } - } - - // 读取 live:Codex(auth.json 必需,config.toml 可空) - let live_codex: Option<(String, Value)> = { - let auth_path = crate::codex_config::get_codex_auth_path(); - if auth_path.exists() { - match crate::config::read_json_file::(&auth_path) { - Ok(auth) => { - let cfg = match crate::codex_config::read_and_validate_codex_config_text() { - Ok(s) => s, - Err(e) => { - log::warn!("读取/校验 Codex live config.toml 失败: {}", e); - String::new() - } - }; - Some(( - "default".to_string(), - serde_json::json!({"auth": auth, "config": cfg}), - )) - } - Err(e) => { - log::warn!("读取 Codex live auth.json 失败: {}", e); - None - } - } - } else { - None - } - }; - - // 合并:Codex(优先 live,然后副本) - 去重键: name + OPENAI_API_KEY(直接比较) - config.ensure_app(&AppType::Codex); - let manager = config.get_manager_mut(&AppType::Codex).unwrap(); - let mut ids: HashSet = manager.providers.keys().cloned().collect(); - let mut live_codex_id: Option = None; - - if let Some((name, value)) = &live_codex { - let cand_key = extract_codex_api_key(value); - let exist_id = manager.providers.iter().find_map(|(id, p)| { - let pk = extract_codex_api_key(&p.settings_config); - if norm_name(&p.name) == norm_name(name) && pk == cand_key { - Some(id.clone()) - } else { - None - } - }); - if let Some(exist_id) = exist_id { - if let Some(prov) = manager.providers.get_mut(&exist_id) { - log::info!("合并到已存在 Codex 供应商 '{}' (by name+key)", name); - prov.settings_config = value.clone(); - live_codex_id = Some(exist_id); - } - } else { - let id = next_unique_id(&ids, name); - ids.insert(id.clone()); - let provider = - crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None); - manager.providers.insert(provider.id.clone(), provider); - live_codex_id = Some(id); - } - } - for (name, authp, cfgp, value) in codex_items.iter() { - let cand_key = extract_codex_api_key(value); - let exist_id = manager.providers.iter().find_map(|(id, p)| { - let pk = extract_codex_api_key(&p.settings_config); - if norm_name(&p.name) == norm_name(name) && pk == cand_key { - Some(id.clone()) - } else { - None - } - }); - if let Some(exist_id) = exist_id { - if let Some(prov) = manager.providers.get_mut(&exist_id) { - log::info!( - "覆盖 Codex 供应商 '{}' 来自 {:?}/{:?} (by name+key)", - name, - authp, - cfgp - ); - prov.settings_config = value.clone(); - } - } else { - let id = next_unique_id(&ids, name); - ids.insert(id.clone()); - let provider = - crate::provider::Provider::with_id(id.clone(), name.clone(), value.clone(), None); - manager.providers.insert(provider.id.clone(), provider); - } - } - - // 若当前为空,将 live 导入项设为当前 - { - let manager = config.get_manager_mut(&AppType::Claude).unwrap(); - if manager.current.is_empty() { - if let Some(id) = live_claude_id { - manager.current = id; - } - } - } - { - let manager = config.get_manager_mut(&AppType::Codex).unwrap(); - if manager.current.is_empty() { - if let Some(id) = live_codex_id { - manager.current = id; - } - } - } - - // 归档副本文件 - for (_, p, _) in claude_items.into_iter() { - match archive_file(ts, "claude", &p) { - Ok(Some(_)) => { - let _ = delete_file(&p); - } - _ => { - // 归档失败则不要删除原文件,保守处理 - } - } - } - for (_, ap, cp, _) in codex_items.into_iter() { - if let Some(ap) = ap { - if let Ok(Some(_)) = archive_file(ts, "codex", &ap) { - let _ = delete_file(&ap); - } - } - if let Some(cp) = cp { - if let Ok(Some(_)) = archive_file(ts, "codex", &cp) { - let _ = delete_file(&cp); - } - } - } - - // 标记完成 - // 仅在迁移阶段执行一次全量去重(忽略大小写的名称 + API Key) - let removed = dedupe_config(config); - if removed > 0 { - log::info!("迁移阶段已去重重复供应商 {} 个", removed); - } - - fs::write(&marker, b"done").map_err(|e| AppError::io(&marker, e))?; - Ok(true) -} - -/// 启动时对现有配置做一次去重:按名称(忽略大小写)+API Key -pub fn dedupe_config(config: &mut MultiAppConfig) -> usize { - use std::collections::HashMap as Map; - - fn dedupe_one( - mgr: &mut crate::provider::ProviderManager, - extract_key: &dyn Fn(&Value) -> Option, - ) -> usize { - let mut keep: Map = Map::new(); // key -> id 保留 - let mut remove: Vec = Vec::new(); - for (id, p) in mgr.providers.iter() { - let k = format!( - "{}|{}", - norm_name(&p.name), - extract_key(&p.settings_config).unwrap_or_default() - ); - if let Some(exist_id) = keep.get(&k) { - // 若当前是正在使用的,则用当前替换之前的,反之丢弃当前 - if *id == mgr.current { - // 替换:把原先的标记为删除,改保留为当前 - remove.push(exist_id.clone()); - keep.insert(k, id.clone()); - } else { - remove.push(id.clone()); - } - } else { - keep.insert(k, id.clone()); - } - } - for id in remove.iter() { - mgr.providers.remove(id); - } - remove.len() - } - - let mut removed = 0; - if let Some(mgr) = config.get_manager_mut(&crate::app_config::AppType::Claude) { - removed += dedupe_one(mgr, &extract_claude_api_key); - } - if let Some(mgr) = config.get_manager_mut(&crate::app_config::AppType::Codex) { - removed += dedupe_one(mgr, &extract_codex_api_key); - } - removed -}