Skip to main content

cc_switch_lib/services/provider/
mod.rs

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