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