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_mcp_servers_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 mut raw_settings = serde_json::Map::new();
548 if let Some(auth) = auth {
549 raw_settings.insert("auth".to_string(), auth);
550 }
551 raw_settings.insert("config".to_string(), Value::String(cfg_text_for_storage));
552 let mut settings_to_store = Self::normalize_settings_config_for_storage(
553 app_type,
554 &provider,
555 Value::Object(raw_settings),
556 effective_common_snippet.as_deref(),
557 )?;
558 Self::restore_codex_model_provider_for_storage_best_effort(
559 &provider,
560 &mut settings_to_store,
561 );
562
563 {
564 let mut guard = state.config.write().map_err(AppError::from)?;
565 if !common_snippet_extracted.trim().is_empty()
566 && guard
567 .common_config_snippets
568 .codex
569 .as_deref()
570 .unwrap_or_default()
571 .trim()
572 .is_empty()
573 {
574 guard.common_config_snippets.codex = Some(common_snippet_extracted.clone());
575 Self::normalize_existing_provider_snapshots_for_storage_best_effort(
576 &mut guard,
577 app_type,
578 Some(common_snippet_extracted.as_str()),
579 );
580 }
581 if let Some(manager) = guard.get_manager_mut(app_type) {
582 if let Some(target) = manager.providers.get_mut(provider_id) {
583 target.settings_config = settings_to_store.clone();
584 }
585 }
586 }
587 state.save()?;
588 }
589 AppType::Gemini => {
590 use crate::gemini_config::{
591 env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
592 };
593
594 let env_path = get_gemini_env_path();
595 if !env_path.exists() {
596 return Err(AppError::localized(
597 "gemini.live.missing",
598 "Gemini .env 文件不存在,无法刷新快照",
599 "Gemini .env file missing; cannot refresh snapshot",
600 ));
601 }
602 let env_map = read_gemini_env()?;
603 let mut live_after = env_to_json(&env_map);
604
605 let settings_path = get_gemini_settings_path();
606 let config_value = if settings_path.exists() {
607 read_json_file(&settings_path)?
608 } else {
609 json!({})
610 };
611
612 if let Some(obj) = live_after.as_object_mut() {
613 obj.insert("config".to_string(), config_value);
614 }
615
616 let (provider, common_snippet) = {
617 let guard = state.config.read().map_err(AppError::from)?;
618 (
619 guard
620 .get_manager(app_type)
621 .and_then(|manager| manager.providers.get(provider_id))
622 .cloned()
623 .ok_or_else(|| {
624 AppError::localized(
625 "provider.not_found",
626 format!("供应商不存在: {provider_id}"),
627 format!("Provider not found: {provider_id}"),
628 )
629 })?,
630 guard.common_config_snippets.gemini.clone(),
631 )
632 };
633 let live_after = Self::normalize_settings_config_for_storage(
634 app_type,
635 &provider,
636 live_after,
637 common_snippet.as_deref(),
638 )?;
639
640 {
641 let mut guard = state.config.write().map_err(AppError::from)?;
642 if let Some(manager) = guard.get_manager_mut(app_type) {
643 if let Some(target) = manager.providers.get_mut(provider_id) {
644 target.settings_config = live_after;
645 }
646 }
647 }
648 state.save()?;
649 }
650 AppType::OpenCode => {
651 let providers = crate::opencode_config::get_providers()?;
652 let live_after = providers.get(provider_id).cloned().ok_or_else(|| {
653 AppError::localized(
654 "opencode.live.missing_provider",
655 format!("OpenCode live 配置中缺少供应商: {provider_id}"),
656 format!("OpenCode live config missing provider: {provider_id}"),
657 )
658 })?;
659
660 {
661 let mut guard = state.config.write().map_err(AppError::from)?;
662 if let Some(manager) = guard.get_manager_mut(app_type) {
663 if let Some(target) = manager.providers.get_mut(provider_id) {
664 target.settings_config = live_after;
665 }
666 }
667 }
668 state.save()?;
669 }
670 AppType::OpenClaw => {
671 let providers = crate::openclaw_config::get_providers()?;
672 let live_after = providers.get(provider_id).cloned().ok_or_else(|| {
673 AppError::localized(
674 "openclaw.live.missing_provider",
675 format!("OpenClaw live 配置中缺少供应商: {provider_id}"),
676 format!("OpenClaw live config missing provider: {provider_id}"),
677 )
678 })?;
679
680 {
681 let mut guard = state.config.write().map_err(AppError::from)?;
682 if let Some(manager) = guard.get_manager_mut(app_type) {
683 if let Some(target) = manager.providers.get_mut(provider_id) {
684 target.settings_config = live_after;
685 }
686 }
687 }
688 state.save()?;
689 }
690 AppType::Hermes => {
691 let providers = crate::hermes_config::get_providers()?;
692 let live_after = providers.get(provider_id).cloned().unwrap_or_else(|| {
693 log::warn!(
694 "Hermes live config missing provider '{provider_id}', using empty config"
695 );
696 serde_json::Value::Object(serde_json::Map::new())
697 });
698
699 {
700 let mut guard = state.config.write().map_err(AppError::from)?;
701 if let Some(manager) = guard.get_manager_mut(app_type) {
702 if let Some(target) = manager.providers.get_mut(provider_id) {
703 target.settings_config = live_after;
704 }
705 }
706 }
707 state.save()?;
708 }
709 }
710 Ok(())
711 }
712
713 fn capture_live_snapshot(app_type: &AppType) -> Result<LiveSnapshot, AppError> {
714 live::capture_live_snapshot(app_type)
715 }
716
717 fn validate_common_config_snippet(
718 app_type: &AppType,
719 snippet: Option<&str>,
720 ) -> Result<(), AppError> {
721 common_config::validate_common_config_snippet(app_type, snippet)
722 }
723
724 fn should_skip_common_config_migration_error(app_type: &AppType, err: &AppError) -> bool {
725 match (app_type, err) {
726 (AppType::Claude, AppError::Localized { key, .. }) => {
727 key.starts_with("common_config.claude.")
728 }
729 (AppType::Codex, AppError::Config(message)) => {
730 message.starts_with("Common config TOML parse error:")
731 }
732 (AppType::Gemini, AppError::Localized { key, .. }) => {
733 key.starts_with("common_config.gemini.")
734 }
735 _ => false,
736 }
737 }
738
739 fn migrate_old_common_config_snippet_best_effort(
740 config: &mut MultiAppConfig,
741 app_type: &AppType,
742 strict_current_provider_id: Option<&str>,
743 old_snippet: Option<&str>,
744 ) -> Result<(), AppError> {
745 let Some(old_snippet) = old_snippet.map(str::trim) else {
746 return Ok(());
747 };
748 if old_snippet.is_empty() {
749 return Ok(());
750 }
751
752 let result = match app_type {
753 AppType::Claude => Self::migrate_claude_common_config_snippet(config, old_snippet),
754 AppType::Codex => Self::migrate_codex_common_config_snippet(
755 config,
756 strict_current_provider_id,
757 old_snippet,
758 ),
759 AppType::Gemini => Self::migrate_gemini_common_config_snippet(
760 config,
761 strict_current_provider_id,
762 old_snippet,
763 ),
764 AppType::OpenCode | AppType::OpenClaw => Ok(()),
765 AppType::Hermes => Ok(()),
766 };
767
768 match result {
769 Ok(()) => Ok(()),
770 Err(err) if Self::should_skip_common_config_migration_error(app_type, &err) => {
771 log::warn!(
772 "skip migrating {app_type} provider snapshots from invalid stored common config snippet: {err}"
773 );
774 Ok(())
775 }
776 Err(err) => Err(err),
777 }
778 }
779
780 #[doc(hidden)]
781 pub fn migrate_common_config_upstream_semantics_if_needed(
782 db: &crate::database::Database,
783 config: &mut MultiAppConfig,
784 ) -> Result<(), AppError> {
785 common_config::migrate_common_config_upstream_semantics_if_needed(db, config)
786 }
787
788 fn build_common_config_post_commit_action(
789 config: &MultiAppConfig,
790 app_type: &AppType,
791 current_provider_id: Option<&str>,
792 takeover_active: bool,
793 ) -> Result<Option<PostCommitAction>, AppError> {
794 if app_type.is_additive_mode() {
795 return Ok(None);
796 }
797
798 let Some(current_provider_id) = current_provider_id else {
799 return Ok(None);
800 };
801
802 Self::build_post_commit_action_for_current_provider(
803 config,
804 app_type,
805 ¤t_provider_id,
806 takeover_active,
807 false,
808 )
809 }
810
811 fn build_post_commit_action_for_current_provider(
812 config: &MultiAppConfig,
813 app_type: &AppType,
814 current_provider_id: &str,
815 takeover_active: bool,
816 preserve_live_preferences: bool,
817 ) -> Result<Option<PostCommitAction>, AppError> {
818 let provider = config
819 .get_manager(app_type)
820 .and_then(|manager| manager.providers.get(current_provider_id).cloned());
821
822 let Some(provider) = provider else {
823 return Ok(None);
824 };
825
826 Ok(Some(PostCommitAction {
827 app_type: app_type.clone(),
828 provider,
829 backup: Self::capture_live_snapshot(app_type)?,
830 write_live_snapshot: true,
831 sync_mcp: false,
832 sync_codex_catalog: matches!(app_type, AppType::Codex),
833 stale_codex_catalog_keys: Vec::new(),
834 refresh_snapshot: false,
835 apply_hermes_switch_defaults: false,
836 common_config_snippet: config.common_config_snippets.get(app_type).cloned(),
837 takeover_active,
838 preserve_live_preferences,
839 }))
840 }
841
842 fn resolve_live_apply_common_config(
843 app_type: &AppType,
844 provider: &Provider,
845 common_config_snippet: Option<&str>,
846 requested_apply_common_config: bool,
847 ) -> bool {
848 if !requested_apply_common_config {
849 return false;
850 }
851
852 common_config::provider_uses_common_config(app_type, provider, common_config_snippet)
853 }
854
855 fn normalize_provider_for_storage(
856 app_type: &AppType,
857 provider: &mut Provider,
858 common_config_snippet: Option<&str>,
859 ) -> Result<(), AppError> {
860 common_config::normalize_provider_common_config_for_storage(
861 app_type,
862 provider,
863 common_config_snippet,
864 )
865 }
866
867 pub(crate) fn normalize_settings_config_for_storage(
868 app_type: &AppType,
869 provider: &Provider,
870 settings_config: Value,
871 common_config_snippet: Option<&str>,
872 ) -> Result<Value, AppError> {
873 let mut snapshot_provider = provider.clone();
874 snapshot_provider.settings_config = settings_config;
875 Self::normalize_provider_for_storage(
876 app_type,
877 &mut snapshot_provider,
878 common_config_snippet,
879 )?;
880 Ok(snapshot_provider.settings_config)
881 }
882
883 fn restore_codex_model_provider_for_storage_best_effort(
884 provider: &Provider,
885 settings_config: &mut Value,
886 ) {
887 if let Err(err) =
888 crate::codex_config::restore_codex_settings_config_model_provider_for_backfill(
889 settings_config,
890 &provider.settings_config,
891 )
892 {
893 log::warn!(
894 "Failed to restore Codex provider id while storing snapshot for '{}': {err}",
895 provider.id
896 );
897 }
898 }
899
900 pub(crate) fn remove_common_config_from_settings_for_preview(
901 app_type: &AppType,
902 settings_config: &Value,
903 common_config_snippet: &str,
904 ) -> Result<Value, AppError> {
905 common_config::remove_common_config_from_settings(
906 app_type,
907 settings_config,
908 common_config_snippet,
909 )
910 }
911
912 fn normalize_existing_provider_snapshots_for_storage(
913 config: &mut MultiAppConfig,
914 app_type: &AppType,
915 common_config_snippet: Option<&str>,
916 ) -> Result<(), AppError> {
917 let Some(manager) = config.get_manager_mut(app_type) else {
918 return Ok(());
919 };
920
921 for provider in manager.providers.values_mut() {
922 common_config::migrate_provider_subset_usage_for_storage(
923 app_type,
924 provider,
925 common_config_snippet,
926 )?;
927 }
928
929 Ok(())
930 }
931
932 fn normalize_existing_provider_snapshots_for_storage_best_effort(
933 config: &mut MultiAppConfig,
934 app_type: &AppType,
935 common_config_snippet: Option<&str>,
936 ) {
937 let Some(manager) = config.get_manager_mut(app_type) else {
938 return;
939 };
940
941 for (provider_id, provider) in manager.providers.iter_mut() {
942 if let Err(err) = common_config::migrate_provider_subset_usage_for_storage(
943 app_type,
944 provider,
945 common_config_snippet,
946 ) {
947 log::warn!(
948 "skip normalizing {app_type} provider snapshot '{provider_id}' while applying auto-extracted common config: {err}"
949 );
950 }
951 }
952 }
953
954 fn normalize_existing_provider_snapshots_for_storage_strict_current_best_effort_others(
955 config: &mut MultiAppConfig,
956 app_type: &AppType,
957 strict_current_provider_id: Option<&str>,
958 common_config_snippet: Option<&str>,
959 ) -> Result<(), AppError> {
960 let Some(current_provider_id) = strict_current_provider_id.and_then(|provider_id| {
961 config.get_manager(app_type).and_then(|manager| {
962 manager
963 .providers
964 .contains_key(provider_id)
965 .then(|| provider_id.to_string())
966 })
967 }) else {
968 return Self::normalize_existing_provider_snapshots_for_storage(
969 config,
970 app_type,
971 common_config_snippet,
972 );
973 };
974
975 let Some(manager) = config.get_manager_mut(app_type) else {
976 return Ok(());
977 };
978
979 if let Some(current_provider) = manager.providers.get_mut(¤t_provider_id) {
980 common_config::migrate_provider_subset_usage_for_storage(
981 app_type,
982 current_provider,
983 common_config_snippet,
984 )?;
985 }
986
987 for (provider_id, provider) in manager.providers.iter_mut() {
988 if provider_id == ¤t_provider_id {
989 continue;
990 }
991
992 if let Err(err) = common_config::migrate_provider_subset_usage_for_storage(
993 app_type,
994 provider,
995 common_config_snippet,
996 ) {
997 log::warn!(
998 "skip normalizing {app_type} non-current provider snapshot '{provider_id}' while updating common config snippet: {err}"
999 );
1000 }
1001 }
1002
1003 Ok(())
1004 }
1005
1006 fn hydrate_missing_provider_snapshots_from_db(
1007 config: &mut MultiAppConfig,
1008 app_type: &AppType,
1009 db_providers: &IndexMap<String, Provider>,
1010 ) -> Result<(), AppError> {
1011 let manager = config
1012 .get_manager_mut(app_type)
1013 .ok_or_else(|| Self::app_not_found(app_type))?;
1014
1015 for (provider_id, provider) in db_providers {
1016 manager
1017 .providers
1018 .entry(provider_id.clone())
1019 .or_insert_with(|| provider.clone());
1020 }
1021
1022 Ok(())
1023 }
1024
1025 pub fn set_common_config_snippet(
1026 state: &AppState,
1027 app_type: AppType,
1028 snippet: Option<String>,
1029 ) -> Result<(), AppError> {
1030 let normalized_snippet = snippet.and_then(|value| {
1031 let trimmed = value.trim();
1032 if trimmed.is_empty() {
1033 None
1034 } else {
1035 Some(trimmed.to_string())
1036 }
1037 });
1038 Self::validate_common_config_snippet(&app_type, normalized_snippet.as_deref())?;
1039
1040 let app_type_clone = app_type.clone();
1041 let (effective_current_provider, db_providers) = if app_type.is_additive_mode() {
1042 (None, None)
1043 } else {
1044 (
1045 crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1046 Some(state.db.get_all_providers(app_type.as_str())?),
1047 )
1048 };
1049 let takeover_active = if app_type.is_additive_mode() {
1050 false
1051 } else {
1052 let is_running = state
1053 .proxy_service
1054 .is_running_blocking()
1055 .map_err(AppError::Message)?;
1056 if !is_running {
1057 false
1058 } else {
1059 state
1060 .proxy_service
1061 .is_app_takeover_active_blocking(&app_type)
1062 .map_err(AppError::Message)?
1063 }
1064 };
1065
1066 Self::run_transaction_preserving_current_providers(
1067 state,
1068 std::slice::from_ref(&app_type),
1069 move |config| {
1070 config.ensure_app(&app_type_clone);
1071
1072 if let Some(db_providers) = db_providers.as_ref() {
1073 Self::hydrate_missing_provider_snapshots_from_db(
1074 config,
1075 &app_type_clone,
1076 db_providers,
1077 )?;
1078 }
1079
1080 let old_snippet = config
1081 .common_config_snippets
1082 .get(&app_type_clone)
1083 .cloned()
1084 .filter(|value| !value.trim().is_empty());
1085
1086 Self::migrate_old_common_config_snippet_best_effort(
1087 config,
1088 &app_type_clone,
1089 effective_current_provider.as_deref(),
1090 old_snippet.as_deref(),
1091 )?;
1092
1093 config
1094 .common_config_snippets
1095 .set(&app_type_clone, normalized_snippet.clone());
1096
1097 if matches!(
1098 app_type_clone,
1099 AppType::Claude | AppType::Codex | AppType::Gemini
1100 ) {
1101 Self::normalize_existing_provider_snapshots_for_storage_strict_current_best_effort_others(
1102 config,
1103 &app_type_clone,
1104 effective_current_provider.as_deref(),
1105 normalized_snippet.as_deref(),
1106 )?;
1107 }
1108
1109 let action = Self::build_common_config_post_commit_action(
1110 config,
1111 &app_type_clone,
1112 effective_current_provider.as_deref(),
1113 takeover_active,
1114 )?;
1115 Ok(((), action))
1116 },
1117 )
1118 }
1119
1120 pub fn clear_common_config_snippet(
1121 state: &AppState,
1122 app_type: AppType,
1123 ) -> Result<(), AppError> {
1124 Self::set_common_config_snippet(state, app_type, None)
1125 }
1126
1127 pub fn list(
1129 state: &AppState,
1130 app_type: AppType,
1131 ) -> Result<IndexMap<String, Provider>, AppError> {
1132 let config = state.config.read().map_err(AppError::from)?;
1133 let manager = config
1134 .get_manager(&app_type)
1135 .ok_or_else(|| Self::app_not_found(&app_type))?;
1136 Ok(manager.get_all_providers().clone())
1137 }
1138
1139 pub(crate) fn sync_openclaw_providers_from_live(state: &AppState) -> Result<(), AppError> {
1140 live::sync_openclaw_providers_from_live(state)?;
1141 Ok(())
1142 }
1143
1144 pub fn current(state: &AppState, app_type: AppType) -> Result<String, AppError> {
1146 if app_type == AppType::Hermes {
1147 return Ok(crate::hermes_config::get_model_config()?
1148 .and_then(|model| model.provider)
1149 .map(|provider| provider.trim().to_string())
1150 .filter(|provider| !provider.is_empty())
1151 .unwrap_or_default());
1152 }
1153 if app_type.is_additive_mode() {
1154 return Ok(String::new());
1155 }
1156 crate::settings::get_effective_current_provider(&state.db, &app_type)
1157 .map(|opt| opt.unwrap_or_default())
1158 }
1159
1160 pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
1162 let mut provider = provider;
1163 Self::normalize_provider_if_claude(&app_type, &mut provider);
1165 Self::validate_provider_settings(&app_type, &provider)?;
1166
1167 let app_type_clone = app_type.clone();
1168 let provider_clone = provider.clone();
1169 let stored_current_provider = if app_type.is_additive_mode() {
1170 None
1171 } else {
1172 state.db.get_current_provider(app_type.as_str())?
1173 };
1174
1175 Self::run_transaction(state, move |config| {
1176 let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1177 let mut provider_to_store = provider_clone.clone();
1178 Self::normalize_provider_for_storage(
1179 &app_type_clone,
1180 &mut provider_to_store,
1181 common_config_snippet.as_deref(),
1182 )?;
1183
1184 if matches!(app_type_clone, AppType::OpenClaw)
1185 && provider_to_store.created_at.is_none()
1186 && live::is_auto_mirrored_openclaw_snapshot(&provider_to_store)
1187 {
1188 provider_to_store.created_at = Some(current_timestamp());
1189 }
1190 if app_type_clone.is_additive_mode() {
1191 Self::set_provider_live_config_managed(&mut provider_to_store, true);
1192 }
1193
1194 config.ensure_app(&app_type_clone);
1195 let manager = config
1196 .get_manager_mut(&app_type_clone)
1197 .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1198
1199 if !app_type_clone.is_additive_mode() {
1200 manager.current = stored_current_provider.clone().unwrap_or_default();
1201 }
1202
1203 let was_empty = manager.providers.is_empty();
1204 manager
1205 .providers
1206 .insert(provider_to_store.id.clone(), provider_to_store.clone());
1207
1208 if !app_type_clone.is_additive_mode()
1209 && stored_current_provider.is_none()
1210 && (was_empty || manager.current.is_empty())
1211 {
1212 manager.current = provider_to_store.id.clone();
1213 }
1214
1215 let is_current =
1216 app_type_clone.is_additive_mode() || manager.current == provider_to_store.id;
1217 let action = if is_current {
1218 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1219 Some(PostCommitAction {
1220 app_type: app_type_clone.clone(),
1221 provider: provider_to_store.clone(),
1222 backup,
1223 write_live_snapshot: true,
1224 sync_mcp: false,
1227 sync_codex_catalog: matches!(&app_type_clone, AppType::Codex),
1228 stale_codex_catalog_keys: Vec::new(),
1229 refresh_snapshot: false,
1230 apply_hermes_switch_defaults: false,
1231 common_config_snippet,
1232 takeover_active: false,
1233 preserve_live_preferences: true,
1234 })
1235 } else if matches!(&app_type_clone, AppType::Codex) {
1236 Some(PostCommitAction {
1237 app_type: app_type_clone.clone(),
1238 provider: provider_to_store.clone(),
1239 backup: Self::capture_live_snapshot(&app_type_clone)?,
1240 write_live_snapshot: false,
1241 sync_mcp: false,
1242 sync_codex_catalog: true,
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 {
1251 None
1252 };
1253
1254 Ok((true, action))
1255 })
1256 }
1257
1258 pub fn update(
1260 state: &AppState,
1261 app_type: AppType,
1262 provider: Provider,
1263 ) -> Result<bool, AppError> {
1264 let mut provider = provider;
1265 Self::normalize_provider_if_claude(&app_type, &mut provider);
1267 Self::validate_provider_settings(&app_type, &provider)?;
1268 let provider_id = provider.id.clone();
1269 let app_type_clone = app_type.clone();
1270 let provider_clone = provider.clone();
1271 let (effective_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
1272 (None, None)
1273 } else {
1274 (
1275 crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1276 state.db.get_current_provider(app_type.as_str())?,
1277 )
1278 };
1279
1280 Self::run_transaction(state, move |config| {
1281 let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1282 let manager = config
1283 .get_manager_mut(&app_type_clone)
1284 .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1285
1286 if !manager.providers.contains_key(&provider_id) {
1287 return Err(AppError::localized(
1288 "provider.not_found",
1289 format!("供应商不存在: {provider_id}"),
1290 format!("Provider not found: {provider_id}"),
1291 ));
1292 }
1293
1294 if !app_type_clone.is_additive_mode() {
1295 manager.current = stored_current_provider.clone().unwrap_or_default();
1296 }
1297
1298 let existing_live_config_managed = manager
1299 .providers
1300 .get(&provider_id)
1301 .and_then(Self::provider_live_config_managed);
1302 let previous_codex_catalog_key = manager
1303 .providers
1304 .get(&provider_id)
1305 .and_then(Self::provider_codex_model_provider_key);
1306 let mut merged = if let Some(existing) = manager.providers.get(&provider_id) {
1307 let mut updated = provider_clone.clone();
1308 match (existing.meta.as_ref(), updated.meta.take()) {
1309 (Some(old_meta), None) => {
1311 updated.meta = Some(old_meta.clone());
1312 }
1313 (None, None) => {
1314 updated.meta = None;
1315 }
1316 (_old, Some(new_meta)) => {
1318 updated.meta = Some(new_meta);
1319 }
1320 }
1321 if matches!(app_type_clone, AppType::OpenClaw)
1322 && updated.created_at.is_none()
1323 && live::is_auto_mirrored_openclaw_snapshot(&updated)
1324 {
1325 updated.created_at = Some(current_timestamp());
1326 }
1327 updated
1328 } else {
1329 provider_clone.clone()
1330 };
1331
1332 Self::normalize_provider_for_storage(
1333 &app_type_clone,
1334 &mut merged,
1335 common_config_snippet.as_deref(),
1336 )?;
1337
1338 let should_write_live = if app_type_clone.is_additive_mode() {
1339 let live_config_managed = Self::additive_provider_exists_in_live_config(
1340 &app_type_clone,
1341 &provider_id,
1342 Self::provider_live_config_managed(&merged).or(existing_live_config_managed),
1343 )?;
1344 Self::set_provider_live_config_managed(&mut merged, live_config_managed);
1345 live_config_managed
1346 } else {
1347 effective_current_provider.as_deref() == Some(provider_id.as_str())
1348 };
1349
1350 manager
1351 .providers
1352 .insert(provider_id.clone(), merged.clone());
1353
1354 let action = if should_write_live {
1355 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1356 Some(PostCommitAction {
1357 app_type: app_type_clone.clone(),
1358 provider: merged,
1359 backup,
1360 write_live_snapshot: true,
1361 sync_mcp: false,
1364 sync_codex_catalog: matches!(&app_type_clone, AppType::Codex),
1365 stale_codex_catalog_keys: Vec::new(),
1366 refresh_snapshot: false,
1367 apply_hermes_switch_defaults: false,
1368 common_config_snippet,
1369 takeover_active: false,
1370 preserve_live_preferences: true,
1371 })
1372 } else if matches!(&app_type_clone, AppType::Codex) {
1373 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1374 let current_codex_catalog_key = Self::provider_codex_model_provider_key(&merged);
1375 let stale_codex_catalog_keys = previous_codex_catalog_key
1376 .filter(|old_key| {
1377 current_codex_catalog_key.as_deref() != Some(old_key.as_str())
1378 })
1379 .into_iter()
1380 .collect();
1381 Some(PostCommitAction {
1382 app_type: app_type_clone.clone(),
1383 provider: merged,
1384 backup,
1385 write_live_snapshot: false,
1386 sync_mcp: false,
1387 sync_codex_catalog: true,
1388 stale_codex_catalog_keys,
1389 refresh_snapshot: false,
1390 apply_hermes_switch_defaults: false,
1391 common_config_snippet,
1392 takeover_active: false,
1393 preserve_live_preferences: true,
1394 })
1395 } else {
1396 None
1397 };
1398
1399 Ok((true, action))
1400 })
1401 }
1402
1403 pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
1407 if app_type.is_additive_mode() {
1408 return Ok(false);
1409 }
1410
1411 if state.db.has_non_official_seed_provider(app_type.as_str())? {
1412 return Ok(false);
1413 }
1414
1415 let settings_config = match app_type {
1416 AppType::Codex => {
1417 let auth_path = get_codex_auth_path();
1418 if !auth_path.exists() {
1419 return Err(AppError::localized(
1420 "codex.live.missing",
1421 "Codex 配置文件不存在",
1422 "Codex configuration file is missing",
1423 ));
1424 }
1425 let auth: Value = read_json_file(&auth_path)?;
1426 let config_str = crate::codex_config::read_and_validate_codex_config_text()?;
1427 json!({ "auth": auth, "config": config_str })
1428 }
1429 AppType::Claude => {
1430 let settings_path = get_claude_settings_path();
1431 if !settings_path.exists() {
1432 return Err(AppError::localized(
1433 "claude.live.missing",
1434 "Claude Code 配置文件不存在",
1435 "Claude settings file is missing",
1436 ));
1437 }
1438 let mut v = read_json_file::<Value>(&settings_path)?;
1439 let _ = Self::normalize_claude_models_in_value(&mut v);
1440 v
1441 }
1442 AppType::Gemini => {
1443 use crate::gemini_config::{
1444 env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1445 };
1446
1447 let env_path = get_gemini_env_path();
1449 if !env_path.exists() {
1450 return Err(AppError::localized(
1451 "gemini.live.missing",
1452 "Gemini 配置文件不存在",
1453 "Gemini configuration file is missing",
1454 ));
1455 }
1456
1457 let env_map = read_gemini_env()?;
1458 let env_json = env_to_json(&env_map);
1459 let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1460
1461 let settings_path = get_gemini_settings_path();
1463 let config_obj = if settings_path.exists() {
1464 read_json_file(&settings_path)?
1465 } else {
1466 json!({})
1467 };
1468
1469 json!({
1471 "env": env_obj,
1472 "config": config_obj
1473 })
1474 }
1475 AppType::OpenCode => unreachable!("additive mode apps are handled earlier"),
1476 AppType::OpenClaw => unreachable!("additive mode apps are handled earlier"),
1477 AppType::Hermes => unreachable!("additive mode apps are handled earlier"),
1478 };
1479
1480 let mut provider = Provider::with_id(
1481 "default".to_string(),
1482 "default".to_string(),
1483 settings_config,
1484 None,
1485 );
1486 provider.category = Some("custom".to_string());
1487
1488 state.db.save_provider(app_type.as_str(), &provider)?;
1489 state
1490 .db
1491 .set_current_provider(app_type.as_str(), &provider.id)?;
1492 {
1493 let mut guard = state.config.write().map_err(AppError::from)?;
1494 guard.ensure_app(&app_type);
1495 let manager = guard
1496 .get_manager_mut(&app_type)
1497 .ok_or_else(|| AppError::Config("manager missing after ensure_app".into()))?;
1498 manager.current = provider.id.clone();
1499 manager.providers.insert(provider.id.clone(), provider);
1500 }
1501 Ok(true)
1502 }
1503
1504 pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
1506 match app_type {
1507 AppType::Codex => {
1508 let auth_path = get_codex_auth_path();
1509 let config_path = get_codex_config_path();
1510 if !config_path.exists() {
1511 return Err(AppError::localized(
1512 "codex.live.missing",
1513 "Codex 配置文件不存在",
1514 "Codex configuration is missing",
1515 ));
1516 }
1517
1518 let mut live_settings = serde_json::Map::new();
1519 if auth_path.exists() {
1520 live_settings.insert("auth".to_string(), read_json_file(&auth_path)?);
1521 }
1522 if config_path.exists() {
1523 let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
1524 live_settings.insert("config".to_string(), Value::String(cfg_text));
1525 }
1526
1527 Ok(Value::Object(live_settings))
1528 }
1529 AppType::Claude => {
1530 let path = get_claude_settings_path();
1531 if !path.exists() {
1532 return Err(AppError::localized(
1533 "claude.live.missing",
1534 "Claude Code 配置文件不存在",
1535 "Claude settings file is missing",
1536 ));
1537 }
1538 read_json_file(&path)
1539 }
1540 AppType::Gemini => {
1541 use crate::gemini_config::{
1542 env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1543 };
1544
1545 let env_path = get_gemini_env_path();
1547 if !env_path.exists() {
1548 return Err(AppError::localized(
1549 "gemini.env.missing",
1550 "Gemini .env 文件不存在",
1551 "Gemini .env file not found",
1552 ));
1553 }
1554
1555 let env_map = read_gemini_env()?;
1556 let env_json = env_to_json(&env_map);
1557 let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1558
1559 let settings_path = get_gemini_settings_path();
1561 let config_obj = if settings_path.exists() {
1562 read_json_file(&settings_path)?
1563 } else {
1564 json!({})
1565 };
1566
1567 Ok(json!({
1569 "env": env_obj,
1570 "config": config_obj
1571 }))
1572 }
1573 AppType::OpenCode => {
1574 let config_path = crate::opencode_config::get_opencode_config_path();
1575 if !config_path.exists() {
1576 return Err(AppError::localized(
1577 "opencode.config.missing",
1578 "OpenCode 配置文件不存在",
1579 "OpenCode configuration file not found",
1580 ));
1581 }
1582 crate::opencode_config::read_opencode_config()
1583 }
1584 AppType::OpenClaw => {
1585 let config_path = crate::openclaw_config::get_openclaw_config_path();
1586 if !config_path.exists() {
1587 return Err(AppError::localized(
1588 "openclaw.config.missing",
1589 "OpenClaw 配置文件不存在",
1590 "OpenClaw configuration file not found",
1591 ));
1592 }
1593 crate::openclaw_config::read_openclaw_config()
1594 }
1595 AppType::Hermes => {
1596 let yaml = crate::hermes_config::read_hermes_config()?;
1597 crate::hermes_config::yaml_to_json(&yaml)
1598 }
1599 }
1600 }
1601
1602 pub fn update_sort_order(
1604 state: &AppState,
1605 app_type: AppType,
1606 updates: Vec<ProviderSortUpdate>,
1607 ) -> Result<bool, AppError> {
1608 {
1609 let mut cfg = state.config.write().map_err(AppError::from)?;
1610 let manager = cfg
1611 .get_manager_mut(&app_type)
1612 .ok_or_else(|| Self::app_not_found(&app_type))?;
1613
1614 for update in updates {
1615 if let Some(provider) = manager.providers.get_mut(&update.id) {
1616 provider.sort_index = Some(update.sort_index);
1617 }
1618 }
1619 }
1620
1621 state.save()?;
1622 Ok(true)
1623 }
1624
1625 pub fn remove_from_live_config(
1626 state: &AppState,
1627 app_type: AppType,
1628 provider_id: &str,
1629 ) -> Result<(), AppError> {
1630 if !app_type.is_additive_mode() {
1631 return Err(AppError::localized(
1632 "provider.remove_from_live_config.unsupported",
1633 "只有累加模式应用支持从 live 配置中移除供应商",
1634 "Only additive-mode apps support removing a provider from live config",
1635 ));
1636 }
1637
1638 let original = {
1639 let config = state.config.read().map_err(AppError::from)?;
1640 let manager = config
1641 .get_manager(&app_type)
1642 .ok_or_else(|| Self::app_not_found(&app_type))?;
1643 if !manager.providers.contains_key(provider_id) {
1644 return Err(AppError::localized(
1645 "provider.not_found",
1646 format!("供应商不存在: {provider_id}"),
1647 format!("Provider not found: {provider_id}"),
1648 ));
1649 }
1650 config.clone()
1651 };
1652
1653 let backup = Self::capture_live_snapshot(&app_type)?;
1654 match &app_type {
1655 AppType::OpenCode => {
1656 if crate::opencode_config::get_opencode_dir().exists() {
1657 crate::opencode_config::remove_provider(provider_id)?;
1658 }
1659 }
1660 AppType::OpenClaw => {
1661 if crate::openclaw_config::get_openclaw_dir().exists() {
1662 crate::openclaw_config::remove_provider(provider_id)?;
1663 }
1664 }
1665 _ => unreachable!("non-additive apps should not enter remove-from-live branch"),
1666 }
1667
1668 {
1669 let mut config = state.config.write().map_err(AppError::from)?;
1670 let manager = config
1671 .get_manager_mut(&app_type)
1672 .ok_or_else(|| Self::app_not_found(&app_type))?;
1673 let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
1674 AppError::localized(
1675 "provider.not_found",
1676 format!("供应商不存在: {provider_id}"),
1677 format!("Provider not found: {provider_id}"),
1678 )
1679 })?;
1680 Self::set_provider_live_config_managed(provider, false);
1681 }
1682
1683 if let Err(save_err) = state.save() {
1684 let config_restore = Self::restore_config_only(state, original);
1685 let live_restore = backup.restore();
1686 if let Err(rollback_err) = config_restore {
1687 return Err(AppError::localized(
1688 "config.save.rollback_failed",
1689 format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
1690 format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
1691 ));
1692 }
1693 if let Err(rollback_err) = live_restore {
1694 return Err(AppError::localized(
1695 "post_commit.rollback_failed",
1696 format!("保存配置失败: {save_err};live 回滚失败: {rollback_err}"),
1697 format!(
1698 "Failed to save config: {save_err}; live rollback failed: {rollback_err}"
1699 ),
1700 ));
1701 }
1702 return Err(save_err);
1703 }
1704
1705 Ok(())
1706 }
1707
1708 pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
1714 use crate::services::mcp::McpService;
1715
1716 let snapshots: Vec<(AppType, Provider, Option<String>)> = {
1718 let guard = state.config.read().map_err(AppError::from)?;
1719 let mut result = Vec::new();
1720 for app_type in AppType::all() {
1721 if app_type.is_additive_mode() {
1722 if let Some(manager) = guard.get_manager(&app_type) {
1723 let snippet = guard.common_config_snippets.get(&app_type).cloned();
1724 for provider in manager.providers.values() {
1725 if Self::provider_live_config_managed(provider) == Some(false) {
1726 continue;
1727 }
1728 result.push((app_type.clone(), provider.clone(), snippet.clone()));
1729 }
1730 }
1731 continue;
1732 }
1733
1734 let current_id =
1735 match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
1736 Some(id) => id,
1737 None => continue,
1738 };
1739 let providers = state.db.get_all_providers(app_type.as_str())?;
1740 match providers.get(¤t_id) {
1741 Some(provider) => {
1742 let snippet = state.db.get_config_snippet(app_type.as_str())?;
1743 result.push((app_type.clone(), provider.clone(), snippet));
1744 }
1745 None => {
1746 log::warn!(
1747 "sync_current_to_live: {app_type} 当前供应商 {} 不存在于数据库,跳过",
1748 current_id
1749 );
1750 }
1751 }
1752 }
1753 result
1754 };
1755
1756 let openclaw_live_provider_ids = match Self::valid_openclaw_live_provider_ids() {
1757 Ok(provider_ids) => provider_ids,
1758 Err(err) => {
1759 log::warn!(
1760 "sync_current_to_live: 读取 OpenClaw live providers 失败,跳过 OpenClaw 同步: {err}"
1761 );
1762 None
1763 }
1764 };
1765
1766 for (app_type, provider, snippet) in &snapshots {
1767 if matches!(app_type, AppType::OpenClaw)
1768 && !openclaw_live_provider_ids
1769 .as_ref()
1770 .is_some_and(|provider_ids| provider_ids.contains(&provider.id))
1771 {
1772 continue;
1773 }
1774
1775 if let Err(e) =
1776 Self::write_live_snapshot(app_type, provider, snippet.as_deref(), true, true)
1777 {
1778 log::warn!("sync_current_to_live: 写入 {app_type} live 配置失败: {e}");
1779 }
1780 }
1781
1782 if snapshots
1783 .iter()
1784 .any(|(app_type, _, _)| matches!(app_type, AppType::Codex))
1785 {
1786 if let Err(e) = Self::sync_codex_provider_catalog_to_live(state, &[]) {
1787 log::warn!("sync_current_to_live: Codex provider catalog 同步失败: {e}");
1788 }
1789 }
1790
1791 if let Err(e) =
1792 crate::services::prompt::PromptService::sync_all_active_to_live_best_effort(state)
1793 {
1794 log::warn!("sync_current_to_live: Prompt 同步失败: {e}");
1795 }
1796
1797 if let Err(e) = McpService::sync_all_enabled_except(state, &[AppType::Codex]) {
1798 log::warn!("sync_current_to_live: 非 Codex MCP 同步失败: {e}");
1799 }
1800
1801 if let Err(e) = crate::services::skill::SkillService::sync_all_enabled_best_effort() {
1802 log::warn!("sync_current_to_live: Skills 同步失败: {e}");
1803 }
1804
1805 Ok(())
1806 }
1807
1808 pub fn switch(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
1810 if !app_type.is_additive_mode() {
1811 let providers = state.db.get_all_providers(app_type.as_str())?;
1812 providers.get(provider_id).ok_or_else(|| {
1813 AppError::localized(
1814 "provider.not_found",
1815 format!("供应商不存在: {provider_id}"),
1816 format!("Provider not found: {provider_id}"),
1817 )
1818 })?;
1819
1820 let is_app_taken_over =
1821 futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
1822 .ok()
1823 .flatten()
1824 .is_some();
1825 let is_proxy_running = state
1826 .proxy_service
1827 .is_running_blocking()
1828 .map_err(AppError::Message)?;
1829 let live_taken_over = state
1830 .proxy_service
1831 .detect_takeover_in_live_config_for_app(&app_type);
1832 let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
1833
1834 if should_hot_switch {
1835 futures::executor::block_on(
1836 state
1837 .proxy_service
1838 .hot_switch_provider(app_type.as_str(), provider_id),
1839 )
1840 .map_err(|e| AppError::Message(format!("热切换失败: {e}")))?;
1841
1842 let mut guard = state.config.write().map_err(AppError::from)?;
1843 if let Some(manager) = guard.get_manager_mut(&app_type) {
1844 manager.current = provider_id.to_string();
1845 }
1846 return Ok(());
1847 }
1848 }
1849
1850 let app_type_clone = app_type.clone();
1851 let provider_id_owned = provider_id.to_string();
1852 let effective_current_provider = if app_type.is_additive_mode() {
1853 None
1854 } else if matches!(app_type, AppType::Codex) {
1855 Self::codex_live_current_provider_id(state)?.or(
1856 crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1857 )
1858 } else {
1859 crate::settings::get_effective_current_provider(&state.db, &app_type)?
1860 };
1861
1862 Self::run_transaction(state, move |config| {
1863 if app_type_clone.is_additive_mode() {
1864 let provider = {
1865 let provider = config
1866 .get_manager_mut(&app_type_clone)
1867 .ok_or_else(|| Self::app_not_found(&app_type_clone))?
1868 .providers
1869 .get_mut(&provider_id_owned)
1870 .ok_or_else(|| {
1871 AppError::localized(
1872 "provider.not_found",
1873 format!("供应商不存在: {provider_id_owned}"),
1874 format!("Provider not found: {provider_id_owned}"),
1875 )
1876 })?;
1877 Self::set_provider_live_config_managed(provider, true);
1878 provider.clone()
1879 };
1880
1881 let action = PostCommitAction {
1882 app_type: app_type_clone.clone(),
1883 provider,
1884 backup: Self::capture_live_snapshot(&app_type_clone)?,
1885 write_live_snapshot: true,
1886 sync_mcp: matches!(app_type_clone, AppType::OpenCode),
1887 sync_codex_catalog: false,
1888 stale_codex_catalog_keys: Vec::new(),
1889 refresh_snapshot: false,
1890 apply_hermes_switch_defaults: matches!(app_type_clone, AppType::Hermes),
1891 common_config_snippet: config
1892 .common_config_snippets
1893 .get(&app_type_clone)
1894 .cloned(),
1895 takeover_active: false,
1896 preserve_live_preferences: true,
1897 };
1898
1899 return Ok(((), Some(action)));
1900 }
1901
1902 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1903 let provider = match app_type_clone {
1904 AppType::Codex => Self::prepare_switch_codex(
1905 config,
1906 &provider_id_owned,
1907 effective_current_provider.as_deref(),
1908 )?,
1909 AppType::Claude => Self::prepare_switch_claude(
1910 config,
1911 &provider_id_owned,
1912 effective_current_provider.as_deref(),
1913 )?,
1914 AppType::Gemini => Self::prepare_switch_gemini(
1915 config,
1916 &provider_id_owned,
1917 effective_current_provider.as_deref(),
1918 )?,
1919 AppType::OpenCode => unreachable!("additive mode handled above"),
1920 AppType::OpenClaw => unreachable!("additive mode handled above"),
1921 AppType::Hermes => unreachable!("additive mode handled above"),
1922 };
1923
1924 let action = PostCommitAction {
1925 app_type: app_type_clone.clone(),
1926 provider,
1927 backup,
1928 write_live_snapshot: true,
1929 sync_mcp: !matches!(app_type_clone, AppType::Codex),
1933 sync_codex_catalog: matches!(app_type_clone, AppType::Codex),
1934 stale_codex_catalog_keys: Vec::new(),
1935 refresh_snapshot: true,
1936 apply_hermes_switch_defaults: false,
1937 common_config_snippet: config.common_config_snippets.get(&app_type_clone).cloned(),
1938 takeover_active: false,
1939 preserve_live_preferences: true,
1940 };
1941
1942 Ok(((), Some(action)))
1943 })?;
1944
1945 if !app_type.is_additive_mode() {
1946 crate::settings::set_current_provider(&app_type, Some(provider_id))?;
1947 }
1948
1949 Ok(())
1950 }
1951
1952 fn write_live_snapshot(
1953 app_type: &AppType,
1954 provider: &Provider,
1955 common_config_snippet: Option<&str>,
1956 apply_common_config: bool,
1957 preserve_live_preferences: bool,
1958 ) -> Result<(), AppError> {
1959 let apply_common_config = Self::resolve_live_apply_common_config(
1960 app_type,
1961 provider,
1962 common_config_snippet,
1963 apply_common_config,
1964 );
1965
1966 match app_type {
1967 AppType::Codex => Self::write_codex_live(
1968 provider,
1969 common_config_snippet,
1970 apply_common_config,
1971 preserve_live_preferences,
1972 ),
1973 AppType::Claude => {
1974 Self::write_claude_live(provider, common_config_snippet, apply_common_config)
1975 }
1976 AppType::Gemini => Self::write_gemini_live(
1977 provider,
1978 if apply_common_config {
1979 common_config_snippet
1980 } else {
1981 None
1982 },
1983 ),
1984 AppType::OpenCode => {
1985 let config_to_write = if let Some(obj) = provider.settings_config.as_object() {
1986 if obj.contains_key("$schema") || obj.contains_key("provider") {
1987 obj.get("provider")
1988 .and_then(|providers| providers.get(&provider.id))
1989 .cloned()
1990 .unwrap_or_else(|| provider.settings_config.clone())
1991 } else {
1992 provider.settings_config.clone()
1993 }
1994 } else {
1995 provider.settings_config.clone()
1996 };
1997
1998 match serde_json::from_value::<crate::provider::OpenCodeProviderConfig>(
1999 config_to_write.clone(),
2000 ) {
2001 Ok(config) => crate::opencode_config::set_typed_provider(&provider.id, &config),
2002 Err(_) => crate::opencode_config::set_provider(&provider.id, config_to_write),
2003 }
2004 }
2005 AppType::OpenClaw => {
2006 let settings_config = provider.settings_config.clone();
2007 let looks_like_provider = settings_config.get("baseUrl").is_some()
2008 || settings_config.get("api").is_some()
2009 || settings_config.get("models").is_some();
2010 if !looks_like_provider {
2011 return Ok(());
2012 }
2013
2014 let config = Self::parse_openclaw_provider_settings(&settings_config)?;
2015 Self::validate_openclaw_provider_models(&provider.id, &config)?;
2016 let write_result =
2017 crate::openclaw_config::set_typed_provider(&provider.id, &config).map(|_| ());
2018
2019 write_result.map_err(Self::normalize_openclaw_live_write_error)
2020 }
2021 AppType::Hermes => {
2022 crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())
2023 .map(|_| ())
2024 }
2025 }
2026 }
2027
2028 fn parse_openclaw_provider_settings(
2029 settings_config: &Value,
2030 ) -> Result<crate::provider::OpenClawProviderConfig, AppError> {
2031 let settings_obj = settings_config.as_object().ok_or_else(|| {
2032 AppError::localized(
2033 "provider.openclaw.settings.not_object",
2034 "OpenClaw 配置必须是 JSON 对象",
2035 "OpenClaw configuration must be a JSON object",
2036 )
2037 })?;
2038
2039 let legacy_aliases = Self::collect_openclaw_legacy_aliases(settings_obj);
2040 if !legacy_aliases.is_empty() {
2041 let aliases = legacy_aliases.join(", ");
2042 return Err(AppError::localized(
2043 "provider.openclaw.settings.invalid",
2044 format!(
2045 "OpenClaw 配置使用了不支持的旧字段: {aliases}。请改用规范 OpenClaw 字段。"
2046 ),
2047 format!(
2048 "OpenClaw config uses unsupported legacy alias keys: {aliases}. Use canonical OpenClaw keys instead."
2049 ),
2050 ));
2051 }
2052
2053 serde_json::from_value(settings_config.clone()).map_err(|err| {
2054 AppError::localized(
2055 "provider.openclaw.settings.invalid",
2056 format!("OpenClaw 配置格式无效: {err}"),
2057 format!("OpenClaw provider schema is invalid: {err}"),
2058 )
2059 })
2060 }
2061
2062 fn validate_openclaw_provider_models(
2063 provider_id: &str,
2064 config: &crate::provider::OpenClawProviderConfig,
2065 ) -> Result<(), AppError> {
2066 if config.models.is_empty() {
2067 return Err(AppError::localized(
2068 "provider.openclaw.models.missing",
2069 format!("OpenClaw 供应商 {provider_id} 至少需要一个模型"),
2070 format!("OpenClaw provider {provider_id} must define at least one model"),
2071 ));
2072 }
2073
2074 Ok(())
2075 }
2076
2077 fn collect_openclaw_legacy_aliases(
2078 settings_obj: &serde_json::Map<String, Value>,
2079 ) -> Vec<String> {
2080 let mut aliases = Vec::new();
2081
2082 for alias in ["api_key", "base_url", "options", "npm"] {
2083 if settings_obj.contains_key(alias) {
2084 aliases.push(alias.to_string());
2085 }
2086 }
2087
2088 if let Some(models) = settings_obj.get("models").and_then(Value::as_array) {
2089 for (index, model) in models.iter().enumerate() {
2090 if let Some(model_obj) = model.as_object() {
2091 if model_obj.contains_key("context_window") {
2092 aliases.push(format!("models[{index}].context_window"));
2093 }
2094 }
2095 }
2096 }
2097
2098 aliases
2099 }
2100
2101 fn normalize_openclaw_live_write_error(err: AppError) -> AppError {
2102 match err {
2103 AppError::Config(message)
2104 if message.starts_with("Failed to parse OpenClaw config as JSON5:") =>
2105 {
2106 AppError::Config(message.replacen(
2107 "Failed to parse OpenClaw config as JSON5",
2108 "Failed to parse OpenClaw config as round-trip JSON5 document",
2109 1,
2110 ))
2111 }
2112 other => other,
2113 }
2114 }
2115
2116 pub(crate) fn build_effective_live_snapshot(
2117 app_type: &AppType,
2118 provider: &Provider,
2119 common_config_snippet: Option<&str>,
2120 apply_common_config: bool,
2121 ) -> Result<Value, AppError> {
2122 let apply_common_config = Self::resolve_live_apply_common_config(
2123 app_type,
2124 provider,
2125 common_config_snippet,
2126 apply_common_config,
2127 );
2128
2129 match app_type {
2130 AppType::Claude => {
2131 let mut effective = common_config::build_effective_settings_with_common_config(
2132 app_type,
2133 provider,
2134 common_config_snippet,
2135 apply_common_config,
2136 )?;
2137 let _ = Self::normalize_claude_models_in_value(&mut effective);
2138 Ok(effective)
2139 }
2140 AppType::Codex => {
2141 let effective = common_config::build_effective_settings_with_common_config(
2142 app_type,
2143 provider,
2144 common_config_snippet,
2145 apply_common_config,
2146 )?;
2147 let settings = effective
2148 .as_object()
2149 .ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
2150 let auth = settings.get("auth").cloned();
2151 let cfg_text = settings.get("config").and_then(Value::as_str).unwrap_or("");
2152
2153 if !cfg_text.trim().is_empty() {
2154 crate::codex_config::validate_config_toml(cfg_text)?;
2155 }
2156
2157 let mut backup = serde_json::Map::new();
2158 if let Some(auth) = auth {
2159 backup.insert("auth".to_string(), auth);
2160 }
2161 backup.insert("config".to_string(), Value::String(cfg_text.to_string()));
2162 Ok(Value::Object(backup))
2163 }
2164 AppType::Gemini => {
2165 let content_to_write = common_config::build_effective_settings_with_common_config(
2166 app_type,
2167 provider,
2168 common_config_snippet,
2169 apply_common_config,
2170 )?;
2171
2172 let env_obj = content_to_write
2173 .get("env")
2174 .cloned()
2175 .unwrap_or_else(|| json!({}));
2176 let settings_path = crate::gemini_config::get_gemini_settings_path();
2177 let config_value = if let Some(config_value) = content_to_write.get("config") {
2178 if config_value.is_null() {
2179 if settings_path.exists() {
2180 read_json_file(&settings_path)?
2181 } else {
2182 json!({})
2183 }
2184 } else if let Some(provider_config) = config_value.as_object() {
2185 if provider_config.is_empty() {
2186 if settings_path.exists() {
2187 read_json_file(&settings_path)?
2188 } else {
2189 json!({})
2190 }
2191 } else {
2192 let mut merged = if settings_path.exists() {
2193 read_json_file(&settings_path)?
2194 } else {
2195 json!({})
2196 };
2197
2198 if !merged.is_object() {
2199 merged = json!({});
2200 }
2201
2202 let merged_map = merged.as_object_mut().ok_or_else(|| {
2203 AppError::localized(
2204 "gemini.validation.invalid_settings",
2205 "Gemini 现有 settings.json 格式错误: 必须是对象",
2206 "Gemini existing settings.json invalid: must be a JSON object",
2207 )
2208 })?;
2209 for (key, value) in provider_config {
2210 merged_map.insert(key.clone(), value.clone());
2211 }
2212 merged
2213 }
2214 } else {
2215 return Err(AppError::localized(
2216 "gemini.validation.invalid_config",
2217 "Gemini 配置格式错误: config 必须是对象或 null",
2218 "Gemini config invalid: config must be an object or null",
2219 ));
2220 }
2221 } else if settings_path.exists() {
2222 read_json_file(&settings_path)?
2223 } else {
2224 json!({})
2225 };
2226
2227 Ok(json!({
2228 "env": env_obj,
2229 "config": config_value,
2230 }))
2231 }
2232 AppType::OpenCode => Err(AppError::Config(
2233 "OpenCode does not support proxy takeover backups".into(),
2234 )),
2235 AppType::OpenClaw => Err(AppError::Config(
2236 "OpenClaw does not support proxy takeover backups".into(),
2237 )),
2238 AppType::Hermes => Err(AppError::Config(
2239 "Hermes does not support proxy takeover backups".into(),
2240 )),
2241 }
2242 }
2243
2244 fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
2245 match app_type {
2246 AppType::Claude => {
2247 if !provider.settings_config.is_object() {
2248 return Err(AppError::localized(
2249 "provider.claude.settings.not_object",
2250 "Claude 配置必须是 JSON 对象",
2251 "Claude configuration must be a JSON object",
2252 ));
2253 }
2254 }
2255 AppType::Codex => {
2256 let settings = provider.settings_config.as_object().ok_or_else(|| {
2257 AppError::localized(
2258 "provider.codex.settings.not_object",
2259 "Codex 配置必须是 JSON 对象",
2260 "Codex configuration must be a JSON object",
2261 )
2262 })?;
2263
2264 let auth = settings.get("auth").ok_or_else(|| {
2265 AppError::localized(
2266 "provider.codex.auth.missing",
2267 format!("供应商 {} 缺少 auth 配置", provider.id),
2268 format!("Provider {} is missing auth configuration", provider.id),
2269 )
2270 })?;
2271 if !auth.is_object() {
2272 return Err(AppError::localized(
2273 "provider.codex.auth.not_object",
2274 format!("供应商 {} 的 auth 配置必须是 JSON 对象", provider.id),
2275 format!(
2276 "Provider {} auth configuration must be a JSON object",
2277 provider.id
2278 ),
2279 ));
2280 }
2281
2282 if let Some(config_value) = settings.get("config") {
2283 if !(config_value.is_string() || config_value.is_null()) {
2284 return Err(AppError::localized(
2285 "provider.codex.config.invalid_type",
2286 "Codex config 字段必须是字符串",
2287 "Codex config field must be a string",
2288 ));
2289 }
2290 if let Some(cfg_text) = config_value.as_str() {
2291 crate::codex_config::validate_config_toml(cfg_text)?;
2292 }
2293 }
2294
2295 if !Self::is_codex_official_provider(provider) {
2296 let config_text = settings
2297 .get("config")
2298 .and_then(Value::as_str)
2299 .unwrap_or_default();
2300 if !Self::codex_config_has_base_url(config_text) {
2301 return Err(AppError::localized(
2302 "provider.codex.base_url.missing",
2303 format!("供应商 {} 缺少有效的 Codex Base URL", provider.id),
2304 format!("Provider {} is missing a valid Codex base_url", provider.id),
2305 ));
2306 }
2307 }
2308 }
2309 AppType::Gemini => {
2310 use crate::gemini_config::validate_gemini_settings;
2311 validate_gemini_settings(&provider.settings_config)?
2312 }
2313 AppType::OpenCode => {
2314 if !provider.settings_config.is_object() {
2315 return Err(AppError::localized(
2316 "provider.opencode.settings.not_object",
2317 "OpenCode 配置必须是 JSON 对象",
2318 "OpenCode configuration must be a JSON object",
2319 ));
2320 }
2321 }
2322 AppType::OpenClaw => {
2323 let config = Self::parse_openclaw_provider_settings(&provider.settings_config)?;
2324 Self::validate_openclaw_provider_models(&provider.id, &config)?;
2325 }
2326 AppType::Hermes => {
2327 if !provider.settings_config.is_object() {
2329 return Err(AppError::localized(
2330 "provider.hermes.settings.not_object",
2331 "Hermes 供应商配置必须是 JSON 对象",
2332 "Hermes provider configuration must be a JSON object",
2333 ));
2334 }
2335 }
2336 }
2337
2338 if let Some(meta) = &provider.meta {
2340 if let Some(usage_script) = &meta.usage_script {
2341 Self::validate_usage_script(usage_script)?;
2342 }
2343 }
2344
2345 Ok(())
2346 }
2347
2348 pub(crate) fn build_live_backup_snapshot(
2349 app_type: &AppType,
2350 provider: &Provider,
2351 common_config_snippet: Option<&str>,
2352 apply_common_config: bool,
2353 ) -> Result<Value, AppError> {
2354 Self::build_effective_live_snapshot(
2355 app_type,
2356 provider,
2357 common_config_snippet,
2358 apply_common_config,
2359 )
2360 }
2361
2362 pub(crate) fn build_effective_live_snapshot_from_state(
2363 state: &AppState,
2364 app_type: AppType,
2365 provider: &Provider,
2366 ) -> Result<Value, AppError> {
2367 let common_config_snippet = {
2368 let config = state.config.read().map_err(AppError::from)?;
2369 config.common_config_snippets.get(&app_type).cloned()
2370 };
2371
2372 Self::build_effective_live_snapshot(
2373 &app_type,
2374 provider,
2375 common_config_snippet.as_deref(),
2376 true,
2377 )
2378 }
2379
2380 pub(crate) fn get_provider(
2381 state: &AppState,
2382 app_type: AppType,
2383 provider_id: &str,
2384 ) -> Result<Provider, AppError> {
2385 let config = state.config.read().map_err(AppError::from)?;
2386 let manager = config
2387 .get_manager(&app_type)
2388 .ok_or_else(|| Self::app_not_found(&app_type))?;
2389
2390 manager.providers.get(provider_id).cloned().ok_or_else(|| {
2391 AppError::localized(
2392 "provider.not_found",
2393 format!("供应商不存在: {provider_id}"),
2394 format!("Provider not found: {provider_id}"),
2395 )
2396 })
2397 }
2398
2399 fn app_not_found(app_type: &AppType) -> AppError {
2400 AppError::localized(
2401 "provider.app_not_found",
2402 format!("应用类型不存在: {app_type:?}"),
2403 format!("App type not found: {app_type:?}"),
2404 )
2405 }
2406
2407 pub fn delete(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
2408 let (local_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
2409 (None, None)
2410 } else {
2411 (
2412 crate::settings::get_current_provider(&app_type),
2413 state.db.get_current_provider(app_type.as_str())?,
2414 )
2415 };
2416 let provider_snapshot = {
2417 let config = state.config.read().map_err(AppError::from)?;
2418 let manager = config
2419 .get_manager(&app_type)
2420 .ok_or_else(|| Self::app_not_found(&app_type))?;
2421
2422 if !app_type.is_additive_mode()
2423 && (local_current_provider.as_deref() == Some(provider_id)
2424 || stored_current_provider.as_deref() == Some(provider_id))
2425 {
2426 return Err(AppError::localized(
2427 "provider.delete.current",
2428 "不能删除当前正在使用的供应商",
2429 "Cannot delete the provider currently in use",
2430 ));
2431 }
2432
2433 manager.providers.get(provider_id).cloned().ok_or_else(|| {
2434 AppError::localized(
2435 "provider.not_found",
2436 format!("供应商不存在: {provider_id}"),
2437 format!("Provider not found: {provider_id}"),
2438 )
2439 })?
2440 };
2441 let stale_codex_catalog_keys = if matches!(app_type, AppType::Codex) {
2442 Self::provider_codex_model_provider_key(&provider_snapshot)
2443 .into_iter()
2444 .collect::<Vec<_>>()
2445 } else {
2446 Vec::new()
2447 };
2448
2449 if app_type.is_additive_mode() {
2450 match app_type {
2451 AppType::OpenCode => {
2452 if crate::opencode_config::get_opencode_dir().exists() {
2453 crate::opencode_config::remove_provider(provider_id)?;
2454 }
2455 }
2456 AppType::OpenClaw => {
2457 if crate::openclaw_config::get_openclaw_dir().exists() {
2458 crate::openclaw_config::remove_provider(provider_id)?;
2459 }
2460 }
2461 AppType::Hermes => {
2462 if crate::hermes_config::get_hermes_dir().exists() {
2463 crate::hermes_config::remove_provider(provider_id)?;
2464 }
2465 }
2466 _ => unreachable!("non-additive apps should not enter additive delete branch"),
2467 }
2468
2469 {
2470 let mut config = state.config.write().map_err(AppError::from)?;
2471 let manager = config
2472 .get_manager_mut(&app_type)
2473 .ok_or_else(|| Self::app_not_found(&app_type))?;
2474 manager.providers.shift_remove(provider_id);
2475 }
2476
2477 return state.save();
2478 }
2479
2480 match app_type {
2481 AppType::Codex => {
2482 crate::codex_config::delete_codex_provider_config(
2483 provider_id,
2484 &provider_snapshot.name,
2485 )?;
2486 }
2487 AppType::Claude => {
2488 let by_name = get_provider_config_path(provider_id, Some(&provider_snapshot.name));
2491 let by_id = get_provider_config_path(provider_id, None);
2492 delete_file(&by_name)?;
2493 delete_file(&by_id)?;
2494 }
2495 AppType::Gemini => {
2496 }
2498 AppType::OpenCode => {
2499 let _ = provider_snapshot;
2500 }
2501 AppType::OpenClaw => {
2502 let _ = provider_snapshot;
2503 }
2504 AppType::Hermes => {
2505 let _ = provider_snapshot;
2506 }
2507 }
2508
2509 {
2510 let mut config = state.config.write().map_err(AppError::from)?;
2511 let manager = config
2512 .get_manager_mut(&app_type)
2513 .ok_or_else(|| Self::app_not_found(&app_type))?;
2514
2515 if !app_type.is_additive_mode()
2516 && (local_current_provider.as_deref() == Some(provider_id)
2517 || stored_current_provider.as_deref() == Some(provider_id))
2518 {
2519 return Err(AppError::localized(
2520 "provider.delete.current",
2521 "不能删除当前正在使用的供应商",
2522 "Cannot delete the provider currently in use",
2523 ));
2524 }
2525
2526 if !app_type.is_additive_mode() && manager.current == provider_id {
2527 manager.current = stored_current_provider.clone().unwrap_or_default();
2528 }
2529
2530 manager.providers.shift_remove(provider_id);
2531 }
2532
2533 state.save()?;
2534 if matches!(app_type, AppType::Codex) {
2535 Self::sync_codex_provider_catalog_to_live(state, &stale_codex_catalog_keys)?;
2536 }
2537 Ok(())
2538 }
2539
2540 pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2541 live::import_openclaw_providers_from_live(state)
2542 }
2543
2544 pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2545 live::import_opencode_providers_from_live(state)
2546 }
2547
2548 pub fn import_hermes_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2549 live::import_hermes_providers_from_live(state)
2550 }
2551}
2552
2553#[derive(Debug, Clone, Deserialize)]
2554pub struct ProviderSortUpdate {
2555 pub id: String,
2556 #[serde(rename = "sortIndex")]
2557 pub sort_index: usize,
2558}