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 == AppType::Hermes {
1098 return Ok(crate::hermes_config::get_model_config()?
1099 .and_then(|model| model.provider)
1100 .map(|provider| provider.trim().to_string())
1101 .filter(|provider| !provider.is_empty())
1102 .unwrap_or_default());
1103 }
1104 if app_type.is_additive_mode() {
1105 return Ok(String::new());
1106 }
1107 crate::settings::get_effective_current_provider(&state.db, &app_type)
1108 .map(|opt| opt.unwrap_or_default())
1109 }
1110
1111 pub fn add(state: &AppState, app_type: AppType, provider: Provider) -> Result<bool, AppError> {
1113 let mut provider = provider;
1114 Self::normalize_provider_if_claude(&app_type, &mut provider);
1116 Self::validate_provider_settings(&app_type, &provider)?;
1117
1118 let app_type_clone = app_type.clone();
1119 let provider_clone = provider.clone();
1120 let stored_current_provider = if app_type.is_additive_mode() {
1121 None
1122 } else {
1123 state.db.get_current_provider(app_type.as_str())?
1124 };
1125
1126 Self::run_transaction(state, move |config| {
1127 let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1128 let mut provider_to_store = provider_clone.clone();
1129 Self::normalize_provider_for_storage(
1130 &app_type_clone,
1131 &mut provider_to_store,
1132 common_config_snippet.as_deref(),
1133 )?;
1134
1135 if matches!(app_type_clone, AppType::OpenClaw)
1136 && provider_to_store.created_at.is_none()
1137 && live::is_auto_mirrored_openclaw_snapshot(&provider_to_store)
1138 {
1139 provider_to_store.created_at = Some(current_timestamp());
1140 }
1141 if app_type_clone.is_additive_mode() {
1142 Self::set_provider_live_config_managed(&mut provider_to_store, true);
1143 }
1144
1145 config.ensure_app(&app_type_clone);
1146 let manager = config
1147 .get_manager_mut(&app_type_clone)
1148 .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1149
1150 if !app_type_clone.is_additive_mode() {
1151 manager.current = stored_current_provider.clone().unwrap_or_default();
1152 }
1153
1154 let was_empty = manager.providers.is_empty();
1155 manager
1156 .providers
1157 .insert(provider_to_store.id.clone(), provider_to_store.clone());
1158
1159 if !app_type_clone.is_additive_mode()
1160 && stored_current_provider.is_none()
1161 && (was_empty || manager.current.is_empty())
1162 {
1163 manager.current = provider_to_store.id.clone();
1164 }
1165
1166 let is_current =
1167 app_type_clone.is_additive_mode() || manager.current == provider_to_store.id;
1168 let action = if is_current {
1169 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1170 Some(PostCommitAction {
1171 app_type: app_type_clone.clone(),
1172 provider: provider_to_store.clone(),
1173 backup,
1174 sync_mcp: matches!(&app_type_clone, AppType::Codex),
1177 refresh_snapshot: false,
1178 common_config_snippet,
1179 takeover_active: false,
1180 })
1181 } else {
1182 None
1183 };
1184
1185 Ok((true, action))
1186 })
1187 }
1188
1189 pub fn update(
1191 state: &AppState,
1192 app_type: AppType,
1193 provider: Provider,
1194 ) -> Result<bool, AppError> {
1195 let mut provider = provider;
1196 Self::normalize_provider_if_claude(&app_type, &mut provider);
1198 Self::validate_provider_settings(&app_type, &provider)?;
1199 let provider_id = provider.id.clone();
1200 let app_type_clone = app_type.clone();
1201 let provider_clone = provider.clone();
1202 let (effective_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
1203 (None, None)
1204 } else {
1205 (
1206 crate::settings::get_effective_current_provider(&state.db, &app_type)?,
1207 state.db.get_current_provider(app_type.as_str())?,
1208 )
1209 };
1210
1211 Self::run_transaction(state, move |config| {
1212 let common_config_snippet = config.common_config_snippets.get(&app_type_clone).cloned();
1213 let manager = config
1214 .get_manager_mut(&app_type_clone)
1215 .ok_or_else(|| Self::app_not_found(&app_type_clone))?;
1216
1217 if !manager.providers.contains_key(&provider_id) {
1218 return Err(AppError::localized(
1219 "provider.not_found",
1220 format!("供应商不存在: {provider_id}"),
1221 format!("Provider not found: {provider_id}"),
1222 ));
1223 }
1224
1225 if !app_type_clone.is_additive_mode() {
1226 manager.current = stored_current_provider.clone().unwrap_or_default();
1227 }
1228
1229 let existing_live_config_managed = manager
1230 .providers
1231 .get(&provider_id)
1232 .and_then(Self::provider_live_config_managed);
1233 let mut merged = if let Some(existing) = manager.providers.get(&provider_id) {
1234 let mut updated = provider_clone.clone();
1235 match (existing.meta.as_ref(), updated.meta.take()) {
1236 (Some(old_meta), None) => {
1238 updated.meta = Some(old_meta.clone());
1239 }
1240 (None, None) => {
1241 updated.meta = None;
1242 }
1243 (_old, Some(new_meta)) => {
1245 updated.meta = Some(new_meta);
1246 }
1247 }
1248 if matches!(app_type_clone, AppType::OpenClaw)
1249 && updated.created_at.is_none()
1250 && live::is_auto_mirrored_openclaw_snapshot(&updated)
1251 {
1252 updated.created_at = Some(current_timestamp());
1253 }
1254 updated
1255 } else {
1256 provider_clone.clone()
1257 };
1258
1259 Self::normalize_provider_for_storage(
1260 &app_type_clone,
1261 &mut merged,
1262 common_config_snippet.as_deref(),
1263 )?;
1264
1265 let should_write_live = if app_type_clone.is_additive_mode() {
1266 let live_config_managed = Self::additive_provider_exists_in_live_config(
1267 &app_type_clone,
1268 &provider_id,
1269 Self::provider_live_config_managed(&merged).or(existing_live_config_managed),
1270 )?;
1271 Self::set_provider_live_config_managed(&mut merged, live_config_managed);
1272 live_config_managed
1273 } else {
1274 effective_current_provider.as_deref() == Some(provider_id.as_str())
1275 };
1276
1277 manager
1278 .providers
1279 .insert(provider_id.clone(), merged.clone());
1280
1281 let action = if should_write_live {
1282 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1283 Some(PostCommitAction {
1284 app_type: app_type_clone.clone(),
1285 provider: merged,
1286 backup,
1287 sync_mcp: matches!(&app_type_clone, AppType::Codex),
1290 refresh_snapshot: false,
1291 common_config_snippet,
1292 takeover_active: false,
1293 })
1294 } else {
1295 None
1296 };
1297
1298 Ok((true, action))
1299 })
1300 }
1301
1302 pub fn import_default_config(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
1306 if app_type.is_additive_mode() {
1307 return Ok(false);
1308 }
1309
1310 if state.db.has_non_official_seed_provider(app_type.as_str())? {
1311 return Ok(false);
1312 }
1313
1314 let settings_config = match app_type {
1315 AppType::Codex => {
1316 let auth_path = get_codex_auth_path();
1317 if !auth_path.exists() {
1318 return Err(AppError::localized(
1319 "codex.live.missing",
1320 "Codex 配置文件不存在",
1321 "Codex configuration file is missing",
1322 ));
1323 }
1324 let auth: Value = read_json_file(&auth_path)?;
1325 let config_str = crate::codex_config::read_and_validate_codex_config_text()?;
1326 json!({ "auth": auth, "config": config_str })
1327 }
1328 AppType::Claude => {
1329 let settings_path = get_claude_settings_path();
1330 if !settings_path.exists() {
1331 return Err(AppError::localized(
1332 "claude.live.missing",
1333 "Claude Code 配置文件不存在",
1334 "Claude settings file is missing",
1335 ));
1336 }
1337 let mut v = read_json_file::<Value>(&settings_path)?;
1338 let _ = Self::normalize_claude_models_in_value(&mut v);
1339 v
1340 }
1341 AppType::Gemini => {
1342 use crate::gemini_config::{
1343 env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1344 };
1345
1346 let env_path = get_gemini_env_path();
1348 if !env_path.exists() {
1349 return Err(AppError::localized(
1350 "gemini.live.missing",
1351 "Gemini 配置文件不存在",
1352 "Gemini configuration file is missing",
1353 ));
1354 }
1355
1356 let env_map = read_gemini_env()?;
1357 let env_json = env_to_json(&env_map);
1358 let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1359
1360 let settings_path = get_gemini_settings_path();
1362 let config_obj = if settings_path.exists() {
1363 read_json_file(&settings_path)?
1364 } else {
1365 json!({})
1366 };
1367
1368 json!({
1370 "env": env_obj,
1371 "config": config_obj
1372 })
1373 }
1374 AppType::OpenCode => unreachable!("additive mode apps are handled earlier"),
1375 AppType::OpenClaw => unreachable!("additive mode apps are handled earlier"),
1376 AppType::Hermes => unreachable!("additive mode apps are handled earlier"),
1377 };
1378
1379 let mut provider = Provider::with_id(
1380 "default".to_string(),
1381 "default".to_string(),
1382 settings_config,
1383 None,
1384 );
1385 provider.category = Some("custom".to_string());
1386
1387 state.db.save_provider(app_type.as_str(), &provider)?;
1388 state
1389 .db
1390 .set_current_provider(app_type.as_str(), &provider.id)?;
1391 Ok(true)
1392 }
1393
1394 pub fn read_live_settings(app_type: AppType) -> Result<Value, AppError> {
1396 match app_type {
1397 AppType::Codex => {
1398 let auth_path = get_codex_auth_path();
1399 let config_path = get_codex_config_path();
1400 if !config_path.exists() {
1401 return Err(AppError::localized(
1402 "codex.live.missing",
1403 "Codex 配置文件不存在",
1404 "Codex configuration is missing",
1405 ));
1406 }
1407
1408 let mut live_settings = serde_json::Map::new();
1409 if auth_path.exists() {
1410 live_settings.insert("auth".to_string(), read_json_file(&auth_path)?);
1411 }
1412 if config_path.exists() {
1413 let cfg_text = crate::codex_config::read_and_validate_codex_config_text()?;
1414 live_settings.insert("config".to_string(), Value::String(cfg_text));
1415 }
1416
1417 Ok(Value::Object(live_settings))
1418 }
1419 AppType::Claude => {
1420 let path = get_claude_settings_path();
1421 if !path.exists() {
1422 return Err(AppError::localized(
1423 "claude.live.missing",
1424 "Claude Code 配置文件不存在",
1425 "Claude settings file is missing",
1426 ));
1427 }
1428 read_json_file(&path)
1429 }
1430 AppType::Gemini => {
1431 use crate::gemini_config::{
1432 env_to_json, get_gemini_env_path, get_gemini_settings_path, read_gemini_env,
1433 };
1434
1435 let env_path = get_gemini_env_path();
1437 if !env_path.exists() {
1438 return Err(AppError::localized(
1439 "gemini.env.missing",
1440 "Gemini .env 文件不存在",
1441 "Gemini .env file not found",
1442 ));
1443 }
1444
1445 let env_map = read_gemini_env()?;
1446 let env_json = env_to_json(&env_map);
1447 let env_obj = env_json.get("env").cloned().unwrap_or_else(|| json!({}));
1448
1449 let settings_path = get_gemini_settings_path();
1451 let config_obj = if settings_path.exists() {
1452 read_json_file(&settings_path)?
1453 } else {
1454 json!({})
1455 };
1456
1457 Ok(json!({
1459 "env": env_obj,
1460 "config": config_obj
1461 }))
1462 }
1463 AppType::OpenCode => {
1464 let config_path = crate::opencode_config::get_opencode_config_path();
1465 if !config_path.exists() {
1466 return Err(AppError::localized(
1467 "opencode.config.missing",
1468 "OpenCode 配置文件不存在",
1469 "OpenCode configuration file not found",
1470 ));
1471 }
1472 crate::opencode_config::read_opencode_config()
1473 }
1474 AppType::OpenClaw => {
1475 let config_path = crate::openclaw_config::get_openclaw_config_path();
1476 if !config_path.exists() {
1477 return Err(AppError::localized(
1478 "openclaw.config.missing",
1479 "OpenClaw 配置文件不存在",
1480 "OpenClaw configuration file not found",
1481 ));
1482 }
1483 crate::openclaw_config::read_openclaw_config()
1484 }
1485 AppType::Hermes => {
1486 let yaml = crate::hermes_config::read_hermes_config()?;
1487 crate::hermes_config::yaml_to_json(&yaml)
1488 }
1489 }
1490 }
1491
1492 pub fn update_sort_order(
1494 state: &AppState,
1495 app_type: AppType,
1496 updates: Vec<ProviderSortUpdate>,
1497 ) -> Result<bool, AppError> {
1498 {
1499 let mut cfg = state.config.write().map_err(AppError::from)?;
1500 let manager = cfg
1501 .get_manager_mut(&app_type)
1502 .ok_or_else(|| Self::app_not_found(&app_type))?;
1503
1504 for update in updates {
1505 if let Some(provider) = manager.providers.get_mut(&update.id) {
1506 provider.sort_index = Some(update.sort_index);
1507 }
1508 }
1509 }
1510
1511 state.save()?;
1512 Ok(true)
1513 }
1514
1515 pub fn remove_from_live_config(
1516 state: &AppState,
1517 app_type: AppType,
1518 provider_id: &str,
1519 ) -> Result<(), AppError> {
1520 if !app_type.is_additive_mode() {
1521 return Err(AppError::localized(
1522 "provider.remove_from_live_config.unsupported",
1523 "只有累加模式应用支持从 live 配置中移除供应商",
1524 "Only additive-mode apps support removing a provider from live config",
1525 ));
1526 }
1527
1528 let original = {
1529 let config = state.config.read().map_err(AppError::from)?;
1530 let manager = config
1531 .get_manager(&app_type)
1532 .ok_or_else(|| Self::app_not_found(&app_type))?;
1533 if !manager.providers.contains_key(provider_id) {
1534 return Err(AppError::localized(
1535 "provider.not_found",
1536 format!("供应商不存在: {provider_id}"),
1537 format!("Provider not found: {provider_id}"),
1538 ));
1539 }
1540 config.clone()
1541 };
1542
1543 let backup = Self::capture_live_snapshot(&app_type)?;
1544 match &app_type {
1545 AppType::OpenCode => {
1546 if crate::opencode_config::get_opencode_dir().exists() {
1547 crate::opencode_config::remove_provider(provider_id)?;
1548 }
1549 }
1550 AppType::OpenClaw => {
1551 if crate::openclaw_config::get_openclaw_dir().exists() {
1552 crate::openclaw_config::remove_provider(provider_id)?;
1553 }
1554 }
1555 _ => unreachable!("non-additive apps should not enter remove-from-live branch"),
1556 }
1557
1558 {
1559 let mut config = state.config.write().map_err(AppError::from)?;
1560 let manager = config
1561 .get_manager_mut(&app_type)
1562 .ok_or_else(|| Self::app_not_found(&app_type))?;
1563 let provider = manager.providers.get_mut(provider_id).ok_or_else(|| {
1564 AppError::localized(
1565 "provider.not_found",
1566 format!("供应商不存在: {provider_id}"),
1567 format!("Provider not found: {provider_id}"),
1568 )
1569 })?;
1570 Self::set_provider_live_config_managed(provider, false);
1571 }
1572
1573 if let Err(save_err) = state.save() {
1574 let config_restore = Self::restore_config_only(state, original);
1575 let live_restore = backup.restore();
1576 if let Err(rollback_err) = config_restore {
1577 return Err(AppError::localized(
1578 "config.save.rollback_failed",
1579 format!("保存配置失败: {save_err};回滚失败: {rollback_err}"),
1580 format!("Failed to save config: {save_err}; rollback failed: {rollback_err}"),
1581 ));
1582 }
1583 if let Err(rollback_err) = live_restore {
1584 return Err(AppError::localized(
1585 "post_commit.rollback_failed",
1586 format!("保存配置失败: {save_err};live 回滚失败: {rollback_err}"),
1587 format!(
1588 "Failed to save config: {save_err}; live rollback failed: {rollback_err}"
1589 ),
1590 ));
1591 }
1592 return Err(save_err);
1593 }
1594
1595 Ok(())
1596 }
1597
1598 pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
1604 use crate::services::mcp::McpService;
1605
1606 let snapshots: Vec<(AppType, Provider, Option<String>)> = {
1608 let guard = state.config.read().map_err(AppError::from)?;
1609 let mut result = Vec::new();
1610 for app_type in AppType::all() {
1611 if app_type.is_additive_mode() {
1612 if let Some(manager) = guard.get_manager(&app_type) {
1613 let snippet = guard.common_config_snippets.get(&app_type).cloned();
1614 for provider in manager.providers.values() {
1615 if Self::provider_live_config_managed(provider) == Some(false) {
1616 continue;
1617 }
1618 result.push((app_type.clone(), provider.clone(), snippet.clone()));
1619 }
1620 }
1621 continue;
1622 }
1623
1624 let current_id =
1625 match crate::settings::get_effective_current_provider(&state.db, &app_type)? {
1626 Some(id) => id,
1627 None => continue,
1628 };
1629 let providers = state.db.get_all_providers(app_type.as_str())?;
1630 match providers.get(¤t_id) {
1631 Some(provider) => {
1632 let snippet = state.db.get_config_snippet(app_type.as_str())?;
1633 result.push((app_type.clone(), provider.clone(), snippet));
1634 }
1635 None => {
1636 log::warn!(
1637 "sync_current_to_live: {app_type} 当前供应商 {} 不存在于数据库,跳过",
1638 current_id
1639 );
1640 }
1641 }
1642 }
1643 result
1644 };
1645
1646 let openclaw_live_provider_ids = match Self::valid_openclaw_live_provider_ids() {
1647 Ok(provider_ids) => provider_ids,
1648 Err(err) => {
1649 log::warn!(
1650 "sync_current_to_live: 读取 OpenClaw live providers 失败,跳过 OpenClaw 同步: {err}"
1651 );
1652 None
1653 }
1654 };
1655
1656 for (app_type, provider, snippet) in &snapshots {
1657 if matches!(app_type, AppType::OpenClaw)
1658 && !openclaw_live_provider_ids
1659 .as_ref()
1660 .is_some_and(|provider_ids| provider_ids.contains(&provider.id))
1661 {
1662 continue;
1663 }
1664
1665 if let Err(e) = Self::write_live_snapshot(app_type, provider, snippet.as_deref(), true)
1666 {
1667 log::warn!("sync_current_to_live: 写入 {app_type} live 配置失败: {e}");
1668 }
1669 }
1670
1671 if let Err(e) =
1672 crate::services::prompt::PromptService::sync_all_active_to_live_best_effort(state)
1673 {
1674 log::warn!("sync_current_to_live: Prompt 同步失败: {e}");
1675 }
1676
1677 if let Err(e) = McpService::sync_all_enabled(state) {
1678 log::warn!("sync_current_to_live: MCP 同步失败: {e}");
1679 }
1680
1681 if let Err(e) = crate::services::skill::SkillService::sync_all_enabled_best_effort() {
1682 log::warn!("sync_current_to_live: Skills 同步失败: {e}");
1683 }
1684
1685 Ok(())
1686 }
1687
1688 pub fn switch(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
1690 if !app_type.is_additive_mode() {
1691 let providers = state.db.get_all_providers(app_type.as_str())?;
1692 providers.get(provider_id).ok_or_else(|| {
1693 AppError::localized(
1694 "provider.not_found",
1695 format!("供应商不存在: {provider_id}"),
1696 format!("Provider not found: {provider_id}"),
1697 )
1698 })?;
1699
1700 let is_app_taken_over =
1701 futures::executor::block_on(state.db.get_live_backup(app_type.as_str()))
1702 .ok()
1703 .flatten()
1704 .is_some();
1705 let is_proxy_running = state
1706 .proxy_service
1707 .is_running_blocking()
1708 .map_err(AppError::Message)?;
1709 let live_taken_over = state
1710 .proxy_service
1711 .detect_takeover_in_live_config_for_app(&app_type);
1712 let should_hot_switch = (is_app_taken_over || live_taken_over) && is_proxy_running;
1713
1714 if should_hot_switch {
1715 futures::executor::block_on(
1716 state
1717 .proxy_service
1718 .hot_switch_provider(app_type.as_str(), provider_id),
1719 )
1720 .map_err(|e| AppError::Message(format!("热切换失败: {e}")))?;
1721
1722 let mut guard = state.config.write().map_err(AppError::from)?;
1723 if let Some(manager) = guard.get_manager_mut(&app_type) {
1724 manager.current = provider_id.to_string();
1725 }
1726 return Ok(());
1727 }
1728 }
1729
1730 let app_type_clone = app_type.clone();
1731 let provider_id_owned = provider_id.to_string();
1732 let effective_current_provider = if app_type.is_additive_mode() {
1733 None
1734 } else {
1735 crate::settings::get_effective_current_provider(&state.db, &app_type)?
1736 };
1737
1738 Self::run_transaction(state, move |config| {
1739 if app_type_clone.is_additive_mode() {
1740 let provider = {
1741 let provider = config
1742 .get_manager_mut(&app_type_clone)
1743 .ok_or_else(|| Self::app_not_found(&app_type_clone))?
1744 .providers
1745 .get_mut(&provider_id_owned)
1746 .ok_or_else(|| {
1747 AppError::localized(
1748 "provider.not_found",
1749 format!("供应商不存在: {provider_id_owned}"),
1750 format!("Provider not found: {provider_id_owned}"),
1751 )
1752 })?;
1753 Self::set_provider_live_config_managed(provider, true);
1754 provider.clone()
1755 };
1756
1757 let action = PostCommitAction {
1758 app_type: app_type_clone.clone(),
1759 provider,
1760 backup: Self::capture_live_snapshot(&app_type_clone)?,
1761 sync_mcp: matches!(app_type_clone, AppType::OpenCode),
1762 refresh_snapshot: false,
1763 common_config_snippet: config
1764 .common_config_snippets
1765 .get(&app_type_clone)
1766 .cloned(),
1767 takeover_active: false,
1768 };
1769
1770 return Ok(((), Some(action)));
1771 }
1772
1773 let backup = Self::capture_live_snapshot(&app_type_clone)?;
1774 let provider = match app_type_clone {
1775 AppType::Codex => Self::prepare_switch_codex(
1776 config,
1777 &provider_id_owned,
1778 effective_current_provider.as_deref(),
1779 )?,
1780 AppType::Claude => Self::prepare_switch_claude(
1781 config,
1782 &provider_id_owned,
1783 effective_current_provider.as_deref(),
1784 )?,
1785 AppType::Gemini => Self::prepare_switch_gemini(
1786 config,
1787 &provider_id_owned,
1788 effective_current_provider.as_deref(),
1789 )?,
1790 AppType::OpenCode => unreachable!("additive mode handled above"),
1791 AppType::OpenClaw => unreachable!("additive mode handled above"),
1792 AppType::Hermes => unreachable!("additive mode handled above"),
1793 };
1794
1795 let action = PostCommitAction {
1796 app_type: app_type_clone.clone(),
1797 provider,
1798 backup,
1799 sync_mcp: true, refresh_snapshot: true,
1801 common_config_snippet: config.common_config_snippets.get(&app_type_clone).cloned(),
1802 takeover_active: false,
1803 };
1804
1805 Ok(((), Some(action)))
1806 })?;
1807
1808 if !app_type.is_additive_mode() {
1809 crate::settings::set_current_provider(&app_type, Some(provider_id))?;
1810 }
1811
1812 Ok(())
1813 }
1814
1815 fn write_live_snapshot(
1816 app_type: &AppType,
1817 provider: &Provider,
1818 common_config_snippet: Option<&str>,
1819 apply_common_config: bool,
1820 ) -> Result<(), AppError> {
1821 let apply_common_config = Self::resolve_live_apply_common_config(
1822 app_type,
1823 provider,
1824 common_config_snippet,
1825 apply_common_config,
1826 );
1827
1828 match app_type {
1829 AppType::Codex => {
1830 Self::write_codex_live(provider, common_config_snippet, apply_common_config)
1831 }
1832 AppType::Claude => {
1833 Self::write_claude_live(provider, common_config_snippet, apply_common_config)
1834 }
1835 AppType::Gemini => Self::write_gemini_live(
1836 provider,
1837 if apply_common_config {
1838 common_config_snippet
1839 } else {
1840 None
1841 },
1842 ),
1843 AppType::OpenCode => {
1844 let config_to_write = if let Some(obj) = provider.settings_config.as_object() {
1845 if obj.contains_key("$schema") || obj.contains_key("provider") {
1846 obj.get("provider")
1847 .and_then(|providers| providers.get(&provider.id))
1848 .cloned()
1849 .unwrap_or_else(|| provider.settings_config.clone())
1850 } else {
1851 provider.settings_config.clone()
1852 }
1853 } else {
1854 provider.settings_config.clone()
1855 };
1856
1857 match serde_json::from_value::<crate::provider::OpenCodeProviderConfig>(
1858 config_to_write.clone(),
1859 ) {
1860 Ok(config) => crate::opencode_config::set_typed_provider(&provider.id, &config),
1861 Err(_) => crate::opencode_config::set_provider(&provider.id, config_to_write),
1862 }
1863 }
1864 AppType::OpenClaw => {
1865 let settings_config = provider.settings_config.clone();
1866 let looks_like_provider = settings_config.get("baseUrl").is_some()
1867 || settings_config.get("api").is_some()
1868 || settings_config.get("models").is_some();
1869 if !looks_like_provider {
1870 return Ok(());
1871 }
1872
1873 let config = Self::parse_openclaw_provider_settings(&settings_config)?;
1874 Self::validate_openclaw_provider_models(&provider.id, &config)?;
1875 let write_result =
1876 crate::openclaw_config::set_typed_provider(&provider.id, &config).map(|_| ());
1877
1878 write_result.map_err(Self::normalize_openclaw_live_write_error)
1879 }
1880 AppType::Hermes => {
1881 crate::hermes_config::set_provider(&provider.id, provider.settings_config.clone())
1882 .map(|_| ())
1883 }
1884 }
1885 }
1886
1887 fn parse_openclaw_provider_settings(
1888 settings_config: &Value,
1889 ) -> Result<crate::provider::OpenClawProviderConfig, AppError> {
1890 let settings_obj = settings_config.as_object().ok_or_else(|| {
1891 AppError::localized(
1892 "provider.openclaw.settings.not_object",
1893 "OpenClaw 配置必须是 JSON 对象",
1894 "OpenClaw configuration must be a JSON object",
1895 )
1896 })?;
1897
1898 let legacy_aliases = Self::collect_openclaw_legacy_aliases(settings_obj);
1899 if !legacy_aliases.is_empty() {
1900 let aliases = legacy_aliases.join(", ");
1901 return Err(AppError::localized(
1902 "provider.openclaw.settings.invalid",
1903 format!(
1904 "OpenClaw 配置使用了不支持的旧字段: {aliases}。请改用规范 OpenClaw 字段。"
1905 ),
1906 format!(
1907 "OpenClaw config uses unsupported legacy alias keys: {aliases}. Use canonical OpenClaw keys instead."
1908 ),
1909 ));
1910 }
1911
1912 serde_json::from_value(settings_config.clone()).map_err(|err| {
1913 AppError::localized(
1914 "provider.openclaw.settings.invalid",
1915 format!("OpenClaw 配置格式无效: {err}"),
1916 format!("OpenClaw provider schema is invalid: {err}"),
1917 )
1918 })
1919 }
1920
1921 fn validate_openclaw_provider_models(
1922 provider_id: &str,
1923 config: &crate::provider::OpenClawProviderConfig,
1924 ) -> Result<(), AppError> {
1925 if config.models.is_empty() {
1926 return Err(AppError::localized(
1927 "provider.openclaw.models.missing",
1928 format!("OpenClaw 供应商 {provider_id} 至少需要一个模型"),
1929 format!("OpenClaw provider {provider_id} must define at least one model"),
1930 ));
1931 }
1932
1933 Ok(())
1934 }
1935
1936 fn collect_openclaw_legacy_aliases(
1937 settings_obj: &serde_json::Map<String, Value>,
1938 ) -> Vec<String> {
1939 let mut aliases = Vec::new();
1940
1941 for alias in ["api_key", "base_url", "options", "npm"] {
1942 if settings_obj.contains_key(alias) {
1943 aliases.push(alias.to_string());
1944 }
1945 }
1946
1947 if let Some(models) = settings_obj.get("models").and_then(Value::as_array) {
1948 for (index, model) in models.iter().enumerate() {
1949 if let Some(model_obj) = model.as_object() {
1950 if model_obj.contains_key("context_window") {
1951 aliases.push(format!("models[{index}].context_window"));
1952 }
1953 }
1954 }
1955 }
1956
1957 aliases
1958 }
1959
1960 fn normalize_openclaw_live_write_error(err: AppError) -> AppError {
1961 match err {
1962 AppError::Config(message)
1963 if message.starts_with("Failed to parse OpenClaw config as JSON5:") =>
1964 {
1965 AppError::Config(message.replacen(
1966 "Failed to parse OpenClaw config as JSON5",
1967 "Failed to parse OpenClaw config as round-trip JSON5 document",
1968 1,
1969 ))
1970 }
1971 other => other,
1972 }
1973 }
1974
1975 pub(crate) fn build_effective_live_snapshot(
1976 app_type: &AppType,
1977 provider: &Provider,
1978 common_config_snippet: Option<&str>,
1979 apply_common_config: bool,
1980 ) -> Result<Value, AppError> {
1981 let apply_common_config = Self::resolve_live_apply_common_config(
1982 app_type,
1983 provider,
1984 common_config_snippet,
1985 apply_common_config,
1986 );
1987
1988 match app_type {
1989 AppType::Claude => {
1990 let mut effective = common_config::build_effective_settings_with_common_config(
1991 app_type,
1992 provider,
1993 common_config_snippet,
1994 apply_common_config,
1995 )?;
1996 let _ = Self::normalize_claude_models_in_value(&mut effective);
1997 Ok(effective)
1998 }
1999 AppType::Codex => {
2000 let effective = common_config::build_effective_settings_with_common_config(
2001 app_type,
2002 provider,
2003 common_config_snippet,
2004 apply_common_config,
2005 )?;
2006 let settings = effective
2007 .as_object()
2008 .ok_or_else(|| AppError::Config("Codex 配置必须是 JSON 对象".into()))?;
2009 let auth = settings.get("auth").cloned();
2010 let cfg_text = settings.get("config").and_then(Value::as_str).unwrap_or("");
2011
2012 if !cfg_text.trim().is_empty() {
2013 crate::codex_config::validate_config_toml(cfg_text)?;
2014 }
2015
2016 let mut backup = serde_json::Map::new();
2017 if let Some(auth) = auth {
2018 backup.insert("auth".to_string(), auth);
2019 }
2020 backup.insert("config".to_string(), Value::String(cfg_text.to_string()));
2021 Ok(Value::Object(backup))
2022 }
2023 AppType::Gemini => {
2024 let content_to_write = common_config::build_effective_settings_with_common_config(
2025 app_type,
2026 provider,
2027 common_config_snippet,
2028 apply_common_config,
2029 )?;
2030
2031 let env_obj = content_to_write
2032 .get("env")
2033 .cloned()
2034 .unwrap_or_else(|| json!({}));
2035 let settings_path = crate::gemini_config::get_gemini_settings_path();
2036 let config_value = if let Some(config_value) = content_to_write.get("config") {
2037 if config_value.is_null() {
2038 if settings_path.exists() {
2039 read_json_file(&settings_path)?
2040 } else {
2041 json!({})
2042 }
2043 } else if let Some(provider_config) = config_value.as_object() {
2044 if provider_config.is_empty() {
2045 if settings_path.exists() {
2046 read_json_file(&settings_path)?
2047 } else {
2048 json!({})
2049 }
2050 } else {
2051 let mut merged = if settings_path.exists() {
2052 read_json_file(&settings_path)?
2053 } else {
2054 json!({})
2055 };
2056
2057 if !merged.is_object() {
2058 merged = json!({});
2059 }
2060
2061 let merged_map = merged.as_object_mut().ok_or_else(|| {
2062 AppError::localized(
2063 "gemini.validation.invalid_settings",
2064 "Gemini 现有 settings.json 格式错误: 必须是对象",
2065 "Gemini existing settings.json invalid: must be a JSON object",
2066 )
2067 })?;
2068 for (key, value) in provider_config {
2069 merged_map.insert(key.clone(), value.clone());
2070 }
2071 merged
2072 }
2073 } else {
2074 return Err(AppError::localized(
2075 "gemini.validation.invalid_config",
2076 "Gemini 配置格式错误: config 必须是对象或 null",
2077 "Gemini config invalid: config must be an object or null",
2078 ));
2079 }
2080 } else if settings_path.exists() {
2081 read_json_file(&settings_path)?
2082 } else {
2083 json!({})
2084 };
2085
2086 Ok(json!({
2087 "env": env_obj,
2088 "config": config_value,
2089 }))
2090 }
2091 AppType::OpenCode => Err(AppError::Config(
2092 "OpenCode does not support proxy takeover backups".into(),
2093 )),
2094 AppType::OpenClaw => Err(AppError::Config(
2095 "OpenClaw does not support proxy takeover backups".into(),
2096 )),
2097 AppType::Hermes => Err(AppError::Config(
2098 "Hermes does not support proxy takeover backups".into(),
2099 )),
2100 }
2101 }
2102
2103 fn validate_provider_settings(app_type: &AppType, provider: &Provider) -> Result<(), AppError> {
2104 match app_type {
2105 AppType::Claude => {
2106 if !provider.settings_config.is_object() {
2107 return Err(AppError::localized(
2108 "provider.claude.settings.not_object",
2109 "Claude 配置必须是 JSON 对象",
2110 "Claude configuration must be a JSON object",
2111 ));
2112 }
2113 }
2114 AppType::Codex => {
2115 let settings = provider.settings_config.as_object().ok_or_else(|| {
2116 AppError::localized(
2117 "provider.codex.settings.not_object",
2118 "Codex 配置必须是 JSON 对象",
2119 "Codex configuration must be a JSON object",
2120 )
2121 })?;
2122
2123 let auth = settings.get("auth").ok_or_else(|| {
2124 AppError::localized(
2125 "provider.codex.auth.missing",
2126 format!("供应商 {} 缺少 auth 配置", provider.id),
2127 format!("Provider {} is missing auth configuration", provider.id),
2128 )
2129 })?;
2130 if !auth.is_object() {
2131 return Err(AppError::localized(
2132 "provider.codex.auth.not_object",
2133 format!("供应商 {} 的 auth 配置必须是 JSON 对象", provider.id),
2134 format!(
2135 "Provider {} auth configuration must be a JSON object",
2136 provider.id
2137 ),
2138 ));
2139 }
2140
2141 if let Some(config_value) = settings.get("config") {
2142 if !(config_value.is_string() || config_value.is_null()) {
2143 return Err(AppError::localized(
2144 "provider.codex.config.invalid_type",
2145 "Codex config 字段必须是字符串",
2146 "Codex config field must be a string",
2147 ));
2148 }
2149 if let Some(cfg_text) = config_value.as_str() {
2150 crate::codex_config::validate_config_toml(cfg_text)?;
2151 }
2152 }
2153
2154 if !Self::is_codex_official_provider(provider) {
2155 let config_text = settings
2156 .get("config")
2157 .and_then(Value::as_str)
2158 .unwrap_or_default();
2159 if !Self::codex_config_has_base_url(config_text) {
2160 return Err(AppError::localized(
2161 "provider.codex.base_url.missing",
2162 format!("供应商 {} 缺少有效的 Codex Base URL", provider.id),
2163 format!("Provider {} is missing a valid Codex base_url", provider.id),
2164 ));
2165 }
2166 }
2167 }
2168 AppType::Gemini => {
2169 use crate::gemini_config::validate_gemini_settings;
2170 validate_gemini_settings(&provider.settings_config)?
2171 }
2172 AppType::OpenCode => {
2173 if !provider.settings_config.is_object() {
2174 return Err(AppError::localized(
2175 "provider.opencode.settings.not_object",
2176 "OpenCode 配置必须是 JSON 对象",
2177 "OpenCode configuration must be a JSON object",
2178 ));
2179 }
2180 }
2181 AppType::OpenClaw => {
2182 let config = Self::parse_openclaw_provider_settings(&provider.settings_config)?;
2183 Self::validate_openclaw_provider_models(&provider.id, &config)?;
2184 }
2185 AppType::Hermes => {
2186 if !provider.settings_config.is_object() {
2188 return Err(AppError::localized(
2189 "provider.hermes.settings.not_object",
2190 "Hermes 供应商配置必须是 JSON 对象",
2191 "Hermes provider configuration must be a JSON object",
2192 ));
2193 }
2194 }
2195 }
2196
2197 if let Some(meta) = &provider.meta {
2199 if let Some(usage_script) = &meta.usage_script {
2200 Self::validate_usage_script(usage_script)?;
2201 }
2202 }
2203
2204 Ok(())
2205 }
2206
2207 pub(crate) fn build_live_backup_snapshot(
2208 app_type: &AppType,
2209 provider: &Provider,
2210 common_config_snippet: Option<&str>,
2211 apply_common_config: bool,
2212 ) -> Result<Value, AppError> {
2213 Self::build_effective_live_snapshot(
2214 app_type,
2215 provider,
2216 common_config_snippet,
2217 apply_common_config,
2218 )
2219 }
2220
2221 pub(crate) fn build_effective_live_snapshot_from_state(
2222 state: &AppState,
2223 app_type: AppType,
2224 provider: &Provider,
2225 ) -> Result<Value, AppError> {
2226 let common_config_snippet = {
2227 let config = state.config.read().map_err(AppError::from)?;
2228 config.common_config_snippets.get(&app_type).cloned()
2229 };
2230
2231 Self::build_effective_live_snapshot(
2232 &app_type,
2233 provider,
2234 common_config_snippet.as_deref(),
2235 true,
2236 )
2237 }
2238
2239 pub(crate) fn get_provider(
2240 state: &AppState,
2241 app_type: AppType,
2242 provider_id: &str,
2243 ) -> Result<Provider, AppError> {
2244 let config = state.config.read().map_err(AppError::from)?;
2245 let manager = config
2246 .get_manager(&app_type)
2247 .ok_or_else(|| Self::app_not_found(&app_type))?;
2248
2249 manager.providers.get(provider_id).cloned().ok_or_else(|| {
2250 AppError::localized(
2251 "provider.not_found",
2252 format!("供应商不存在: {provider_id}"),
2253 format!("Provider not found: {provider_id}"),
2254 )
2255 })
2256 }
2257
2258 fn app_not_found(app_type: &AppType) -> AppError {
2259 AppError::localized(
2260 "provider.app_not_found",
2261 format!("应用类型不存在: {app_type:?}"),
2262 format!("App type not found: {app_type:?}"),
2263 )
2264 }
2265
2266 pub fn delete(state: &AppState, app_type: AppType, provider_id: &str) -> Result<(), AppError> {
2267 let (local_current_provider, stored_current_provider) = if app_type.is_additive_mode() {
2268 (None, None)
2269 } else {
2270 (
2271 crate::settings::get_current_provider(&app_type),
2272 state.db.get_current_provider(app_type.as_str())?,
2273 )
2274 };
2275 let provider_snapshot = {
2276 let config = state.config.read().map_err(AppError::from)?;
2277 let manager = config
2278 .get_manager(&app_type)
2279 .ok_or_else(|| Self::app_not_found(&app_type))?;
2280
2281 if !app_type.is_additive_mode()
2282 && (local_current_provider.as_deref() == Some(provider_id)
2283 || stored_current_provider.as_deref() == Some(provider_id))
2284 {
2285 return Err(AppError::localized(
2286 "provider.delete.current",
2287 "不能删除当前正在使用的供应商",
2288 "Cannot delete the provider currently in use",
2289 ));
2290 }
2291
2292 manager.providers.get(provider_id).cloned().ok_or_else(|| {
2293 AppError::localized(
2294 "provider.not_found",
2295 format!("供应商不存在: {provider_id}"),
2296 format!("Provider not found: {provider_id}"),
2297 )
2298 })?
2299 };
2300
2301 if app_type.is_additive_mode() {
2302 match app_type {
2303 AppType::OpenCode => {
2304 if crate::opencode_config::get_opencode_dir().exists() {
2305 crate::opencode_config::remove_provider(provider_id)?;
2306 }
2307 }
2308 AppType::OpenClaw => {
2309 if crate::openclaw_config::get_openclaw_dir().exists() {
2310 crate::openclaw_config::remove_provider(provider_id)?;
2311 }
2312 }
2313 _ => unreachable!("non-additive apps should not enter additive delete branch"),
2314 }
2315
2316 {
2317 let mut config = state.config.write().map_err(AppError::from)?;
2318 let manager = config
2319 .get_manager_mut(&app_type)
2320 .ok_or_else(|| Self::app_not_found(&app_type))?;
2321 manager.providers.shift_remove(provider_id);
2322 }
2323
2324 return state.save();
2325 }
2326
2327 match app_type {
2328 AppType::Codex => {
2329 crate::codex_config::delete_codex_provider_config(
2330 provider_id,
2331 &provider_snapshot.name,
2332 )?;
2333 }
2334 AppType::Claude => {
2335 let by_name = get_provider_config_path(provider_id, Some(&provider_snapshot.name));
2338 let by_id = get_provider_config_path(provider_id, None);
2339 delete_file(&by_name)?;
2340 delete_file(&by_id)?;
2341 }
2342 AppType::Gemini => {
2343 }
2345 AppType::OpenCode => {
2346 let _ = provider_snapshot;
2347 }
2348 AppType::OpenClaw => {
2349 let _ = provider_snapshot;
2350 }
2351 AppType::Hermes => {
2352 let _ = provider_snapshot;
2353 }
2354 }
2355
2356 {
2357 let mut config = state.config.write().map_err(AppError::from)?;
2358 let manager = config
2359 .get_manager_mut(&app_type)
2360 .ok_or_else(|| Self::app_not_found(&app_type))?;
2361
2362 if !app_type.is_additive_mode()
2363 && (local_current_provider.as_deref() == Some(provider_id)
2364 || stored_current_provider.as_deref() == Some(provider_id))
2365 {
2366 return Err(AppError::localized(
2367 "provider.delete.current",
2368 "不能删除当前正在使用的供应商",
2369 "Cannot delete the provider currently in use",
2370 ));
2371 }
2372
2373 if !app_type.is_additive_mode() && manager.current == provider_id {
2374 manager.current = stored_current_provider.clone().unwrap_or_default();
2375 }
2376
2377 manager.providers.shift_remove(provider_id);
2378 }
2379
2380 state.save()
2381 }
2382
2383 pub fn import_openclaw_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2384 live::import_openclaw_providers_from_live(state)
2385 }
2386
2387 pub fn import_opencode_providers_from_live(state: &AppState) -> Result<usize, AppError> {
2388 live::import_opencode_providers_from_live(state)
2389 }
2390}
2391
2392#[derive(Debug, Clone, Deserialize)]
2393pub struct ProviderSortUpdate {
2394 pub id: String,
2395 #[serde(rename = "sortIndex")]
2396 pub sort_index: usize,
2397}