use indexmap::IndexMap; use tauri::State; use crate::app_config::AppType; use crate::commands::copilot::CopilotAuthState; use crate::error::AppError; use crate::provider::Provider; use crate::services::{ EndpointLatency, ProviderService, ProviderSortUpdate, SpeedtestService, SwitchResult, }; use crate::store::AppState; use std::str::FromStr; // 常量定义 const TEMPLATE_TYPE_GITHUB_COPILOT: &str = "github_copilot"; const COPILOT_UNIT_PREMIUM: &str = "requests"; /// 获取所有供应商 #[tauri::command] pub fn get_providers( state: State<'_, AppState>, app: String, ) -> Result, String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::list(state.inner(), app_type).map_err(|e| e.to_string()) } #[tauri::command] pub fn get_current_provider(state: State<'_, AppState>, app: String) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::current(state.inner(), app_type).map_err(|e| e.to_string()) } #[tauri::command] pub fn add_provider( state: State<'_, AppState>, app: String, provider: Provider, #[allow(non_snake_case)] addToLive: Option, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::add(state.inner(), app_type, provider, addToLive.unwrap_or(true)) .map_err(|e| e.to_string()) } #[tauri::command] pub fn update_provider( state: State<'_, AppState>, app: String, provider: Provider, #[allow(non_snake_case)] originalId: Option, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::update(state.inner(), app_type, originalId.as_deref(), provider) .map_err(|e| e.to_string()) } #[tauri::command] pub fn delete_provider( state: State<'_, AppState>, app: String, id: String, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::delete(state.inner(), app_type, &id) .map(|_| true) .map_err(|e| e.to_string()) } #[tauri::command] pub fn remove_provider_from_live_config( state: tauri::State<'_, AppState>, app: String, id: String, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::remove_from_live_config(state.inner(), app_type, &id) .map(|_| true) .map_err(|e| e.to_string()) } fn switch_provider_internal( state: &AppState, app_type: AppType, id: &str, ) -> Result { ProviderService::switch(state, app_type, id) } #[cfg_attr(not(feature = "test-hooks"), doc(hidden))] pub fn switch_provider_test_hook( state: &AppState, app_type: AppType, id: &str, ) -> Result { switch_provider_internal(state, app_type, id) } #[tauri::command] pub fn switch_provider( state: State<'_, AppState>, app: String, id: String, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; switch_provider_internal(&state, app_type, &id).map_err(|e| e.to_string()) } fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result { let imported = ProviderService::import_default_config(state, app_type.clone())?; if imported { // Extract common config snippet (mirrors old startup logic in lib.rs) if state .db .should_auto_extract_config_snippet(app_type.as_str())? { match ProviderService::extract_common_config_snippet(state, app_type.clone()) { Ok(snippet) if !snippet.is_empty() && snippet != "{}" => { let _ = state .db .set_config_snippet(app_type.as_str(), Some(snippet)); let _ = state .db .set_config_snippet_cleared(app_type.as_str(), false); } _ => {} } } ProviderService::migrate_legacy_common_config_usage_if_needed(state, app_type.clone())?; } Ok(imported) } #[cfg_attr(not(feature = "test-hooks"), doc(hidden))] pub fn import_default_config_test_hook( state: &AppState, app_type: AppType, ) -> Result { import_default_config_internal(state, app_type) } #[tauri::command] pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; import_default_config_internal(&state, app_type).map_err(Into::into) } #[allow(non_snake_case)] #[tauri::command] pub async fn queryProviderUsage( state: State<'_, AppState>, copilot_state: State<'_, CopilotAuthState>, #[allow(non_snake_case)] providerId: String, // 使用 camelCase 匹配前端 app: String, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; // 检查是否为 GitHub Copilot 模板类型,并解析绑定账号 let (is_copilot_template, copilot_account_id) = { let providers = state .db .get_all_providers(app_type.as_str()) .map_err(|e| format!("Failed to get providers: {e}"))?; let provider = providers.get(&providerId); let is_copilot = provider .and_then(|p| p.meta.as_ref()) .and_then(|m| m.usage_script.as_ref()) .and_then(|s| s.template_type.as_ref()) .map(|t| t == TEMPLATE_TYPE_GITHUB_COPILOT) .unwrap_or(false); let account_id = provider .and_then(|p| p.meta.as_ref()) .and_then(|m| m.managed_account_id_for(TEMPLATE_TYPE_GITHUB_COPILOT)); (is_copilot, account_id) }; if is_copilot_template { // 使用 Copilot 专用 API let auth_manager = copilot_state.0.read().await; let usage = match copilot_account_id.as_deref() { Some(account_id) => auth_manager .fetch_usage_for_account(account_id) .await .map_err(|e| format!("Failed to fetch Copilot usage: {e}"))?, None => auth_manager .fetch_usage() .await .map_err(|e| format!("Failed to fetch Copilot usage: {e}"))?, }; let premium = &usage.quota_snapshots.premium_interactions; let used = premium.entitlement - premium.remaining; return Ok(crate::provider::UsageResult { success: true, data: Some(vec![crate::provider::UsageData { plan_name: Some(usage.copilot_plan), remaining: Some(premium.remaining as f64), total: Some(premium.entitlement as f64), used: Some(used as f64), unit: Some(COPILOT_UNIT_PREMIUM.to_string()), is_valid: Some(true), invalid_message: None, extra: Some(format!("Reset: {}", usage.quota_reset_date)), }]), error: None, }); } ProviderService::query_usage(state.inner(), app_type, &providerId) .await .map_err(|e| e.to_string()) } #[allow(non_snake_case)] #[allow(clippy::too_many_arguments)] #[tauri::command] pub async fn testUsageScript( state: State<'_, AppState>, #[allow(non_snake_case)] providerId: String, app: String, #[allow(non_snake_case)] scriptCode: String, timeout: Option, #[allow(non_snake_case)] apiKey: Option, #[allow(non_snake_case)] baseUrl: Option, #[allow(non_snake_case)] accessToken: Option, #[allow(non_snake_case)] userId: Option, #[allow(non_snake_case)] templateType: Option, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::test_usage_script( state.inner(), app_type, &providerId, &scriptCode, timeout.unwrap_or(10), apiKey.as_deref(), baseUrl.as_deref(), accessToken.as_deref(), userId.as_deref(), templateType.as_deref(), ) .await .map_err(|e| e.to_string()) } #[tauri::command] pub fn read_live_provider_settings(app: String) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::read_live_settings(app_type).map_err(|e| e.to_string()) } #[tauri::command] pub async fn test_api_endpoints( urls: Vec, #[allow(non_snake_case)] timeoutSecs: Option, ) -> Result, String> { SpeedtestService::test_endpoints(urls, timeoutSecs) .await .map_err(|e| e.to_string()) } #[tauri::command] pub fn get_custom_endpoints( state: State<'_, AppState>, app: String, #[allow(non_snake_case)] providerId: String, ) -> Result, String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::get_custom_endpoints(state.inner(), app_type, &providerId) .map_err(|e| e.to_string()) } #[tauri::command] pub fn add_custom_endpoint( state: State<'_, AppState>, app: String, #[allow(non_snake_case)] providerId: String, url: String, ) -> Result<(), String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::add_custom_endpoint(state.inner(), app_type, &providerId, url) .map_err(|e| e.to_string()) } #[tauri::command] pub fn remove_custom_endpoint( state: State<'_, AppState>, app: String, #[allow(non_snake_case)] providerId: String, url: String, ) -> Result<(), String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::remove_custom_endpoint(state.inner(), app_type, &providerId, url) .map_err(|e| e.to_string()) } #[tauri::command] pub fn update_endpoint_last_used( state: State<'_, AppState>, app: String, #[allow(non_snake_case)] providerId: String, url: String, ) -> Result<(), String> { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::update_endpoint_last_used(state.inner(), app_type, &providerId, url) .map_err(|e| e.to_string()) } #[tauri::command] pub fn update_providers_sort_order( state: State<'_, AppState>, app: String, updates: Vec, ) -> Result { let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?; ProviderService::update_sort_order(state.inner(), app_type, updates).map_err(|e| e.to_string()) } use crate::provider::UniversalProvider; use std::collections::HashMap; use tauri::{AppHandle, Emitter}; #[derive(Clone, serde::Serialize)] pub struct UniversalProviderSyncedEvent { pub action: String, pub id: String, } fn emit_universal_provider_synced(app: &AppHandle, action: &str, id: &str) { let _ = app.emit( "universal-provider-synced", UniversalProviderSyncedEvent { action: action.to_string(), id: id.to_string(), }, ); } #[tauri::command] pub fn get_universal_providers( state: State<'_, AppState>, ) -> Result, String> { ProviderService::list_universal(state.inner()).map_err(|e| e.to_string()) } #[tauri::command] pub fn get_universal_provider( state: State<'_, AppState>, id: String, ) -> Result, String> { ProviderService::get_universal(state.inner(), &id).map_err(|e| e.to_string()) } #[tauri::command] pub fn upsert_universal_provider( app: AppHandle, state: State<'_, AppState>, provider: UniversalProvider, ) -> Result { let id = provider.id.clone(); let result = ProviderService::upsert_universal(state.inner(), provider).map_err(|e| e.to_string())?; emit_universal_provider_synced(&app, "upsert", &id); Ok(result) } #[tauri::command] pub fn delete_universal_provider( app: AppHandle, state: State<'_, AppState>, id: String, ) -> Result { let result = ProviderService::delete_universal(state.inner(), &id).map_err(|e| e.to_string())?; emit_universal_provider_synced(&app, "delete", &id); Ok(result) } #[tauri::command] pub fn sync_universal_provider( app: AppHandle, state: State<'_, AppState>, id: String, ) -> Result { let result = ProviderService::sync_universal_to_apps(state.inner(), &id).map_err(|e| e.to_string())?; emit_universal_provider_synced(&app, "sync", &id); Ok(result) } #[tauri::command] pub fn import_opencode_providers_from_live(state: State<'_, AppState>) -> Result { crate::services::provider::import_opencode_providers_from_live(state.inner()) .map_err(|e| e.to_string()) } #[tauri::command] pub fn get_opencode_live_provider_ids() -> Result, String> { crate::opencode_config::get_providers() .map(|providers| providers.keys().cloned().collect()) .map_err(|e| e.to_string()) } // ============================================================================ // OpenClaw 专属命令 → 已迁移至 commands/openclaw.rs // ============================================================================