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