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