Skip to main content

cc_switch_lib/services/provider/
mod.rs

1use std::collections::HashSet;
2
3mod claude;
4mod codex;
5#[cfg(test)]
6mod codex_openai_auth_tests;
7mod common;
8mod common_config;
9mod endpoints;
10mod gemini;
11mod gemini_auth;
12mod live;
13mod models;
14#[cfg(test)]
15mod tests;
16mod usage;
17
18use indexmap::IndexMap;
19use serde::Deserialize;
20use serde_json::{json, Value};
21
22use crate::app_config::{AppType, MultiAppConfig};
23use crate::codex_config::{get_codex_auth_path, get_codex_config_path};
24use crate::config::{
25    delete_file, get_claude_settings_path, get_provider_config_path, read_json_file,
26    write_json_file,
27};
28use crate::error::AppError;
29use crate::provider::Provider;
30use crate::store::AppState;
31
32use gemini_auth::GeminiAuthType;
33use live::LiveSnapshot;
34
35pub use common::migrate_legacy_codex_config;
36#[cfg(test)]
37use common::strip_codex_common_config_from_full_text;
38
39/// 供应商相关业务逻辑
40pub struct ProviderService;
41
42fn current_timestamp() -> i64 {
43    std::time::SystemTime::now()
44        .duration_since(std::time::UNIX_EPOCH)
45        .unwrap_or_default()
46        .as_millis() as i64
47}
48
49#[cfg(test)]
50fn state_from_config(config: MultiAppConfig) -> AppState {
51    let db = std::sync::Arc::new(crate::Database::memory().expect("create memory database"));
52    db.migrate_from_json(&config)
53        .expect("seed memory database from config");
54    let mut config = config;
55    ProviderService::migrate_common_config_upstream_semantics_if_needed(&db, &mut config)
56        .expect("migrate common config semantics for test state");
57    AppState {
58        db: db.clone(),
59        config: std::sync::RwLock::new(config),
60        proxy_service: crate::ProxyService::new(db),
61    }
62}
63
64#[derive(Clone)]
65struct PostCommitAction {
66    app_type: AppType,
67    provider: Provider,
68    backup: LiveSnapshot,
69    write_live_snapshot: bool,
70    sync_mcp: bool,
71    sync_codex_catalog: bool,
72    stale_codex_catalog_keys: Vec<String>,
73    refresh_snapshot: bool,
74    apply_hermes_switch_defaults: bool,
75    common_config_snippet: Option<String>,
76    takeover_active: bool,
77    /// When true, user-facing preference keys (approval_mode, disable_response_storage,
78    /// etc.) in the current live config are preserved when writing the Codex live config.
79    /// Set to false for common-snippet-only operations where old snippet values should
80    /// not bleed through.
81    preserve_live_preferences: bool,
82}
83
84#[derive(Debug, Clone, Default, PartialEq, Eq)]
85pub struct CodexImportReport {
86    pub created: usize,
87    pub merged_by_key: usize,
88    pub merged_by_name: usize,
89    pub needs_auth: usize,
90    pub conflicts: usize,
91    pub used_default_fallback: bool,
92}
93
94impl CodexImportReport {
95    pub fn imported_any(&self) -> bool {
96        self.created > 0
97            || self.merged_by_key > 0
98            || self.merged_by_name > 0
99            || self.used_default_fallback
100    }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub(crate) struct CodexCurrentProviderMismatch {
105    pub(crate) stored_provider_id: String,
106    pub(crate) stored_provider_name: String,
107    pub(crate) live_provider_id: String,
108    pub(crate) live_provider_name: String,
109    pub(crate) live_model_provider_key: String,
110}
111
112impl ProviderService {
113    fn is_codex_official_provider(provider: &Provider) -> bool {
114        provider
115            .meta
116            .as_ref()
117            .and_then(|meta| meta.codex_official)
118            .unwrap_or(false)
119            || provider
120                .category
121                .as_deref()
122                .is_some_and(|value| value.eq_ignore_ascii_case("official"))
123    }
124
125    fn codex_config_has_base_url(config_text: &str) -> bool {
126        let Ok(table) = toml::from_str::<toml::Table>(config_text.trim()) else {
127            return false;
128        };
129
130        if table
131            .get("base_url")
132            .and_then(|value| value.as_str())
133            .is_some_and(|value| !value.trim().is_empty())
134        {
135            return true;
136        }
137
138        let Some(provider_key) = table.get("model_provider").and_then(|value| value.as_str())
139        else {
140            return false;
141        };
142
143        table
144            .get("model_providers")
145            .and_then(|value| value.as_table())
146            .and_then(|providers| providers.get(provider_key))
147            .and_then(|value| value.as_table())
148            .and_then(|provider| provider.get("base_url"))
149            .and_then(|value| value.as_str())
150            .is_some_and(|value| !value.trim().is_empty())
151    }
152
153    pub fn sync_openclaw_to_live(state: &AppState) -> Result<(), AppError> {
154        let (providers, snippet) = {
155            let guard = state.config.read().map_err(AppError::from)?;
156            let Some(manager) = guard.get_manager(&AppType::OpenClaw) else {
157                return Ok(());
158            };
159
160            (
161                manager
162                    .providers
163                    .values()
164                    .filter(|provider| Self::provider_live_config_managed(provider) != Some(false))
165                    .cloned()
166                    .collect::<Vec<_>>(),
167                guard
168                    .common_config_snippets
169                    .get(&AppType::OpenClaw)
170                    .cloned(),
171            )
172        };
173
174        for provider in &providers {
175            Self::write_live_snapshot(
176                &AppType::OpenClaw,
177                provider,
178                snippet.as_deref(),
179                true,
180                true,
181            )?;
182        }
183
184        Ok(())
185    }
186
187    pub(crate) fn valid_openclaw_live_provider_ids() -> Result<Option<HashSet<String>>, AppError> {
188        if !crate::openclaw_config::get_openclaw_config_path().exists() {
189            return Ok(None);
190        }
191
192        let mut valid_provider_ids = HashSet::new();
193        for (provider_id, live_provider) in crate::openclaw_config::get_providers()? {
194            if provider_id.trim().is_empty() {
195                continue;
196            }
197
198            let Ok(config) = Self::parse_openclaw_provider_settings(&live_provider) else {
199                continue;
200            };
201
202            if Self::validate_openclaw_provider_models(&provider_id, &config).is_err() {
203                continue;
204            }
205
206            if config.models.iter().any(|model| model.id.trim().is_empty()) {
207                continue;
208            }
209
210            valid_provider_ids.insert(provider_id);
211        }
212
213        Ok(Some(valid_provider_ids))
214    }
215
216    fn provider_live_config_managed(provider: &Provider) -> Option<bool> {
217        provider
218            .meta
219            .as_ref()
220            .and_then(|meta| meta.live_config_managed)
221    }
222
223    fn set_provider_live_config_managed(provider: &mut Provider, managed: bool) {
224        provider
225            .meta
226            .get_or_insert_with(Default::default)
227            .live_config_managed = Some(managed);
228    }
229
230    fn additive_provider_exists_in_live_config(
231        app_type: &AppType,
232        provider_id: &str,
233        live_config_managed: Option<bool>,
234    ) -> Result<bool, AppError> {
235        let read_presence = || match app_type {
236            AppType::OpenCode => crate::opencode_config::get_providers()
237                .map(|providers| providers.contains_key(provider_id)),
238            AppType::OpenClaw => Self::valid_openclaw_live_provider_ids()
239                .map(|ids| ids.is_some_and(|ids| ids.contains(provider_id))),
240            AppType::Hermes => crate::hermes_config::get_providers()
241                .map(|providers| providers.contains_key(provider_id)),
242            _ => Ok(false),
243        };
244
245        if live_config_managed == Some(false) {
246            Ok(read_presence().unwrap_or(false))
247        } else {
248            read_presence()
249        }
250    }
251
252    fn run_transaction<R, F>(state: &AppState, f: F) -> Result<R, AppError>
253    where
254        F: FnOnce(&mut MultiAppConfig) -> Result<(R, Option<PostCommitAction>), AppError>,
255    {
256        let mut guard = state.config.write().map_err(AppError::from)?;
257        let original = guard.clone();
258        let (result, action) = match f(&mut guard) {
259            Ok(value) => value,
260            Err(err) => {
261                *guard = original;
262                return Err(err);
263            }
264        };
265        drop(guard);
266
267        if let Err(save_err) = state.save() {
268            if let Err(rollback_err) = Self::restore_config_only(state, original.clone()) {
269                return Err(AppError::localized(
270                    "config.save.rollback_failed",
271                    format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
272                    format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
273                ));
274            }
275            return Err(save_err);
276        }
277
278        if let Some(action) = action {
279            if let Err(err) = Self::apply_post_commit(state, &action) {
280                if let Err(rollback_err) =
281                    Self::rollback_after_failure(state, original.clone(), action.backup.clone())
282                {
283                    return Err(AppError::localized(
284                        "post_commit.rollback_failed",
285                        format!("后置操作失败: {err};回滚失败: {rollback_err}"),
286                        format!("Post-commit step failed: {err}; rollback failed: {rollback_err}"),
287                    ));
288                }
289                return Err(err);
290            }
291        }
292
293        Ok(result)
294    }
295
296    fn run_transaction_preserving_current_providers<R, F>(
297        state: &AppState,
298        preserved_current_apps: &[AppType],
299        f: F,
300    ) -> Result<R, AppError>
301    where
302        F: FnOnce(&mut MultiAppConfig) -> Result<(R, Option<PostCommitAction>), AppError>,
303    {
304        let mut guard = state.config.write().map_err(AppError::from)?;
305        let original = guard.clone();
306        let (result, action) = match f(&mut guard) {
307            Ok(value) => value,
308            Err(err) => {
309                *guard = original;
310                return Err(err);
311            }
312        };
313        drop(guard);
314
315        if let Err(save_err) = state.save_preserving_current_providers(preserved_current_apps) {
316            if let Err(rollback_err) = Self::restore_config_only_preserving_current_providers(
317                state,
318                original.clone(),
319                preserved_current_apps,
320            ) {
321                return Err(AppError::localized(
322                    "config.save.rollback_failed",
323                    format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
324                    format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
325                ));
326            }
327            return Err(save_err);
328        }
329
330        if let Some(action) = action {
331            if let Err(err) = Self::apply_post_commit(state, &action) {
332                if let Err(rollback_err) = Self::rollback_after_failure_preserving_current_providers(
333                    state,
334                    original.clone(),
335                    preserved_current_apps,
336                    action.backup.clone(),
337                ) {
338                    return Err(AppError::localized(
339                        "post_commit.rollback_failed",
340                        format!("后置操作失败: {err};回滚失败: {rollback_err}"),
341                        format!("Post-commit step failed: {err}; rollback failed: {rollback_err}"),
342                    ));
343                }
344                return Err(err);
345            }
346        }
347
348        Ok(result)
349    }
350
351    fn restore_config_only(state: &AppState, snapshot: MultiAppConfig) -> Result<(), AppError> {
352        {
353            let mut guard = state.config.write().map_err(AppError::from)?;
354            *guard = snapshot;
355        }
356        state.save()
357    }
358
359    fn restore_config_only_preserving_current_providers(
360        state: &AppState,
361        snapshot: MultiAppConfig,
362        preserved_current_apps: &[AppType],
363    ) -> Result<(), AppError> {
364        {
365            let mut guard = state.config.write().map_err(AppError::from)?;
366            *guard = snapshot;
367        }
368        state.save_preserving_current_providers(preserved_current_apps)
369    }
370
371    fn rollback_after_failure(
372        state: &AppState,
373        snapshot: MultiAppConfig,
374        backup: LiveSnapshot,
375    ) -> Result<(), AppError> {
376        Self::restore_config_only(state, snapshot)?;
377        backup.restore()
378    }
379
380    fn rollback_after_failure_preserving_current_providers(
381        state: &AppState,
382        snapshot: MultiAppConfig,
383        preserved_current_apps: &[AppType],
384        backup: LiveSnapshot,
385    ) -> Result<(), AppError> {
386        Self::restore_config_only_preserving_current_providers(
387            state,
388            snapshot,
389            preserved_current_apps,
390        )?;
391        backup.restore()
392    }
393
394    fn apply_post_commit(state: &AppState, action: &PostCommitAction) -> Result<(), AppError> {
395        if action.takeover_active {
396            futures::executor::block_on(
397                state
398                    .proxy_service
399                    .update_live_backup_from_provider(action.app_type.as_str(), &action.provider),
400            )
401            .map_err(AppError::Message)?;
402        } else if action.write_live_snapshot {
403            let apply_common_config = action
404                .provider
405                .meta
406                .as_ref()
407                .and_then(|meta| meta.apply_common_config)
408                .unwrap_or(true);
409            Self::write_live_snapshot(
410                &action.app_type,
411                &action.provider,
412                action.common_config_snippet.as_deref(),
413                apply_common_config,
414                action.preserve_live_preferences,
415            )?;
416            if action.apply_hermes_switch_defaults {
417                crate::hermes_config::apply_switch_defaults(
418                    &action.provider.id,
419                    &action.provider.settings_config,
420                )
421                .map(|_| ())?;
422            }
423        }
424        if action.sync_mcp {
425            // Provider/config side effects must not erase Codex live drift;
426            // Codex MCP changes are handled by explicit resolve/sync actions.
427            use crate::services::mcp::McpService;
428            McpService::sync_all_enabled_except(state, &[AppType::Codex])?;
429        }
430        if !action.takeover_active
431            && action.refresh_snapshot
432            && crate::sync_policy::should_sync_live(&action.app_type)
433        {
434            Self::refresh_provider_snapshot(state, &action.app_type, &action.provider.id)?;
435        }
436        if !action.takeover_active
437            && action.sync_codex_catalog
438            && crate::sync_policy::should_sync_live(&AppType::Codex)
439        {
440            Self::sync_codex_provider_catalog_to_live(state, &action.stale_codex_catalog_keys)?;
441        }
442
443        // D6: Align upstream live flows - also sync skills (best effort, should not block provider ops).
444        if let Err(e) = crate::services::skill::SkillService::sync_all_enabled_best_effort() {
445            log::warn!("同步 Skills 失败: {e}");
446        }
447        Ok(())
448    }
449
450    fn refresh_provider_snapshot(
451        state: &AppState,
452        app_type: &AppType,
453        provider_id: &str,
454    ) -> Result<(), AppError> {
455        match app_type {
456            AppType::Claude => {
457                let settings_path = get_claude_settings_path();
458                if !settings_path.exists() {
459                    return Err(AppError::localized(
460                        "claude.live.missing",
461                        "Claude 设置文件不存在,无法刷新快照",
462                        "Claude settings file missing; cannot refresh snapshot",
463                    ));
464                }
465                let mut live_after = read_json_file::<Value>(&settings_path)?;
466                let _ = Self::normalize_claude_models_in_value(&mut live_after);
467
468                let (provider, common_snippet) = {
469                    let guard = state.config.read().map_err(AppError::from)?;
470                    (
471                        guard
472                            .get_manager(app_type)
473                            .and_then(|manager| manager.providers.get(provider_id))
474                            .cloned()
475                            .ok_or_else(|| {
476                                AppError::localized(
477                                    "provider.not_found",
478                                    format!("供应商不存在: {provider_id}"),
479                                    format!("Provider not found: {provider_id}"),
480                                )
481                            })?,
482                        guard.common_config_snippets.claude.clone(),
483                    )
484                };
485                live_after = common_config::strip_common_config_from_live_settings(
486                    app_type,
487                    &provider,
488                    live_after,
489                    common_snippet.as_deref(),
490                );
491                {
492                    let mut guard = state.config.write().map_err(AppError::from)?;
493                    if let Some(manager) = guard.get_manager_mut(app_type) {
494                        if let Some(target) = manager.providers.get_mut(provider_id) {
495                            target.settings_config = live_after;
496                        }
497                    }
498                }
499                state.save()?;
500            }
501            AppType::Codex => {
502                let auth_path = get_codex_auth_path();
503                let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
504                let common_snippet_extracted =
505                    Self::extract_codex_common_config_from_config_toml(&cfg_text)?;
506                let cfg_text_for_storage =
507                    Self::strip_codex_mcp_servers_from_snapshot_config(&cfg_text)?;
508
509                let (provider, common_snippet_for_strip) = {
510                    let guard = state.config.read().map_err(AppError::from)?;
511                    (
512                        guard
513                            .get_manager(app_type)
514                            .and_then(|manager| manager.providers.get(provider_id))
515                            .cloned()
516                            .ok_or_else(|| {
517                                AppError::localized(
518                                    "provider.not_found",
519                                    format!("供应商不存在: {provider_id}"),
520                                    format!("Provider not found: {provider_id}"),
521                                )
522                            })?,
523                        guard.common_config_snippets.codex.clone(),
524                    )
525                };
526
527                // Read auth from disk; if absent, fall back to the DB snapshot's auth
528                // so that WebDAV-synced credentials are not overwritten with empty data.
529                let auth = if auth_path.exists() {
530                    Some(read_json_file::<Value>(&auth_path)?)
531                } else {
532                    provider.settings_config.get("auth").cloned()
533                };
534
535                let effective_common_snippet = if common_snippet_for_strip
536                    .as_deref()
537                    .unwrap_or_default()
538                    .trim()
539                    .is_empty()
540                    && !common_snippet_extracted.trim().is_empty()
541                {
542                    Some(common_snippet_extracted.clone())
543                } else {
544                    common_snippet_for_strip.clone()
545                };
546
547                let mut raw_settings = serde_json::Map::new();
548                if let Some(auth) = auth {
549                    raw_settings.insert("auth".to_string(), auth);
550                }
551                raw_settings.insert("config".to_string(), Value::String(cfg_text_for_storage));
552                let mut settings_to_store = Self::normalize_settings_config_for_storage(
553                    app_type,
554                    &provider,
555                    Value::Object(raw_settings),
556                    effective_common_snippet.as_deref(),
557                )?;
558                Self::restore_codex_model_provider_for_storage_best_effort(
559                    &provider,
560                    &mut settings_to_store,
561                );
562
563                {
564                    let mut guard = state.config.write().map_err(AppError::from)?;
565                    if !common_snippet_extracted.trim().is_empty()
566                        && guard
567                            .common_config_snippets
568                            .codex
569                            .as_deref()
570                            .unwrap_or_default()
571                            .trim()
572                            .is_empty()
573                    {
574                        guard.common_config_snippets.codex = Some(common_snippet_extracted.clone());
575                        Self::normalize_existing_provider_snapshots_for_storage_best_effort(
576                            &mut guard,
577                            app_type,
578                            Some(common_snippet_extracted.as_str()),
579                        );
580                    }
581                    if let Some(manager) = guard.get_manager_mut(app_type) {
582                        if let Some(target) = manager.providers.get_mut(provider_id) {
583                            target.settings_config = settings_to_store.clone();
584                        }
585                    }
586                }
587                state.save()?;
588            }
589            AppType::Gemini => {
590                use crate::gemini_config::{
591                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
592                };
593
594                let env_path = get_gemini_env_path();
595                if !env_path.exists() {
596                    return Err(AppError::localized(
597                        "gemini.live.missing",
598                        "Gemini .env 文件不存在,无法刷新快照",
599                        "Gemini .env file missing; cannot refresh snapshot",
600                    ));
601                }
602                let env_map = read_gemini_env()?;
603                let mut live_after = env_to_json(&env_map);
604
605                let settings_path = get_gemini_settings_path();
606                let config_value = if settings_path.exists() {
607                    read_json_file(&settings_path)?
608                } else {
609                    json!({})
610                };
611
612                if let Some(obj) = live_after.as_object_mut() {
613                    obj.insert("config".to_string(), config_value);
614                }
615
616                let (provider, common_snippet) = {
617                    let guard = state.config.read().map_err(AppError::from)?;
618                    (
619                        guard
620                            .get_manager(app_type)
621                            .and_then(|manager| manager.providers.get(provider_id))
622                            .cloned()
623                            .ok_or_else(|| {
624                                AppError::localized(
625                                    "provider.not_found",
626                                    format!("供应商不存在: {provider_id}"),
627                                    format!("Provider not found: {provider_id}"),
628                                )
629                            })?,
630                        guard.common_config_snippets.gemini.clone(),
631                    )
632                };
633                let live_after = Self::normalize_settings_config_for_storage(
634                    app_type,
635                    &provider,
636                    live_after,
637                    common_snippet.as_deref(),
638                )?;
639
640                {
641                    let mut guard = state.config.write().map_err(AppError::from)?;
642                    if let Some(manager) = guard.get_manager_mut(app_type) {
643                        if let Some(target) = manager.providers.get_mut(provider_id) {
644                            target.settings_config = live_after;
645                        }
646                    }
647                }
648                state.save()?;
649            }
650            AppType::OpenCode => {
651                let providers = crate::opencode_config::get_providers()?;
652                let live_after = providers.get(provider_id).cloned().ok_or_else(|| {
653                    AppError::localized(
654                        "opencode.live.missing_provider",
655                        format!("OpenCode live 配置中缺少供应商: {provider_id}"),
656                        format!("OpenCode live config missing provider: {provider_id}"),
657                    )
658                })?;
659
660                {
661                    let mut guard = state.config.write().map_err(AppError::from)?;
662                    if let Some(manager) = guard.get_manager_mut(app_type) {
663                        if let Some(target) = manager.providers.get_mut(provider_id) {
664                            target.settings_config = live_after;
665                        }
666                    }
667                }
668                state.save()?;
669            }
670            AppType::OpenClaw => {
671                let providers = crate::openclaw_config::get_providers()?;
672                let live_after = providers.get(provider_id).cloned().ok_or_else(|| {
673                    AppError::localized(
674                        "openclaw.live.missing_provider",
675                        format!("OpenClaw live 配置中缺少供应商: {provider_id}"),
676                        format!("OpenClaw live config missing provider: {provider_id}"),
677                    )
678                })?;
679
680                {
681                    let mut guard = state.config.write().map_err(AppError::from)?;
682                    if let Some(manager) = guard.get_manager_mut(app_type) {
683                        if let Some(target) = manager.providers.get_mut(provider_id) {
684                            target.settings_config = live_after;
685                        }
686                    }
687                }
688                state.save()?;
689            }
690            AppType::Hermes => {
691                let providers = crate::hermes_config::get_providers()?;
692                let live_after = providers.get(provider_id).cloned().unwrap_or_else(|| {
693                    log::warn!(
694                        "Hermes live config missing provider '{provider_id}', using empty config"
695                    );
696                    serde_json::Value::Object(serde_json::Map::new())
697                });
698
699                {
700                    let mut guard = state.config.write().map_err(AppError::from)?;
701                    if let Some(manager) = guard.get_manager_mut(app_type) {
702                        if let Some(target) = manager.providers.get_mut(provider_id) {
703                            target.settings_config = live_after;
704                        }
705                    }
706                }
707                state.save()?;
708            }
709        }
710        Ok(())
711    }
712
713    fn capture_live_snapshot(app_type: &AppType) -> Result<LiveSnapshot, AppError> {
714        live::capture_live_snapshot(app_type)
715    }
716
717    fn validate_common_config_snippet(
718        app_type: &AppType,
719        snippet: Option<&str>,
720    ) -> Result<(), AppError> {
721        common_config::validate_common_config_snippet(app_type, snippet)
722    }
723
724    fn should_skip_common_config_migration_error(app_type: &AppType, err: &AppError) -> bool {
725        match (app_type, err) {
726            (AppType::Claude, AppError::Localized { key, .. }) => {
727                key.starts_with("common_config.claude.")
728            }
729            (AppType::Codex, AppError::Config(message)) => {
730                message.starts_with("Common config TOML parse error:")
731            }
732            (AppType::Gemini, AppError::Localized { key, .. }) => {
733                key.starts_with("common_config.gemini.")
734            }
735            _ => false,
736        }
737    }
738
739    fn migrate_old_common_config_snippet_best_effort(
740        config: &mut MultiAppConfig,
741        app_type: &AppType,
742        strict_current_provider_id: Option<&str>,
743        old_snippet: Option<&str>,
744    ) -> Result<(), AppError> {
745        let Some(old_snippet) = old_snippet.map(str::trim) else {
746            return Ok(());
747        };
748        if old_snippet.is_empty() {
749            return Ok(());
750        }
751
752        let result = match app_type {
753            AppType::Claude => Self::migrate_claude_common_config_snippet(config, old_snippet),
754            AppType::Codex => Self::migrate_codex_common_config_snippet(
755                config,
756                strict_current_provider_id,
757                old_snippet,
758            ),
759            AppType::Gemini => Self::migrate_gemini_common_config_snippet(
760                config,
761                strict_current_provider_id,
762                old_snippet,
763            ),
764            AppType::OpenCode | AppType::OpenClaw => Ok(()),
765            AppType::Hermes => Ok(()),
766        };
767
768        match result {
769            Ok(()) => Ok(()),
770            Err(err) if Self::should_skip_common_config_migration_error(app_type, &err) => {
771                log::warn!(
772                    "skip migrating {app_type} provider snapshots from invalid stored common config snippet: {err}"
773                );
774                Ok(())
775            }
776            Err(err) => Err(err),
777        }
778    }
779
780    #[doc(hidden)]
781    pub fn migrate_common_config_upstream_semantics_if_needed(
782        db: &crate::database::Database,
783        config: &mut MultiAppConfig,
784    ) -> Result<(), AppError> {
785        common_config::migrate_common_config_upstream_semantics_if_needed(db, config)
786    }
787
788    fn build_common_config_post_commit_action(
789        config: &MultiAppConfig,
790        app_type: &AppType,
791        current_provider_id: Option<&str>,
792        takeover_active: bool,
793    ) -> Result<Option<PostCommitAction>, AppError> {
794        if app_type.is_additive_mode() {
795            return Ok(None);
796        }
797
798        let Some(current_provider_id) = current_provider_id else {
799            return Ok(None);
800        };
801
802        Self::build_post_commit_action_for_current_provider(
803            config,
804            app_type,
805            &current_provider_id,
806            takeover_active,
807            false,
808        )
809    }
810
811    fn build_post_commit_action_for_current_provider(
812        config: &MultiAppConfig,
813        app_type: &AppType,
814        current_provider_id: &str,
815        takeover_active: bool,
816        preserve_live_preferences: bool,
817    ) -> Result<Option<PostCommitAction>, AppError> {
818        let provider = config
819            .get_manager(app_type)
820            .and_then(|manager| manager.providers.get(current_provider_id).cloned());
821
822        let Some(provider) = provider else {
823            return Ok(None);
824        };
825
826        Ok(Some(PostCommitAction {
827            app_type: app_type.clone(),
828            provider,
829            backup: Self::capture_live_snapshot(app_type)?,
830            write_live_snapshot: true,
831            sync_mcp: false,
832            sync_codex_catalog: matches!(app_type, AppType::Codex),
833            stale_codex_catalog_keys: Vec::new(),
834            refresh_snapshot: false,
835            apply_hermes_switch_defaults: false,
836            common_config_snippet: config.common_config_snippets.get(app_type).cloned(),
837            takeover_active,
838            preserve_live_preferences,
839        }))
840    }
841
842    fn resolve_live_apply_common_config(
843        app_type: &AppType,
844        provider: &Provider,
845        common_config_snippet: Option<&str>,
846        requested_apply_common_config: bool,
847    ) -> bool {
848        if !requested_apply_common_config {
849            return false;
850        }
851
852        common_config::provider_uses_common_config(app_type, provider, common_config_snippet)
853    }
854
855    fn normalize_provider_for_storage(
856        app_type: &AppType,
857        provider: &mut Provider,
858        common_config_snippet: Option<&str>,
859    ) -> Result<(), AppError> {
860        common_config::normalize_provider_common_config_for_storage(
861            app_type,
862            provider,
863            common_config_snippet,
864        )
865    }
866
867    pub(crate) fn normalize_settings_config_for_storage(
868        app_type: &AppType,
869        provider: &Provider,
870        settings_config: Value,
871        common_config_snippet: Option<&str>,
872    ) -> Result<Value, AppError> {
873        let mut snapshot_provider = provider.clone();
874        snapshot_provider.settings_config = settings_config;
875        Self::normalize_provider_for_storage(
876            app_type,
877            &mut snapshot_provider,
878            common_config_snippet,
879        )?;
880        Ok(snapshot_provider.settings_config)
881    }
882
883    fn restore_codex_model_provider_for_storage_best_effort(
884        provider: &Provider,
885        settings_config: &mut Value,
886    ) {
887        if let Err(err) =
888            crate::codex_config::restore_codex_settings_config_model_provider_for_backfill(
889                settings_config,
890                &provider.settings_config,
891            )
892        {
893            log::warn!(
894                "Failed to restore Codex provider id while storing snapshot for '{}': {err}",
895                provider.id
896            );
897        }
898    }
899
900    pub(crate) fn remove_common_config_from_settings_for_preview(
901        app_type: &AppType,
902        settings_config: &Value,
903        common_config_snippet: &str,
904    ) -> Result<Value, AppError> {
905        common_config::remove_common_config_from_settings(
906            app_type,
907            settings_config,
908            common_config_snippet,
909        )
910    }
911
912    fn normalize_existing_provider_snapshots_for_storage(
913        config: &mut MultiAppConfig,
914        app_type: &AppType,
915        common_config_snippet: Option<&str>,
916    ) -> Result<(), AppError> {
917        let Some(manager) = config.get_manager_mut(app_type) else {
918            return Ok(());
919        };
920
921        for provider in manager.providers.values_mut() {
922            common_config::migrate_provider_subset_usage_for_storage(
923                app_type,
924                provider,
925                common_config_snippet,
926            )?;
927        }
928
929        Ok(())
930    }
931
932    fn normalize_existing_provider_snapshots_for_storage_best_effort(
933        config: &mut MultiAppConfig,
934        app_type: &AppType,
935        common_config_snippet: Option<&str>,
936    ) {
937        let Some(manager) = config.get_manager_mut(app_type) else {
938            return;
939        };
940
941        for (provider_id, provider) in manager.providers.iter_mut() {
942            if let Err(err) = common_config::migrate_provider_subset_usage_for_storage(
943                app_type,
944                provider,
945                common_config_snippet,
946            ) {
947                log::warn!(
948                    "skip normalizing {app_type} provider snapshot '{provider_id}' while applying auto-extracted common config: {err}"
949                );
950            }
951        }
952    }
953
954    fn normalize_existing_provider_snapshots_for_storage_strict_current_best_effort_others(
955        config: &mut MultiAppConfig,
956        app_type: &AppType,
957        strict_current_provider_id: Option<&str>,
958        common_config_snippet: Option<&str>,
959    ) -> Result<(), AppError> {
960        let Some(current_provider_id) = strict_current_provider_id.and_then(|provider_id| {
961            config.get_manager(app_type).and_then(|manager| {
962                manager
963                    .providers
964                    .contains_key(provider_id)
965                    .then(|| provider_id.to_string())
966            })
967        }) else {
968            return Self::normalize_existing_provider_snapshots_for_storage(
969                config,
970                app_type,
971                common_config_snippet,
972            );
973        };
974
975        let Some(manager) = config.get_manager_mut(app_type) else {
976            return Ok(());
977        };
978
979        if let Some(current_provider) = manager.providers.get_mut(&current_provider_id) {
980            common_config::migrate_provider_subset_usage_for_storage(
981                app_type,
982                current_provider,
983                common_config_snippet,
984            )?;
985        }
986
987        for (provider_id, provider) in manager.providers.iter_mut() {
988            if provider_id == &current_provider_id {
989                continue;
990            }
991
992            if let Err(err) = common_config::migrate_provider_subset_usage_for_storage(
993                app_type,
994                provider,
995                common_config_snippet,
996            ) {
997                log::warn!(
998                    "skip normalizing {app_type} non-current provider snapshot '{provider_id}' while updating common config snippet: {err}"
999                );
1000            }
1001        }
1002
1003        Ok(())
1004    }
1005
1006    fn hydrate_missing_provider_snapshots_from_db(
1007        config: &mut MultiAppConfig,
1008        app_type: &AppType,
1009        db_providers: &IndexMap<String, Provider>,
1010    ) -> Result<(), AppError> {
1011        let manager = config
1012            .get_manager_mut(app_type)
1013            .ok_or_else(|| Self::app_not_found(app_type))?;
1014
1015        for (provider_id, provider) in db_providers {
1016            manager
1017                .providers
1018                .entry(provider_id.clone())
1019                .or_insert_with(|| provider.clone());
1020        }
1021
1022        Ok(())
1023    }
1024
1025    pub fn set_common_config_snippet(
1026        state: &AppState,
1027        app_type: AppType,
1028        snippet: Option<String>,
1029    ) -> Result<(), AppError> {
1030        let normalized_snippet = snippet.and_then(|value| {
1031            let trimmed = value.trim();
1032            if trimmed.is_empty() {
1033                None
1034            } else {
1035                Some(trimmed.to_string())
1036            }
1037        });
1038        Self::validate_common_config_snippet(&app_type, normalized_snippet.as_deref())?;
1039
1040        let app_type_clone = app_type.clone();
1041        let (effective_current_provider, db_providers) = if app_type.is_additive_mode() {
1042            (None, None)
1043        } else {
1044            (
1045                crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1046                Some(state.db.get_all_providers(app_type.as_str())?),
1047            )
1048        };
1049        let takeover_active = if app_type.is_additive_mode() {
1050            false
1051        } else {
1052            let is_running = state
1053                .proxy_service
1054                .is_running_blocking()
1055                .map_err(AppError::Message)?;
1056            if !is_running {
1057                false
1058            } else {
1059                state
1060                    .proxy_service
1061                    .is_app_takeover_active_blocking(&app_type)
1062                    .map_err(AppError::Message)?
1063            }
1064        };
1065
1066        Self::run_transaction_preserving_current_providers(
1067            state,
1068            std::slice::from_ref(&app_type),
1069            move |config| {
1070                config.ensure_app(&app_type_clone);
1071
1072                if let Some(db_providers) = db_providers.as_ref() {
1073                    Self::hydrate_missing_provider_snapshots_from_db(
1074                        config,
1075                        &app_type_clone,
1076                        db_providers,
1077                    )?;
1078                }
1079
1080                let old_snippet = config
1081                    .common_config_snippets
1082                    .get(&app_type_clone)
1083                    .cloned()
1084                    .filter(|value| !value.trim().is_empty());
1085
1086                Self::migrate_old_common_config_snippet_best_effort(
1087                    config,
1088                    &app_type_clone,
1089                    effective_current_provider.as_deref(),
1090                    old_snippet.as_deref(),
1091                )?;
1092
1093                config
1094                    .common_config_snippets
1095                    .set(&app_type_clone, normalized_snippet.clone());
1096
1097                if matches!(
1098                    app_type_clone,
1099                    AppType::Claude | AppType::Codex | AppType::Gemini
1100                ) {
1101                    Self::normalize_existing_provider_snapshots_for_storage_strict_current_best_effort_others(
1102                    config,
1103                    &app_type_clone,
1104                    effective_current_provider.as_deref(),
1105                    normalized_snippet.as_deref(),
1106                )?;
1107                }
1108
1109                let action = Self::build_common_config_post_commit_action(
1110                    config,
1111                    &app_type_clone,
1112                    effective_current_provider.as_deref(),
1113                    takeover_active,
1114                )?;
1115                Ok(((), action))
1116            },
1117        )
1118    }
1119
1120    pub fn clear_common_config_snippet(
1121        state: &AppState,
1122        app_type: AppType,
1123    ) -> Result<(), AppError> {
1124        Self::set_common_config_snippet(state, app_type, None)
1125    }
1126
1127    /// 列出指定应用下的所有供应商
1128    pub fn list(
1129        state: &AppState,
1130        app_type: AppType,
1131    ) -> Result<IndexMap<String, Provider>, AppError> {
1132        let config = state.config.read().map_err(AppError::from)?;
1133        let manager = config
1134            .get_manager(&app_type)
1135            .ok_or_else(|| Self::app_not_found(&app_type))?;
1136        Ok(manager.get_all_providers().clone())
1137    }
1138
1139    pub(crate) fn sync_openclaw_providers_from_live(state: &AppState) -> Result<(), AppError> {
1140        live::sync_openclaw_providers_from_live(state)?;
1141        Ok(())
1142    }
1143
1144    /// 获取当前供应商 ID
1145    pub fn current(state: &AppState, app_type: AppType) -> Result<String, AppError> {
1146        if app_type == AppType::Hermes {
1147            return Ok(crate::hermes_config::get_model_config()?
1148                .and_then(|model| model.provider)
1149                .map(|provider| provider.trim().to_string())
1150                .filter(|provider| !provider.is_empty())
1151                .unwrap_or_default());
1152        }
1153        if app_type.is_additive_mode() {
1154            return Ok(String::new());
1155        }
1156        crate::settings::get_effective_current_provider(&state.db, &app_type)
1157            .map(|opt| opt.unwrap_or_default())
1158    }
1159
1160    /// 新增供应商
1161    pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
1162        let mut provider = provider;
1163        // 归一化 Claude 模型键
1164        Self::normalize_provider_if_claude(&app_type, &mut provider);
1165        Self::validate_provider_settings(&app_type, &provider)?;
1166
1167        let app_type_clone = app_type.clone();
1168        let provider_clone = provider.clone();
1169        let stored_current_provider = if app_type.is_additive_mode() {
1170            None
1171        } else {
1172            state.db.get_current_provider(app_type.as_str())?
1173        };
1174
1175        Self::run_transaction(state, move |config| {
1176            let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1177            let mut provider_to_store = provider_clone.clone();
1178            Self::normalize_provider_for_storage(
1179                &app_type_clone,
1180                &mut provider_to_store,
1181                common_config_snippet.as_deref(),
1182            )?;
1183
1184            if matches!(app_type_clone, AppType::OpenClaw)
1185                && provider_to_store.created_at.is_none()
1186                && live::is_auto_mirrored_openclaw_snapshot(&provider_to_store)
1187            {
1188                provider_to_store.created_at = Some(current_timestamp());
1189            }
1190            if app_type_clone.is_additive_mode() {
1191                Self::set_provider_live_config_managed(&mut provider_to_store, true);
1192            }
1193
1194            config.ensure_app(&app_type_clone);
1195            let manager = config
1196                .get_manager_mut(&app_type_clone)
1197                .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1198
1199            if !app_type_clone.is_additive_mode() {
1200                manager.current = stored_current_provider.clone().unwrap_or_default();
1201            }
1202
1203            let was_empty = manager.providers.is_empty();
1204            manager
1205                .providers
1206                .insert(provider_to_store.id.clone(), provider_to_store.clone());
1207
1208            if !app_type_clone.is_additive_mode()
1209                && stored_current_provider.is_none()
1210                && (was_empty || manager.current.is_empty())
1211            {
1212                manager.current = provider_to_store.id.clone();
1213            }
1214
1215            let is_current =
1216                app_type_clone.is_additive_mode() || manager.current == provider_to_store.id;
1217            let action = if is_current {
1218                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1219                Some(PostCommitAction {
1220                    app_type: app_type_clone.clone(),
1221                    provider: provider_to_store.clone(),
1222                    backup,
1223                    write_live_snapshot: true,
1224                    // Codex write uses merge which preserves [mcp_servers] as-is,
1225                    // so no MCP re-sync is needed (would lose comment-only lines).
1226                    sync_mcp: false,
1227                    sync_codex_catalog: matches!(&app_type_clone, AppType::Codex),
1228                    stale_codex_catalog_keys: Vec::new(),
1229                    refresh_snapshot: false,
1230                    apply_hermes_switch_defaults: false,
1231                    common_config_snippet,
1232                    takeover_active: false,
1233                    preserve_live_preferences: true,
1234                })
1235            } else if matches!(&app_type_clone, AppType::Codex) {
1236                Some(PostCommitAction {
1237                    app_type: app_type_clone.clone(),
1238                    provider: provider_to_store.clone(),
1239                    backup: Self::capture_live_snapshot(&app_type_clone)?,
1240                    write_live_snapshot: false,
1241                    sync_mcp: false,
1242                    sync_codex_catalog: true,
1243                    stale_codex_catalog_keys: Vec::new(),
1244                    refresh_snapshot: false,
1245                    apply_hermes_switch_defaults: false,
1246                    common_config_snippet,
1247                    takeover_active: false,
1248                    preserve_live_preferences: true,
1249                })
1250            } else {
1251                None
1252            };
1253
1254            Ok((true, action))
1255        })
1256    }
1257
1258    /// 更新供应商
1259    pub fn update(
1260        state: &AppState,
1261        app_type: AppType,
1262        provider: Provider,
1263    ) -> Result<bool, AppError> {
1264        let mut provider = provider;
1265        // 归一化 Claude 模型键
1266        Self::normalize_provider_if_claude(&app_type, &mut provider);
1267        Self::validate_provider_settings(&app_type, &provider)?;
1268        let provider_id = provider.id.clone();
1269        let app_type_clone = app_type.clone();
1270        let provider_clone = provider.clone();
1271        let (effective_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
1272            (None, None)
1273        } else {
1274            (
1275                crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1276                state.db.get_current_provider(app_type.as_str())?,
1277            )
1278        };
1279
1280        Self::run_transaction(state, move |config| {
1281            let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1282            let manager = config
1283                .get_manager_mut(&app_type_clone)
1284                .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1285
1286            if !manager.providers.contains_key(&provider_id) {
1287                return Err(AppError::localized(
1288                    "provider.not_found",
1289                    format!("供应商不存在: {provider_id}"),
1290                    format!("Provider not found: {provider_id}"),
1291                ));
1292            }
1293
1294            if !app_type_clone.is_additive_mode() {
1295                manager.current = stored_current_provider.clone().unwrap_or_default();
1296            }
1297
1298            let existing_live_config_managed = manager
1299                .providers
1300                .get(&provider_id)
1301                .and_then(Self::provider_live_config_managed);
1302            let previous_codex_catalog_key = manager
1303                .providers
1304                .get(&provider_id)
1305                .and_then(Self::provider_codex_model_provider_key);
1306            let mut merged = if let Some(existing) = manager.providers.get(&provider_id) {
1307                let mut updated = provider_clone.clone();
1308                match (existing.meta.as_ref(), updated.meta.take()) {
1309                    // 前端未提供 meta,表示不修改,沿用旧值
1310                    (Some(old_meta), None) => {
1311                        updated.meta = Some(old_meta.clone());
1312                    }
1313                    (None, None) => {
1314                        updated.meta = None;
1315                    }
1316                    // 前端提供的 meta 视为权威,直接覆盖(其中 custom_endpoints 允许是空,表示删除所有自定义端点)
1317                    (_old, Some(new_meta)) => {
1318                        updated.meta = Some(new_meta);
1319                    }
1320                }
1321                if matches!(app_type_clone, AppType::OpenClaw)
1322                    && updated.created_at.is_none()
1323                    && live::is_auto_mirrored_openclaw_snapshot(&updated)
1324                {
1325                    updated.created_at = Some(current_timestamp());
1326                }
1327                updated
1328            } else {
1329                provider_clone.clone()
1330            };
1331
1332            Self::normalize_provider_for_storage(
1333                &app_type_clone,
1334                &mut merged,
1335                common_config_snippet.as_deref(),
1336            )?;
1337
1338            let should_write_live = if app_type_clone.is_additive_mode() {
1339                let live_config_managed = Self::additive_provider_exists_in_live_config(
1340                    &app_type_clone,
1341                    &provider_id,
1342                    Self::provider_live_config_managed(&merged).or(existing_live_config_managed),
1343                )?;
1344                Self::set_provider_live_config_managed(&mut merged, live_config_managed);
1345                live_config_managed
1346            } else {
1347                effective_current_provider.as_deref() == Some(provider_id.as_str())
1348            };
1349
1350            manager
1351                .providers
1352                .insert(provider_id.clone(), merged.clone());
1353
1354            let action = if should_write_live {
1355                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1356                Some(PostCommitAction {
1357                    app_type: app_type_clone.clone(),
1358                    provider: merged,
1359                    backup,
1360                    write_live_snapshot: true,
1361                    // Codex write uses merge which preserves [mcp_servers] as-is,
1362                    // so no MCP re-sync is needed (would lose comment-only lines).
1363                    sync_mcp: false,
1364                    sync_codex_catalog: matches!(&app_type_clone, AppType::Codex),
1365                    stale_codex_catalog_keys: Vec::new(),
1366                    refresh_snapshot: false,
1367                    apply_hermes_switch_defaults: false,
1368                    common_config_snippet,
1369                    takeover_active: false,
1370                    preserve_live_preferences: true,
1371                })
1372            } else if matches!(&app_type_clone, AppType::Codex) {
1373                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1374                let current_codex_catalog_key = Self::provider_codex_model_provider_key(&merged);
1375                let stale_codex_catalog_keys = previous_codex_catalog_key
1376                    .filter(|old_key| {
1377                        current_codex_catalog_key.as_deref() != Some(old_key.as_str())
1378                    })
1379                    .into_iter()
1380                    .collect();
1381                Some(PostCommitAction {
1382                    app_type: app_type_clone.clone(),
1383                    provider: merged,
1384                    backup,
1385                    write_live_snapshot: false,
1386                    sync_mcp: false,
1387                    sync_codex_catalog: true,
1388                    stale_codex_catalog_keys,
1389                    refresh_snapshot: false,
1390                    apply_hermes_switch_defaults: false,
1391                    common_config_snippet,
1392                    takeover_active: false,
1393                    preserve_live_preferences: true,
1394                })
1395            } else {
1396                None
1397            };
1398
1399            Ok((true, action))
1400        })
1401    }
1402
1403    /// 导入当前 live 配置为默认供应商。
1404    ///
1405    /// 返回 `Ok(true)` 表示实际导入,`Ok(false)` 表示该 app 已有非官方 seed provider 而跳过。
1406    pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
1407        if app_type.is_additive_mode() {
1408            return Ok(false);
1409        }
1410
1411        if state.db.has_non_official_seed_provider(app_type.as_str())? {
1412            return Ok(false);
1413        }
1414
1415        let settings_config = match app_type {
1416            AppType::Codex => {
1417                let auth_path = get_codex_auth_path();
1418                if !auth_path.exists() {
1419                    return Err(AppError::localized(
1420                        "codex.live.missing",
1421                        "Codex 配置文件不存在",
1422                        "Codex configuration file is missing",
1423                    ));
1424                }
1425                let auth: Value = read_json_file(&auth_path)?;
1426                let config_str = crate::codex_config::read_and_validate_codex_config_text()?;
1427                json!({ "auth": auth, "config": config_str })
1428            }
1429            AppType::Claude => {
1430                let settings_path = get_claude_settings_path();
1431                if !settings_path.exists() {
1432                    return Err(AppError::localized(
1433                        "claude.live.missing",
1434                        "Claude Code 配置文件不存在",
1435                        "Claude settings file is missing",
1436                    ));
1437                }
1438                let mut v = read_json_file::<Value>(&settings_path)?;
1439                let _ = Self::normalize_claude_models_in_value(&mut v);
1440                v
1441            }
1442            AppType::Gemini => {
1443                use crate::gemini_config::{
1444                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1445                };
1446
1447                // 读取 .env 文件(环境变量)
1448                let env_path = get_gemini_env_path();
1449                if !env_path.exists() {
1450                    return Err(AppError::localized(
1451                        "gemini.live.missing",
1452                        "Gemini 配置文件不存在",
1453                        "Gemini configuration file is missing",
1454                    ));
1455                }
1456
1457                let env_map = read_gemini_env()?;
1458                let env_json = env_to_json(&env_map);
1459                let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1460
1461                // 读取 settings.json 文件(MCP 配置等)
1462                let settings_path = get_gemini_settings_path();
1463                let config_obj = if settings_path.exists() {
1464                    read_json_file(&settings_path)?
1465                } else {
1466                    json!({})
1467                };
1468
1469                // 返回完整结构:{ "env": {...}, "config": {...} }
1470                json!({
1471                    "env": env_obj,
1472                    "config": config_obj
1473                })
1474            }
1475            AppType::OpenCode => unreachable!("additive mode apps are handled earlier"),
1476            AppType::OpenClaw => unreachable!("additive mode apps are handled earlier"),
1477            AppType::Hermes => unreachable!("additive mode apps are handled earlier"),
1478        };
1479
1480        let mut provider = Provider::with_id(
1481            "default".to_string(),
1482            "default".to_string(),
1483            settings_config,
1484            None,
1485        );
1486        provider.category = Some("custom".to_string());
1487
1488        state.db.save_provider(app_type.as_str(), &provider)?;
1489        state
1490            .db
1491            .set_current_provider(app_type.as_str(), &provider.id)?;
1492        {
1493            let mut guard = state.config.write().map_err(AppError::from)?;
1494            guard.ensure_app(&app_type);
1495            let manager = guard
1496                .get_manager_mut(&app_type)
1497                .ok_or_else(|| AppError::Config("manager missing after ensure_app".into()))?;
1498            manager.current = provider.id.clone();
1499            manager.providers.insert(provider.id.clone(), provider);
1500        }
1501        Ok(true)
1502    }
1503
1504    /// 读取当前 live 配置
1505    pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
1506        match app_type {
1507            AppType::Codex => {
1508                let auth_path = get_codex_auth_path();
1509                let config_path = get_codex_config_path();
1510                if !config_path.exists() {
1511                    return Err(AppError::localized(
1512                        "codex.live.missing",
1513                        "Codex 配置文件不存在",
1514                        "Codex configuration is missing",
1515                    ));
1516                }
1517
1518                let mut live_settings = serde_json::Map::new();
1519                if auth_path.exists() {
1520                    live_settings.insert("auth".to_string(), read_json_file(&auth_path)?);
1521                }
1522                if config_path.exists() {
1523                    let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
1524                    live_settings.insert("config".to_string(), Value::String(cfg_text));
1525                }
1526
1527                Ok(Value::Object(live_settings))
1528            }
1529            AppType::Claude => {
1530                let path = get_claude_settings_path();
1531                if !path.exists() {
1532                    return Err(AppError::localized(
1533                        "claude.live.missing",
1534                        "Claude Code 配置文件不存在",
1535                        "Claude settings file is missing",
1536                    ));
1537                }
1538                read_json_file(&path)
1539            }
1540            AppType::Gemini => {
1541                use crate::gemini_config::{
1542                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1543                };
1544
1545                // 读取 .env 文件(环境变量)
1546                let env_path = get_gemini_env_path();
1547                if !env_path.exists() {
1548                    return Err(AppError::localized(
1549                        "gemini.env.missing",
1550                        "Gemini .env 文件不存在",
1551                        "Gemini .env file not found",
1552                    ));
1553                }
1554
1555                let env_map = read_gemini_env()?;
1556                let env_json = env_to_json(&env_map);
1557                let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1558
1559                // 读取 settings.json 文件(MCP 配置等)
1560                let settings_path = get_gemini_settings_path();
1561                let config_obj = if settings_path.exists() {
1562                    read_json_file(&settings_path)?
1563                } else {
1564                    json!({})
1565                };
1566
1567                // 返回完整结构:{ "env": {...}, "config": {...} }
1568                Ok(json!({
1569                    "env": env_obj,
1570                    "config": config_obj
1571                }))
1572            }
1573            AppType::OpenCode => {
1574                let config_path = crate::opencode_config::get_opencode_config_path();
1575                if !config_path.exists() {
1576                    return Err(AppError::localized(
1577                        "opencode.config.missing",
1578                        "OpenCode 配置文件不存在",
1579                        "OpenCode configuration file not found",
1580                    ));
1581                }
1582                crate::opencode_config::read_opencode_config()
1583            }
1584            AppType::OpenClaw => {
1585                let config_path = crate::openclaw_config::get_openclaw_config_path();
1586                if !config_path.exists() {
1587                    return Err(AppError::localized(
1588                        "openclaw.config.missing",
1589                        "OpenClaw 配置文件不存在",
1590                        "OpenClaw configuration file not found",
1591                    ));
1592                }
1593                crate::openclaw_config::read_openclaw_config()
1594            }
1595            AppType::Hermes => {
1596                let yaml = crate::hermes_config::read_hermes_config()?;
1597                crate::hermes_config::yaml_to_json(&yaml)
1598            }
1599        }
1600    }
1601
1602    /// 更新供应商排序
1603    pub fn update_sort_order(
1604        state: &AppState,
1605        app_type: AppType,
1606        updates: Vec<ProviderSortUpdate>,
1607    ) -> Result<bool, AppError> {
1608        {
1609            let mut cfg = state.config.write().map_err(AppError::from)?;
1610            let manager = cfg
1611                .get_manager_mut(&app_type)
1612                .ok_or_else(|| Self::app_not_found(&app_type))?;
1613
1614            for update in updates {
1615                if let Some(provider) = manager.providers.get_mut(&update.id) {
1616                    provider.sort_index = Some(update.sort_index);
1617                }
1618            }
1619        }
1620
1621        state.save()?;
1622        Ok(true)
1623    }
1624
1625    pub fn remove_from_live_config(
1626        state: &AppState,
1627        app_type: AppType,
1628        provider_id: &str,
1629    ) -> Result<(), AppError> {
1630        if !app_type.is_additive_mode() {
1631            return Err(AppError::localized(
1632                "provider.remove_from_live_config.unsupported",
1633                "只有累加模式应用支持从 live 配置中移除供应商",
1634                "Only additive-mode apps support removing a provider from live config",
1635            ));
1636        }
1637
1638        let original = {
1639            let config = state.config.read().map_err(AppError::from)?;
1640            let manager = config
1641                .get_manager(&app_type)
1642                .ok_or_else(|| Self::app_not_found(&app_type))?;
1643            if !manager.providers.contains_key(provider_id) {
1644                return Err(AppError::localized(
1645                    "provider.not_found",
1646                    format!("供应商不存在: {provider_id}"),
1647                    format!("Provider not found: {provider_id}"),
1648                ));
1649            }
1650            config.clone()
1651        };
1652
1653        let backup = Self::capture_live_snapshot(&app_type)?;
1654        match &app_type {
1655            AppType::OpenCode => {
1656                if crate::opencode_config::get_opencode_dir().exists() {
1657                    crate::opencode_config::remove_provider(provider_id)?;
1658                }
1659            }
1660            AppType::OpenClaw => {
1661                if crate::openclaw_config::get_openclaw_dir().exists() {
1662                    crate::openclaw_config::remove_provider(provider_id)?;
1663                }
1664            }
1665            _ => unreachable!("non-additive apps should not enter remove-from-live branch"),
1666        }
1667
1668        {
1669            let mut config = state.config.write().map_err(AppError::from)?;
1670            let manager = config
1671                .get_manager_mut(&app_type)
1672                .ok_or_else(|| Self::app_not_found(&app_type))?;
1673            let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
1674                AppError::localized(
1675                    "provider.not_found",
1676                    format!("供应商不存在: {provider_id}"),
1677                    format!("Provider not found: {provider_id}"),
1678                )
1679            })?;
1680            Self::set_provider_live_config_managed(provider, false);
1681        }
1682
1683        if let Err(save_err) = state.save() {
1684            let config_restore = Self::restore_config_only(state, original);
1685            let live_restore = backup.restore();
1686            if let Err(rollback_err) = config_restore {
1687                return Err(AppError::localized(
1688                    "config.save.rollback_failed",
1689                    format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
1690                    format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
1691                ));
1692            }
1693            if let Err(rollback_err) = live_restore {
1694                return Err(AppError::localized(
1695                    "post_commit.rollback_failed",
1696                    format!("保存配置失败: {save_err};live 回滚失败: {rollback_err}"),
1697                    format!(
1698                        "Failed to save config: {save_err}; live rollback failed: {rollback_err}"
1699                    ),
1700                ));
1701            }
1702            return Err(save_err);
1703        }
1704
1705        Ok(())
1706    }
1707
1708    /// 将所有应用的当前供应商配置同步到 live 文件。
1709    ///
1710    /// 用于 WebDAV 下载、备份恢复等场景:数据库已更新,但 live 配置文件
1711    /// (`~/.codex/config.toml`、Claude `settings.json` 等)尚未同步。
1712    /// 对齐上游 `sync_current_to_live` 行为。
1713    pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
1714        use crate::services::mcp::McpService;
1715
1716        // 在读锁下收集所有需要的数据,避免持锁写文件
1717        let snapshots: Vec<(AppType, Provider, Option<String>)> = {
1718            let guard = state.config.read().map_err(AppError::from)?;
1719            let mut result = Vec::new();
1720            for app_type in AppType::all() {
1721                if app_type.is_additive_mode() {
1722                    if let Some(manager) = guard.get_manager(&app_type) {
1723                        let snippet = guard.common_config_snippets.get(&app_type).cloned();
1724                        for provider in manager.providers.values() {
1725                            if Self::provider_live_config_managed(provider) == Some(false) {
1726                                continue;
1727                            }
1728                            result.push((app_type.clone(), provider.clone(), snippet.clone()));
1729                        }
1730                    }
1731                    continue;
1732                }
1733
1734                let current_id =
1735                    match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
1736                        Some(id) => id,
1737                        None => continue,
1738                    };
1739                let providers = state.db.get_all_providers(app_type.as_str())?;
1740                match providers.get(&current_id) {
1741                    Some(provider) => {
1742                        let snippet = state.db.get_config_snippet(app_type.as_str())?;
1743                        result.push((app_type.clone(), provider.clone(), snippet));
1744                    }
1745                    None => {
1746                        log::warn!(
1747                            "sync_current_to_live: {app_type} 当前供应商 {} 不存在于数据库,跳过",
1748                            current_id
1749                        );
1750                    }
1751                }
1752            }
1753            result
1754        };
1755
1756        let openclaw_live_provider_ids = match Self::valid_openclaw_live_provider_ids() {
1757            Ok(provider_ids) => provider_ids,
1758            Err(err) => {
1759                log::warn!(
1760                    "sync_current_to_live: 读取 OpenClaw live providers 失败,跳过 OpenClaw 同步: {err}"
1761                );
1762                None
1763            }
1764        };
1765
1766        for (app_type, provider, snippet) in &snapshots {
1767            if matches!(app_type, AppType::OpenClaw)
1768                && !openclaw_live_provider_ids
1769                    .as_ref()
1770                    .is_some_and(|provider_ids| provider_ids.contains(&provider.id))
1771            {
1772                continue;
1773            }
1774
1775            if let Err(e) =
1776                Self::write_live_snapshot(app_type, provider, snippet.as_deref(), true, true)
1777            {
1778                log::warn!("sync_current_to_live: 写入 {app_type} live 配置失败: {e}");
1779            }
1780        }
1781
1782        if snapshots
1783            .iter()
1784            .any(|(app_type, _, _)| matches!(app_type, AppType::Codex))
1785        {
1786            if let Err(e) = Self::sync_codex_provider_catalog_to_live(state, &[]) {
1787                log::warn!("sync_current_to_live: Codex provider catalog 同步失败: {e}");
1788            }
1789        }
1790
1791        if let Err(e) =
1792            crate::services::prompt::PromptService::sync_all_active_to_live_best_effort(state)
1793        {
1794            log::warn!("sync_current_to_live: Prompt 同步失败: {e}");
1795        }
1796
1797        if let Err(e) = McpService::sync_all_enabled_except(state, &[AppType::Codex]) {
1798            log::warn!("sync_current_to_live: 非 Codex MCP 同步失败: {e}");
1799        }
1800
1801        if let Err(e) = crate::services::skill::SkillService::sync_all_enabled_best_effort() {
1802            log::warn!("sync_current_to_live: Skills 同步失败: {e}");
1803        }
1804
1805        Ok(())
1806    }
1807
1808    /// 切换指定应用的供应商
1809    pub fn switch(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
1810        if !app_type.is_additive_mode() {
1811            let providers = state.db.get_all_providers(app_type.as_str())?;
1812            providers.get(provider_id).ok_or_else(|| {
1813                AppError::localized(
1814                    "provider.not_found",
1815                    format!("供应商不存在: {provider_id}"),
1816                    format!("Provider not found: {provider_id}"),
1817                )
1818            })?;
1819
1820            let is_app_taken_over =
1821                futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
1822                    .ok()
1823                    .flatten()
1824                    .is_some();
1825            let is_proxy_running = state
1826                .proxy_service
1827                .is_running_blocking()
1828                .map_err(AppError::Message)?;
1829            let live_taken_over = state
1830                .proxy_service
1831                .detect_takeover_in_live_config_for_app(&app_type);
1832            let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
1833
1834            if should_hot_switch {
1835                futures::executor::block_on(
1836                    state
1837                        .proxy_service
1838                        .hot_switch_provider(app_type.as_str(), provider_id),
1839                )
1840                .map_err(|e| AppError::Message(format!("热切换失败: {e}")))?;
1841
1842                let mut guard = state.config.write().map_err(AppError::from)?;
1843                if let Some(manager) = guard.get_manager_mut(&app_type) {
1844                    manager.current = provider_id.to_string();
1845                }
1846                return Ok(());
1847            }
1848        }
1849
1850        let app_type_clone = app_type.clone();
1851        let provider_id_owned = provider_id.to_string();
1852        let effective_current_provider = if app_type.is_additive_mode() {
1853            None
1854        } else if matches!(app_type, AppType::Codex) {
1855            Self::codex_live_current_provider_id(state)?.or(
1856                crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1857            )
1858        } else {
1859            crate::settings::get_effective_current_provider(&state.db, &app_type)?
1860        };
1861
1862        Self::run_transaction(state, move |config| {
1863            if app_type_clone.is_additive_mode() {
1864                let provider = {
1865                    let provider = config
1866                        .get_manager_mut(&app_type_clone)
1867                        .ok_or_else(|| Self::app_not_found(&app_type_clone))?
1868                        .providers
1869                        .get_mut(&provider_id_owned)
1870                        .ok_or_else(|| {
1871                            AppError::localized(
1872                                "provider.not_found",
1873                                format!("供应商不存在: {provider_id_owned}"),
1874                                format!("Provider not found: {provider_id_owned}"),
1875                            )
1876                        })?;
1877                    Self::set_provider_live_config_managed(provider, true);
1878                    provider.clone()
1879                };
1880
1881                let action = PostCommitAction {
1882                    app_type: app_type_clone.clone(),
1883                    provider,
1884                    backup: Self::capture_live_snapshot(&app_type_clone)?,
1885                    write_live_snapshot: true,
1886                    sync_mcp: matches!(app_type_clone, AppType::OpenCode),
1887                    sync_codex_catalog: false,
1888                    stale_codex_catalog_keys: Vec::new(),
1889                    refresh_snapshot: false,
1890                    apply_hermes_switch_defaults: matches!(app_type_clone, AppType::Hermes),
1891                    common_config_snippet: config
1892                        .common_config_snippets
1893                        .get(&app_type_clone)
1894                        .cloned(),
1895                    takeover_active: false,
1896                    preserve_live_preferences: true,
1897                };
1898
1899                return Ok(((), Some(action)));
1900            }
1901
1902            let backup = Self::capture_live_snapshot(&app_type_clone)?;
1903            let provider = match app_type_clone {
1904                AppType::Codex => Self::prepare_switch_codex(
1905                    config,
1906                    &provider_id_owned,
1907                    effective_current_provider.as_deref(),
1908                )?,
1909                AppType::Claude => Self::prepare_switch_claude(
1910                    config,
1911                    &provider_id_owned,
1912                    effective_current_provider.as_deref(),
1913                )?,
1914                AppType::Gemini => Self::prepare_switch_gemini(
1915                    config,
1916                    &provider_id_owned,
1917                    effective_current_provider.as_deref(),
1918                )?,
1919                AppType::OpenCode => unreachable!("additive mode handled above"),
1920                AppType::OpenClaw => unreachable!("additive mode handled above"),
1921                AppType::Hermes => unreachable!("additive mode handled above"),
1922            };
1923
1924            let action = PostCommitAction {
1925                app_type: app_type_clone.clone(),
1926                provider,
1927                backup,
1928                write_live_snapshot: true,
1929                // Codex writes provider config through a TOML merge that preserves
1930                // [mcp_servers]. A global MCP resync here would overwrite live
1931                // edits with cc-switch's stored MCP snapshot.
1932                sync_mcp: !matches!(app_type_clone, AppType::Codex),
1933                sync_codex_catalog: matches!(app_type_clone, AppType::Codex),
1934                stale_codex_catalog_keys: Vec::new(),
1935                refresh_snapshot: true,
1936                apply_hermes_switch_defaults: false,
1937                common_config_snippet: config.common_config_snippets.get(&app_type_clone).cloned(),
1938                takeover_active: false,
1939                preserve_live_preferences: true,
1940            };
1941
1942            Ok(((), Some(action)))
1943        })?;
1944
1945        if !app_type.is_additive_mode() {
1946            crate::settings::set_current_provider(&app_type, Some(provider_id))?;
1947        }
1948
1949        Ok(())
1950    }
1951
1952    fn write_live_snapshot(
1953        app_type: &AppType,
1954        provider: &Provider,
1955        common_config_snippet: Option<&str>,
1956        apply_common_config: bool,
1957        preserve_live_preferences: bool,
1958    ) -> Result<(), AppError> {
1959        let apply_common_config = Self::resolve_live_apply_common_config(
1960            app_type,
1961            provider,
1962            common_config_snippet,
1963            apply_common_config,
1964        );
1965
1966        match app_type {
1967            AppType::Codex => Self::write_codex_live(
1968                provider,
1969                common_config_snippet,
1970                apply_common_config,
1971                preserve_live_preferences,
1972            ),
1973            AppType::Claude => {
1974                Self::write_claude_live(provider, common_config_snippet, apply_common_config)
1975            }
1976            AppType::Gemini => Self::write_gemini_live(
1977                provider,
1978                if apply_common_config {
1979                    common_config_snippet
1980                } else {
1981                    None
1982                },
1983            ),
1984            AppType::OpenCode => {
1985                let config_to_write = if let Some(obj) = provider.settings_config.as_object() {
1986                    if obj.contains_key("$schema") || obj.contains_key("provider") {
1987                        obj.get("provider")
1988                            .and_then(|providers| providers.get(&provider.id))
1989                            .cloned()
1990                            .unwrap_or_else(|| provider.settings_config.clone())
1991                    } else {
1992                        provider.settings_config.clone()
1993                    }
1994                } else {
1995                    provider.settings_config.clone()
1996                };
1997
1998                match serde_json::from_value::<crate::provider::OpenCodeProviderConfig>(
1999                    config_to_write.clone(),
2000                ) {
2001                    Ok(config) => crate::opencode_config::set_typed_provider(&provider.id, &config),
2002                    Err(_) => crate::opencode_config::set_provider(&provider.id, config_to_write),
2003                }
2004            }
2005            AppType::OpenClaw => {
2006                let settings_config = provider.settings_config.clone();
2007                let looks_like_provider = settings_config.get("baseUrl").is_some()
2008                    || settings_config.get("api").is_some()
2009                    || settings_config.get("models").is_some();
2010                if !looks_like_provider {
2011                    return Ok(());
2012                }
2013
2014                let config = Self::parse_openclaw_provider_settings(&settings_config)?;
2015                Self::validate_openclaw_provider_models(&provider.id, &config)?;
2016                let write_result =
2017                    crate::openclaw_config::set_typed_provider(&provider.id, &config).map(|_| ());
2018
2019                write_result.map_err(Self::normalize_openclaw_live_write_error)
2020            }
2021            AppType::Hermes => {
2022                crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())
2023                    .map(|_| ())
2024            }
2025        }
2026    }
2027
2028    fn parse_openclaw_provider_settings(
2029        settings_config: &Value,
2030    ) -> Result<crate::provider::OpenClawProviderConfig, AppError> {
2031        let settings_obj = settings_config.as_object().ok_or_else(|| {
2032            AppError::localized(
2033                "provider.openclaw.settings.not_object",
2034                "OpenClaw 配置必须是 JSON 对象",
2035                "OpenClaw configuration must be a JSON object",
2036            )
2037        })?;
2038
2039        let legacy_aliases = Self::collect_openclaw_legacy_aliases(settings_obj);
2040        if !legacy_aliases.is_empty() {
2041            let aliases = legacy_aliases.join(", ");
2042            return Err(AppError::localized(
2043                "provider.openclaw.settings.invalid",
2044                format!(
2045                    "OpenClaw 配置使用了不支持的旧字段: {aliases}。请改用规范 OpenClaw 字段。"
2046                ),
2047                format!(
2048                    "OpenClaw config uses unsupported legacy alias keys: {aliases}. Use canonical OpenClaw keys instead."
2049                ),
2050            ));
2051        }
2052
2053        serde_json::from_value(settings_config.clone()).map_err(|err| {
2054            AppError::localized(
2055                "provider.openclaw.settings.invalid",
2056                format!("OpenClaw 配置格式无效: {err}"),
2057                format!("OpenClaw provider schema is invalid: {err}"),
2058            )
2059        })
2060    }
2061
2062    fn validate_openclaw_provider_models(
2063        provider_id: &str,
2064        config: &crate::provider::OpenClawProviderConfig,
2065    ) -> Result<(), AppError> {
2066        if config.models.is_empty() {
2067            return Err(AppError::localized(
2068                "provider.openclaw.models.missing",
2069                format!("OpenClaw 供应商 {provider_id} 至少需要一个模型"),
2070                format!("OpenClaw provider {provider_id} must define at least one model"),
2071            ));
2072        }
2073
2074        Ok(())
2075    }
2076
2077    fn collect_openclaw_legacy_aliases(
2078        settings_obj: &serde_json::Map<String, Value>,
2079    ) -> Vec<String> {
2080        let mut aliases = Vec::new();
2081
2082        for alias in ["api_key", "base_url", "options", "npm"] {
2083            if settings_obj.contains_key(alias) {
2084                aliases.push(alias.to_string());
2085            }
2086        }
2087
2088        if let Some(models) = settings_obj.get("models").and_then(Value::as_array) {
2089            for (index, model) in models.iter().enumerate() {
2090                if let Some(model_obj) = model.as_object() {
2091                    if model_obj.contains_key("context_window") {
2092                        aliases.push(format!("models[{index}].context_window"));
2093                    }
2094                }
2095            }
2096        }
2097
2098        aliases
2099    }
2100
2101    fn normalize_openclaw_live_write_error(err: AppError) -> AppError {
2102        match err {
2103            AppError::Config(message)
2104                if message.starts_with("Failed to parse OpenClaw config as JSON5:") =>
2105            {
2106                AppError::Config(message.replacen(
2107                    "Failed to parse OpenClaw config as JSON5",
2108                    "Failed to parse OpenClaw config as round-trip JSON5 document",
2109                    1,
2110                ))
2111            }
2112            other => other,
2113        }
2114    }
2115
2116    pub(crate) fn build_effective_live_snapshot(
2117        app_type: &AppType,
2118        provider: &Provider,
2119        common_config_snippet: Option<&str>,
2120        apply_common_config: bool,
2121    ) -> Result<Value, AppError> {
2122        let apply_common_config = Self::resolve_live_apply_common_config(
2123            app_type,
2124            provider,
2125            common_config_snippet,
2126            apply_common_config,
2127        );
2128
2129        match app_type {
2130            AppType::Claude => {
2131                let mut effective = common_config::build_effective_settings_with_common_config(
2132                    app_type,
2133                    provider,
2134                    common_config_snippet,
2135                    apply_common_config,
2136                )?;
2137                let _ = Self::normalize_claude_models_in_value(&mut effective);
2138                Ok(effective)
2139            }
2140            AppType::Codex => {
2141                let effective = common_config::build_effective_settings_with_common_config(
2142                    app_type,
2143                    provider,
2144                    common_config_snippet,
2145                    apply_common_config,
2146                )?;
2147                let settings = effective
2148                    .as_object()
2149                    .ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
2150                let auth = settings.get("auth").cloned();
2151                let cfg_text = settings.get("config").and_then(Value::as_str).unwrap_or("");
2152
2153                if !cfg_text.trim().is_empty() {
2154                    crate::codex_config::validate_config_toml(cfg_text)?;
2155                }
2156
2157                let mut backup = serde_json::Map::new();
2158                if let Some(auth) = auth {
2159                    backup.insert("auth".to_string(), auth);
2160                }
2161                backup.insert("config".to_string(), Value::String(cfg_text.to_string()));
2162                Ok(Value::Object(backup))
2163            }
2164            AppType::Gemini => {
2165                let content_to_write = common_config::build_effective_settings_with_common_config(
2166                    app_type,
2167                    provider,
2168                    common_config_snippet,
2169                    apply_common_config,
2170                )?;
2171
2172                let env_obj = content_to_write
2173                    .get("env")
2174                    .cloned()
2175                    .unwrap_or_else(|| json!({}));
2176                let settings_path = crate::gemini_config::get_gemini_settings_path();
2177                let config_value = if let Some(config_value) = content_to_write.get("config") {
2178                    if config_value.is_null() {
2179                        if settings_path.exists() {
2180                            read_json_file(&settings_path)?
2181                        } else {
2182                            json!({})
2183                        }
2184                    } else if let Some(provider_config) = config_value.as_object() {
2185                        if provider_config.is_empty() {
2186                            if settings_path.exists() {
2187                                read_json_file(&settings_path)?
2188                            } else {
2189                                json!({})
2190                            }
2191                        } else {
2192                            let mut merged = if settings_path.exists() {
2193                                read_json_file(&settings_path)?
2194                            } else {
2195                                json!({})
2196                            };
2197
2198                            if !merged.is_object() {
2199                                merged = json!({});
2200                            }
2201
2202                            let merged_map = merged.as_object_mut().ok_or_else(|| {
2203                                AppError::localized(
2204                                    "gemini.validation.invalid_settings",
2205                                    "Gemini 现有 settings.json 格式错误: 必须是对象",
2206                                    "Gemini existing settings.json invalid: must be a JSON object",
2207                                )
2208                            })?;
2209                            for (key, value) in provider_config {
2210                                merged_map.insert(key.clone(), value.clone());
2211                            }
2212                            merged
2213                        }
2214                    } else {
2215                        return Err(AppError::localized(
2216                            "gemini.validation.invalid_config",
2217                            "Gemini 配置格式错误: config 必须是对象或 null",
2218                            "Gemini config invalid: config must be an object or null",
2219                        ));
2220                    }
2221                } else if settings_path.exists() {
2222                    read_json_file(&settings_path)?
2223                } else {
2224                    json!({})
2225                };
2226
2227                Ok(json!({
2228                    "env": env_obj,
2229                    "config": config_value,
2230                }))
2231            }
2232            AppType::OpenCode => Err(AppError::Config(
2233                "OpenCode does not support proxy takeover backups".into(),
2234            )),
2235            AppType::OpenClaw => Err(AppError::Config(
2236                "OpenClaw does not support proxy takeover backups".into(),
2237            )),
2238            AppType::Hermes => Err(AppError::Config(
2239                "Hermes does not support proxy takeover backups".into(),
2240            )),
2241        }
2242    }
2243
2244    fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
2245        match app_type {
2246            AppType::Claude => {
2247                if !provider.settings_config.is_object() {
2248                    return Err(AppError::localized(
2249                        "provider.claude.settings.not_object",
2250                        "Claude 配置必须是 JSON 对象",
2251                        "Claude configuration must be a JSON object",
2252                    ));
2253                }
2254            }
2255            AppType::Codex => {
2256                let settings = provider.settings_config.as_object().ok_or_else(|| {
2257                    AppError::localized(
2258                        "provider.codex.settings.not_object",
2259                        "Codex 配置必须是 JSON 对象",
2260                        "Codex configuration must be a JSON object",
2261                    )
2262                })?;
2263
2264                let auth = settings.get("auth").ok_or_else(|| {
2265                    AppError::localized(
2266                        "provider.codex.auth.missing",
2267                        format!("供应商 {} 缺少 auth 配置", provider.id),
2268                        format!("Provider {} is missing auth configuration", provider.id),
2269                    )
2270                })?;
2271                if !auth.is_object() {
2272                    return Err(AppError::localized(
2273                        "provider.codex.auth.not_object",
2274                        format!("供应商 {} 的 auth 配置必须是 JSON 对象", provider.id),
2275                        format!(
2276                            "Provider {} auth configuration must be a JSON object",
2277                            provider.id
2278                        ),
2279                    ));
2280                }
2281
2282                if let Some(config_value) = settings.get("config") {
2283                    if !(config_value.is_string() || config_value.is_null()) {
2284                        return Err(AppError::localized(
2285                            "provider.codex.config.invalid_type",
2286                            "Codex config 字段必须是字符串",
2287                            "Codex config field must be a string",
2288                        ));
2289                    }
2290                    if let Some(cfg_text) = config_value.as_str() {
2291                        crate::codex_config::validate_config_toml(cfg_text)?;
2292                    }
2293                }
2294
2295                if !Self::is_codex_official_provider(provider) {
2296                    let config_text = settings
2297                        .get("config")
2298                        .and_then(Value::as_str)
2299                        .unwrap_or_default();
2300                    if !Self::codex_config_has_base_url(config_text) {
2301                        return Err(AppError::localized(
2302                            "provider.codex.base_url.missing",
2303                            format!("供应商 {} 缺少有效的 Codex Base URL", provider.id),
2304                            format!("Provider {} is missing a valid Codex base_url", provider.id),
2305                        ));
2306                    }
2307                }
2308            }
2309            AppType::Gemini => {
2310                use crate::gemini_config::validate_gemini_settings;
2311                validate_gemini_settings(&provider.settings_config)?
2312            }
2313            AppType::OpenCode => {
2314                if !provider.settings_config.is_object() {
2315                    return Err(AppError::localized(
2316                        "provider.opencode.settings.not_object",
2317                        "OpenCode 配置必须是 JSON 对象",
2318                        "OpenCode configuration must be a JSON object",
2319                    ));
2320                }
2321            }
2322            AppType::OpenClaw => {
2323                let config = Self::parse_openclaw_provider_settings(&provider.settings_config)?;
2324                Self::validate_openclaw_provider_models(&provider.id, &config)?;
2325            }
2326            AppType::Hermes => {
2327                // Hermes uses flexible YAML config; basic check that settings is an object
2328                if !provider.settings_config.is_object() {
2329                    return Err(AppError::localized(
2330                        "provider.hermes.settings.not_object",
2331                        "Hermes 供应商配置必须是 JSON 对象",
2332                        "Hermes provider configuration must be a JSON object",
2333                    ));
2334                }
2335            }
2336        }
2337
2338        // 🔧 验证并清理 UsageScript 配置(所有应用类型通用)
2339        if let Some(meta) = &provider.meta {
2340            if let Some(usage_script) = &meta.usage_script {
2341                Self::validate_usage_script(usage_script)?;
2342            }
2343        }
2344
2345        Ok(())
2346    }
2347
2348    pub(crate) fn build_live_backup_snapshot(
2349        app_type: &AppType,
2350        provider: &Provider,
2351        common_config_snippet: Option<&str>,
2352        apply_common_config: bool,
2353    ) -> Result<Value, AppError> {
2354        Self::build_effective_live_snapshot(
2355            app_type,
2356            provider,
2357            common_config_snippet,
2358            apply_common_config,
2359        )
2360    }
2361
2362    pub(crate) fn build_effective_live_snapshot_from_state(
2363        state: &AppState,
2364        app_type: AppType,
2365        provider: &Provider,
2366    ) -> Result<Value, AppError> {
2367        let common_config_snippet = {
2368            let config = state.config.read().map_err(AppError::from)?;
2369            config.common_config_snippets.get(&app_type).cloned()
2370        };
2371
2372        Self::build_effective_live_snapshot(
2373            &app_type,
2374            provider,
2375            common_config_snippet.as_deref(),
2376            true,
2377        )
2378    }
2379
2380    pub(crate) fn get_provider(
2381        state: &AppState,
2382        app_type: AppType,
2383        provider_id: &str,
2384    ) -> Result<Provider, AppError> {
2385        let config = state.config.read().map_err(AppError::from)?;
2386        let manager = config
2387            .get_manager(&app_type)
2388            .ok_or_else(|| Self::app_not_found(&app_type))?;
2389
2390        manager.providers.get(provider_id).cloned().ok_or_else(|| {
2391            AppError::localized(
2392                "provider.not_found",
2393                format!("供应商不存在: {provider_id}"),
2394                format!("Provider not found: {provider_id}"),
2395            )
2396        })
2397    }
2398
2399    fn app_not_found(app_type: &AppType) -> AppError {
2400        AppError::localized(
2401            "provider.app_not_found",
2402            format!("应用类型不存在: {app_type:?}"),
2403            format!("App type not found: {app_type:?}"),
2404        )
2405    }
2406
2407    pub fn delete(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
2408        let (local_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
2409            (None, None)
2410        } else {
2411            (
2412                crate::settings::get_current_provider(&app_type),
2413                state.db.get_current_provider(app_type.as_str())?,
2414            )
2415        };
2416        let provider_snapshot = {
2417            let config = state.config.read().map_err(AppError::from)?;
2418            let manager = config
2419                .get_manager(&app_type)
2420                .ok_or_else(|| Self::app_not_found(&app_type))?;
2421
2422            if !app_type.is_additive_mode()
2423                && (local_current_provider.as_deref() == Some(provider_id)
2424                    || stored_current_provider.as_deref() == Some(provider_id))
2425            {
2426                return Err(AppError::localized(
2427                    "provider.delete.current",
2428                    "不能删除当前正在使用的供应商",
2429                    "Cannot delete the provider currently in use",
2430                ));
2431            }
2432
2433            manager.providers.get(provider_id).cloned().ok_or_else(|| {
2434                AppError::localized(
2435                    "provider.not_found",
2436                    format!("供应商不存在: {provider_id}"),
2437                    format!("Provider not found: {provider_id}"),
2438                )
2439            })?
2440        };
2441        let stale_codex_catalog_keys = if matches!(app_type, AppType::Codex) {
2442            Self::provider_codex_model_provider_key(&provider_snapshot)
2443                .into_iter()
2444                .collect::<Vec<_>>()
2445        } else {
2446            Vec::new()
2447        };
2448
2449        if app_type.is_additive_mode() {
2450            match app_type {
2451                AppType::OpenCode => {
2452                    if crate::opencode_config::get_opencode_dir().exists() {
2453                        crate::opencode_config::remove_provider(provider_id)?;
2454                    }
2455                }
2456                AppType::OpenClaw => {
2457                    if crate::openclaw_config::get_openclaw_dir().exists() {
2458                        crate::openclaw_config::remove_provider(provider_id)?;
2459                    }
2460                }
2461                AppType::Hermes => {
2462                    if crate::hermes_config::get_hermes_dir().exists() {
2463                        crate::hermes_config::remove_provider(provider_id)?;
2464                    }
2465                }
2466                _ => unreachable!("non-additive apps should not enter additive delete branch"),
2467            }
2468
2469            {
2470                let mut config = state.config.write().map_err(AppError::from)?;
2471                let manager = config
2472                    .get_manager_mut(&app_type)
2473                    .ok_or_else(|| Self::app_not_found(&app_type))?;
2474                manager.providers.shift_remove(provider_id);
2475            }
2476
2477            return state.save();
2478        }
2479
2480        match app_type {
2481            AppType::Codex => {
2482                crate::codex_config::delete_codex_provider_config(
2483                    provider_id,
2484                    &provider_snapshot.name,
2485                )?;
2486            }
2487            AppType::Claude => {
2488                // 兼容旧版本:历史上会在 Claude 目录内为每个供应商生成 settings-*.json 副本
2489                // 这里继续清理这些遗留文件,避免堆积过期配置。
2490                let by_name = get_provider_config_path(provider_id, Some(&provider_snapshot.name));
2491                let by_id = get_provider_config_path(provider_id, None);
2492                delete_file(&by_name)?;
2493                delete_file(&by_id)?;
2494            }
2495            AppType::Gemini => {
2496                // Gemini 使用单一的 .env 文件,不需要删除单独的供应商配置文件
2497            }
2498            AppType::OpenCode => {
2499                let _ = provider_snapshot;
2500            }
2501            AppType::OpenClaw => {
2502                let _ = provider_snapshot;
2503            }
2504            AppType::Hermes => {
2505                let _ = provider_snapshot;
2506            }
2507        }
2508
2509        {
2510            let mut config = state.config.write().map_err(AppError::from)?;
2511            let manager = config
2512                .get_manager_mut(&app_type)
2513                .ok_or_else(|| Self::app_not_found(&app_type))?;
2514
2515            if !app_type.is_additive_mode()
2516                && (local_current_provider.as_deref() == Some(provider_id)
2517                    || stored_current_provider.as_deref() == Some(provider_id))
2518            {
2519                return Err(AppError::localized(
2520                    "provider.delete.current",
2521                    "不能删除当前正在使用的供应商",
2522                    "Cannot delete the provider currently in use",
2523                ));
2524            }
2525
2526            if !app_type.is_additive_mode() && manager.current == provider_id {
2527                manager.current = stored_current_provider.clone().unwrap_or_default();
2528            }
2529
2530            manager.providers.shift_remove(provider_id);
2531        }
2532
2533        state.save()?;
2534        if matches!(app_type, AppType::Codex) {
2535            Self::sync_codex_provider_catalog_to_live(state, &stale_codex_catalog_keys)?;
2536        }
2537        Ok(())
2538    }
2539
2540    pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2541        live::import_openclaw_providers_from_live(state)
2542    }
2543
2544    pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2545        live::import_opencode_providers_from_live(state)
2546    }
2547
2548    pub fn import_hermes_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2549        live::import_hermes_providers_from_live(state)
2550    }
2551}
2552
2553#[derive(Debug, Clone, Deserialize)]
2554pub struct ProviderSortUpdate {
2555    pub id: String,
2556    #[serde(rename = "sortIndex")]
2557    pub sort_index: usize,
2558}