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