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 let live_config_managed = Self::additive_provider_exists_in_live_config(
1355 &app_type_clone,
1356 &provider_id,
1357 Self::provider_live_config_managed(&merged).or(existing_live_config_managed),
1358 )?;
1359 Self::set_provider_live_config_managed(&mut merged, live_config_managed);
1360 live_config_managed
1361 } else {
1362 effective_current_provider.as_deref() == Some(provider_id.as_str())
1363 };
1364
1365 manager
1366 .providers
1367 .insert(provider_id.clone(), merged.clone());
1368
1369 let action = if should_write_live {
1370 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1371 Some(PostCommitAction {
1372 app_type: app_type_clone.clone(),
1373 provider: merged,
1374 backup,
1375 write_live_snapshot: true,
1376 sync_mcp: false,
1379 sync_codex_catalog: matches!(&app_type_clone, AppType::Codex),
1380 stale_codex_catalog_keys: Vec::new(),
1381 refresh_snapshot: false,
1382 apply_hermes_switch_defaults: false,
1383 common_config_snippet,
1384 takeover_active: false,
1385 preserve_live_preferences: true,
1386 })
1387 } else if matches!(&app_type_clone, AppType::Codex) {
1388 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1389 let current_codex_catalog_key = Self::provider_codex_model_provider_key(&merged);
1390 let stale_codex_catalog_keys = previous_codex_catalog_key
1391 .filter(|old_key| {
1392 current_codex_catalog_key.as_deref() != Some(old_key.as_str())
1393 })
1394 .into_iter()
1395 .collect();
1396 Some(PostCommitAction {
1397 app_type: app_type_clone.clone(),
1398 provider: merged,
1399 backup,
1400 write_live_snapshot: false,
1401 sync_mcp: false,
1402 sync_codex_catalog: true,
1403 stale_codex_catalog_keys,
1404 refresh_snapshot: false,
1405 apply_hermes_switch_defaults: false,
1406 common_config_snippet,
1407 takeover_active: false,
1408 preserve_live_preferences: true,
1409 })
1410 } else {
1411 None
1412 };
1413
1414 Ok((true, action))
1415 })
1416 }
1417
1418 pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
1422 if app_type.is_additive_mode() {
1423 return Ok(false);
1424 }
1425
1426 if state.db.has_non_official_seed_provider(app_type.as_str())? {
1427 return Ok(false);
1428 }
1429
1430 let settings_config = match app_type {
1431 AppType::Codex => {
1432 let auth_path = get_codex_auth_path();
1433 if !auth_path.exists() {
1434 return Err(AppError::localized(
1435 "codex.live.missing",
1436 "Codex 配置文件不存在",
1437 "Codex configuration file is missing",
1438 ));
1439 }
1440 let auth: Value = read_json_file(&auth_path)?;
1441 let config_str = crate::codex_config::read_and_validate_codex_config_text()?;
1442 crate::codex_config::codex_settings_snapshot_from_toml(Some(auth), &config_str)?
1443 }
1444 AppType::Claude => {
1445 let settings_path = get_claude_settings_path();
1446 if !settings_path.exists() {
1447 return Err(AppError::localized(
1448 "claude.live.missing",
1449 "Claude Code 配置文件不存在",
1450 "Claude settings file is missing",
1451 ));
1452 }
1453 let mut v = read_json_file::<Value>(&settings_path)?;
1454 let _ = Self::normalize_claude_models_in_value(&mut v);
1455 v
1456 }
1457 AppType::Gemini => {
1458 use crate::gemini_config::{
1459 env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1460 };
1461
1462 let env_path = get_gemini_env_path();
1464 if !env_path.exists() {
1465 return Err(AppError::localized(
1466 "gemini.live.missing",
1467 "Gemini 配置文件不存在",
1468 "Gemini configuration file is missing",
1469 ));
1470 }
1471
1472 let env_map = read_gemini_env()?;
1473 let env_json = env_to_json(&env_map);
1474 let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1475
1476 let settings_path = get_gemini_settings_path();
1478 let config_obj = if settings_path.exists() {
1479 read_json_file(&settings_path)?
1480 } else {
1481 json!({})
1482 };
1483
1484 json!({
1486 "env": env_obj,
1487 "config": config_obj
1488 })
1489 }
1490 AppType::OpenCode => unreachable!("additive mode apps are handled earlier"),
1491 AppType::OpenClaw => unreachable!("additive mode apps are handled earlier"),
1492 AppType::Hermes => unreachable!("additive mode apps are handled earlier"),
1493 };
1494
1495 let mut provider = Provider::with_id(
1496 "default".to_string(),
1497 "default".to_string(),
1498 settings_config,
1499 None,
1500 );
1501 provider.category = Some("custom".to_string());
1502 let common_config_snippet = {
1503 let guard = state.config.read().map_err(AppError::from)?;
1504 guard.common_config_snippets.get(&app_type).cloned()
1505 };
1506 Self::normalize_provider_for_storage(
1507 &app_type,
1508 &mut provider,
1509 common_config_snippet.as_deref(),
1510 )?;
1511
1512 state.db.save_provider(app_type.as_str(), &provider)?;
1513 state
1514 .db
1515 .set_current_provider(app_type.as_str(), &provider.id)?;
1516 {
1517 let mut guard = state.config.write().map_err(AppError::from)?;
1518 guard.ensure_app(&app_type);
1519 let manager = guard
1520 .get_manager_mut(&app_type)
1521 .ok_or_else(|| AppError::Config("manager missing after ensure_app".into()))?;
1522 manager.current = provider.id.clone();
1523 manager.providers.insert(provider.id.clone(), provider);
1524 }
1525 Ok(true)
1526 }
1527
1528 pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
1530 match app_type {
1531 AppType::Codex => {
1532 let auth_path = get_codex_auth_path();
1533 let config_path = get_codex_config_path();
1534 if !config_path.exists() {
1535 return Err(AppError::localized(
1536 "codex.live.missing",
1537 "Codex 配置文件不存在",
1538 "Codex configuration is missing",
1539 ));
1540 }
1541
1542 let mut live_settings = serde_json::Map::new();
1543 if auth_path.exists() {
1544 live_settings.insert("auth".to_string(), read_json_file(&auth_path)?);
1545 }
1546 if config_path.exists() {
1547 let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
1548 live_settings.insert("config".to_string(), Value::String(cfg_text));
1549 }
1550
1551 Ok(Value::Object(live_settings))
1552 }
1553 AppType::Claude => {
1554 let path = get_claude_settings_path();
1555 if !path.exists() {
1556 return Err(AppError::localized(
1557 "claude.live.missing",
1558 "Claude Code 配置文件不存在",
1559 "Claude settings file is missing",
1560 ));
1561 }
1562 read_json_file(&path)
1563 }
1564 AppType::Gemini => {
1565 use crate::gemini_config::{
1566 env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1567 };
1568
1569 let env_path = get_gemini_env_path();
1571 if !env_path.exists() {
1572 return Err(AppError::localized(
1573 "gemini.env.missing",
1574 "Gemini .env 文件不存在",
1575 "Gemini .env file not found",
1576 ));
1577 }
1578
1579 let env_map = read_gemini_env()?;
1580 let env_json = env_to_json(&env_map);
1581 let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1582
1583 let settings_path = get_gemini_settings_path();
1585 let config_obj = if settings_path.exists() {
1586 read_json_file(&settings_path)?
1587 } else {
1588 json!({})
1589 };
1590
1591 Ok(json!({
1593 "env": env_obj,
1594 "config": config_obj
1595 }))
1596 }
1597 AppType::OpenCode => {
1598 let config_path = crate::opencode_config::get_opencode_config_path();
1599 if !config_path.exists() {
1600 return Err(AppError::localized(
1601 "opencode.config.missing",
1602 "OpenCode 配置文件不存在",
1603 "OpenCode configuration file not found",
1604 ));
1605 }
1606 crate::opencode_config::read_opencode_config()
1607 }
1608 AppType::OpenClaw => {
1609 let config_path = crate::openclaw_config::get_openclaw_config_path();
1610 if !config_path.exists() {
1611 return Err(AppError::localized(
1612 "openclaw.config.missing",
1613 "OpenClaw 配置文件不存在",
1614 "OpenClaw configuration file not found",
1615 ));
1616 }
1617 crate::openclaw_config::read_openclaw_config()
1618 }
1619 AppType::Hermes => {
1620 let yaml = crate::hermes_config::read_hermes_config()?;
1621 crate::hermes_config::yaml_to_json(&yaml)
1622 }
1623 }
1624 }
1625
1626 pub fn update_sort_order(
1628 state: &AppState,
1629 app_type: AppType,
1630 updates: Vec<ProviderSortUpdate>,
1631 ) -> Result<bool, AppError> {
1632 {
1633 let mut cfg = state.config.write().map_err(AppError::from)?;
1634 let manager = cfg
1635 .get_manager_mut(&app_type)
1636 .ok_or_else(|| Self::app_not_found(&app_type))?;
1637
1638 for update in updates {
1639 if let Some(provider) = manager.providers.get_mut(&update.id) {
1640 provider.sort_index = Some(update.sort_index);
1641 }
1642 }
1643 }
1644
1645 state.save()?;
1646 Ok(true)
1647 }
1648
1649 pub fn remove_from_live_config(
1650 state: &AppState,
1651 app_type: AppType,
1652 provider_id: &str,
1653 ) -> Result<(), AppError> {
1654 if !app_type.is_additive_mode() {
1655 return Err(AppError::localized(
1656 "provider.remove_from_live_config.unsupported",
1657 "只有累加模式应用支持从 live 配置中移除供应商",
1658 "Only additive-mode apps support removing a provider from live config",
1659 ));
1660 }
1661
1662 let original = {
1663 let config = state.config.read().map_err(AppError::from)?;
1664 let manager = config
1665 .get_manager(&app_type)
1666 .ok_or_else(|| Self::app_not_found(&app_type))?;
1667 if !manager.providers.contains_key(provider_id) {
1668 return Err(AppError::localized(
1669 "provider.not_found",
1670 format!("供应商不存在: {provider_id}"),
1671 format!("Provider not found: {provider_id}"),
1672 ));
1673 }
1674 config.clone()
1675 };
1676
1677 let backup = Self::capture_live_snapshot(&app_type)?;
1678 match &app_type {
1679 AppType::OpenCode => {
1680 if crate::opencode_config::get_opencode_dir().exists() {
1681 crate::opencode_config::remove_provider(provider_id)?;
1682 }
1683 }
1684 AppType::OpenClaw => {
1685 if crate::openclaw_config::get_openclaw_dir().exists() {
1686 crate::openclaw_config::remove_provider(provider_id)?;
1687 }
1688 }
1689 _ => unreachable!("non-additive apps should not enter remove-from-live branch"),
1690 }
1691
1692 {
1693 let mut config = state.config.write().map_err(AppError::from)?;
1694 let manager = config
1695 .get_manager_mut(&app_type)
1696 .ok_or_else(|| Self::app_not_found(&app_type))?;
1697 let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
1698 AppError::localized(
1699 "provider.not_found",
1700 format!("供应商不存在: {provider_id}"),
1701 format!("Provider not found: {provider_id}"),
1702 )
1703 })?;
1704 Self::set_provider_live_config_managed(provider, false);
1705 }
1706
1707 if let Err(save_err) = state.save() {
1708 let config_restore = Self::restore_config_only(state, original);
1709 let live_restore = backup.restore();
1710 if let Err(rollback_err) = config_restore {
1711 return Err(AppError::localized(
1712 "config.save.rollback_failed",
1713 format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
1714 format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
1715 ));
1716 }
1717 if let Err(rollback_err) = live_restore {
1718 return Err(AppError::localized(
1719 "post_commit.rollback_failed",
1720 format!("保存配置失败: {save_err};live 回滚失败: {rollback_err}"),
1721 format!(
1722 "Failed to save config: {save_err}; live rollback failed: {rollback_err}"
1723 ),
1724 ));
1725 }
1726 return Err(save_err);
1727 }
1728
1729 Ok(())
1730 }
1731
1732 pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
1738 use crate::services::mcp::McpService;
1739
1740 let snapshots: Vec<(AppType, Provider, Option<String>)> = {
1742 let guard = state.config.read().map_err(AppError::from)?;
1743 let mut result = Vec::new();
1744 for app_type in AppType::all() {
1745 if app_type.is_additive_mode() {
1746 if let Some(manager) = guard.get_manager(&app_type) {
1747 let snippet = guard.common_config_snippets.get(&app_type).cloned();
1748 for provider in manager.providers.values() {
1749 if Self::provider_live_config_managed(provider) == Some(false) {
1750 continue;
1751 }
1752 result.push((app_type.clone(), provider.clone(), snippet.clone()));
1753 }
1754 }
1755 continue;
1756 }
1757
1758 let current_id =
1759 match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
1760 Some(id) => id,
1761 None => continue,
1762 };
1763 let providers = state.db.get_all_providers(app_type.as_str())?;
1764 match providers.get(¤t_id) {
1765 Some(provider) => {
1766 let snippet = state.db.get_config_snippet(app_type.as_str())?;
1767 result.push((app_type.clone(), provider.clone(), snippet));
1768 }
1769 None => {
1770 log::warn!(
1771 "sync_current_to_live: {app_type} 当前供应商 {} 不存在于数据库,跳过",
1772 current_id
1773 );
1774 }
1775 }
1776 }
1777 result
1778 };
1779
1780 let openclaw_live_provider_ids = match Self::valid_openclaw_live_provider_ids() {
1781 Ok(provider_ids) => provider_ids,
1782 Err(err) => {
1783 log::warn!(
1784 "sync_current_to_live: 读取 OpenClaw live providers 失败,跳过 OpenClaw 同步: {err}"
1785 );
1786 None
1787 }
1788 };
1789
1790 for (app_type, provider, snippet) in &snapshots {
1791 if matches!(app_type, AppType::OpenClaw)
1792 && !openclaw_live_provider_ids
1793 .as_ref()
1794 .is_some_and(|provider_ids| provider_ids.contains(&provider.id))
1795 {
1796 continue;
1797 }
1798
1799 if let Err(e) =
1800 Self::write_live_snapshot(app_type, provider, snippet.as_deref(), true, true)
1801 {
1802 log::warn!("sync_current_to_live: 写入 {app_type} live 配置失败: {e}");
1803 }
1804 }
1805
1806 if snapshots
1807 .iter()
1808 .any(|(app_type, _, _)| matches!(app_type, AppType::Codex))
1809 {
1810 if let Err(e) = Self::sync_codex_provider_catalog_to_live(state, &[]) {
1811 log::warn!("sync_current_to_live: Codex provider catalog 同步失败: {e}");
1812 }
1813 }
1814
1815 if let Err(e) =
1816 crate::services::prompt::PromptService::sync_all_active_to_live_best_effort(state)
1817 {
1818 log::warn!("sync_current_to_live: Prompt 同步失败: {e}");
1819 }
1820
1821 if let Err(e) = McpService::sync_all_enabled_except(state, &[AppType::Codex]) {
1822 log::warn!("sync_current_to_live: 非 Codex MCP 同步失败: {e}");
1823 }
1824
1825 if let Err(e) = crate::services::skill::SkillService::sync_all_enabled_best_effort() {
1826 log::warn!("sync_current_to_live: Skills 同步失败: {e}");
1827 }
1828
1829 Ok(())
1830 }
1831
1832 pub fn switch(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
1834 if !app_type.is_additive_mode() {
1835 let providers = state.db.get_all_providers(app_type.as_str())?;
1836 providers.get(provider_id).ok_or_else(|| {
1837 AppError::localized(
1838 "provider.not_found",
1839 format!("供应商不存在: {provider_id}"),
1840 format!("Provider not found: {provider_id}"),
1841 )
1842 })?;
1843
1844 let is_app_taken_over =
1845 futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
1846 .ok()
1847 .flatten()
1848 .is_some();
1849 let is_proxy_running = state
1850 .proxy_service
1851 .is_running_blocking()
1852 .map_err(AppError::Message)?;
1853 let live_taken_over = state
1854 .proxy_service
1855 .detect_takeover_in_live_config_for_app(&app_type);
1856 let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
1857
1858 if should_hot_switch {
1859 futures::executor::block_on(
1860 state
1861 .proxy_service
1862 .hot_switch_provider(app_type.as_str(), provider_id),
1863 )
1864 .map_err(|e| AppError::Message(format!("热切换失败: {e}")))?;
1865
1866 let mut guard = state.config.write().map_err(AppError::from)?;
1867 if let Some(manager) = guard.get_manager_mut(&app_type) {
1868 manager.current = provider_id.to_string();
1869 }
1870 return Ok(());
1871 }
1872 }
1873
1874 let app_type_clone = app_type.clone();
1875 let provider_id_owned = provider_id.to_string();
1876 let effective_current_provider = if app_type.is_additive_mode() {
1877 None
1878 } else if matches!(app_type, AppType::Codex) {
1879 Self::codex_live_current_provider_id(state)?.or(
1880 crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1881 )
1882 } else {
1883 crate::settings::get_effective_current_provider(&state.db, &app_type)?
1884 };
1885
1886 Self::run_transaction(state, move |config| {
1887 if app_type_clone.is_additive_mode() {
1888 let provider = {
1889 let provider = config
1890 .get_manager_mut(&app_type_clone)
1891 .ok_or_else(|| Self::app_not_found(&app_type_clone))?
1892 .providers
1893 .get_mut(&provider_id_owned)
1894 .ok_or_else(|| {
1895 AppError::localized(
1896 "provider.not_found",
1897 format!("供应商不存在: {provider_id_owned}"),
1898 format!("Provider not found: {provider_id_owned}"),
1899 )
1900 })?;
1901 Self::set_provider_live_config_managed(provider, true);
1902 provider.clone()
1903 };
1904
1905 let action = PostCommitAction {
1906 app_type: app_type_clone.clone(),
1907 provider,
1908 backup: Self::capture_live_snapshot(&app_type_clone)?,
1909 write_live_snapshot: true,
1910 sync_mcp: matches!(app_type_clone, AppType::OpenCode),
1911 sync_codex_catalog: false,
1912 stale_codex_catalog_keys: Vec::new(),
1913 refresh_snapshot: false,
1914 apply_hermes_switch_defaults: matches!(app_type_clone, AppType::Hermes),
1915 common_config_snippet: config
1916 .common_config_snippets
1917 .get(&app_type_clone)
1918 .cloned(),
1919 takeover_active: false,
1920 preserve_live_preferences: true,
1921 };
1922
1923 return Ok(((), Some(action)));
1924 }
1925
1926 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1927 let provider = match app_type_clone {
1928 AppType::Codex => Self::prepare_switch_codex(
1929 config,
1930 &provider_id_owned,
1931 effective_current_provider.as_deref(),
1932 )?,
1933 AppType::Claude => Self::prepare_switch_claude(
1934 config,
1935 &provider_id_owned,
1936 effective_current_provider.as_deref(),
1937 )?,
1938 AppType::Gemini => Self::prepare_switch_gemini(
1939 config,
1940 &provider_id_owned,
1941 effective_current_provider.as_deref(),
1942 )?,
1943 AppType::OpenCode => unreachable!("additive mode handled above"),
1944 AppType::OpenClaw => unreachable!("additive mode handled above"),
1945 AppType::Hermes => unreachable!("additive mode handled above"),
1946 };
1947
1948 let action = PostCommitAction {
1949 app_type: app_type_clone.clone(),
1950 provider,
1951 backup,
1952 write_live_snapshot: true,
1953 sync_mcp: !matches!(app_type_clone, AppType::Codex),
1957 sync_codex_catalog: matches!(app_type_clone, AppType::Codex),
1958 stale_codex_catalog_keys: Vec::new(),
1959 refresh_snapshot: true,
1960 apply_hermes_switch_defaults: false,
1961 common_config_snippet: config.common_config_snippets.get(&app_type_clone).cloned(),
1962 takeover_active: false,
1963 preserve_live_preferences: true,
1964 };
1965
1966 Ok(((), Some(action)))
1967 })?;
1968
1969 if !app_type.is_additive_mode() {
1970 crate::settings::set_current_provider(&app_type, Some(provider_id))?;
1971 }
1972
1973 Ok(())
1974 }
1975
1976 fn write_live_snapshot(
1977 app_type: &AppType,
1978 provider: &Provider,
1979 common_config_snippet: Option<&str>,
1980 apply_common_config: bool,
1981 preserve_live_preferences: bool,
1982 ) -> Result<(), AppError> {
1983 let apply_common_config = Self::resolve_live_apply_common_config(
1984 app_type,
1985 provider,
1986 common_config_snippet,
1987 apply_common_config,
1988 );
1989
1990 match app_type {
1991 AppType::Codex => Self::write_codex_live(
1992 provider,
1993 common_config_snippet,
1994 apply_common_config,
1995 preserve_live_preferences,
1996 ),
1997 AppType::Claude => {
1998 Self::write_claude_live(provider, common_config_snippet, apply_common_config)
1999 }
2000 AppType::Gemini => Self::write_gemini_live(
2001 provider,
2002 if apply_common_config {
2003 common_config_snippet
2004 } else {
2005 None
2006 },
2007 ),
2008 AppType::OpenCode => {
2009 let config_to_write = if let Some(obj) = provider.settings_config.as_object() {
2010 if obj.contains_key("$schema") || obj.contains_key("provider") {
2011 obj.get("provider")
2012 .and_then(|providers| providers.get(&provider.id))
2013 .cloned()
2014 .unwrap_or_else(|| provider.settings_config.clone())
2015 } else {
2016 provider.settings_config.clone()
2017 }
2018 } else {
2019 provider.settings_config.clone()
2020 };
2021
2022 match serde_json::from_value::<crate::provider::OpenCodeProviderConfig>(
2023 config_to_write.clone(),
2024 ) {
2025 Ok(config) => crate::opencode_config::set_typed_provider(&provider.id, &config),
2026 Err(_) => crate::opencode_config::set_provider(&provider.id, config_to_write),
2027 }
2028 }
2029 AppType::OpenClaw => {
2030 let settings_config = provider.settings_config.clone();
2031 let looks_like_provider = settings_config.get("baseUrl").is_some()
2032 || settings_config.get("api").is_some()
2033 || settings_config.get("models").is_some();
2034 if !looks_like_provider {
2035 return Ok(());
2036 }
2037
2038 let config = Self::parse_openclaw_provider_settings(&settings_config)?;
2039 Self::validate_openclaw_provider_models(&provider.id, &config)?;
2040 let write_result =
2041 crate::openclaw_config::set_typed_provider(&provider.id, &config).map(|_| ());
2042
2043 write_result.map_err(Self::normalize_openclaw_live_write_error)
2044 }
2045 AppType::Hermes => {
2046 crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())
2047 .map(|_| ())
2048 }
2049 }
2050 }
2051
2052 fn parse_openclaw_provider_settings(
2053 settings_config: &Value,
2054 ) -> Result<crate::provider::OpenClawProviderConfig, AppError> {
2055 let settings_obj = settings_config.as_object().ok_or_else(|| {
2056 AppError::localized(
2057 "provider.openclaw.settings.not_object",
2058 "OpenClaw 配置必须是 JSON 对象",
2059 "OpenClaw configuration must be a JSON object",
2060 )
2061 })?;
2062
2063 let legacy_aliases = Self::collect_openclaw_legacy_aliases(settings_obj);
2064 if !legacy_aliases.is_empty() {
2065 let aliases = legacy_aliases.join(", ");
2066 return Err(AppError::localized(
2067 "provider.openclaw.settings.invalid",
2068 format!(
2069 "OpenClaw 配置使用了不支持的旧字段: {aliases}。请改用规范 OpenClaw 字段。"
2070 ),
2071 format!(
2072 "OpenClaw config uses unsupported legacy alias keys: {aliases}. Use canonical OpenClaw keys instead."
2073 ),
2074 ));
2075 }
2076
2077 serde_json::from_value(settings_config.clone()).map_err(|err| {
2078 AppError::localized(
2079 "provider.openclaw.settings.invalid",
2080 format!("OpenClaw 配置格式无效: {err}"),
2081 format!("OpenClaw provider schema is invalid: {err}"),
2082 )
2083 })
2084 }
2085
2086 fn validate_openclaw_provider_models(
2087 provider_id: &str,
2088 config: &crate::provider::OpenClawProviderConfig,
2089 ) -> Result<(), AppError> {
2090 if config.models.is_empty() {
2091 return Err(AppError::localized(
2092 "provider.openclaw.models.missing",
2093 format!("OpenClaw 供应商 {provider_id} 至少需要一个模型"),
2094 format!("OpenClaw provider {provider_id} must define at least one model"),
2095 ));
2096 }
2097
2098 Ok(())
2099 }
2100
2101 fn collect_openclaw_legacy_aliases(
2102 settings_obj: &serde_json::Map<String, Value>,
2103 ) -> Vec<String> {
2104 let mut aliases = Vec::new();
2105
2106 for alias in ["api_key", "base_url", "options", "npm"] {
2107 if settings_obj.contains_key(alias) {
2108 aliases.push(alias.to_string());
2109 }
2110 }
2111
2112 if let Some(models) = settings_obj.get("models").and_then(Value::as_array) {
2113 for (index, model) in models.iter().enumerate() {
2114 if let Some(model_obj) = model.as_object() {
2115 if model_obj.contains_key("context_window") {
2116 aliases.push(format!("models[{index}].context_window"));
2117 }
2118 }
2119 }
2120 }
2121
2122 aliases
2123 }
2124
2125 fn normalize_openclaw_live_write_error(err: AppError) -> AppError {
2126 match err {
2127 AppError::Config(message)
2128 if message.starts_with("Failed to parse OpenClaw config as JSON5:") =>
2129 {
2130 AppError::Config(message.replacen(
2131 "Failed to parse OpenClaw config as JSON5",
2132 "Failed to parse OpenClaw config as round-trip JSON5 document",
2133 1,
2134 ))
2135 }
2136 other => other,
2137 }
2138 }
2139
2140 pub(crate) fn build_effective_live_snapshot(
2141 app_type: &AppType,
2142 provider: &Provider,
2143 common_config_snippet: Option<&str>,
2144 apply_common_config: bool,
2145 ) -> Result<Value, AppError> {
2146 let apply_common_config = Self::resolve_live_apply_common_config(
2147 app_type,
2148 provider,
2149 common_config_snippet,
2150 apply_common_config,
2151 );
2152
2153 match app_type {
2154 AppType::Claude => {
2155 let mut effective = common_config::build_effective_settings_with_common_config(
2156 app_type,
2157 provider,
2158 common_config_snippet,
2159 apply_common_config,
2160 )?;
2161 let _ = Self::normalize_claude_models_in_value(&mut effective);
2162 Ok(effective)
2163 }
2164 AppType::Codex => {
2165 let effective = common_config::build_effective_settings_with_common_config(
2166 app_type,
2167 provider,
2168 common_config_snippet,
2169 apply_common_config,
2170 )?;
2171 let settings = effective
2172 .as_object()
2173 .ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
2174 let auth = settings.get("auth").cloned();
2175 let cfg_text = crate::codex_config::codex_config_text_from_settings(&effective)?;
2176
2177 if !cfg_text.trim().is_empty() {
2178 crate::codex_config::validate_config_toml(&cfg_text)?;
2179 }
2180
2181 let mut backup = serde_json::Map::new();
2182 if let Some(auth) = auth {
2183 backup.insert("auth".to_string(), auth);
2184 }
2185 backup.insert("config".to_string(), Value::String(cfg_text));
2186 Ok(Value::Object(backup))
2187 }
2188 AppType::Gemini => {
2189 let content_to_write = common_config::build_effective_settings_with_common_config(
2190 app_type,
2191 provider,
2192 common_config_snippet,
2193 apply_common_config,
2194 )?;
2195
2196 let env_obj = content_to_write
2197 .get("env")
2198 .cloned()
2199 .unwrap_or_else(|| json!({}));
2200 let settings_path = crate::gemini_config::get_gemini_settings_path();
2201 let config_value = if let Some(config_value) = content_to_write.get("config") {
2202 if config_value.is_null() {
2203 if settings_path.exists() {
2204 read_json_file(&settings_path)?
2205 } else {
2206 json!({})
2207 }
2208 } else if let Some(provider_config) = config_value.as_object() {
2209 if provider_config.is_empty() {
2210 if settings_path.exists() {
2211 read_json_file(&settings_path)?
2212 } else {
2213 json!({})
2214 }
2215 } else {
2216 let mut merged = if settings_path.exists() {
2217 read_json_file(&settings_path)?
2218 } else {
2219 json!({})
2220 };
2221
2222 if !merged.is_object() {
2223 merged = json!({});
2224 }
2225
2226 let merged_map = merged.as_object_mut().ok_or_else(|| {
2227 AppError::localized(
2228 "gemini.validation.invalid_settings",
2229 "Gemini 现有 settings.json 格式错误: 必须是对象",
2230 "Gemini existing settings.json invalid: must be a JSON object",
2231 )
2232 })?;
2233 for (key, value) in provider_config {
2234 merged_map.insert(key.clone(), value.clone());
2235 }
2236 merged
2237 }
2238 } else {
2239 return Err(AppError::localized(
2240 "gemini.validation.invalid_config",
2241 "Gemini 配置格式错误: config 必须是对象或 null",
2242 "Gemini config invalid: config must be an object or null",
2243 ));
2244 }
2245 } else if settings_path.exists() {
2246 read_json_file(&settings_path)?
2247 } else {
2248 json!({})
2249 };
2250
2251 Ok(json!({
2252 "env": env_obj,
2253 "config": config_value,
2254 }))
2255 }
2256 AppType::OpenCode => Err(AppError::Config(
2257 "OpenCode does not support proxy takeover backups".into(),
2258 )),
2259 AppType::OpenClaw => Err(AppError::Config(
2260 "OpenClaw does not support proxy takeover backups".into(),
2261 )),
2262 AppType::Hermes => Err(AppError::Config(
2263 "Hermes does not support proxy takeover backups".into(),
2264 )),
2265 }
2266 }
2267
2268 fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
2269 match app_type {
2270 AppType::Claude => {
2271 if !provider.settings_config.is_object() {
2272 return Err(AppError::localized(
2273 "provider.claude.settings.not_object",
2274 "Claude 配置必须是 JSON 对象",
2275 "Claude configuration must be a JSON object",
2276 ));
2277 }
2278 }
2279 AppType::Codex => {
2280 let settings = provider.settings_config.as_object().ok_or_else(|| {
2281 AppError::localized(
2282 "provider.codex.settings.not_object",
2283 "Codex 配置必须是 JSON 对象",
2284 "Codex configuration must be a JSON object",
2285 )
2286 })?;
2287
2288 let auth = settings.get("auth").ok_or_else(|| {
2289 AppError::localized(
2290 "provider.codex.auth.missing",
2291 format!("供应商 {} 缺少 auth 配置", provider.id),
2292 format!("Provider {} is missing auth configuration", provider.id),
2293 )
2294 })?;
2295 if !auth.is_object() {
2296 return Err(AppError::localized(
2297 "provider.codex.auth.not_object",
2298 format!("供应商 {} 的 auth 配置必须是 JSON 对象", provider.id),
2299 format!(
2300 "Provider {} auth configuration must be a JSON object",
2301 provider.id
2302 ),
2303 ));
2304 }
2305
2306 if let Some(config_value) = settings.get("config") {
2307 if !(config_value.is_string() || config_value.is_null()) {
2308 return Err(AppError::localized(
2309 "provider.codex.config.invalid_type",
2310 "Codex config 字段必须是字符串",
2311 "Codex config field must be a string",
2312 ));
2313 }
2314 if let Some(cfg_text) = config_value.as_str() {
2315 crate::codex_config::validate_config_toml(cfg_text)?;
2316 }
2317 }
2318 if let Some(codex_value) = settings.get("codex") {
2319 if !(codex_value.is_object() || codex_value.is_null()) {
2320 return Err(AppError::localized(
2321 "provider.codex.config.invalid_type",
2322 "Codex codex 字段必须是对象",
2323 "Codex codex field must be an object",
2324 ));
2325 }
2326 let cfg_text =
2327 crate::codex_config::codex_structured_config_to_toml(codex_value)?;
2328 crate::codex_config::validate_config_toml(&cfg_text)?;
2329 }
2330
2331 if !Self::is_codex_official_provider(provider) {
2332 let config_text = crate::codex_config::codex_config_text_from_settings(
2333 &provider.settings_config,
2334 )?;
2335 if !Self::codex_config_has_base_url(&config_text) {
2336 return Err(AppError::localized(
2337 "provider.codex.base_url.missing",
2338 format!("供应商 {} 缺少有效的 Codex Base URL", provider.id),
2339 format!("Provider {} is missing a valid Codex base_url", provider.id),
2340 ));
2341 }
2342 }
2343 }
2344 AppType::Gemini => {
2345 use crate::gemini_config::validate_gemini_settings;
2346 validate_gemini_settings(&provider.settings_config)?
2347 }
2348 AppType::OpenCode => {
2349 if !provider.settings_config.is_object() {
2350 return Err(AppError::localized(
2351 "provider.opencode.settings.not_object",
2352 "OpenCode 配置必须是 JSON 对象",
2353 "OpenCode configuration must be a JSON object",
2354 ));
2355 }
2356 }
2357 AppType::OpenClaw => {
2358 let config = Self::parse_openclaw_provider_settings(&provider.settings_config)?;
2359 Self::validate_openclaw_provider_models(&provider.id, &config)?;
2360 }
2361 AppType::Hermes => {
2362 if !provider.settings_config.is_object() {
2364 return Err(AppError::localized(
2365 "provider.hermes.settings.not_object",
2366 "Hermes 供应商配置必须是 JSON 对象",
2367 "Hermes provider configuration must be a JSON object",
2368 ));
2369 }
2370 }
2371 }
2372
2373 if let Some(meta) = &provider.meta {
2375 if let Some(usage_script) = &meta.usage_script {
2376 Self::validate_usage_script(usage_script)?;
2377 }
2378 }
2379
2380 Ok(())
2381 }
2382
2383 pub(crate) fn build_live_backup_snapshot(
2384 app_type: &AppType,
2385 provider: &Provider,
2386 common_config_snippet: Option<&str>,
2387 apply_common_config: bool,
2388 ) -> Result<Value, AppError> {
2389 Self::build_effective_live_snapshot(
2390 app_type,
2391 provider,
2392 common_config_snippet,
2393 apply_common_config,
2394 )
2395 }
2396
2397 pub(crate) fn build_effective_live_snapshot_from_state(
2398 state: &AppState,
2399 app_type: AppType,
2400 provider: &Provider,
2401 ) -> Result<Value, AppError> {
2402 let common_config_snippet = {
2403 let config = state.config.read().map_err(AppError::from)?;
2404 config.common_config_snippets.get(&app_type).cloned()
2405 };
2406
2407 Self::build_effective_live_snapshot(
2408 &app_type,
2409 provider,
2410 common_config_snippet.as_deref(),
2411 true,
2412 )
2413 }
2414
2415 pub(crate) fn get_provider(
2416 state: &AppState,
2417 app_type: AppType,
2418 provider_id: &str,
2419 ) -> Result<Provider, AppError> {
2420 let config = state.config.read().map_err(AppError::from)?;
2421 let manager = config
2422 .get_manager(&app_type)
2423 .ok_or_else(|| Self::app_not_found(&app_type))?;
2424
2425 manager.providers.get(provider_id).cloned().ok_or_else(|| {
2426 AppError::localized(
2427 "provider.not_found",
2428 format!("供应商不存在: {provider_id}"),
2429 format!("Provider not found: {provider_id}"),
2430 )
2431 })
2432 }
2433
2434 fn app_not_found(app_type: &AppType) -> AppError {
2435 AppError::localized(
2436 "provider.app_not_found",
2437 format!("应用类型不存在: {app_type:?}"),
2438 format!("App type not found: {app_type:?}"),
2439 )
2440 }
2441
2442 pub fn delete(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
2443 let (local_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
2444 (None, None)
2445 } else {
2446 (
2447 crate::settings::get_current_provider(&app_type),
2448 state.db.get_current_provider(app_type.as_str())?,
2449 )
2450 };
2451 let provider_snapshot = {
2452 let config = state.config.read().map_err(AppError::from)?;
2453 let manager = config
2454 .get_manager(&app_type)
2455 .ok_or_else(|| Self::app_not_found(&app_type))?;
2456
2457 if !app_type.is_additive_mode()
2458 && (local_current_provider.as_deref() == Some(provider_id)
2459 || stored_current_provider.as_deref() == Some(provider_id))
2460 {
2461 return Err(AppError::localized(
2462 "provider.delete.current",
2463 "不能删除当前正在使用的供应商",
2464 "Cannot delete the provider currently in use",
2465 ));
2466 }
2467
2468 manager.providers.get(provider_id).cloned().ok_or_else(|| {
2469 AppError::localized(
2470 "provider.not_found",
2471 format!("供应商不存在: {provider_id}"),
2472 format!("Provider not found: {provider_id}"),
2473 )
2474 })?
2475 };
2476 let stale_codex_catalog_keys = if matches!(app_type, AppType::Codex) {
2477 Self::provider_codex_model_provider_key(&provider_snapshot)
2478 .into_iter()
2479 .collect::<Vec<_>>()
2480 } else {
2481 Vec::new()
2482 };
2483
2484 if app_type.is_additive_mode() {
2485 match app_type {
2486 AppType::OpenCode => {
2487 if crate::opencode_config::get_opencode_dir().exists() {
2488 crate::opencode_config::remove_provider(provider_id)?;
2489 }
2490 }
2491 AppType::OpenClaw => {
2492 if crate::openclaw_config::get_openclaw_dir().exists() {
2493 crate::openclaw_config::remove_provider(provider_id)?;
2494 }
2495 }
2496 AppType::Hermes => {
2497 if crate::hermes_config::get_hermes_dir().exists() {
2498 crate::hermes_config::remove_provider(provider_id)?;
2499 }
2500 }
2501 _ => unreachable!("non-additive apps should not enter additive delete branch"),
2502 }
2503
2504 {
2505 let mut config = state.config.write().map_err(AppError::from)?;
2506 let manager = config
2507 .get_manager_mut(&app_type)
2508 .ok_or_else(|| Self::app_not_found(&app_type))?;
2509 manager.providers.shift_remove(provider_id);
2510 }
2511
2512 return state.save();
2513 }
2514
2515 match app_type {
2516 AppType::Codex => {
2517 crate::codex_config::delete_codex_provider_config(
2518 provider_id,
2519 &provider_snapshot.name,
2520 )?;
2521 }
2522 AppType::Claude => {
2523 let by_name = get_provider_config_path(provider_id, Some(&provider_snapshot.name));
2526 let by_id = get_provider_config_path(provider_id, None);
2527 delete_file(&by_name)?;
2528 delete_file(&by_id)?;
2529 }
2530 AppType::Gemini => {
2531 }
2533 AppType::OpenCode => {
2534 let _ = provider_snapshot;
2535 }
2536 AppType::OpenClaw => {
2537 let _ = provider_snapshot;
2538 }
2539 AppType::Hermes => {
2540 let _ = provider_snapshot;
2541 }
2542 }
2543
2544 {
2545 let mut config = state.config.write().map_err(AppError::from)?;
2546 let manager = config
2547 .get_manager_mut(&app_type)
2548 .ok_or_else(|| Self::app_not_found(&app_type))?;
2549
2550 if !app_type.is_additive_mode()
2551 && (local_current_provider.as_deref() == Some(provider_id)
2552 || stored_current_provider.as_deref() == Some(provider_id))
2553 {
2554 return Err(AppError::localized(
2555 "provider.delete.current",
2556 "不能删除当前正在使用的供应商",
2557 "Cannot delete the provider currently in use",
2558 ));
2559 }
2560
2561 if !app_type.is_additive_mode() && manager.current == provider_id {
2562 manager.current = stored_current_provider.clone().unwrap_or_default();
2563 }
2564
2565 manager.providers.shift_remove(provider_id);
2566 }
2567
2568 state.save()?;
2569 if matches!(app_type, AppType::Codex) {
2570 Self::sync_codex_provider_catalog_to_live(state, &stale_codex_catalog_keys)?;
2571 }
2572 Ok(())
2573 }
2574
2575 pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2576 live::import_openclaw_providers_from_live(state)
2577 }
2578
2579 pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2580 live::import_opencode_providers_from_live(state)
2581 }
2582
2583 pub fn import_hermes_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2584 live::import_hermes_providers_from_live(state)
2585 }
2586}
2587
2588#[derive(Debug, Clone, Deserialize)]
2589pub struct ProviderSortUpdate {
2590 pub id: String,
2591 #[serde(rename = "sortIndex")]
2592 pub sort_index: usize,
2593}