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_runtime_local_keys_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 raw_settings = crate::codex_config::codex_settings_snapshot_from_toml(
548                    auth,
549                    &cfg_text_for_storage,
550                )?;
551                let mut settings_to_store = Self::normalize_settings_config_for_storage(
552                    app_type,
553                    &provider,
554                    raw_settings,
555                    effective_common_snippet.as_deref(),
556                )?;
557                Self::restore_codex_model_provider_for_storage_best_effort(
558                    &provider,
559                    &mut settings_to_store,
560                );
561
562                {
563                    let mut guard = state.config.write().map_err(AppError::from)?;
564                    if !common_snippet_extracted.trim().is_empty()
565                        && guard
566                            .common_config_snippets
567                            .codex
568                            .as_deref()
569                            .unwrap_or_default()
570                            .trim()
571                            .is_empty()
572                    {
573                        guard.common_config_snippets.codex = Some(common_snippet_extracted.clone());
574                        Self::normalize_existing_provider_snapshots_for_storage_best_effort(
575                            &mut guard,
576                            app_type,
577                            Some(common_snippet_extracted.as_str()),
578                        );
579                    }
580                    if let Some(manager) = guard.get_manager_mut(app_type) {
581                        if let Some(target) = manager.providers.get_mut(provider_id) {
582                            target.settings_config = settings_to_store.clone();
583                        }
584                    }
585                }
586                state.save()?;
587            }
588            AppType::Gemini => {
589                use crate::gemini_config::{
590                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
591                };
592
593                let env_path = get_gemini_env_path();
594                if !env_path.exists() {
595                    return Err(AppError::localized(
596                        "gemini.live.missing",
597                        "Gemini .env 文件不存在,无法刷新快照",
598                        "Gemini .env file missing; cannot refresh snapshot",
599                    ));
600                }
601                let env_map = read_gemini_env()?;
602                let mut live_after = env_to_json(&env_map);
603
604                let settings_path = get_gemini_settings_path();
605                let config_value = if settings_path.exists() {
606                    read_json_file(&settings_path)?
607                } else {
608                    json!({})
609                };
610
611                if let Some(obj) = live_after.as_object_mut() {
612                    obj.insert("config".to_string(), config_value);
613                }
614
615                let (provider, common_snippet) = {
616                    let guard = state.config.read().map_err(AppError::from)?;
617                    (
618                        guard
619                            .get_manager(app_type)
620                            .and_then(|manager| manager.providers.get(provider_id))
621                            .cloned()
622                            .ok_or_else(|| {
623                                AppError::localized(
624                                    "provider.not_found",
625                                    format!("供应商不存在: {provider_id}"),
626                                    format!("Provider not found: {provider_id}"),
627                                )
628                            })?,
629                        guard.common_config_snippets.gemini.clone(),
630                    )
631                };
632                let live_after = Self::normalize_settings_config_for_storage(
633                    app_type,
634                    &provider,
635                    live_after,
636                    common_snippet.as_deref(),
637                )?;
638
639                {
640                    let mut guard = state.config.write().map_err(AppError::from)?;
641                    if let Some(manager) = guard.get_manager_mut(app_type) {
642                        if let Some(target) = manager.providers.get_mut(provider_id) {
643                            target.settings_config = live_after;
644                        }
645                    }
646                }
647                state.save()?;
648            }
649            AppType::OpenCode => {
650                let providers = crate::opencode_config::get_providers()?;
651                let live_after = providers.get(provider_id).cloned().ok_or_else(|| {
652                    AppError::localized(
653                        "opencode.live.missing_provider",
654                        format!("OpenCode live 配置中缺少供应商: {provider_id}"),
655                        format!("OpenCode live config missing provider: {provider_id}"),
656                    )
657                })?;
658
659                {
660                    let mut guard = state.config.write().map_err(AppError::from)?;
661                    if let Some(manager) = guard.get_manager_mut(app_type) {
662                        if let Some(target) = manager.providers.get_mut(provider_id) {
663                            target.settings_config = live_after;
664                        }
665                    }
666                }
667                state.save()?;
668            }
669            AppType::OpenClaw => {
670                let providers = crate::openclaw_config::get_providers()?;
671                let live_after = providers.get(provider_id).cloned().ok_or_else(|| {
672                    AppError::localized(
673                        "openclaw.live.missing_provider",
674                        format!("OpenClaw live 配置中缺少供应商: {provider_id}"),
675                        format!("OpenClaw live config missing provider: {provider_id}"),
676                    )
677                })?;
678
679                {
680                    let mut guard = state.config.write().map_err(AppError::from)?;
681                    if let Some(manager) = guard.get_manager_mut(app_type) {
682                        if let Some(target) = manager.providers.get_mut(provider_id) {
683                            target.settings_config = live_after;
684                        }
685                    }
686                }
687                state.save()?;
688            }
689            AppType::Hermes => {
690                let providers = crate::hermes_config::get_providers()?;
691                let live_after = providers.get(provider_id).cloned().unwrap_or_else(|| {
692                    log::warn!(
693                        "Hermes live config missing provider '{provider_id}', using empty config"
694                    );
695                    serde_json::Value::Object(serde_json::Map::new())
696                });
697
698                {
699                    let mut guard = state.config.write().map_err(AppError::from)?;
700                    if let Some(manager) = guard.get_manager_mut(app_type) {
701                        if let Some(target) = manager.providers.get_mut(provider_id) {
702                            target.settings_config = live_after;
703                        }
704                    }
705                }
706                state.save()?;
707            }
708        }
709        Ok(())
710    }
711
712    fn capture_live_snapshot(app_type: &AppType) -> Result<LiveSnapshot, AppError> {
713        live::capture_live_snapshot(app_type)
714    }
715
716    fn validate_common_config_snippet(
717        app_type: &AppType,
718        snippet: Option<&str>,
719    ) -> Result<(), AppError> {
720        common_config::validate_common_config_snippet(app_type, snippet)
721    }
722
723    fn should_skip_common_config_migration_error(app_type: &AppType, err: &AppError) -> bool {
724        match (app_type, err) {
725            (AppType::Claude, AppError::Localized { key, .. }) => {
726                key.starts_with("common_config.claude.")
727            }
728            (AppType::Codex, AppError::Config(message)) => {
729                message.starts_with("Common config TOML parse error:")
730            }
731            (AppType::Gemini, AppError::Localized { key, .. }) => {
732                key.starts_with("common_config.gemini.")
733            }
734            _ => false,
735        }
736    }
737
738    fn migrate_old_common_config_snippet_best_effort(
739        config: &mut MultiAppConfig,
740        app_type: &AppType,
741        strict_current_provider_id: Option<&str>,
742        old_snippet: Option<&str>,
743    ) -> Result<(), AppError> {
744        let Some(old_snippet) = old_snippet.map(str::trim) else {
745            return Ok(());
746        };
747        if old_snippet.is_empty() {
748            return Ok(());
749        }
750
751        let result = match app_type {
752            AppType::Claude => Self::migrate_claude_common_config_snippet(config, old_snippet),
753            AppType::Codex => Self::migrate_codex_common_config_snippet(
754                config,
755                strict_current_provider_id,
756                old_snippet,
757            ),
758            AppType::Gemini => Self::migrate_gemini_common_config_snippet(
759                config,
760                strict_current_provider_id,
761                old_snippet,
762            ),
763            AppType::OpenCode | AppType::OpenClaw => Ok(()),
764            AppType::Hermes => Ok(()),
765        };
766
767        match result {
768            Ok(()) => Ok(()),
769            Err(err) if Self::should_skip_common_config_migration_error(app_type, &err) => {
770                log::warn!(
771                    "skip migrating {app_type} provider snapshots from invalid stored common config snippet: {err}"
772                );
773                Ok(())
774            }
775            Err(err) => Err(err),
776        }
777    }
778
779    #[doc(hidden)]
780    pub fn migrate_common_config_upstream_semantics_if_needed(
781        db: &crate::database::Database,
782        config: &mut MultiAppConfig,
783    ) -> Result<(), AppError> {
784        common_config::migrate_common_config_upstream_semantics_if_needed(db, config)
785    }
786
787    fn build_common_config_post_commit_action(
788        config: &MultiAppConfig,
789        app_type: &AppType,
790        current_provider_id: Option<&str>,
791        takeover_active: bool,
792    ) -> Result<Option<PostCommitAction>, AppError> {
793        if app_type.is_additive_mode() {
794            return Ok(None);
795        }
796
797        let Some(current_provider_id) = current_provider_id else {
798            return Ok(None);
799        };
800
801        Self::build_post_commit_action_for_current_provider(
802            config,
803            app_type,
804            &current_provider_id,
805            takeover_active,
806            false,
807        )
808    }
809
810    fn build_post_commit_action_for_current_provider(
811        config: &MultiAppConfig,
812        app_type: &AppType,
813        current_provider_id: &str,
814        takeover_active: bool,
815        preserve_live_preferences: bool,
816    ) -> Result<Option<PostCommitAction>, AppError> {
817        let provider = config
818            .get_manager(app_type)
819            .and_then(|manager| manager.providers.get(current_provider_id).cloned());
820
821        let Some(provider) = provider else {
822            return Ok(None);
823        };
824
825        Ok(Some(PostCommitAction {
826            app_type: app_type.clone(),
827            provider,
828            backup: Self::capture_live_snapshot(app_type)?,
829            write_live_snapshot: true,
830            sync_mcp: false,
831            sync_codex_catalog: matches!(app_type, AppType::Codex),
832            stale_codex_catalog_keys: Vec::new(),
833            refresh_snapshot: false,
834            apply_hermes_switch_defaults: false,
835            common_config_snippet: config.common_config_snippets.get(app_type).cloned(),
836            takeover_active,
837            preserve_live_preferences,
838        }))
839    }
840
841    fn resolve_live_apply_common_config(
842        app_type: &AppType,
843        provider: &Provider,
844        common_config_snippet: Option<&str>,
845        requested_apply_common_config: bool,
846    ) -> bool {
847        if !requested_apply_common_config {
848            return false;
849        }
850
851        common_config::provider_uses_common_config(app_type, provider, common_config_snippet)
852    }
853
854    fn normalize_provider_for_storage(
855        app_type: &AppType,
856        provider: &mut Provider,
857        common_config_snippet: Option<&str>,
858    ) -> Result<(), AppError> {
859        common_config::normalize_provider_common_config_for_storage(
860            app_type,
861            provider,
862            common_config_snippet,
863        )?;
864
865        if matches!(app_type, AppType::Codex) {
866            crate::codex_config::normalize_codex_settings_config_for_storage(
867                &mut provider.settings_config,
868            )?;
869        }
870
871        Ok(())
872    }
873
874    fn migrate_provider_for_storage(
875        app_type: &AppType,
876        provider: &mut Provider,
877        common_config_snippet: Option<&str>,
878    ) -> Result<(), AppError> {
879        common_config::migrate_provider_subset_usage_for_storage(
880            app_type,
881            provider,
882            common_config_snippet,
883        )?;
884
885        if matches!(app_type, AppType::Codex) {
886            crate::codex_config::normalize_codex_settings_config_for_storage(
887                &mut provider.settings_config,
888            )?;
889        }
890
891        Ok(())
892    }
893
894    pub(crate) fn normalize_settings_config_for_storage(
895        app_type: &AppType,
896        provider: &Provider,
897        settings_config: Value,
898        common_config_snippet: Option<&str>,
899    ) -> Result<Value, AppError> {
900        let mut snapshot_provider = provider.clone();
901        snapshot_provider.settings_config = settings_config;
902        Self::normalize_provider_for_storage(
903            app_type,
904            &mut snapshot_provider,
905            common_config_snippet,
906        )?;
907        Ok(snapshot_provider.settings_config)
908    }
909
910    fn restore_codex_model_provider_for_storage_best_effort(
911        provider: &Provider,
912        settings_config: &mut Value,
913    ) {
914        if let Err(err) =
915            crate::codex_config::restore_codex_settings_config_model_provider_for_backfill(
916                settings_config,
917                &provider.settings_config,
918            )
919        {
920            log::warn!(
921                "Failed to restore Codex provider id while storing snapshot for '{}': {err}",
922                provider.id
923            );
924        }
925    }
926
927    pub(crate) fn remove_common_config_from_settings_for_preview(
928        app_type: &AppType,
929        settings_config: &Value,
930        common_config_snippet: &str,
931    ) -> Result<Value, AppError> {
932        common_config::remove_common_config_from_settings(
933            app_type,
934            settings_config,
935            common_config_snippet,
936        )
937    }
938
939    fn normalize_existing_provider_snapshots_for_storage(
940        config: &mut MultiAppConfig,
941        app_type: &AppType,
942        common_config_snippet: Option<&str>,
943    ) -> Result<(), AppError> {
944        let Some(manager) = config.get_manager_mut(app_type) else {
945            return Ok(());
946        };
947
948        for provider in manager.providers.values_mut() {
949            Self::migrate_provider_for_storage(app_type, provider, common_config_snippet)?;
950        }
951
952        Ok(())
953    }
954
955    fn normalize_existing_provider_snapshots_for_storage_best_effort(
956        config: &mut MultiAppConfig,
957        app_type: &AppType,
958        common_config_snippet: Option<&str>,
959    ) {
960        let Some(manager) = config.get_manager_mut(app_type) else {
961            return;
962        };
963
964        for (provider_id, provider) in manager.providers.iter_mut() {
965            if let Err(err) =
966                Self::migrate_provider_for_storage(app_type, provider, common_config_snippet)
967            {
968                log::warn!(
969                    "skip normalizing {app_type} provider snapshot '{provider_id}' while applying auto-extracted common config: {err}"
970                );
971            }
972        }
973    }
974
975    fn normalize_existing_provider_snapshots_for_storage_strict_current_best_effort_others(
976        config: &mut MultiAppConfig,
977        app_type: &AppType,
978        strict_current_provider_id: Option<&str>,
979        common_config_snippet: Option<&str>,
980    ) -> Result<(), AppError> {
981        let Some(current_provider_id) = strict_current_provider_id.and_then(|provider_id| {
982            config.get_manager(app_type).and_then(|manager| {
983                manager
984                    .providers
985                    .contains_key(provider_id)
986                    .then(|| provider_id.to_string())
987            })
988        }) else {
989            return Self::normalize_existing_provider_snapshots_for_storage(
990                config,
991                app_type,
992                common_config_snippet,
993            );
994        };
995
996        let Some(manager) = config.get_manager_mut(app_type) else {
997            return Ok(());
998        };
999
1000        if let Some(current_provider) = manager.providers.get_mut(&current_provider_id) {
1001            Self::migrate_provider_for_storage(app_type, current_provider, common_config_snippet)?;
1002        }
1003
1004        for (provider_id, provider) in manager.providers.iter_mut() {
1005            if provider_id == &current_provider_id {
1006                continue;
1007            }
1008
1009            if let Err(err) =
1010                Self::migrate_provider_for_storage(app_type, provider, common_config_snippet)
1011            {
1012                log::warn!(
1013                    "skip normalizing {app_type} non-current provider snapshot '{provider_id}' while updating common config snippet: {err}"
1014                );
1015            }
1016        }
1017
1018        Ok(())
1019    }
1020
1021    fn hydrate_missing_provider_snapshots_from_db(
1022        config: &mut MultiAppConfig,
1023        app_type: &AppType,
1024        db_providers: &IndexMap<String, Provider>,
1025    ) -> Result<(), AppError> {
1026        let manager = config
1027            .get_manager_mut(app_type)
1028            .ok_or_else(|| Self::app_not_found(app_type))?;
1029
1030        for (provider_id, provider) in db_providers {
1031            manager
1032                .providers
1033                .entry(provider_id.clone())
1034                .or_insert_with(|| provider.clone());
1035        }
1036
1037        Ok(())
1038    }
1039
1040    pub fn set_common_config_snippet(
1041        state: &AppState,
1042        app_type: AppType,
1043        snippet: Option<String>,
1044    ) -> Result<(), AppError> {
1045        let normalized_snippet = snippet.and_then(|value| {
1046            let trimmed = value.trim();
1047            if trimmed.is_empty() {
1048                None
1049            } else {
1050                Some(trimmed.to_string())
1051            }
1052        });
1053        Self::validate_common_config_snippet(&app_type, normalized_snippet.as_deref())?;
1054
1055        let app_type_clone = app_type.clone();
1056        let (effective_current_provider, db_providers) = if app_type.is_additive_mode() {
1057            (None, None)
1058        } else {
1059            (
1060                crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1061                Some(state.db.get_all_providers(app_type.as_str())?),
1062            )
1063        };
1064        let takeover_active = if app_type.is_additive_mode() {
1065            false
1066        } else {
1067            let is_running = state
1068                .proxy_service
1069                .is_running_blocking()
1070                .map_err(AppError::Message)?;
1071            if !is_running {
1072                false
1073            } else {
1074                state
1075                    .proxy_service
1076                    .is_app_takeover_active_blocking(&app_type)
1077                    .map_err(AppError::Message)?
1078            }
1079        };
1080
1081        Self::run_transaction_preserving_current_providers(
1082            state,
1083            std::slice::from_ref(&app_type),
1084            move |config| {
1085                config.ensure_app(&app_type_clone);
1086
1087                if let Some(db_providers) = db_providers.as_ref() {
1088                    Self::hydrate_missing_provider_snapshots_from_db(
1089                        config,
1090                        &app_type_clone,
1091                        db_providers,
1092                    )?;
1093                }
1094
1095                let old_snippet = config
1096                    .common_config_snippets
1097                    .get(&app_type_clone)
1098                    .cloned()
1099                    .filter(|value| !value.trim().is_empty());
1100
1101                Self::migrate_old_common_config_snippet_best_effort(
1102                    config,
1103                    &app_type_clone,
1104                    effective_current_provider.as_deref(),
1105                    old_snippet.as_deref(),
1106                )?;
1107
1108                config
1109                    .common_config_snippets
1110                    .set(&app_type_clone, normalized_snippet.clone());
1111
1112                if matches!(
1113                    app_type_clone,
1114                    AppType::Claude | AppType::Codex | AppType::Gemini
1115                ) {
1116                    Self::normalize_existing_provider_snapshots_for_storage_strict_current_best_effort_others(
1117                    config,
1118                    &app_type_clone,
1119                    effective_current_provider.as_deref(),
1120                    normalized_snippet.as_deref(),
1121                )?;
1122                }
1123
1124                let action = Self::build_common_config_post_commit_action(
1125                    config,
1126                    &app_type_clone,
1127                    effective_current_provider.as_deref(),
1128                    takeover_active,
1129                )?;
1130                Ok(((), action))
1131            },
1132        )
1133    }
1134
1135    pub fn clear_common_config_snippet(
1136        state: &AppState,
1137        app_type: AppType,
1138    ) -> Result<(), AppError> {
1139        Self::set_common_config_snippet(state, app_type, None)
1140    }
1141
1142    /// 列出指定应用下的所有供应商
1143    pub fn list(
1144        state: &AppState,
1145        app_type: AppType,
1146    ) -> Result<IndexMap<String, Provider>, AppError> {
1147        let config = state.config.read().map_err(AppError::from)?;
1148        let manager = config
1149            .get_manager(&app_type)
1150            .ok_or_else(|| Self::app_not_found(&app_type))?;
1151        Ok(manager.get_all_providers().clone())
1152    }
1153
1154    pub(crate) fn sync_openclaw_providers_from_live(state: &AppState) -> Result<(), AppError> {
1155        live::sync_openclaw_providers_from_live(state)?;
1156        Ok(())
1157    }
1158
1159    /// 获取当前供应商 ID
1160    pub fn current(state: &AppState, app_type: AppType) -> Result<String, AppError> {
1161        if app_type == AppType::Hermes {
1162            return Ok(crate::hermes_config::get_model_config()?
1163                .and_then(|model| model.provider)
1164                .map(|provider| provider.trim().to_string())
1165                .filter(|provider| !provider.is_empty())
1166                .unwrap_or_default());
1167        }
1168        if app_type.is_additive_mode() {
1169            return Ok(String::new());
1170        }
1171        crate::settings::get_effective_current_provider(&state.db, &app_type)
1172            .map(|opt| opt.unwrap_or_default())
1173    }
1174
1175    /// 新增供应商
1176    pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
1177        let mut provider = provider;
1178        // 归一化 Claude 模型键
1179        Self::normalize_provider_if_claude(&app_type, &mut provider);
1180        Self::validate_provider_settings(&app_type, &provider)?;
1181
1182        let app_type_clone = app_type.clone();
1183        let provider_clone = provider.clone();
1184        let stored_current_provider = if app_type.is_additive_mode() {
1185            None
1186        } else {
1187            state.db.get_current_provider(app_type.as_str())?
1188        };
1189
1190        Self::run_transaction(state, move |config| {
1191            let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1192            let mut provider_to_store = provider_clone.clone();
1193            Self::normalize_provider_for_storage(
1194                &app_type_clone,
1195                &mut provider_to_store,
1196                common_config_snippet.as_deref(),
1197            )?;
1198
1199            if matches!(app_type_clone, AppType::OpenClaw)
1200                && provider_to_store.created_at.is_none()
1201                && live::is_auto_mirrored_openclaw_snapshot(&provider_to_store)
1202            {
1203                provider_to_store.created_at = Some(current_timestamp());
1204            }
1205            if app_type_clone.is_additive_mode() {
1206                Self::set_provider_live_config_managed(&mut provider_to_store, true);
1207            }
1208
1209            config.ensure_app(&app_type_clone);
1210            let manager = config
1211                .get_manager_mut(&app_type_clone)
1212                .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1213
1214            if !app_type_clone.is_additive_mode() {
1215                manager.current = stored_current_provider.clone().unwrap_or_default();
1216            }
1217
1218            let was_empty = manager.providers.is_empty();
1219            manager
1220                .providers
1221                .insert(provider_to_store.id.clone(), provider_to_store.clone());
1222
1223            if !app_type_clone.is_additive_mode()
1224                && stored_current_provider.is_none()
1225                && (was_empty || manager.current.is_empty())
1226            {
1227                manager.current = provider_to_store.id.clone();
1228            }
1229
1230            let is_current =
1231                app_type_clone.is_additive_mode() || manager.current == provider_to_store.id;
1232            let action = if is_current {
1233                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1234                Some(PostCommitAction {
1235                    app_type: app_type_clone.clone(),
1236                    provider: provider_to_store.clone(),
1237                    backup,
1238                    write_live_snapshot: true,
1239                    // Codex write uses merge which preserves [mcp_servers] as-is,
1240                    // so no MCP re-sync is needed (would lose comment-only lines).
1241                    sync_mcp: false,
1242                    sync_codex_catalog: matches!(&app_type_clone, AppType::Codex),
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 if matches!(&app_type_clone, AppType::Codex) {
1251                Some(PostCommitAction {
1252                    app_type: app_type_clone.clone(),
1253                    provider: provider_to_store.clone(),
1254                    backup: Self::capture_live_snapshot(&app_type_clone)?,
1255                    write_live_snapshot: false,
1256                    sync_mcp: false,
1257                    sync_codex_catalog: true,
1258                    stale_codex_catalog_keys: Vec::new(),
1259                    refresh_snapshot: false,
1260                    apply_hermes_switch_defaults: false,
1261                    common_config_snippet,
1262                    takeover_active: false,
1263                    preserve_live_preferences: true,
1264                })
1265            } else {
1266                None
1267            };
1268
1269            Ok((true, action))
1270        })
1271    }
1272
1273    /// 更新供应商
1274    pub fn update(
1275        state: &AppState,
1276        app_type: AppType,
1277        provider: Provider,
1278    ) -> Result<bool, AppError> {
1279        let mut provider = provider;
1280        // 归一化 Claude 模型键
1281        Self::normalize_provider_if_claude(&app_type, &mut provider);
1282        Self::validate_provider_settings(&app_type, &provider)?;
1283        let provider_id = provider.id.clone();
1284        let app_type_clone = app_type.clone();
1285        let provider_clone = provider.clone();
1286        let (effective_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
1287            (None, None)
1288        } else {
1289            (
1290                crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1291                state.db.get_current_provider(app_type.as_str())?,
1292            )
1293        };
1294
1295        Self::run_transaction(state, move |config| {
1296            let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1297            let manager = config
1298                .get_manager_mut(&app_type_clone)
1299                .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1300
1301            if !manager.providers.contains_key(&provider_id) {
1302                return Err(AppError::localized(
1303                    "provider.not_found",
1304                    format!("供应商不存在: {provider_id}"),
1305                    format!("Provider not found: {provider_id}"),
1306                ));
1307            }
1308
1309            if !app_type_clone.is_additive_mode() {
1310                manager.current = stored_current_provider.clone().unwrap_or_default();
1311            }
1312
1313            let existing_live_config_managed = manager
1314                .providers
1315                .get(&provider_id)
1316                .and_then(Self::provider_live_config_managed);
1317            let previous_codex_catalog_key = manager
1318                .providers
1319                .get(&provider_id)
1320                .and_then(Self::provider_codex_model_provider_key);
1321            let mut merged = if let Some(existing) = manager.providers.get(&provider_id) {
1322                let mut updated = provider_clone.clone();
1323                match (existing.meta.as_ref(), updated.meta.take()) {
1324                    // 前端未提供 meta,表示不修改,沿用旧值
1325                    (Some(old_meta), None) => {
1326                        updated.meta = Some(old_meta.clone());
1327                    }
1328                    (None, None) => {
1329                        updated.meta = None;
1330                    }
1331                    // 前端提供的 meta 视为权威,直接覆盖(其中 custom_endpoints 允许是空,表示删除所有自定义端点)
1332                    (_old, Some(new_meta)) => {
1333                        updated.meta = Some(new_meta);
1334                    }
1335                }
1336                if matches!(app_type_clone, AppType::OpenClaw)
1337                    && updated.created_at.is_none()
1338                    && live::is_auto_mirrored_openclaw_snapshot(&updated)
1339                {
1340                    updated.created_at = Some(current_timestamp());
1341                }
1342                updated
1343            } else {
1344                provider_clone.clone()
1345            };
1346
1347            Self::normalize_provider_for_storage(
1348                &app_type_clone,
1349                &mut merged,
1350                common_config_snippet.as_deref(),
1351            )?;
1352
1353            let should_write_live = if app_type_clone.is_additive_mode() {
1354                let live_config_managed = Self::additive_provider_exists_in_live_config(
1355                    &app_type_clone,
1356                    &provider_id,
1357                    Self::provider_live_config_managed(&merged).or(existing_live_config_managed),
1358                )?;
1359                Self::set_provider_live_config_managed(&mut merged, live_config_managed);
1360                live_config_managed
1361            } else {
1362                effective_current_provider.as_deref() == Some(provider_id.as_str())
1363            };
1364
1365            manager
1366                .providers
1367                .insert(provider_id.clone(), merged.clone());
1368
1369            let action = if should_write_live {
1370                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1371                Some(PostCommitAction {
1372                    app_type: app_type_clone.clone(),
1373                    provider: merged,
1374                    backup,
1375                    write_live_snapshot: true,
1376                    // Codex write uses merge which preserves [mcp_servers] as-is,
1377                    // so no MCP re-sync is needed (would lose comment-only lines).
1378                    sync_mcp: false,
1379                    sync_codex_catalog: matches!(&app_type_clone, AppType::Codex),
1380                    stale_codex_catalog_keys: Vec::new(),
1381                    refresh_snapshot: false,
1382                    apply_hermes_switch_defaults: false,
1383                    common_config_snippet,
1384                    takeover_active: false,
1385                    preserve_live_preferences: true,
1386                })
1387            } else if matches!(&app_type_clone, AppType::Codex) {
1388                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1389                let current_codex_catalog_key = Self::provider_codex_model_provider_key(&merged);
1390                let stale_codex_catalog_keys = previous_codex_catalog_key
1391                    .filter(|old_key| {
1392                        current_codex_catalog_key.as_deref() != Some(old_key.as_str())
1393                    })
1394                    .into_iter()
1395                    .collect();
1396                Some(PostCommitAction {
1397                    app_type: app_type_clone.clone(),
1398                    provider: merged,
1399                    backup,
1400                    write_live_snapshot: false,
1401                    sync_mcp: false,
1402                    sync_codex_catalog: true,
1403                    stale_codex_catalog_keys,
1404                    refresh_snapshot: false,
1405                    apply_hermes_switch_defaults: false,
1406                    common_config_snippet,
1407                    takeover_active: false,
1408                    preserve_live_preferences: true,
1409                })
1410            } else {
1411                None
1412            };
1413
1414            Ok((true, action))
1415        })
1416    }
1417
1418    /// 导入当前 live 配置为默认供应商。
1419    ///
1420    /// 返回 `Ok(true)` 表示实际导入,`Ok(false)` 表示该 app 已有非官方 seed provider 而跳过。
1421    pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
1422        if app_type.is_additive_mode() {
1423            return Ok(false);
1424        }
1425
1426        if state.db.has_non_official_seed_provider(app_type.as_str())? {
1427            return Ok(false);
1428        }
1429
1430        let settings_config = match app_type {
1431            AppType::Codex => {
1432                let auth_path = get_codex_auth_path();
1433                if !auth_path.exists() {
1434                    return Err(AppError::localized(
1435                        "codex.live.missing",
1436                        "Codex 配置文件不存在",
1437                        "Codex configuration file is missing",
1438                    ));
1439                }
1440                let auth: Value = read_json_file(&auth_path)?;
1441                let config_str = crate::codex_config::read_and_validate_codex_config_text()?;
1442                crate::codex_config::codex_settings_snapshot_from_toml(Some(auth), &config_str)?
1443            }
1444            AppType::Claude => {
1445                let settings_path = get_claude_settings_path();
1446                if !settings_path.exists() {
1447                    return Err(AppError::localized(
1448                        "claude.live.missing",
1449                        "Claude Code 配置文件不存在",
1450                        "Claude settings file is missing",
1451                    ));
1452                }
1453                let mut v = read_json_file::<Value>(&settings_path)?;
1454                let _ = Self::normalize_claude_models_in_value(&mut v);
1455                v
1456            }
1457            AppType::Gemini => {
1458                use crate::gemini_config::{
1459                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1460                };
1461
1462                // 读取 .env 文件(环境变量)
1463                let env_path = get_gemini_env_path();
1464                if !env_path.exists() {
1465                    return Err(AppError::localized(
1466                        "gemini.live.missing",
1467                        "Gemini 配置文件不存在",
1468                        "Gemini configuration file is missing",
1469                    ));
1470                }
1471
1472                let env_map = read_gemini_env()?;
1473                let env_json = env_to_json(&env_map);
1474                let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1475
1476                // 读取 settings.json 文件(MCP 配置等)
1477                let settings_path = get_gemini_settings_path();
1478                let config_obj = if settings_path.exists() {
1479                    read_json_file(&settings_path)?
1480                } else {
1481                    json!({})
1482                };
1483
1484                // 返回完整结构:{ "env": {...}, "config": {...} }
1485                json!({
1486                    "env": env_obj,
1487                    "config": config_obj
1488                })
1489            }
1490            AppType::OpenCode => unreachable!("additive mode apps are handled earlier"),
1491            AppType::OpenClaw => unreachable!("additive mode apps are handled earlier"),
1492            AppType::Hermes => unreachable!("additive mode apps are handled earlier"),
1493        };
1494
1495        let mut provider = Provider::with_id(
1496            "default".to_string(),
1497            "default".to_string(),
1498            settings_config,
1499            None,
1500        );
1501        provider.category = Some("custom".to_string());
1502        let common_config_snippet = {
1503            let guard = state.config.read().map_err(AppError::from)?;
1504            guard.common_config_snippets.get(&app_type).cloned()
1505        };
1506        Self::normalize_provider_for_storage(
1507            &app_type,
1508            &mut provider,
1509            common_config_snippet.as_deref(),
1510        )?;
1511
1512        state.db.save_provider(app_type.as_str(), &provider)?;
1513        state
1514            .db
1515            .set_current_provider(app_type.as_str(), &provider.id)?;
1516        {
1517            let mut guard = state.config.write().map_err(AppError::from)?;
1518            guard.ensure_app(&app_type);
1519            let manager = guard
1520                .get_manager_mut(&app_type)
1521                .ok_or_else(|| AppError::Config("manager missing after ensure_app".into()))?;
1522            manager.current = provider.id.clone();
1523            manager.providers.insert(provider.id.clone(), provider);
1524        }
1525        Ok(true)
1526    }
1527
1528    /// 读取当前 live 配置
1529    pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
1530        match app_type {
1531            AppType::Codex => {
1532                let auth_path = get_codex_auth_path();
1533                let config_path = get_codex_config_path();
1534                if !config_path.exists() {
1535                    return Err(AppError::localized(
1536                        "codex.live.missing",
1537                        "Codex 配置文件不存在",
1538                        "Codex configuration is missing",
1539                    ));
1540                }
1541
1542                let mut live_settings = serde_json::Map::new();
1543                if auth_path.exists() {
1544                    live_settings.insert("auth".to_string(), read_json_file(&auth_path)?);
1545                }
1546                if config_path.exists() {
1547                    let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
1548                    live_settings.insert("config".to_string(), Value::String(cfg_text));
1549                }
1550
1551                Ok(Value::Object(live_settings))
1552            }
1553            AppType::Claude => {
1554                let path = get_claude_settings_path();
1555                if !path.exists() {
1556                    return Err(AppError::localized(
1557                        "claude.live.missing",
1558                        "Claude Code 配置文件不存在",
1559                        "Claude settings file is missing",
1560                    ));
1561                }
1562                read_json_file(&path)
1563            }
1564            AppType::Gemini => {
1565                use crate::gemini_config::{
1566                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1567                };
1568
1569                // 读取 .env 文件(环境变量)
1570                let env_path = get_gemini_env_path();
1571                if !env_path.exists() {
1572                    return Err(AppError::localized(
1573                        "gemini.env.missing",
1574                        "Gemini .env 文件不存在",
1575                        "Gemini .env file not found",
1576                    ));
1577                }
1578
1579                let env_map = read_gemini_env()?;
1580                let env_json = env_to_json(&env_map);
1581                let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1582
1583                // 读取 settings.json 文件(MCP 配置等)
1584                let settings_path = get_gemini_settings_path();
1585                let config_obj = if settings_path.exists() {
1586                    read_json_file(&settings_path)?
1587                } else {
1588                    json!({})
1589                };
1590
1591                // 返回完整结构:{ "env": {...}, "config": {...} }
1592                Ok(json!({
1593                    "env": env_obj,
1594                    "config": config_obj
1595                }))
1596            }
1597            AppType::OpenCode => {
1598                let config_path = crate::opencode_config::get_opencode_config_path();
1599                if !config_path.exists() {
1600                    return Err(AppError::localized(
1601                        "opencode.config.missing",
1602                        "OpenCode 配置文件不存在",
1603                        "OpenCode configuration file not found",
1604                    ));
1605                }
1606                crate::opencode_config::read_opencode_config()
1607            }
1608            AppType::OpenClaw => {
1609                let config_path = crate::openclaw_config::get_openclaw_config_path();
1610                if !config_path.exists() {
1611                    return Err(AppError::localized(
1612                        "openclaw.config.missing",
1613                        "OpenClaw 配置文件不存在",
1614                        "OpenClaw configuration file not found",
1615                    ));
1616                }
1617                crate::openclaw_config::read_openclaw_config()
1618            }
1619            AppType::Hermes => {
1620                let yaml = crate::hermes_config::read_hermes_config()?;
1621                crate::hermes_config::yaml_to_json(&yaml)
1622            }
1623        }
1624    }
1625
1626    /// 更新供应商排序
1627    pub fn update_sort_order(
1628        state: &AppState,
1629        app_type: AppType,
1630        updates: Vec<ProviderSortUpdate>,
1631    ) -> Result<bool, AppError> {
1632        {
1633            let mut cfg = state.config.write().map_err(AppError::from)?;
1634            let manager = cfg
1635                .get_manager_mut(&app_type)
1636                .ok_or_else(|| Self::app_not_found(&app_type))?;
1637
1638            for update in updates {
1639                if let Some(provider) = manager.providers.get_mut(&update.id) {
1640                    provider.sort_index = Some(update.sort_index);
1641                }
1642            }
1643        }
1644
1645        state.save()?;
1646        Ok(true)
1647    }
1648
1649    pub fn remove_from_live_config(
1650        state: &AppState,
1651        app_type: AppType,
1652        provider_id: &str,
1653    ) -> Result<(), AppError> {
1654        if !app_type.is_additive_mode() {
1655            return Err(AppError::localized(
1656                "provider.remove_from_live_config.unsupported",
1657                "只有累加模式应用支持从 live 配置中移除供应商",
1658                "Only additive-mode apps support removing a provider from live config",
1659            ));
1660        }
1661
1662        let original = {
1663            let config = state.config.read().map_err(AppError::from)?;
1664            let manager = config
1665                .get_manager(&app_type)
1666                .ok_or_else(|| Self::app_not_found(&app_type))?;
1667            if !manager.providers.contains_key(provider_id) {
1668                return Err(AppError::localized(
1669                    "provider.not_found",
1670                    format!("供应商不存在: {provider_id}"),
1671                    format!("Provider not found: {provider_id}"),
1672                ));
1673            }
1674            config.clone()
1675        };
1676
1677        let backup = Self::capture_live_snapshot(&app_type)?;
1678        match &app_type {
1679            AppType::OpenCode => {
1680                if crate::opencode_config::get_opencode_dir().exists() {
1681                    crate::opencode_config::remove_provider(provider_id)?;
1682                }
1683            }
1684            AppType::OpenClaw => {
1685                if crate::openclaw_config::get_openclaw_dir().exists() {
1686                    crate::openclaw_config::remove_provider(provider_id)?;
1687                }
1688            }
1689            _ => unreachable!("non-additive apps should not enter remove-from-live branch"),
1690        }
1691
1692        {
1693            let mut config = state.config.write().map_err(AppError::from)?;
1694            let manager = config
1695                .get_manager_mut(&app_type)
1696                .ok_or_else(|| Self::app_not_found(&app_type))?;
1697            let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
1698                AppError::localized(
1699                    "provider.not_found",
1700                    format!("供应商不存在: {provider_id}"),
1701                    format!("Provider not found: {provider_id}"),
1702                )
1703            })?;
1704            Self::set_provider_live_config_managed(provider, false);
1705        }
1706
1707        if let Err(save_err) = state.save() {
1708            let config_restore = Self::restore_config_only(state, original);
1709            let live_restore = backup.restore();
1710            if let Err(rollback_err) = config_restore {
1711                return Err(AppError::localized(
1712                    "config.save.rollback_failed",
1713                    format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
1714                    format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
1715                ));
1716            }
1717            if let Err(rollback_err) = live_restore {
1718                return Err(AppError::localized(
1719                    "post_commit.rollback_failed",
1720                    format!("保存配置失败: {save_err};live 回滚失败: {rollback_err}"),
1721                    format!(
1722                        "Failed to save config: {save_err}; live rollback failed: {rollback_err}"
1723                    ),
1724                ));
1725            }
1726            return Err(save_err);
1727        }
1728
1729        Ok(())
1730    }
1731
1732    /// 将所有应用的当前供应商配置同步到 live 文件。
1733    ///
1734    /// 用于 WebDAV 下载、备份恢复等场景:数据库已更新,但 live 配置文件
1735    /// (`~/.codex/config.toml`、Claude `settings.json` 等)尚未同步。
1736    /// 对齐上游 `sync_current_to_live` 行为。
1737    pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
1738        use crate::services::mcp::McpService;
1739
1740        // 在读锁下收集所有需要的数据,避免持锁写文件
1741        let snapshots: Vec<(AppType, Provider, Option<String>)> = {
1742            let guard = state.config.read().map_err(AppError::from)?;
1743            let mut result = Vec::new();
1744            for app_type in AppType::all() {
1745                if app_type.is_additive_mode() {
1746                    if let Some(manager) = guard.get_manager(&app_type) {
1747                        let snippet = guard.common_config_snippets.get(&app_type).cloned();
1748                        for provider in manager.providers.values() {
1749                            if Self::provider_live_config_managed(provider) == Some(false) {
1750                                continue;
1751                            }
1752                            result.push((app_type.clone(), provider.clone(), snippet.clone()));
1753                        }
1754                    }
1755                    continue;
1756                }
1757
1758                let current_id =
1759                    match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
1760                        Some(id) => id,
1761                        None => continue,
1762                    };
1763                let providers = state.db.get_all_providers(app_type.as_str())?;
1764                match providers.get(&current_id) {
1765                    Some(provider) => {
1766                        let snippet = state.db.get_config_snippet(app_type.as_str())?;
1767                        result.push((app_type.clone(), provider.clone(), snippet));
1768                    }
1769                    None => {
1770                        log::warn!(
1771                            "sync_current_to_live: {app_type} 当前供应商 {} 不存在于数据库,跳过",
1772                            current_id
1773                        );
1774                    }
1775                }
1776            }
1777            result
1778        };
1779
1780        let openclaw_live_provider_ids = match Self::valid_openclaw_live_provider_ids() {
1781            Ok(provider_ids) => provider_ids,
1782            Err(err) => {
1783                log::warn!(
1784                    "sync_current_to_live: 读取 OpenClaw live providers 失败,跳过 OpenClaw 同步: {err}"
1785                );
1786                None
1787            }
1788        };
1789
1790        for (app_type, provider, snippet) in &snapshots {
1791            if matches!(app_type, AppType::OpenClaw)
1792                && !openclaw_live_provider_ids
1793                    .as_ref()
1794                    .is_some_and(|provider_ids| provider_ids.contains(&provider.id))
1795            {
1796                continue;
1797            }
1798
1799            if let Err(e) =
1800                Self::write_live_snapshot(app_type, provider, snippet.as_deref(), true, true)
1801            {
1802                log::warn!("sync_current_to_live: 写入 {app_type} live 配置失败: {e}");
1803            }
1804        }
1805
1806        if snapshots
1807            .iter()
1808            .any(|(app_type, _, _)| matches!(app_type, AppType::Codex))
1809        {
1810            if let Err(e) = Self::sync_codex_provider_catalog_to_live(state, &[]) {
1811                log::warn!("sync_current_to_live: Codex provider catalog 同步失败: {e}");
1812            }
1813        }
1814
1815        if let Err(e) =
1816            crate::services::prompt::PromptService::sync_all_active_to_live_best_effort(state)
1817        {
1818            log::warn!("sync_current_to_live: Prompt 同步失败: {e}");
1819        }
1820
1821        if let Err(e) = McpService::sync_all_enabled_except(state, &[AppType::Codex]) {
1822            log::warn!("sync_current_to_live: 非 Codex MCP 同步失败: {e}");
1823        }
1824
1825        if let Err(e) = crate::services::skill::SkillService::sync_all_enabled_best_effort() {
1826            log::warn!("sync_current_to_live: Skills 同步失败: {e}");
1827        }
1828
1829        Ok(())
1830    }
1831
1832    /// 切换指定应用的供应商
1833    pub fn switch(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
1834        if !app_type.is_additive_mode() {
1835            let providers = state.db.get_all_providers(app_type.as_str())?;
1836            providers.get(provider_id).ok_or_else(|| {
1837                AppError::localized(
1838                    "provider.not_found",
1839                    format!("供应商不存在: {provider_id}"),
1840                    format!("Provider not found: {provider_id}"),
1841                )
1842            })?;
1843
1844            let is_app_taken_over =
1845                futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
1846                    .ok()
1847                    .flatten()
1848                    .is_some();
1849            let is_proxy_running = state
1850                .proxy_service
1851                .is_running_blocking()
1852                .map_err(AppError::Message)?;
1853            let live_taken_over = state
1854                .proxy_service
1855                .detect_takeover_in_live_config_for_app(&app_type);
1856            let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
1857
1858            if should_hot_switch {
1859                futures::executor::block_on(
1860                    state
1861                        .proxy_service
1862                        .hot_switch_provider(app_type.as_str(), provider_id),
1863                )
1864                .map_err(|e| AppError::Message(format!("热切换失败: {e}")))?;
1865
1866                let mut guard = state.config.write().map_err(AppError::from)?;
1867                if let Some(manager) = guard.get_manager_mut(&app_type) {
1868                    manager.current = provider_id.to_string();
1869                }
1870                return Ok(());
1871            }
1872        }
1873
1874        let app_type_clone = app_type.clone();
1875        let provider_id_owned = provider_id.to_string();
1876        let effective_current_provider = if app_type.is_additive_mode() {
1877            None
1878        } else if matches!(app_type, AppType::Codex) {
1879            Self::codex_live_current_provider_id(state)?.or(
1880                crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1881            )
1882        } else {
1883            crate::settings::get_effective_current_provider(&state.db, &app_type)?
1884        };
1885
1886        Self::run_transaction(state, move |config| {
1887            if app_type_clone.is_additive_mode() {
1888                let provider = {
1889                    let provider = config
1890                        .get_manager_mut(&app_type_clone)
1891                        .ok_or_else(|| Self::app_not_found(&app_type_clone))?
1892                        .providers
1893                        .get_mut(&provider_id_owned)
1894                        .ok_or_else(|| {
1895                            AppError::localized(
1896                                "provider.not_found",
1897                                format!("供应商不存在: {provider_id_owned}"),
1898                                format!("Provider not found: {provider_id_owned}"),
1899                            )
1900                        })?;
1901                    Self::set_provider_live_config_managed(provider, true);
1902                    provider.clone()
1903                };
1904
1905                let action = PostCommitAction {
1906                    app_type: app_type_clone.clone(),
1907                    provider,
1908                    backup: Self::capture_live_snapshot(&app_type_clone)?,
1909                    write_live_snapshot: true,
1910                    sync_mcp: matches!(app_type_clone, AppType::OpenCode),
1911                    sync_codex_catalog: false,
1912                    stale_codex_catalog_keys: Vec::new(),
1913                    refresh_snapshot: false,
1914                    apply_hermes_switch_defaults: matches!(app_type_clone, AppType::Hermes),
1915                    common_config_snippet: config
1916                        .common_config_snippets
1917                        .get(&app_type_clone)
1918                        .cloned(),
1919                    takeover_active: false,
1920                    preserve_live_preferences: true,
1921                };
1922
1923                return Ok(((), Some(action)));
1924            }
1925
1926            let backup = Self::capture_live_snapshot(&app_type_clone)?;
1927            let provider = match app_type_clone {
1928                AppType::Codex => Self::prepare_switch_codex(
1929                    config,
1930                    &provider_id_owned,
1931                    effective_current_provider.as_deref(),
1932                )?,
1933                AppType::Claude => Self::prepare_switch_claude(
1934                    config,
1935                    &provider_id_owned,
1936                    effective_current_provider.as_deref(),
1937                )?,
1938                AppType::Gemini => Self::prepare_switch_gemini(
1939                    config,
1940                    &provider_id_owned,
1941                    effective_current_provider.as_deref(),
1942                )?,
1943                AppType::OpenCode => unreachable!("additive mode handled above"),
1944                AppType::OpenClaw => unreachable!("additive mode handled above"),
1945                AppType::Hermes => unreachable!("additive mode handled above"),
1946            };
1947
1948            let action = PostCommitAction {
1949                app_type: app_type_clone.clone(),
1950                provider,
1951                backup,
1952                write_live_snapshot: true,
1953                // Codex writes provider config through a TOML merge that preserves
1954                // [mcp_servers]. A global MCP resync here would overwrite live
1955                // edits with cc-switch's stored MCP snapshot.
1956                sync_mcp: !matches!(app_type_clone, AppType::Codex),
1957                sync_codex_catalog: matches!(app_type_clone, AppType::Codex),
1958                stale_codex_catalog_keys: Vec::new(),
1959                refresh_snapshot: true,
1960                apply_hermes_switch_defaults: false,
1961                common_config_snippet: config.common_config_snippets.get(&app_type_clone).cloned(),
1962                takeover_active: false,
1963                preserve_live_preferences: true,
1964            };
1965
1966            Ok(((), Some(action)))
1967        })?;
1968
1969        if !app_type.is_additive_mode() {
1970            crate::settings::set_current_provider(&app_type, Some(provider_id))?;
1971        }
1972
1973        Ok(())
1974    }
1975
1976    fn write_live_snapshot(
1977        app_type: &AppType,
1978        provider: &Provider,
1979        common_config_snippet: Option<&str>,
1980        apply_common_config: bool,
1981        preserve_live_preferences: bool,
1982    ) -> Result<(), AppError> {
1983        let apply_common_config = Self::resolve_live_apply_common_config(
1984            app_type,
1985            provider,
1986            common_config_snippet,
1987            apply_common_config,
1988        );
1989
1990        match app_type {
1991            AppType::Codex => Self::write_codex_live(
1992                provider,
1993                common_config_snippet,
1994                apply_common_config,
1995                preserve_live_preferences,
1996            ),
1997            AppType::Claude => {
1998                Self::write_claude_live(provider, common_config_snippet, apply_common_config)
1999            }
2000            AppType::Gemini => Self::write_gemini_live(
2001                provider,
2002                if apply_common_config {
2003                    common_config_snippet
2004                } else {
2005                    None
2006                },
2007            ),
2008            AppType::OpenCode => {
2009                let config_to_write = if let Some(obj) = provider.settings_config.as_object() {
2010                    if obj.contains_key("$schema") || obj.contains_key("provider") {
2011                        obj.get("provider")
2012                            .and_then(|providers| providers.get(&provider.id))
2013                            .cloned()
2014                            .unwrap_or_else(|| provider.settings_config.clone())
2015                    } else {
2016                        provider.settings_config.clone()
2017                    }
2018                } else {
2019                    provider.settings_config.clone()
2020                };
2021
2022                match serde_json::from_value::<crate::provider::OpenCodeProviderConfig>(
2023                    config_to_write.clone(),
2024                ) {
2025                    Ok(config) => crate::opencode_config::set_typed_provider(&provider.id, &config),
2026                    Err(_) => crate::opencode_config::set_provider(&provider.id, config_to_write),
2027                }
2028            }
2029            AppType::OpenClaw => {
2030                let settings_config = provider.settings_config.clone();
2031                let looks_like_provider = settings_config.get("baseUrl").is_some()
2032                    || settings_config.get("api").is_some()
2033                    || settings_config.get("models").is_some();
2034                if !looks_like_provider {
2035                    return Ok(());
2036                }
2037
2038                let config = Self::parse_openclaw_provider_settings(&settings_config)?;
2039                Self::validate_openclaw_provider_models(&provider.id, &config)?;
2040                let write_result =
2041                    crate::openclaw_config::set_typed_provider(&provider.id, &config).map(|_| ());
2042
2043                write_result.map_err(Self::normalize_openclaw_live_write_error)
2044            }
2045            AppType::Hermes => {
2046                crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())
2047                    .map(|_| ())
2048            }
2049        }
2050    }
2051
2052    fn parse_openclaw_provider_settings(
2053        settings_config: &Value,
2054    ) -> Result<crate::provider::OpenClawProviderConfig, AppError> {
2055        let settings_obj = settings_config.as_object().ok_or_else(|| {
2056            AppError::localized(
2057                "provider.openclaw.settings.not_object",
2058                "OpenClaw 配置必须是 JSON 对象",
2059                "OpenClaw configuration must be a JSON object",
2060            )
2061        })?;
2062
2063        let legacy_aliases = Self::collect_openclaw_legacy_aliases(settings_obj);
2064        if !legacy_aliases.is_empty() {
2065            let aliases = legacy_aliases.join(", ");
2066            return Err(AppError::localized(
2067                "provider.openclaw.settings.invalid",
2068                format!(
2069                    "OpenClaw 配置使用了不支持的旧字段: {aliases}。请改用规范 OpenClaw 字段。"
2070                ),
2071                format!(
2072                    "OpenClaw config uses unsupported legacy alias keys: {aliases}. Use canonical OpenClaw keys instead."
2073                ),
2074            ));
2075        }
2076
2077        serde_json::from_value(settings_config.clone()).map_err(|err| {
2078            AppError::localized(
2079                "provider.openclaw.settings.invalid",
2080                format!("OpenClaw 配置格式无效: {err}"),
2081                format!("OpenClaw provider schema is invalid: {err}"),
2082            )
2083        })
2084    }
2085
2086    fn validate_openclaw_provider_models(
2087        provider_id: &str,
2088        config: &crate::provider::OpenClawProviderConfig,
2089    ) -> Result<(), AppError> {
2090        if config.models.is_empty() {
2091            return Err(AppError::localized(
2092                "provider.openclaw.models.missing",
2093                format!("OpenClaw 供应商 {provider_id} 至少需要一个模型"),
2094                format!("OpenClaw provider {provider_id} must define at least one model"),
2095            ));
2096        }
2097
2098        Ok(())
2099    }
2100
2101    fn collect_openclaw_legacy_aliases(
2102        settings_obj: &serde_json::Map<String, Value>,
2103    ) -> Vec<String> {
2104        let mut aliases = Vec::new();
2105
2106        for alias in ["api_key", "base_url", "options", "npm"] {
2107            if settings_obj.contains_key(alias) {
2108                aliases.push(alias.to_string());
2109            }
2110        }
2111
2112        if let Some(models) = settings_obj.get("models").and_then(Value::as_array) {
2113            for (index, model) in models.iter().enumerate() {
2114                if let Some(model_obj) = model.as_object() {
2115                    if model_obj.contains_key("context_window") {
2116                        aliases.push(format!("models[{index}].context_window"));
2117                    }
2118                }
2119            }
2120        }
2121
2122        aliases
2123    }
2124
2125    fn normalize_openclaw_live_write_error(err: AppError) -> AppError {
2126        match err {
2127            AppError::Config(message)
2128                if message.starts_with("Failed to parse OpenClaw config as JSON5:") =>
2129            {
2130                AppError::Config(message.replacen(
2131                    "Failed to parse OpenClaw config as JSON5",
2132                    "Failed to parse OpenClaw config as round-trip JSON5 document",
2133                    1,
2134                ))
2135            }
2136            other => other,
2137        }
2138    }
2139
2140    pub(crate) fn build_effective_live_snapshot(
2141        app_type: &AppType,
2142        provider: &Provider,
2143        common_config_snippet: Option<&str>,
2144        apply_common_config: bool,
2145    ) -> Result<Value, AppError> {
2146        let apply_common_config = Self::resolve_live_apply_common_config(
2147            app_type,
2148            provider,
2149            common_config_snippet,
2150            apply_common_config,
2151        );
2152
2153        match app_type {
2154            AppType::Claude => {
2155                let mut effective = common_config::build_effective_settings_with_common_config(
2156                    app_type,
2157                    provider,
2158                    common_config_snippet,
2159                    apply_common_config,
2160                )?;
2161                let _ = Self::normalize_claude_models_in_value(&mut effective);
2162                Ok(effective)
2163            }
2164            AppType::Codex => {
2165                let effective = common_config::build_effective_settings_with_common_config(
2166                    app_type,
2167                    provider,
2168                    common_config_snippet,
2169                    apply_common_config,
2170                )?;
2171                let settings = effective
2172                    .as_object()
2173                    .ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
2174                let auth = settings.get("auth").cloned();
2175                let cfg_text = crate::codex_config::codex_config_text_from_settings(&effective)?;
2176
2177                if !cfg_text.trim().is_empty() {
2178                    crate::codex_config::validate_config_toml(&cfg_text)?;
2179                }
2180
2181                let mut backup = serde_json::Map::new();
2182                if let Some(auth) = auth {
2183                    backup.insert("auth".to_string(), auth);
2184                }
2185                backup.insert("config".to_string(), Value::String(cfg_text));
2186                Ok(Value::Object(backup))
2187            }
2188            AppType::Gemini => {
2189                let content_to_write = common_config::build_effective_settings_with_common_config(
2190                    app_type,
2191                    provider,
2192                    common_config_snippet,
2193                    apply_common_config,
2194                )?;
2195
2196                let env_obj = content_to_write
2197                    .get("env")
2198                    .cloned()
2199                    .unwrap_or_else(|| json!({}));
2200                let settings_path = crate::gemini_config::get_gemini_settings_path();
2201                let config_value = if let Some(config_value) = content_to_write.get("config") {
2202                    if config_value.is_null() {
2203                        if settings_path.exists() {
2204                            read_json_file(&settings_path)?
2205                        } else {
2206                            json!({})
2207                        }
2208                    } else if let Some(provider_config) = config_value.as_object() {
2209                        if provider_config.is_empty() {
2210                            if settings_path.exists() {
2211                                read_json_file(&settings_path)?
2212                            } else {
2213                                json!({})
2214                            }
2215                        } else {
2216                            let mut merged = if settings_path.exists() {
2217                                read_json_file(&settings_path)?
2218                            } else {
2219                                json!({})
2220                            };
2221
2222                            if !merged.is_object() {
2223                                merged = json!({});
2224                            }
2225
2226                            let merged_map = merged.as_object_mut().ok_or_else(|| {
2227                                AppError::localized(
2228                                    "gemini.validation.invalid_settings",
2229                                    "Gemini 现有 settings.json 格式错误: 必须是对象",
2230                                    "Gemini existing settings.json invalid: must be a JSON object",
2231                                )
2232                            })?;
2233                            for (key, value) in provider_config {
2234                                merged_map.insert(key.clone(), value.clone());
2235                            }
2236                            merged
2237                        }
2238                    } else {
2239                        return Err(AppError::localized(
2240                            "gemini.validation.invalid_config",
2241                            "Gemini 配置格式错误: config 必须是对象或 null",
2242                            "Gemini config invalid: config must be an object or null",
2243                        ));
2244                    }
2245                } else if settings_path.exists() {
2246                    read_json_file(&settings_path)?
2247                } else {
2248                    json!({})
2249                };
2250
2251                Ok(json!({
2252                    "env": env_obj,
2253                    "config": config_value,
2254                }))
2255            }
2256            AppType::OpenCode => Err(AppError::Config(
2257                "OpenCode does not support proxy takeover backups".into(),
2258            )),
2259            AppType::OpenClaw => Err(AppError::Config(
2260                "OpenClaw does not support proxy takeover backups".into(),
2261            )),
2262            AppType::Hermes => Err(AppError::Config(
2263                "Hermes does not support proxy takeover backups".into(),
2264            )),
2265        }
2266    }
2267
2268    fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
2269        match app_type {
2270            AppType::Claude => {
2271                if !provider.settings_config.is_object() {
2272                    return Err(AppError::localized(
2273                        "provider.claude.settings.not_object",
2274                        "Claude 配置必须是 JSON 对象",
2275                        "Claude configuration must be a JSON object",
2276                    ));
2277                }
2278            }
2279            AppType::Codex => {
2280                let settings = provider.settings_config.as_object().ok_or_else(|| {
2281                    AppError::localized(
2282                        "provider.codex.settings.not_object",
2283                        "Codex 配置必须是 JSON 对象",
2284                        "Codex configuration must be a JSON object",
2285                    )
2286                })?;
2287
2288                let auth = settings.get("auth").ok_or_else(|| {
2289                    AppError::localized(
2290                        "provider.codex.auth.missing",
2291                        format!("供应商 {} 缺少 auth 配置", provider.id),
2292                        format!("Provider {} is missing auth configuration", provider.id),
2293                    )
2294                })?;
2295                if !auth.is_object() {
2296                    return Err(AppError::localized(
2297                        "provider.codex.auth.not_object",
2298                        format!("供应商 {} 的 auth 配置必须是 JSON 对象", provider.id),
2299                        format!(
2300                            "Provider {} auth configuration must be a JSON object",
2301                            provider.id
2302                        ),
2303                    ));
2304                }
2305
2306                if let Some(config_value) = settings.get("config") {
2307                    if !(config_value.is_string() || config_value.is_null()) {
2308                        return Err(AppError::localized(
2309                            "provider.codex.config.invalid_type",
2310                            "Codex config 字段必须是字符串",
2311                            "Codex config field must be a string",
2312                        ));
2313                    }
2314                    if let Some(cfg_text) = config_value.as_str() {
2315                        crate::codex_config::validate_config_toml(cfg_text)?;
2316                    }
2317                }
2318                if let Some(codex_value) = settings.get("codex") {
2319                    if !(codex_value.is_object() || codex_value.is_null()) {
2320                        return Err(AppError::localized(
2321                            "provider.codex.config.invalid_type",
2322                            "Codex codex 字段必须是对象",
2323                            "Codex codex field must be an object",
2324                        ));
2325                    }
2326                    let cfg_text =
2327                        crate::codex_config::codex_structured_config_to_toml(codex_value)?;
2328                    crate::codex_config::validate_config_toml(&cfg_text)?;
2329                }
2330
2331                if !Self::is_codex_official_provider(provider) {
2332                    let config_text = crate::codex_config::codex_config_text_from_settings(
2333                        &provider.settings_config,
2334                    )?;
2335                    if !Self::codex_config_has_base_url(&config_text) {
2336                        return Err(AppError::localized(
2337                            "provider.codex.base_url.missing",
2338                            format!("供应商 {} 缺少有效的 Codex Base URL", provider.id),
2339                            format!("Provider {} is missing a valid Codex base_url", provider.id),
2340                        ));
2341                    }
2342                }
2343            }
2344            AppType::Gemini => {
2345                use crate::gemini_config::validate_gemini_settings;
2346                validate_gemini_settings(&provider.settings_config)?
2347            }
2348            AppType::OpenCode => {
2349                if !provider.settings_config.is_object() {
2350                    return Err(AppError::localized(
2351                        "provider.opencode.settings.not_object",
2352                        "OpenCode 配置必须是 JSON 对象",
2353                        "OpenCode configuration must be a JSON object",
2354                    ));
2355                }
2356            }
2357            AppType::OpenClaw => {
2358                let config = Self::parse_openclaw_provider_settings(&provider.settings_config)?;
2359                Self::validate_openclaw_provider_models(&provider.id, &config)?;
2360            }
2361            AppType::Hermes => {
2362                // Hermes uses flexible YAML config; basic check that settings is an object
2363                if !provider.settings_config.is_object() {
2364                    return Err(AppError::localized(
2365                        "provider.hermes.settings.not_object",
2366                        "Hermes 供应商配置必须是 JSON 对象",
2367                        "Hermes provider configuration must be a JSON object",
2368                    ));
2369                }
2370            }
2371        }
2372
2373        // 🔧 验证并清理 UsageScript 配置(所有应用类型通用)
2374        if let Some(meta) = &provider.meta {
2375            if let Some(usage_script) = &meta.usage_script {
2376                Self::validate_usage_script(usage_script)?;
2377            }
2378        }
2379
2380        Ok(())
2381    }
2382
2383    pub(crate) fn build_live_backup_snapshot(
2384        app_type: &AppType,
2385        provider: &Provider,
2386        common_config_snippet: Option<&str>,
2387        apply_common_config: bool,
2388    ) -> Result<Value, AppError> {
2389        Self::build_effective_live_snapshot(
2390            app_type,
2391            provider,
2392            common_config_snippet,
2393            apply_common_config,
2394        )
2395    }
2396
2397    pub(crate) fn build_effective_live_snapshot_from_state(
2398        state: &AppState,
2399        app_type: AppType,
2400        provider: &Provider,
2401    ) -> Result<Value, AppError> {
2402        let common_config_snippet = {
2403            let config = state.config.read().map_err(AppError::from)?;
2404            config.common_config_snippets.get(&app_type).cloned()
2405        };
2406
2407        Self::build_effective_live_snapshot(
2408            &app_type,
2409            provider,
2410            common_config_snippet.as_deref(),
2411            true,
2412        )
2413    }
2414
2415    pub(crate) fn get_provider(
2416        state: &AppState,
2417        app_type: AppType,
2418        provider_id: &str,
2419    ) -> Result<Provider, AppError> {
2420        let config = state.config.read().map_err(AppError::from)?;
2421        let manager = config
2422            .get_manager(&app_type)
2423            .ok_or_else(|| Self::app_not_found(&app_type))?;
2424
2425        manager.providers.get(provider_id).cloned().ok_or_else(|| {
2426            AppError::localized(
2427                "provider.not_found",
2428                format!("供应商不存在: {provider_id}"),
2429                format!("Provider not found: {provider_id}"),
2430            )
2431        })
2432    }
2433
2434    fn app_not_found(app_type: &AppType) -> AppError {
2435        AppError::localized(
2436            "provider.app_not_found",
2437            format!("应用类型不存在: {app_type:?}"),
2438            format!("App type not found: {app_type:?}"),
2439        )
2440    }
2441
2442    pub fn delete(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
2443        let (local_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
2444            (None, None)
2445        } else {
2446            (
2447                crate::settings::get_current_provider(&app_type),
2448                state.db.get_current_provider(app_type.as_str())?,
2449            )
2450        };
2451        let provider_snapshot = {
2452            let config = state.config.read().map_err(AppError::from)?;
2453            let manager = config
2454                .get_manager(&app_type)
2455                .ok_or_else(|| Self::app_not_found(&app_type))?;
2456
2457            if !app_type.is_additive_mode()
2458                && (local_current_provider.as_deref() == Some(provider_id)
2459                    || stored_current_provider.as_deref() == Some(provider_id))
2460            {
2461                return Err(AppError::localized(
2462                    "provider.delete.current",
2463                    "不能删除当前正在使用的供应商",
2464                    "Cannot delete the provider currently in use",
2465                ));
2466            }
2467
2468            manager.providers.get(provider_id).cloned().ok_or_else(|| {
2469                AppError::localized(
2470                    "provider.not_found",
2471                    format!("供应商不存在: {provider_id}"),
2472                    format!("Provider not found: {provider_id}"),
2473                )
2474            })?
2475        };
2476        let stale_codex_catalog_keys = if matches!(app_type, AppType::Codex) {
2477            Self::provider_codex_model_provider_key(&provider_snapshot)
2478                .into_iter()
2479                .collect::<Vec<_>>()
2480        } else {
2481            Vec::new()
2482        };
2483
2484        if app_type.is_additive_mode() {
2485            match app_type {
2486                AppType::OpenCode => {
2487                    if crate::opencode_config::get_opencode_dir().exists() {
2488                        crate::opencode_config::remove_provider(provider_id)?;
2489                    }
2490                }
2491                AppType::OpenClaw => {
2492                    if crate::openclaw_config::get_openclaw_dir().exists() {
2493                        crate::openclaw_config::remove_provider(provider_id)?;
2494                    }
2495                }
2496                AppType::Hermes => {
2497                    if crate::hermes_config::get_hermes_dir().exists() {
2498                        crate::hermes_config::remove_provider(provider_id)?;
2499                    }
2500                }
2501                _ => unreachable!("non-additive apps should not enter additive delete branch"),
2502            }
2503
2504            {
2505                let mut config = state.config.write().map_err(AppError::from)?;
2506                let manager = config
2507                    .get_manager_mut(&app_type)
2508                    .ok_or_else(|| Self::app_not_found(&app_type))?;
2509                manager.providers.shift_remove(provider_id);
2510            }
2511
2512            return state.save();
2513        }
2514
2515        match app_type {
2516            AppType::Codex => {
2517                crate::codex_config::delete_codex_provider_config(
2518                    provider_id,
2519                    &provider_snapshot.name,
2520                )?;
2521            }
2522            AppType::Claude => {
2523                // 兼容旧版本:历史上会在 Claude 目录内为每个供应商生成 settings-*.json 副本
2524                // 这里继续清理这些遗留文件,避免堆积过期配置。
2525                let by_name = get_provider_config_path(provider_id, Some(&provider_snapshot.name));
2526                let by_id = get_provider_config_path(provider_id, None);
2527                delete_file(&by_name)?;
2528                delete_file(&by_id)?;
2529            }
2530            AppType::Gemini => {
2531                // Gemini 使用单一的 .env 文件,不需要删除单独的供应商配置文件
2532            }
2533            AppType::OpenCode => {
2534                let _ = provider_snapshot;
2535            }
2536            AppType::OpenClaw => {
2537                let _ = provider_snapshot;
2538            }
2539            AppType::Hermes => {
2540                let _ = provider_snapshot;
2541            }
2542        }
2543
2544        {
2545            let mut config = state.config.write().map_err(AppError::from)?;
2546            let manager = config
2547                .get_manager_mut(&app_type)
2548                .ok_or_else(|| Self::app_not_found(&app_type))?;
2549
2550            if !app_type.is_additive_mode()
2551                && (local_current_provider.as_deref() == Some(provider_id)
2552                    || stored_current_provider.as_deref() == Some(provider_id))
2553            {
2554                return Err(AppError::localized(
2555                    "provider.delete.current",
2556                    "不能删除当前正在使用的供应商",
2557                    "Cannot delete the provider currently in use",
2558                ));
2559            }
2560
2561            if !app_type.is_additive_mode() && manager.current == provider_id {
2562                manager.current = stored_current_provider.clone().unwrap_or_default();
2563            }
2564
2565            manager.providers.shift_remove(provider_id);
2566        }
2567
2568        state.save()?;
2569        if matches!(app_type, AppType::Codex) {
2570            Self::sync_codex_provider_catalog_to_live(state, &stale_codex_catalog_keys)?;
2571        }
2572        Ok(())
2573    }
2574
2575    pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2576        live::import_openclaw_providers_from_live(state)
2577    }
2578
2579    pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2580        live::import_opencode_providers_from_live(state)
2581    }
2582
2583    pub fn import_hermes_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2584        live::import_hermes_providers_from_live(state)
2585    }
2586}
2587
2588#[derive(Debug, Clone, Deserialize)]
2589pub struct ProviderSortUpdate {
2590    pub id: String,
2591    #[serde(rename = "sortIndex")]
2592    pub sort_index: usize,
2593}