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