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 == AppType::Hermes {
1098            return Ok(crate::hermes_config::get_model_config()?
1099                .and_then(|model| model.provider)
1100                .map(|provider| provider.trim().to_string())
1101                .filter(|provider| !provider.is_empty())
1102                .unwrap_or_default());
1103        }
1104        if app_type.is_additive_mode() {
1105            return Ok(String::new());
1106        }
1107        crate::settings::get_effective_current_provider(&state.db, &app_type)
1108            .map(|opt| opt.unwrap_or_default())
1109    }
1110
1111    /// 新增供应商
1112    pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
1113        let mut provider = provider;
1114        // 归一化 Claude 模型键
1115        Self::normalize_provider_if_claude(&app_type, &mut provider);
1116        Self::validate_provider_settings(&app_type, &provider)?;
1117
1118        let app_type_clone = app_type.clone();
1119        let provider_clone = provider.clone();
1120        let stored_current_provider = if app_type.is_additive_mode() {
1121            None
1122        } else {
1123            state.db.get_current_provider(app_type.as_str())?
1124        };
1125
1126        Self::run_transaction(state, move |config| {
1127            let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1128            let mut provider_to_store = provider_clone.clone();
1129            Self::normalize_provider_for_storage(
1130                &app_type_clone,
1131                &mut provider_to_store,
1132                common_config_snippet.as_deref(),
1133            )?;
1134
1135            if matches!(app_type_clone, AppType::OpenClaw)
1136                && provider_to_store.created_at.is_none()
1137                && live::is_auto_mirrored_openclaw_snapshot(&provider_to_store)
1138            {
1139                provider_to_store.created_at = Some(current_timestamp());
1140            }
1141            if app_type_clone.is_additive_mode() {
1142                Self::set_provider_live_config_managed(&mut provider_to_store, true);
1143            }
1144
1145            config.ensure_app(&app_type_clone);
1146            let manager = config
1147                .get_manager_mut(&app_type_clone)
1148                .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1149
1150            if !app_type_clone.is_additive_mode() {
1151                manager.current = stored_current_provider.clone().unwrap_or_default();
1152            }
1153
1154            let was_empty = manager.providers.is_empty();
1155            manager
1156                .providers
1157                .insert(provider_to_store.id.clone(), provider_to_store.clone());
1158
1159            if !app_type_clone.is_additive_mode()
1160                && stored_current_provider.is_none()
1161                && (was_empty || manager.current.is_empty())
1162            {
1163                manager.current = provider_to_store.id.clone();
1164            }
1165
1166            let is_current =
1167                app_type_clone.is_additive_mode() || manager.current == provider_to_store.id;
1168            let action = if is_current {
1169                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1170                Some(PostCommitAction {
1171                    app_type: app_type_clone.clone(),
1172                    provider: provider_to_store.clone(),
1173                    backup,
1174                    // Codex current-provider saves rewrite live config from the stored snapshot,
1175                    // so managed MCP must be synced back after the write.
1176                    sync_mcp: matches!(&app_type_clone, AppType::Codex),
1177                    refresh_snapshot: false,
1178                    common_config_snippet,
1179                    takeover_active: false,
1180                })
1181            } else {
1182                None
1183            };
1184
1185            Ok((true, action))
1186        })
1187    }
1188
1189    /// 更新供应商
1190    pub fn update(
1191        state: &AppState,
1192        app_type: AppType,
1193        provider: Provider,
1194    ) -> Result<bool, AppError> {
1195        let mut provider = provider;
1196        // 归一化 Claude 模型键
1197        Self::normalize_provider_if_claude(&app_type, &mut provider);
1198        Self::validate_provider_settings(&app_type, &provider)?;
1199        let provider_id = provider.id.clone();
1200        let app_type_clone = app_type.clone();
1201        let provider_clone = provider.clone();
1202        let (effective_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
1203            (None, None)
1204        } else {
1205            (
1206                crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1207                state.db.get_current_provider(app_type.as_str())?,
1208            )
1209        };
1210
1211        Self::run_transaction(state, move |config| {
1212            let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1213            let manager = config
1214                .get_manager_mut(&app_type_clone)
1215                .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1216
1217            if !manager.providers.contains_key(&provider_id) {
1218                return Err(AppError::localized(
1219                    "provider.not_found",
1220                    format!("供应商不存在: {provider_id}"),
1221                    format!("Provider not found: {provider_id}"),
1222                ));
1223            }
1224
1225            if !app_type_clone.is_additive_mode() {
1226                manager.current = stored_current_provider.clone().unwrap_or_default();
1227            }
1228
1229            let existing_live_config_managed = manager
1230                .providers
1231                .get(&provider_id)
1232                .and_then(Self::provider_live_config_managed);
1233            let mut merged = if let Some(existing) = manager.providers.get(&provider_id) {
1234                let mut updated = provider_clone.clone();
1235                match (existing.meta.as_ref(), updated.meta.take()) {
1236                    // 前端未提供 meta,表示不修改,沿用旧值
1237                    (Some(old_meta), None) => {
1238                        updated.meta = Some(old_meta.clone());
1239                    }
1240                    (None, None) => {
1241                        updated.meta = None;
1242                    }
1243                    // 前端提供的 meta 视为权威,直接覆盖(其中 custom_endpoints 允许是空,表示删除所有自定义端点)
1244                    (_old, Some(new_meta)) => {
1245                        updated.meta = Some(new_meta);
1246                    }
1247                }
1248                if matches!(app_type_clone, AppType::OpenClaw)
1249                    && updated.created_at.is_none()
1250                    && live::is_auto_mirrored_openclaw_snapshot(&updated)
1251                {
1252                    updated.created_at = Some(current_timestamp());
1253                }
1254                updated
1255            } else {
1256                provider_clone.clone()
1257            };
1258
1259            Self::normalize_provider_for_storage(
1260                &app_type_clone,
1261                &mut merged,
1262                common_config_snippet.as_deref(),
1263            )?;
1264
1265            let should_write_live = if app_type_clone.is_additive_mode() {
1266                let live_config_managed = Self::additive_provider_exists_in_live_config(
1267                    &app_type_clone,
1268                    &provider_id,
1269                    Self::provider_live_config_managed(&merged).or(existing_live_config_managed),
1270                )?;
1271                Self::set_provider_live_config_managed(&mut merged, live_config_managed);
1272                live_config_managed
1273            } else {
1274                effective_current_provider.as_deref() == Some(provider_id.as_str())
1275            };
1276
1277            manager
1278                .providers
1279                .insert(provider_id.clone(), merged.clone());
1280
1281            let action = if should_write_live {
1282                let backup = Self::capture_live_snapshot(&app_type_clone)?;
1283                Some(PostCommitAction {
1284                    app_type: app_type_clone.clone(),
1285                    provider: merged,
1286                    backup,
1287                    // Codex current-provider saves rewrite live config from the stored snapshot,
1288                    // so managed MCP must be synced back after the write.
1289                    sync_mcp: matches!(&app_type_clone, AppType::Codex),
1290                    refresh_snapshot: false,
1291                    common_config_snippet,
1292                    takeover_active: false,
1293                })
1294            } else {
1295                None
1296            };
1297
1298            Ok((true, action))
1299        })
1300    }
1301
1302    /// 导入当前 live 配置为默认供应商。
1303    ///
1304    /// 返回 `Ok(true)` 表示实际导入,`Ok(false)` 表示该 app 已有非官方 seed provider 而跳过。
1305    pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
1306        if app_type.is_additive_mode() {
1307            return Ok(false);
1308        }
1309
1310        if state.db.has_non_official_seed_provider(app_type.as_str())? {
1311            return Ok(false);
1312        }
1313
1314        let settings_config = match app_type {
1315            AppType::Codex => {
1316                let auth_path = get_codex_auth_path();
1317                if !auth_path.exists() {
1318                    return Err(AppError::localized(
1319                        "codex.live.missing",
1320                        "Codex 配置文件不存在",
1321                        "Codex configuration file is missing",
1322                    ));
1323                }
1324                let auth: Value = read_json_file(&auth_path)?;
1325                let config_str = crate::codex_config::read_and_validate_codex_config_text()?;
1326                json!({ "auth": auth, "config": config_str })
1327            }
1328            AppType::Claude => {
1329                let settings_path = get_claude_settings_path();
1330                if !settings_path.exists() {
1331                    return Err(AppError::localized(
1332                        "claude.live.missing",
1333                        "Claude Code 配置文件不存在",
1334                        "Claude settings file is missing",
1335                    ));
1336                }
1337                let mut v = read_json_file::<Value>(&settings_path)?;
1338                let _ = Self::normalize_claude_models_in_value(&mut v);
1339                v
1340            }
1341            AppType::Gemini => {
1342                use crate::gemini_config::{
1343                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1344                };
1345
1346                // 读取 .env 文件(环境变量)
1347                let env_path = get_gemini_env_path();
1348                if !env_path.exists() {
1349                    return Err(AppError::localized(
1350                        "gemini.live.missing",
1351                        "Gemini 配置文件不存在",
1352                        "Gemini configuration file is missing",
1353                    ));
1354                }
1355
1356                let env_map = read_gemini_env()?;
1357                let env_json = env_to_json(&env_map);
1358                let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1359
1360                // 读取 settings.json 文件(MCP 配置等)
1361                let settings_path = get_gemini_settings_path();
1362                let config_obj = if settings_path.exists() {
1363                    read_json_file(&settings_path)?
1364                } else {
1365                    json!({})
1366                };
1367
1368                // 返回完整结构:{ "env": {...}, "config": {...} }
1369                json!({
1370                    "env": env_obj,
1371                    "config": config_obj
1372                })
1373            }
1374            AppType::OpenCode => unreachable!("additive mode apps are handled earlier"),
1375            AppType::OpenClaw => unreachable!("additive mode apps are handled earlier"),
1376            AppType::Hermes => unreachable!("additive mode apps are handled earlier"),
1377        };
1378
1379        let mut provider = Provider::with_id(
1380            "default".to_string(),
1381            "default".to_string(),
1382            settings_config,
1383            None,
1384        );
1385        provider.category = Some("custom".to_string());
1386
1387        state.db.save_provider(app_type.as_str(), &provider)?;
1388        state
1389            .db
1390            .set_current_provider(app_type.as_str(), &provider.id)?;
1391        Ok(true)
1392    }
1393
1394    /// 读取当前 live 配置
1395    pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
1396        match app_type {
1397            AppType::Codex => {
1398                let auth_path = get_codex_auth_path();
1399                let config_path = get_codex_config_path();
1400                if !config_path.exists() {
1401                    return Err(AppError::localized(
1402                        "codex.live.missing",
1403                        "Codex 配置文件不存在",
1404                        "Codex configuration is missing",
1405                    ));
1406                }
1407
1408                let mut live_settings = serde_json::Map::new();
1409                if auth_path.exists() {
1410                    live_settings.insert("auth".to_string(), read_json_file(&auth_path)?);
1411                }
1412                if config_path.exists() {
1413                    let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
1414                    live_settings.insert("config".to_string(), Value::String(cfg_text));
1415                }
1416
1417                Ok(Value::Object(live_settings))
1418            }
1419            AppType::Claude => {
1420                let path = get_claude_settings_path();
1421                if !path.exists() {
1422                    return Err(AppError::localized(
1423                        "claude.live.missing",
1424                        "Claude Code 配置文件不存在",
1425                        "Claude settings file is missing",
1426                    ));
1427                }
1428                read_json_file(&path)
1429            }
1430            AppType::Gemini => {
1431                use crate::gemini_config::{
1432                    env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1433                };
1434
1435                // 读取 .env 文件(环境变量)
1436                let env_path = get_gemini_env_path();
1437                if !env_path.exists() {
1438                    return Err(AppError::localized(
1439                        "gemini.env.missing",
1440                        "Gemini .env 文件不存在",
1441                        "Gemini .env file not found",
1442                    ));
1443                }
1444
1445                let env_map = read_gemini_env()?;
1446                let env_json = env_to_json(&env_map);
1447                let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1448
1449                // 读取 settings.json 文件(MCP 配置等)
1450                let settings_path = get_gemini_settings_path();
1451                let config_obj = if settings_path.exists() {
1452                    read_json_file(&settings_path)?
1453                } else {
1454                    json!({})
1455                };
1456
1457                // 返回完整结构:{ "env": {...}, "config": {...} }
1458                Ok(json!({
1459                    "env": env_obj,
1460                    "config": config_obj
1461                }))
1462            }
1463            AppType::OpenCode => {
1464                let config_path = crate::opencode_config::get_opencode_config_path();
1465                if !config_path.exists() {
1466                    return Err(AppError::localized(
1467                        "opencode.config.missing",
1468                        "OpenCode 配置文件不存在",
1469                        "OpenCode configuration file not found",
1470                    ));
1471                }
1472                crate::opencode_config::read_opencode_config()
1473            }
1474            AppType::OpenClaw => {
1475                let config_path = crate::openclaw_config::get_openclaw_config_path();
1476                if !config_path.exists() {
1477                    return Err(AppError::localized(
1478                        "openclaw.config.missing",
1479                        "OpenClaw 配置文件不存在",
1480                        "OpenClaw configuration file not found",
1481                    ));
1482                }
1483                crate::openclaw_config::read_openclaw_config()
1484            }
1485            AppType::Hermes => {
1486                let yaml = crate::hermes_config::read_hermes_config()?;
1487                crate::hermes_config::yaml_to_json(&yaml)
1488            }
1489        }
1490    }
1491
1492    /// 更新供应商排序
1493    pub fn update_sort_order(
1494        state: &AppState,
1495        app_type: AppType,
1496        updates: Vec<ProviderSortUpdate>,
1497    ) -> Result<bool, AppError> {
1498        {
1499            let mut cfg = state.config.write().map_err(AppError::from)?;
1500            let manager = cfg
1501                .get_manager_mut(&app_type)
1502                .ok_or_else(|| Self::app_not_found(&app_type))?;
1503
1504            for update in updates {
1505                if let Some(provider) = manager.providers.get_mut(&update.id) {
1506                    provider.sort_index = Some(update.sort_index);
1507                }
1508            }
1509        }
1510
1511        state.save()?;
1512        Ok(true)
1513    }
1514
1515    pub fn remove_from_live_config(
1516        state: &AppState,
1517        app_type: AppType,
1518        provider_id: &str,
1519    ) -> Result<(), AppError> {
1520        if !app_type.is_additive_mode() {
1521            return Err(AppError::localized(
1522                "provider.remove_from_live_config.unsupported",
1523                "只有累加模式应用支持从 live 配置中移除供应商",
1524                "Only additive-mode apps support removing a provider from live config",
1525            ));
1526        }
1527
1528        let original = {
1529            let config = state.config.read().map_err(AppError::from)?;
1530            let manager = config
1531                .get_manager(&app_type)
1532                .ok_or_else(|| Self::app_not_found(&app_type))?;
1533            if !manager.providers.contains_key(provider_id) {
1534                return Err(AppError::localized(
1535                    "provider.not_found",
1536                    format!("供应商不存在: {provider_id}"),
1537                    format!("Provider not found: {provider_id}"),
1538                ));
1539            }
1540            config.clone()
1541        };
1542
1543        let backup = Self::capture_live_snapshot(&app_type)?;
1544        match &app_type {
1545            AppType::OpenCode => {
1546                if crate::opencode_config::get_opencode_dir().exists() {
1547                    crate::opencode_config::remove_provider(provider_id)?;
1548                }
1549            }
1550            AppType::OpenClaw => {
1551                if crate::openclaw_config::get_openclaw_dir().exists() {
1552                    crate::openclaw_config::remove_provider(provider_id)?;
1553                }
1554            }
1555            _ => unreachable!("non-additive apps should not enter remove-from-live branch"),
1556        }
1557
1558        {
1559            let mut config = state.config.write().map_err(AppError::from)?;
1560            let manager = config
1561                .get_manager_mut(&app_type)
1562                .ok_or_else(|| Self::app_not_found(&app_type))?;
1563            let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
1564                AppError::localized(
1565                    "provider.not_found",
1566                    format!("供应商不存在: {provider_id}"),
1567                    format!("Provider not found: {provider_id}"),
1568                )
1569            })?;
1570            Self::set_provider_live_config_managed(provider, false);
1571        }
1572
1573        if let Err(save_err) = state.save() {
1574            let config_restore = Self::restore_config_only(state, original);
1575            let live_restore = backup.restore();
1576            if let Err(rollback_err) = config_restore {
1577                return Err(AppError::localized(
1578                    "config.save.rollback_failed",
1579                    format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
1580                    format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
1581                ));
1582            }
1583            if let Err(rollback_err) = live_restore {
1584                return Err(AppError::localized(
1585                    "post_commit.rollback_failed",
1586                    format!("保存配置失败: {save_err};live 回滚失败: {rollback_err}"),
1587                    format!(
1588                        "Failed to save config: {save_err}; live rollback failed: {rollback_err}"
1589                    ),
1590                ));
1591            }
1592            return Err(save_err);
1593        }
1594
1595        Ok(())
1596    }
1597
1598    /// 将所有应用的当前供应商配置同步到 live 文件。
1599    ///
1600    /// 用于 WebDAV 下载、备份恢复等场景:数据库已更新,但 live 配置文件
1601    /// (`~/.codex/config.toml`、Claude `settings.json` 等)尚未同步。
1602    /// 对齐上游 `sync_current_to_live` 行为。
1603    pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
1604        use crate::services::mcp::McpService;
1605
1606        // 在读锁下收集所有需要的数据,避免持锁写文件
1607        let snapshots: Vec<(AppType, Provider, Option<String>)> = {
1608            let guard = state.config.read().map_err(AppError::from)?;
1609            let mut result = Vec::new();
1610            for app_type in AppType::all() {
1611                if app_type.is_additive_mode() {
1612                    if let Some(manager) = guard.get_manager(&app_type) {
1613                        let snippet = guard.common_config_snippets.get(&app_type).cloned();
1614                        for provider in manager.providers.values() {
1615                            if Self::provider_live_config_managed(provider) == Some(false) {
1616                                continue;
1617                            }
1618                            result.push((app_type.clone(), provider.clone(), snippet.clone()));
1619                        }
1620                    }
1621                    continue;
1622                }
1623
1624                let current_id =
1625                    match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
1626                        Some(id) => id,
1627                        None => continue,
1628                    };
1629                let providers = state.db.get_all_providers(app_type.as_str())?;
1630                match providers.get(&current_id) {
1631                    Some(provider) => {
1632                        let snippet = state.db.get_config_snippet(app_type.as_str())?;
1633                        result.push((app_type.clone(), provider.clone(), snippet));
1634                    }
1635                    None => {
1636                        log::warn!(
1637                            "sync_current_to_live: {app_type} 当前供应商 {} 不存在于数据库,跳过",
1638                            current_id
1639                        );
1640                    }
1641                }
1642            }
1643            result
1644        };
1645
1646        let openclaw_live_provider_ids = match Self::valid_openclaw_live_provider_ids() {
1647            Ok(provider_ids) => provider_ids,
1648            Err(err) => {
1649                log::warn!(
1650                    "sync_current_to_live: 读取 OpenClaw live providers 失败,跳过 OpenClaw 同步: {err}"
1651                );
1652                None
1653            }
1654        };
1655
1656        for (app_type, provider, snippet) in &snapshots {
1657            if matches!(app_type, AppType::OpenClaw)
1658                && !openclaw_live_provider_ids
1659                    .as_ref()
1660                    .is_some_and(|provider_ids| provider_ids.contains(&provider.id))
1661            {
1662                continue;
1663            }
1664
1665            if let Err(e) = Self::write_live_snapshot(app_type, provider, snippet.as_deref(), true)
1666            {
1667                log::warn!("sync_current_to_live: 写入 {app_type} live 配置失败: {e}");
1668            }
1669        }
1670
1671        if let Err(e) =
1672            crate::services::prompt::PromptService::sync_all_active_to_live_best_effort(state)
1673        {
1674            log::warn!("sync_current_to_live: Prompt 同步失败: {e}");
1675        }
1676
1677        if let Err(e) = McpService::sync_all_enabled(state) {
1678            log::warn!("sync_current_to_live: MCP 同步失败: {e}");
1679        }
1680
1681        if let Err(e) = crate::services::skill::SkillService::sync_all_enabled_best_effort() {
1682            log::warn!("sync_current_to_live: Skills 同步失败: {e}");
1683        }
1684
1685        Ok(())
1686    }
1687
1688    /// 切换指定应用的供应商
1689    pub fn switch(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
1690        if !app_type.is_additive_mode() {
1691            let providers = state.db.get_all_providers(app_type.as_str())?;
1692            providers.get(provider_id).ok_or_else(|| {
1693                AppError::localized(
1694                    "provider.not_found",
1695                    format!("供应商不存在: {provider_id}"),
1696                    format!("Provider not found: {provider_id}"),
1697                )
1698            })?;
1699
1700            let is_app_taken_over =
1701                futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
1702                    .ok()
1703                    .flatten()
1704                    .is_some();
1705            let is_proxy_running = state
1706                .proxy_service
1707                .is_running_blocking()
1708                .map_err(AppError::Message)?;
1709            let live_taken_over = state
1710                .proxy_service
1711                .detect_takeover_in_live_config_for_app(&app_type);
1712            let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
1713
1714            if should_hot_switch {
1715                futures::executor::block_on(
1716                    state
1717                        .proxy_service
1718                        .hot_switch_provider(app_type.as_str(), provider_id),
1719                )
1720                .map_err(|e| AppError::Message(format!("热切换失败: {e}")))?;
1721
1722                let mut guard = state.config.write().map_err(AppError::from)?;
1723                if let Some(manager) = guard.get_manager_mut(&app_type) {
1724                    manager.current = provider_id.to_string();
1725                }
1726                return Ok(());
1727            }
1728        }
1729
1730        let app_type_clone = app_type.clone();
1731        let provider_id_owned = provider_id.to_string();
1732        let effective_current_provider = if app_type.is_additive_mode() {
1733            None
1734        } else {
1735            crate::settings::get_effective_current_provider(&state.db, &app_type)?
1736        };
1737
1738        Self::run_transaction(state, move |config| {
1739            if app_type_clone.is_additive_mode() {
1740                let provider = {
1741                    let provider = config
1742                        .get_manager_mut(&app_type_clone)
1743                        .ok_or_else(|| Self::app_not_found(&app_type_clone))?
1744                        .providers
1745                        .get_mut(&provider_id_owned)
1746                        .ok_or_else(|| {
1747                            AppError::localized(
1748                                "provider.not_found",
1749                                format!("供应商不存在: {provider_id_owned}"),
1750                                format!("Provider not found: {provider_id_owned}"),
1751                            )
1752                        })?;
1753                    Self::set_provider_live_config_managed(provider, true);
1754                    provider.clone()
1755                };
1756
1757                let action = PostCommitAction {
1758                    app_type: app_type_clone.clone(),
1759                    provider,
1760                    backup: Self::capture_live_snapshot(&app_type_clone)?,
1761                    sync_mcp: matches!(app_type_clone, AppType::OpenCode),
1762                    refresh_snapshot: false,
1763                    common_config_snippet: config
1764                        .common_config_snippets
1765                        .get(&app_type_clone)
1766                        .cloned(),
1767                    takeover_active: false,
1768                };
1769
1770                return Ok(((), Some(action)));
1771            }
1772
1773            let backup = Self::capture_live_snapshot(&app_type_clone)?;
1774            let provider = match app_type_clone {
1775                AppType::Codex => Self::prepare_switch_codex(
1776                    config,
1777                    &provider_id_owned,
1778                    effective_current_provider.as_deref(),
1779                )?,
1780                AppType::Claude => Self::prepare_switch_claude(
1781                    config,
1782                    &provider_id_owned,
1783                    effective_current_provider.as_deref(),
1784                )?,
1785                AppType::Gemini => Self::prepare_switch_gemini(
1786                    config,
1787                    &provider_id_owned,
1788                    effective_current_provider.as_deref(),
1789                )?,
1790                AppType::OpenCode => unreachable!("additive mode handled above"),
1791                AppType::OpenClaw => unreachable!("additive mode handled above"),
1792                AppType::Hermes => unreachable!("additive mode handled above"),
1793            };
1794
1795            let action = PostCommitAction {
1796                app_type: app_type_clone.clone(),
1797                provider,
1798                backup,
1799                sync_mcp: true, // v3.7.0: 所有应用切换时都同步 MCP,防止配置丢失
1800                refresh_snapshot: true,
1801                common_config_snippet: config.common_config_snippets.get(&app_type_clone).cloned(),
1802                takeover_active: false,
1803            };
1804
1805            Ok(((), Some(action)))
1806        })?;
1807
1808        if !app_type.is_additive_mode() {
1809            crate::settings::set_current_provider(&app_type, Some(provider_id))?;
1810        }
1811
1812        Ok(())
1813    }
1814
1815    fn write_live_snapshot(
1816        app_type: &AppType,
1817        provider: &Provider,
1818        common_config_snippet: Option<&str>,
1819        apply_common_config: bool,
1820    ) -> Result<(), AppError> {
1821        let apply_common_config = Self::resolve_live_apply_common_config(
1822            app_type,
1823            provider,
1824            common_config_snippet,
1825            apply_common_config,
1826        );
1827
1828        match app_type {
1829            AppType::Codex => {
1830                Self::write_codex_live(provider, common_config_snippet, apply_common_config)
1831            }
1832            AppType::Claude => {
1833                Self::write_claude_live(provider, common_config_snippet, apply_common_config)
1834            }
1835            AppType::Gemini => Self::write_gemini_live(
1836                provider,
1837                if apply_common_config {
1838                    common_config_snippet
1839                } else {
1840                    None
1841                },
1842            ),
1843            AppType::OpenCode => {
1844                let config_to_write = if let Some(obj) = provider.settings_config.as_object() {
1845                    if obj.contains_key("$schema") || obj.contains_key("provider") {
1846                        obj.get("provider")
1847                            .and_then(|providers| providers.get(&provider.id))
1848                            .cloned()
1849                            .unwrap_or_else(|| provider.settings_config.clone())
1850                    } else {
1851                        provider.settings_config.clone()
1852                    }
1853                } else {
1854                    provider.settings_config.clone()
1855                };
1856
1857                match serde_json::from_value::<crate::provider::OpenCodeProviderConfig>(
1858                    config_to_write.clone(),
1859                ) {
1860                    Ok(config) => crate::opencode_config::set_typed_provider(&provider.id, &config),
1861                    Err(_) => crate::opencode_config::set_provider(&provider.id, config_to_write),
1862                }
1863            }
1864            AppType::OpenClaw => {
1865                let settings_config = provider.settings_config.clone();
1866                let looks_like_provider = settings_config.get("baseUrl").is_some()
1867                    || settings_config.get("api").is_some()
1868                    || settings_config.get("models").is_some();
1869                if !looks_like_provider {
1870                    return Ok(());
1871                }
1872
1873                let config = Self::parse_openclaw_provider_settings(&settings_config)?;
1874                Self::validate_openclaw_provider_models(&provider.id, &config)?;
1875                let write_result =
1876                    crate::openclaw_config::set_typed_provider(&provider.id, &config).map(|_| ());
1877
1878                write_result.map_err(Self::normalize_openclaw_live_write_error)
1879            }
1880            AppType::Hermes => {
1881                crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())
1882                    .map(|_| ())
1883            }
1884        }
1885    }
1886
1887    fn parse_openclaw_provider_settings(
1888        settings_config: &Value,
1889    ) -> Result<crate::provider::OpenClawProviderConfig, AppError> {
1890        let settings_obj = settings_config.as_object().ok_or_else(|| {
1891            AppError::localized(
1892                "provider.openclaw.settings.not_object",
1893                "OpenClaw 配置必须是 JSON 对象",
1894                "OpenClaw configuration must be a JSON object",
1895            )
1896        })?;
1897
1898        let legacy_aliases = Self::collect_openclaw_legacy_aliases(settings_obj);
1899        if !legacy_aliases.is_empty() {
1900            let aliases = legacy_aliases.join(", ");
1901            return Err(AppError::localized(
1902                "provider.openclaw.settings.invalid",
1903                format!(
1904                    "OpenClaw 配置使用了不支持的旧字段: {aliases}。请改用规范 OpenClaw 字段。"
1905                ),
1906                format!(
1907                    "OpenClaw config uses unsupported legacy alias keys: {aliases}. Use canonical OpenClaw keys instead."
1908                ),
1909            ));
1910        }
1911
1912        serde_json::from_value(settings_config.clone()).map_err(|err| {
1913            AppError::localized(
1914                "provider.openclaw.settings.invalid",
1915                format!("OpenClaw 配置格式无效: {err}"),
1916                format!("OpenClaw provider schema is invalid: {err}"),
1917            )
1918        })
1919    }
1920
1921    fn validate_openclaw_provider_models(
1922        provider_id: &str,
1923        config: &crate::provider::OpenClawProviderConfig,
1924    ) -> Result<(), AppError> {
1925        if config.models.is_empty() {
1926            return Err(AppError::localized(
1927                "provider.openclaw.models.missing",
1928                format!("OpenClaw 供应商 {provider_id} 至少需要一个模型"),
1929                format!("OpenClaw provider {provider_id} must define at least one model"),
1930            ));
1931        }
1932
1933        Ok(())
1934    }
1935
1936    fn collect_openclaw_legacy_aliases(
1937        settings_obj: &serde_json::Map<String, Value>,
1938    ) -> Vec<String> {
1939        let mut aliases = Vec::new();
1940
1941        for alias in ["api_key", "base_url", "options", "npm"] {
1942            if settings_obj.contains_key(alias) {
1943                aliases.push(alias.to_string());
1944            }
1945        }
1946
1947        if let Some(models) = settings_obj.get("models").and_then(Value::as_array) {
1948            for (index, model) in models.iter().enumerate() {
1949                if let Some(model_obj) = model.as_object() {
1950                    if model_obj.contains_key("context_window") {
1951                        aliases.push(format!("models[{index}].context_window"));
1952                    }
1953                }
1954            }
1955        }
1956
1957        aliases
1958    }
1959
1960    fn normalize_openclaw_live_write_error(err: AppError) -> AppError {
1961        match err {
1962            AppError::Config(message)
1963                if message.starts_with("Failed to parse OpenClaw config as JSON5:") =>
1964            {
1965                AppError::Config(message.replacen(
1966                    "Failed to parse OpenClaw config as JSON5",
1967                    "Failed to parse OpenClaw config as round-trip JSON5 document",
1968                    1,
1969                ))
1970            }
1971            other => other,
1972        }
1973    }
1974
1975    pub(crate) fn build_effective_live_snapshot(
1976        app_type: &AppType,
1977        provider: &Provider,
1978        common_config_snippet: Option<&str>,
1979        apply_common_config: bool,
1980    ) -> Result<Value, AppError> {
1981        let apply_common_config = Self::resolve_live_apply_common_config(
1982            app_type,
1983            provider,
1984            common_config_snippet,
1985            apply_common_config,
1986        );
1987
1988        match app_type {
1989            AppType::Claude => {
1990                let mut effective = common_config::build_effective_settings_with_common_config(
1991                    app_type,
1992                    provider,
1993                    common_config_snippet,
1994                    apply_common_config,
1995                )?;
1996                let _ = Self::normalize_claude_models_in_value(&mut effective);
1997                Ok(effective)
1998            }
1999            AppType::Codex => {
2000                let effective = common_config::build_effective_settings_with_common_config(
2001                    app_type,
2002                    provider,
2003                    common_config_snippet,
2004                    apply_common_config,
2005                )?;
2006                let settings = effective
2007                    .as_object()
2008                    .ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
2009                let auth = settings.get("auth").cloned();
2010                let cfg_text = settings.get("config").and_then(Value::as_str).unwrap_or("");
2011
2012                if !cfg_text.trim().is_empty() {
2013                    crate::codex_config::validate_config_toml(cfg_text)?;
2014                }
2015
2016                let mut backup = serde_json::Map::new();
2017                if let Some(auth) = auth {
2018                    backup.insert("auth".to_string(), auth);
2019                }
2020                backup.insert("config".to_string(), Value::String(cfg_text.to_string()));
2021                Ok(Value::Object(backup))
2022            }
2023            AppType::Gemini => {
2024                let content_to_write = common_config::build_effective_settings_with_common_config(
2025                    app_type,
2026                    provider,
2027                    common_config_snippet,
2028                    apply_common_config,
2029                )?;
2030
2031                let env_obj = content_to_write
2032                    .get("env")
2033                    .cloned()
2034                    .unwrap_or_else(|| json!({}));
2035                let settings_path = crate::gemini_config::get_gemini_settings_path();
2036                let config_value = if let Some(config_value) = content_to_write.get("config") {
2037                    if config_value.is_null() {
2038                        if settings_path.exists() {
2039                            read_json_file(&settings_path)?
2040                        } else {
2041                            json!({})
2042                        }
2043                    } else if let Some(provider_config) = config_value.as_object() {
2044                        if provider_config.is_empty() {
2045                            if settings_path.exists() {
2046                                read_json_file(&settings_path)?
2047                            } else {
2048                                json!({})
2049                            }
2050                        } else {
2051                            let mut merged = if settings_path.exists() {
2052                                read_json_file(&settings_path)?
2053                            } else {
2054                                json!({})
2055                            };
2056
2057                            if !merged.is_object() {
2058                                merged = json!({});
2059                            }
2060
2061                            let merged_map = merged.as_object_mut().ok_or_else(|| {
2062                                AppError::localized(
2063                                    "gemini.validation.invalid_settings",
2064                                    "Gemini 现有 settings.json 格式错误: 必须是对象",
2065                                    "Gemini existing settings.json invalid: must be a JSON object",
2066                                )
2067                            })?;
2068                            for (key, value) in provider_config {
2069                                merged_map.insert(key.clone(), value.clone());
2070                            }
2071                            merged
2072                        }
2073                    } else {
2074                        return Err(AppError::localized(
2075                            "gemini.validation.invalid_config",
2076                            "Gemini 配置格式错误: config 必须是对象或 null",
2077                            "Gemini config invalid: config must be an object or null",
2078                        ));
2079                    }
2080                } else if settings_path.exists() {
2081                    read_json_file(&settings_path)?
2082                } else {
2083                    json!({})
2084                };
2085
2086                Ok(json!({
2087                    "env": env_obj,
2088                    "config": config_value,
2089                }))
2090            }
2091            AppType::OpenCode => Err(AppError::Config(
2092                "OpenCode does not support proxy takeover backups".into(),
2093            )),
2094            AppType::OpenClaw => Err(AppError::Config(
2095                "OpenClaw does not support proxy takeover backups".into(),
2096            )),
2097            AppType::Hermes => Err(AppError::Config(
2098                "Hermes does not support proxy takeover backups".into(),
2099            )),
2100        }
2101    }
2102
2103    fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
2104        match app_type {
2105            AppType::Claude => {
2106                if !provider.settings_config.is_object() {
2107                    return Err(AppError::localized(
2108                        "provider.claude.settings.not_object",
2109                        "Claude 配置必须是 JSON 对象",
2110                        "Claude configuration must be a JSON object",
2111                    ));
2112                }
2113            }
2114            AppType::Codex => {
2115                let settings = provider.settings_config.as_object().ok_or_else(|| {
2116                    AppError::localized(
2117                        "provider.codex.settings.not_object",
2118                        "Codex 配置必须是 JSON 对象",
2119                        "Codex configuration must be a JSON object",
2120                    )
2121                })?;
2122
2123                let auth = settings.get("auth").ok_or_else(|| {
2124                    AppError::localized(
2125                        "provider.codex.auth.missing",
2126                        format!("供应商 {} 缺少 auth 配置", provider.id),
2127                        format!("Provider {} is missing auth configuration", provider.id),
2128                    )
2129                })?;
2130                if !auth.is_object() {
2131                    return Err(AppError::localized(
2132                        "provider.codex.auth.not_object",
2133                        format!("供应商 {} 的 auth 配置必须是 JSON 对象", provider.id),
2134                        format!(
2135                            "Provider {} auth configuration must be a JSON object",
2136                            provider.id
2137                        ),
2138                    ));
2139                }
2140
2141                if let Some(config_value) = settings.get("config") {
2142                    if !(config_value.is_string() || config_value.is_null()) {
2143                        return Err(AppError::localized(
2144                            "provider.codex.config.invalid_type",
2145                            "Codex config 字段必须是字符串",
2146                            "Codex config field must be a string",
2147                        ));
2148                    }
2149                    if let Some(cfg_text) = config_value.as_str() {
2150                        crate::codex_config::validate_config_toml(cfg_text)?;
2151                    }
2152                }
2153
2154                if !Self::is_codex_official_provider(provider) {
2155                    let config_text = settings
2156                        .get("config")
2157                        .and_then(Value::as_str)
2158                        .unwrap_or_default();
2159                    if !Self::codex_config_has_base_url(config_text) {
2160                        return Err(AppError::localized(
2161                            "provider.codex.base_url.missing",
2162                            format!("供应商 {} 缺少有效的 Codex Base URL", provider.id),
2163                            format!("Provider {} is missing a valid Codex base_url", provider.id),
2164                        ));
2165                    }
2166                }
2167            }
2168            AppType::Gemini => {
2169                use crate::gemini_config::validate_gemini_settings;
2170                validate_gemini_settings(&provider.settings_config)?
2171            }
2172            AppType::OpenCode => {
2173                if !provider.settings_config.is_object() {
2174                    return Err(AppError::localized(
2175                        "provider.opencode.settings.not_object",
2176                        "OpenCode 配置必须是 JSON 对象",
2177                        "OpenCode configuration must be a JSON object",
2178                    ));
2179                }
2180            }
2181            AppType::OpenClaw => {
2182                let config = Self::parse_openclaw_provider_settings(&provider.settings_config)?;
2183                Self::validate_openclaw_provider_models(&provider.id, &config)?;
2184            }
2185            AppType::Hermes => {
2186                // Hermes uses flexible YAML config; basic check that settings is an object
2187                if !provider.settings_config.is_object() {
2188                    return Err(AppError::localized(
2189                        "provider.hermes.settings.not_object",
2190                        "Hermes 供应商配置必须是 JSON 对象",
2191                        "Hermes provider configuration must be a JSON object",
2192                    ));
2193                }
2194            }
2195        }
2196
2197        // 🔧 验证并清理 UsageScript 配置(所有应用类型通用)
2198        if let Some(meta) = &provider.meta {
2199            if let Some(usage_script) = &meta.usage_script {
2200                Self::validate_usage_script(usage_script)?;
2201            }
2202        }
2203
2204        Ok(())
2205    }
2206
2207    pub(crate) fn build_live_backup_snapshot(
2208        app_type: &AppType,
2209        provider: &Provider,
2210        common_config_snippet: Option<&str>,
2211        apply_common_config: bool,
2212    ) -> Result<Value, AppError> {
2213        Self::build_effective_live_snapshot(
2214            app_type,
2215            provider,
2216            common_config_snippet,
2217            apply_common_config,
2218        )
2219    }
2220
2221    pub(crate) fn build_effective_live_snapshot_from_state(
2222        state: &AppState,
2223        app_type: AppType,
2224        provider: &Provider,
2225    ) -> Result<Value, AppError> {
2226        let common_config_snippet = {
2227            let config = state.config.read().map_err(AppError::from)?;
2228            config.common_config_snippets.get(&app_type).cloned()
2229        };
2230
2231        Self::build_effective_live_snapshot(
2232            &app_type,
2233            provider,
2234            common_config_snippet.as_deref(),
2235            true,
2236        )
2237    }
2238
2239    pub(crate) fn get_provider(
2240        state: &AppState,
2241        app_type: AppType,
2242        provider_id: &str,
2243    ) -> Result<Provider, AppError> {
2244        let config = state.config.read().map_err(AppError::from)?;
2245        let manager = config
2246            .get_manager(&app_type)
2247            .ok_or_else(|| Self::app_not_found(&app_type))?;
2248
2249        manager.providers.get(provider_id).cloned().ok_or_else(|| {
2250            AppError::localized(
2251                "provider.not_found",
2252                format!("供应商不存在: {provider_id}"),
2253                format!("Provider not found: {provider_id}"),
2254            )
2255        })
2256    }
2257
2258    fn app_not_found(app_type: &AppType) -> AppError {
2259        AppError::localized(
2260            "provider.app_not_found",
2261            format!("应用类型不存在: {app_type:?}"),
2262            format!("App type not found: {app_type:?}"),
2263        )
2264    }
2265
2266    pub fn delete(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
2267        let (local_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
2268            (None, None)
2269        } else {
2270            (
2271                crate::settings::get_current_provider(&app_type),
2272                state.db.get_current_provider(app_type.as_str())?,
2273            )
2274        };
2275        let provider_snapshot = {
2276            let config = state.config.read().map_err(AppError::from)?;
2277            let manager = config
2278                .get_manager(&app_type)
2279                .ok_or_else(|| Self::app_not_found(&app_type))?;
2280
2281            if !app_type.is_additive_mode()
2282                && (local_current_provider.as_deref() == Some(provider_id)
2283                    || stored_current_provider.as_deref() == Some(provider_id))
2284            {
2285                return Err(AppError::localized(
2286                    "provider.delete.current",
2287                    "不能删除当前正在使用的供应商",
2288                    "Cannot delete the provider currently in use",
2289                ));
2290            }
2291
2292            manager.providers.get(provider_id).cloned().ok_or_else(|| {
2293                AppError::localized(
2294                    "provider.not_found",
2295                    format!("供应商不存在: {provider_id}"),
2296                    format!("Provider not found: {provider_id}"),
2297                )
2298            })?
2299        };
2300
2301        if app_type.is_additive_mode() {
2302            match app_type {
2303                AppType::OpenCode => {
2304                    if crate::opencode_config::get_opencode_dir().exists() {
2305                        crate::opencode_config::remove_provider(provider_id)?;
2306                    }
2307                }
2308                AppType::OpenClaw => {
2309                    if crate::openclaw_config::get_openclaw_dir().exists() {
2310                        crate::openclaw_config::remove_provider(provider_id)?;
2311                    }
2312                }
2313                _ => unreachable!("non-additive apps should not enter additive delete branch"),
2314            }
2315
2316            {
2317                let mut config = state.config.write().map_err(AppError::from)?;
2318                let manager = config
2319                    .get_manager_mut(&app_type)
2320                    .ok_or_else(|| Self::app_not_found(&app_type))?;
2321                manager.providers.shift_remove(provider_id);
2322            }
2323
2324            return state.save();
2325        }
2326
2327        match app_type {
2328            AppType::Codex => {
2329                crate::codex_config::delete_codex_provider_config(
2330                    provider_id,
2331                    &provider_snapshot.name,
2332                )?;
2333            }
2334            AppType::Claude => {
2335                // 兼容旧版本:历史上会在 Claude 目录内为每个供应商生成 settings-*.json 副本
2336                // 这里继续清理这些遗留文件,避免堆积过期配置。
2337                let by_name = get_provider_config_path(provider_id, Some(&provider_snapshot.name));
2338                let by_id = get_provider_config_path(provider_id, None);
2339                delete_file(&by_name)?;
2340                delete_file(&by_id)?;
2341            }
2342            AppType::Gemini => {
2343                // Gemini 使用单一的 .env 文件,不需要删除单独的供应商配置文件
2344            }
2345            AppType::OpenCode => {
2346                let _ = provider_snapshot;
2347            }
2348            AppType::OpenClaw => {
2349                let _ = provider_snapshot;
2350            }
2351            AppType::Hermes => {
2352                let _ = provider_snapshot;
2353            }
2354        }
2355
2356        {
2357            let mut config = state.config.write().map_err(AppError::from)?;
2358            let manager = config
2359                .get_manager_mut(&app_type)
2360                .ok_or_else(|| Self::app_not_found(&app_type))?;
2361
2362            if !app_type.is_additive_mode()
2363                && (local_current_provider.as_deref() == Some(provider_id)
2364                    || stored_current_provider.as_deref() == Some(provider_id))
2365            {
2366                return Err(AppError::localized(
2367                    "provider.delete.current",
2368                    "不能删除当前正在使用的供应商",
2369                    "Cannot delete the provider currently in use",
2370                ));
2371            }
2372
2373            if !app_type.is_additive_mode() && manager.current == provider_id {
2374                manager.current = stored_current_provider.clone().unwrap_or_default();
2375            }
2376
2377            manager.providers.shift_remove(provider_id);
2378        }
2379
2380        state.save()
2381    }
2382
2383    pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2384        live::import_openclaw_providers_from_live(state)
2385    }
2386
2387    pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2388        live::import_opencode_providers_from_live(state)
2389    }
2390}
2391
2392#[derive(Debug, Clone, Deserialize)]
2393pub struct ProviderSortUpdate {
2394    pub id: String,
2395    #[serde(rename = "sortIndex")]
2396    pub sort_index: usize,
2397}