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