Files
cc-switch/src-tauri/src/migration.rs

210 lines
7.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::app_config::{AppType, MultiAppConfig};
use crate::config::{
archive_file, get_app_config_dir, get_app_config_path, get_claude_config_dir,
};
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 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 {
fs::read_to_string(cfgp).unwrap_or_default()
} else {
String::new()
};
// 校验 TOML若非空
if !config_str.trim().is_empty() {
if let Err(e) = toml::from_str::<toml::Table>(&config_str) {
log::warn!("跳过无效 Codex config-{}.toml: {}", name, e);
}
}
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, String> {
// 如果已迁移过则跳过
let marker = get_marker_path();
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| format!("写入迁移标记失败: {}", 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);
}
// 合并Claude
config.ensure_app(&AppType::Claude);
let manager = config.get_manager_mut(&AppType::Claude).unwrap();
let mut ids: HashSet<String> = manager.providers.keys().cloned().collect();
for (name, path, value) in claude_items.iter() {
if let Some((id, prov)) = manager
.providers
.iter_mut()
.find(|(_, p)| p.name == *name)
{
// 重名:覆盖为副本内容
log::info!("覆盖 Claude 供应商 '{}' 来自 {}", 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,
name.clone(),
value.clone(),
None,
);
manager.providers.insert(provider.id.clone(), provider);
}
}
// 合并Codex
config.ensure_app(&AppType::Codex);
let manager = config.get_manager_mut(&AppType::Codex).unwrap();
let mut ids: HashSet<String> = manager.providers.keys().cloned().collect();
for (name, authp, cfgp, value) in codex_items.iter() {
if let Some((_id, prov)) = manager
.providers
.iter_mut()
.find(|(_, p)| p.name == *name)
{
log::info!("覆盖 Codex 供应商 '{}' 来自 {:?} / {:?}", 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,
name.clone(),
value.clone(),
None,
);
manager.providers.insert(provider.id.clone(), provider);
}
}
// 归档副本文件
for (_, p, _) in claude_items.into_iter() {
let _ = archive_file(ts, "claude", &p);
}
for (_, ap, cp, _) in codex_items.into_iter() {
if let Some(ap) = ap {
let _ = archive_file(ts, "codex", &ap);
}
if let Some(cp) = cp {
let _ = archive_file(ts, "codex", &cp);
}
}
// 标记完成
fs::write(&marker, b"done").map_err(|e| format!("写入迁移标记失败: {}", e))?;
Ok(true)
}