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