refactor(migration): remove legacy v1 copy file migration logic
Remove the entire migration module (435 lines) that was used for one-time migration from v3.1.0 to v3.2.0. This cleans up technical debt and improves startup performance. Changes: - Delete src-tauri/src/migration.rs (copy file scanning and merging) - Remove migration module reference from lib.rs - Simplify startup logic to only ensure config structure exists - Remove automatic deduplication and archiving logic BREAKING CHANGE: Users upgrading from v3.1.0 must first upgrade to v3.2.x to automatically migrate their configurations.
This commit is contained in:
@@ -8,7 +8,6 @@ mod config;
|
|||||||
mod error;
|
mod error;
|
||||||
mod init_status;
|
mod init_status;
|
||||||
mod mcp;
|
mod mcp;
|
||||||
mod migration;
|
|
||||||
mod provider;
|
mod provider;
|
||||||
mod services;
|
mod services;
|
||||||
mod settings;
|
mod settings;
|
||||||
@@ -466,14 +465,9 @@ pub fn run() {
|
|||||||
log::warn!("迁移 app_config_dir 失败: {}", e);
|
log::warn!("迁移 app_config_dir 失败: {}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首次启动迁移:扫描副本文件,合并到 config.json,并归档副本;旧 config.json 先归档
|
// 确保配置结构就绪(已移除旧版本的副本迁移逻辑)
|
||||||
{
|
{
|
||||||
let mut config_guard = app_state.config.write().unwrap();
|
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::Claude);
|
||||||
config_guard.ensure_app(&app_config::AppType::Codex);
|
config_guard.ensure_app(&app_config::AppType::Codex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String>, 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<String> {
|
|
||||||
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<String> {
|
|
||||||
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::<Value>(&p) {
|
|
||||||
items.push((name.to_string(), p, val));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
fn scan_codex_copies() -> Vec<(String, Option<PathBuf>, Option<PathBuf>, Value)> {
|
|
||||||
let mut by_name: HashMap<String, (Option<PathBuf>, Option<PathBuf>)> = 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::<Value>(&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<bool, AppError> {
|
|
||||||
// 如果已迁移过则跳过;若目录不存在则先创建,避免新装用户写入标记时失败
|
|
||||||
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::<Value>(&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<String> = manager.providers.keys().cloned().collect();
|
|
||||||
let mut live_claude_id: Option<String> = 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::<Value>(&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<String> = manager.providers.keys().cloned().collect();
|
|
||||||
let mut live_codex_id: Option<String> = 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<String>,
|
|
||||||
) -> usize {
|
|
||||||
let mut keep: Map<String, String> = Map::new(); // key -> id 保留
|
|
||||||
let mut remove: Vec<String> = 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
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user