Skip to main content

cc_switch_lib/services/
config.rs

1use super::provider::ProviderService;
2use crate::app_config::{AppType, MultiAppConfig};
3use crate::database::Database;
4use crate::error::AppError;
5use crate::provider::Provider;
6use crate::store::AppState;
7use chrono::Utc;
8use serde_json::Value;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12const MAX_BACKUPS: usize = 10;
13
14/// 备份信息
15#[derive(Debug, Clone)]
16pub struct BackupInfo {
17    /// 备份 ID(文件名不含扩展名)
18    pub id: String,
19    /// 完整文件路径
20    pub path: PathBuf,
21    /// 创建时间戳(格式化字符串)
22    pub timestamp: String,
23    /// 显示名称(用于 UI)
24    pub display_name: String,
25}
26
27/// 配置导入导出相关业务逻辑
28pub struct ConfigService;
29
30impl ConfigService {
31    /// 为当前数据库创建 SQL 备份,返回备份 ID(若数据库不存在则返回空字符串)。
32    ///
33    /// # 参数
34    /// - `config_path`: 兼容参数(忽略),保留给旧调用方
35    /// - `custom_name`: 可选的自定义名称
36    ///
37    /// # 命名规则
38    /// - 有自定义名称:`{custom_name}_{timestamp}.sql`
39    /// - 无自定义名称:`backup_{timestamp}.sql`
40    pub fn create_backup(
41        config_path: &Path,
42        custom_name: Option<String>,
43    ) -> Result<String, AppError> {
44        let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
45        if !db_path.exists() {
46            return Ok(String::new());
47        }
48
49        let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
50        let backup_id = if let Some(name) = custom_name {
51            format!("{}_{}", name, timestamp)
52        } else {
53            format!("backup_{}", timestamp)
54        };
55
56        let backup_dir = config_path
57            .parent()
58            .or_else(|| db_path.parent())
59            .ok_or_else(|| AppError::Config("Invalid config path".into()))?
60            .join("backups");
61
62        fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
63
64        let backup_path = backup_dir.join(format!("{backup_id}.sql"));
65        let db = Database::init()?;
66        db.export_sql(&backup_path)?;
67
68        Self::cleanup_old_backups(&backup_dir, MAX_BACKUPS)?;
69
70        Ok(backup_id)
71    }
72
73    /// 列出所有可用的备份
74    pub fn list_backups(config_path: &Path) -> Result<Vec<BackupInfo>, AppError> {
75        let backup_dir = config_path
76            .parent()
77            .ok_or_else(|| AppError::Config("Invalid config path".into()))?
78            .join("backups");
79
80        if !backup_dir.exists() {
81            return Ok(Vec::new());
82        }
83
84        let entries = fs::read_dir(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
85
86        let mut backups: Vec<BackupInfo> = entries
87            .filter_map(|entry| entry.ok())
88            .filter(|entry| {
89                entry
90                    .path()
91                    .extension()
92                    .map(|ext| ext == "sql")
93                    .unwrap_or(false)
94            })
95            .filter_map(|entry| {
96                let path = entry.path();
97                let filename = path.file_stem()?.to_str()?.to_string();
98
99                // 提取时间戳(假设格式为 xxx_YYYYMMDD_HHMMSS)
100                let timestamp = Self::extract_timestamp(&filename)?;
101
102                // 生成显示名称
103                let display_name = Self::format_display_name(&filename, &timestamp);
104
105                Some(BackupInfo {
106                    id: filename.clone(),
107                    path: path.clone(),
108                    timestamp,
109                    display_name,
110                })
111            })
112            .collect();
113
114        // 按时间戳降序排序(最新的在前)
115        backups.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
116
117        Ok(backups)
118    }
119
120    /// 根据备份 ID 恢复配置
121    pub fn restore_from_backup_id(backup_id: &str, state: &AppState) -> Result<String, AppError> {
122        let config_path = crate::config::get_app_config_path();
123        let backup_dir = config_path
124            .parent()
125            .ok_or_else(|| AppError::Config("Invalid config path".into()))?
126            .join("backups");
127
128        let backup_path = backup_dir.join(format!("{}.sql", backup_id));
129
130        if !backup_path.exists() {
131            return Err(AppError::Message(format!("备份文件不存在: {}", backup_id)));
132        }
133
134        Self::import_config_from_path(&backup_path, state)
135    }
136
137    /// 从文件名提取时间戳字符串
138    fn extract_timestamp(filename: &str) -> Option<String> {
139        // 尝试匹配格式:xxx_YYYYMMDD_HHMMSS
140        let parts: Vec<&str> = filename.rsplitn(3, '_').collect();
141        if parts.len() >= 2 {
142            // parts 顺序是反的:[HHMMSS, YYYYMMDD, ...]
143            Some(format!("{}_{}", parts[1], parts[0]))
144        } else {
145            None
146        }
147    }
148
149    /// 格式化显示名称
150    fn format_display_name(filename: &str, timestamp: &str) -> String {
151        // 从时间戳格式 YYYYMMDD_HHMMSS 转换为可读格式
152        if timestamp.len() == 15 {
153            // YYYYMMDD_HHMMSS
154            let date = &timestamp[0..8];
155            let time = &timestamp[9..15];
156
157            if let (Ok(y), Ok(m), Ok(d), Ok(h), Ok(min), Ok(s)) = (
158                date[0..4].parse::<u32>(),
159                date[4..6].parse::<u32>(),
160                date[6..8].parse::<u32>(),
161                time[0..2].parse::<u32>(),
162                time[2..4].parse::<u32>(),
163                time[4..6].parse::<u32>(),
164            ) {
165                let formatted_time =
166                    format!("{:04}-{:02}-{:02} {:02}:{:02}:{:02}", y, m, d, h, min, s);
167
168                // 如果是自定义名称,显示名称和时间
169                if !filename.starts_with("backup_") {
170                    let custom_name = filename.rsplitn(3, '_').nth(2).unwrap_or(filename);
171                    return format!("{} ({})", custom_name, formatted_time);
172                }
173
174                return formatted_time;
175            }
176        }
177
178        // 回退:直接返回文件名
179        filename.to_string()
180    }
181
182    fn cleanup_old_backups(backup_dir: &Path, retain: usize) -> Result<(), AppError> {
183        if retain == 0 {
184            return Ok(());
185        }
186
187        let entries = match fs::read_dir(backup_dir) {
188            Ok(iter) => iter
189                .filter_map(|entry| entry.ok())
190                .filter(|entry| {
191                    entry
192                        .path()
193                        .extension()
194                        .map(|ext| ext == "sql")
195                        .unwrap_or(false)
196                })
197                .collect::<Vec<_>>(),
198            Err(_) => return Ok(()),
199        };
200
201        if entries.len() <= retain {
202            return Ok(());
203        }
204
205        let remove_count = entries.len().saturating_sub(retain);
206        let mut sorted = entries;
207
208        sorted.sort_by(|a, b| {
209            let a_time = a.metadata().and_then(|m| m.modified()).ok();
210            let b_time = b.metadata().and_then(|m| m.modified()).ok();
211            a_time.cmp(&b_time)
212        });
213
214        for entry in sorted.into_iter().take(remove_count) {
215            if let Err(err) = fs::remove_file(entry.path()) {
216                log::warn!(
217                    "Failed to remove old backup {}: {}",
218                    entry.path().display(),
219                    err
220                );
221            }
222        }
223
224        Ok(())
225    }
226
227    /// 将当前 config.json 拷贝到目标路径。
228    pub fn export_config_to_path(target_path: &Path) -> Result<(), AppError> {
229        let db = Database::init()?;
230        db.export_sql(target_path)
231    }
232
233    pub fn import_config_from_path(file_path: &Path, state: &AppState) -> Result<String, AppError> {
234        let db_path = crate::config::get_app_config_dir().join("cc-switch.db");
235        if !db_path.exists() {
236            return Err(AppError::Config("数据库不存在,无法导入".to_string()));
237        }
238
239        // Pre-import backup (SQL).
240        let backup_id = Self::create_backup(&db_path, None)?;
241
242        // Import SQL into DB (also performs an internal binary snapshot backup).
243        state.db.import_sql(file_path)?;
244        state.refresh_config_from_db()?;
245
246        Ok(backup_id)
247    }
248
249    /// 同步当前供应商到对应的 live 配置。
250    pub fn sync_current_providers_to_live(config: &mut MultiAppConfig) -> Result<(), AppError> {
251        Self::sync_current_provider_for_app(config, &AppType::Claude)?;
252        Self::sync_current_provider_for_app(config, &AppType::Codex)?;
253        Self::sync_current_provider_for_app(config, &AppType::Gemini)?;
254        Self::sync_current_provider_for_app(config, &AppType::OpenCode)?;
255        Self::sync_current_provider_for_app(config, &AppType::OpenClaw)?;
256        Ok(())
257    }
258
259    fn sync_current_provider_for_app(
260        config: &mut MultiAppConfig,
261        app_type: &AppType,
262    ) -> Result<(), AppError> {
263        let (current_id, provider) = {
264            let manager = match config.get_manager(app_type) {
265                Some(manager) => manager,
266                None => return Ok(()),
267            };
268
269            if manager.current.is_empty() {
270                return Ok(());
271            }
272
273            let current_id = manager.current.clone();
274            let provider = match manager.providers.get(&current_id) {
275                Some(provider) => provider.clone(),
276                None => {
277                    log::warn!(
278                        "当前应用 {app_type:?} 的供应商 {current_id} 不存在,跳过 live 同步"
279                    );
280                    return Ok(());
281                }
282            };
283            (current_id, provider)
284        };
285
286        match app_type {
287            AppType::Codex => Self::sync_codex_live(config, &current_id, &provider)?,
288            AppType::Claude => Self::sync_claude_live(config, &current_id, &provider)?,
289            AppType::Gemini => Self::sync_gemini_live(config, &current_id, &provider)?,
290            AppType::OpenCode => {}
291            AppType::OpenClaw => {}
292            AppType::Hermes => {}
293        }
294
295        Ok(())
296    }
297
298    fn sync_codex_live(
299        config: &mut MultiAppConfig,
300        provider_id: &str,
301        provider: &Provider,
302    ) -> Result<(), AppError> {
303        let common_config_snippet = config.common_config_snippets.codex.clone();
304        let effective = ProviderService::build_effective_live_snapshot(
305            &AppType::Codex,
306            provider,
307            common_config_snippet.as_deref(),
308            true,
309        )?;
310        let settings = effective.as_object().ok_or_else(|| {
311            AppError::Config(format!("供应商 {provider_id} 的 Codex 配置必须是对象"))
312        })?;
313        let auth = settings.get("auth").ok_or_else(|| {
314            AppError::Config(format!("供应商 {provider_id} 的 Codex 配置缺少 auth 字段"))
315        })?;
316        if !auth.is_object() {
317            return Err(AppError::Config(format!(
318                "供应商 {provider_id} 的 Codex auth 配置必须是 JSON 对象"
319            )));
320        }
321        let cfg_text = settings.get("config").and_then(Value::as_str);
322
323        crate::codex_config::write_codex_live_atomic_with_stable_provider(auth, cfg_text)?;
324        crate::mcp::sync_enabled_to_codex(config)?;
325
326        let cfg_text_after = crate::codex_config::read_and_validate_codex_config_text()?;
327        if let Some(manager) = config.get_manager_mut(&AppType::Codex) {
328            if let Some(target) = manager.providers.get_mut(provider_id) {
329                let mut raw_settings = serde_json::Map::new();
330                raw_settings.insert("auth".to_string(), auth.clone());
331                raw_settings.insert(
332                    "config".to_string(),
333                    serde_json::Value::String(cfg_text_after),
334                );
335                target.settings_config = ProviderService::normalize_settings_config_for_storage(
336                    &AppType::Codex,
337                    provider,
338                    serde_json::Value::Object(raw_settings),
339                    common_config_snippet.as_deref(),
340                )?;
341            }
342        }
343
344        ProviderService::sync_codex_provider_catalog_to_live_from_config(config, &[])?;
345
346        Ok(())
347    }
348
349    fn sync_claude_live(
350        config: &mut MultiAppConfig,
351        provider_id: &str,
352        provider: &Provider,
353    ) -> Result<(), AppError> {
354        use crate::config::{read_json_file, write_json_file};
355
356        let settings_path = crate::config::get_claude_settings_path();
357        if let Some(parent) = settings_path.parent() {
358            fs::create_dir_all(parent).map_err(|e| AppError::io(parent, e))?;
359        }
360
361        let common_config_snippet = config.common_config_snippets.claude.clone();
362        let effective = ProviderService::build_effective_live_snapshot(
363            &AppType::Claude,
364            provider,
365            common_config_snippet.as_deref(),
366            true,
367        )?;
368
369        write_json_file(&settings_path, &effective)?;
370
371        let live_after = read_json_file::<serde_json::Value>(&settings_path)?;
372        if let Some(manager) = config.get_manager_mut(&AppType::Claude) {
373            if let Some(target) = manager.providers.get_mut(provider_id) {
374                target.settings_config = ProviderService::normalize_settings_config_for_storage(
375                    &AppType::Claude,
376                    provider,
377                    live_after,
378                    common_config_snippet.as_deref(),
379                )?;
380            }
381        }
382
383        Ok(())
384    }
385
386    fn sync_gemini_live(
387        config: &mut MultiAppConfig,
388        provider_id: &str,
389        provider: &Provider,
390    ) -> Result<(), AppError> {
391        use crate::gemini_config::{env_to_json, read_gemini_env};
392
393        let common_config_snippet = config.common_config_snippets.gemini.clone();
394        ProviderService::write_gemini_live_force(provider, common_config_snippet.as_deref())?;
395
396        // 读回实际写入的内容并更新到配置中(包含 settings.json)
397        let live_after_env = read_gemini_env()?;
398        let settings_path = crate::gemini_config::get_gemini_settings_path();
399        let live_after_config = if settings_path.exists() {
400            crate::config::read_json_file(&settings_path)?
401        } else {
402            serde_json::json!({})
403        };
404        let mut live_after = env_to_json(&live_after_env);
405        if let Some(obj) = live_after.as_object_mut() {
406            obj.insert("config".to_string(), live_after_config);
407        }
408
409        if let Some(manager) = config.get_manager_mut(&AppType::Gemini) {
410            if let Some(target) = manager.providers.get_mut(provider_id) {
411                target.settings_config = ProviderService::normalize_settings_config_for_storage(
412                    &AppType::Gemini,
413                    provider,
414                    live_after,
415                    common_config_snippet.as_deref(),
416                )?;
417            }
418        }
419
420        Ok(())
421    }
422}