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#[derive(Debug, Clone)]
15pub struct BackupInfo {
16 pub id: String,
18 pub path: PathBuf,
20 pub timestamp: String,
22 pub display_name: String,
24}
25
26pub struct ConfigService;
28
29impl ConfigService {
30 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 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 let timestamp = Self::extract_timestamp(&filename)?;
100
101 let display_name = Self::format_display_name(&filename, ×tamp);
103
104 Some(BackupInfo {
105 id: filename.clone(),
106 path: path.clone(),
107 timestamp,
108 display_name,
109 })
110 })
111 .collect();
112
113 backups.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
115
116 Ok(backups)
117 }
118
119 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 fn extract_timestamp(filename: &str) -> Option<String> {
138 let parts: Vec<&str> = filename.rsplitn(3, '_').collect();
140 if parts.len() >= 2 {
141 Some(format!("{}_{}", parts[1], parts[0]))
143 } else {
144 None
145 }
146 }
147
148 fn format_display_name(filename: &str, timestamp: &str) -> String {
150 if timestamp.len() == 15 {
152 let date = ×tamp[0..8];
154 let time = ×tamp[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 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 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 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 let backup_id = Self::create_backup(&db_path, None)?;
240
241 state.db.import_sql(file_path)?;
243 state.refresh_config_from_db()?;
244
245 Ok(backup_id)
246 }
247
248 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(¤t_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, ¤t_id, &provider)?,
287 AppType::Claude => Self::sync_claude_live(config, ¤t_id, &provider)?,
288 AppType::Gemini => Self::sync_gemini_live(config, ¤t_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 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}