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