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