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