mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
8fe5c1041a
* feat: add Universal Provider feature - Add Universal Provider data structures and type definitions - Implement backend CRUD operations and sync functionality - Add frontend UI components (UniversalProviderPanel, Card, FormModal) - Add NewAPI icon and preset configuration - Support cross-app (Claude/Codex/Gemini) configuration sync - Add website URL field for providers - Implement real-time refresh via event notifications - Add i18n support (Chinese/English/Japanese) * feat: integrate universal provider presets into add provider dialog - Add universal provider presets (NewAPI, Custom Gateway) to preset selector - Show universal presets with Layers icon badge in preset selector - Open UniversalProviderFormModal when universal preset is clicked - Pass initialPreset to auto-fill form when opened from add dialog - Add i18n keys for addSuccess/addFailed messages - Keep separate universal provider panel for management * refactor: move universal provider management to add dialog - Remove Layers button from main navigation header - Add 'Manage' button next to universal provider presets - Open UniversalProviderPanel from within add provider dialog - Add i18n keys for 'manage' in all locales * style: display universal provider presets on separate line - Move universal provider section to a new row with border separator - Add label '统一供应商:' to clarify the section * style: unify universal provider label style with preset label - Use FormLabel component for consistent styling - Add background to 'Manage' button matching preset buttons - Update icon size and button padding for consistency * feat: add sync functionality and JSON preview for Universal Provider * fix: add missing in_failover_queue field to Provider structs After rebasing to main, the Provider struct gained a new `in_failover_queue` field. This fix adds the missing field to the three to_*_provider() methods in UniversalProvider. * refactor: redesign AddProviderDialog with tab-based layout - Add tabs to separate app-specific providers and universal providers - Move "Add Universal Provider" button from panel header to footer - Remove unused handleAdd callback and clean up imports - Update emptyHint i18n text to reference the footer button * fix: append /v1 suffix to Codex base_url in Universal Provider Codex uses OpenAI-compatible API which requires the /v1 endpoint suffix. The Universal Provider now automatically appends /v1 to base_url when generating Codex provider config if not already present. - Handle trailing slashes to avoid double slashes - Apply fix to both backend (to_codex_provider) and frontend preview * feat: auto-sync universal provider to apps on creation Previously, users had to manually click sync after adding a universal provider. Now it automatically syncs to Claude/Codex/Gemini on creation, providing a smoother user experience. --------- Co-authored-by: Jason <farion1231@gmail.com>
326 lines
10 KiB
Rust
326 lines
10 KiB
Rust
use indexmap::IndexMap;
|
|
use tauri::State;
|
|
|
|
use crate::app_config::AppType;
|
|
use crate::error::AppError;
|
|
use crate::provider::Provider;
|
|
use crate::services::{EndpointLatency, ProviderService, ProviderSortUpdate, SpeedtestService};
|
|
use crate::store::AppState;
|
|
use std::str::FromStr;
|
|
|
|
/// 获取所有供应商
|
|
#[tauri::command]
|
|
pub fn get_providers(
|
|
state: State<'_, AppState>,
|
|
app: String,
|
|
) -> Result<IndexMap<String, Provider>, 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())
|
|
}
|
|
|
|
/// 获取当前供应商ID
|
|
#[tauri::command]
|
|
pub fn get_current_provider(state: State<'_, AppState>, app: String) -> Result<String, String> {
|
|
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,
|
|
) -> Result<bool, String> {
|
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
|
ProviderService::add(state.inner(), app_type, provider).map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// 更新供应商
|
|
#[tauri::command]
|
|
pub fn update_provider(
|
|
state: State<'_, AppState>,
|
|
app: String,
|
|
provider: Provider,
|
|
) -> Result<bool, String> {
|
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
|
ProviderService::update(state.inner(), app_type, provider).map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// 删除供应商
|
|
#[tauri::command]
|
|
pub fn delete_provider(
|
|
state: State<'_, AppState>,
|
|
app: String,
|
|
id: String,
|
|
) -> Result<bool, String> {
|
|
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())
|
|
}
|
|
|
|
/// 切换供应商
|
|
fn switch_provider_internal(state: &AppState, app_type: AppType, id: &str) -> Result<(), AppError> {
|
|
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<(), AppError> {
|
|
switch_provider_internal(state, app_type, id)
|
|
}
|
|
|
|
#[tauri::command]
|
|
pub fn switch_provider(
|
|
state: State<'_, AppState>,
|
|
app: String,
|
|
id: String,
|
|
) -> Result<bool, String> {
|
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
|
switch_provider_internal(&state, app_type, &id)
|
|
.map(|_| true)
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
fn import_default_config_internal(state: &AppState, app_type: AppType) -> Result<bool, AppError> {
|
|
ProviderService::import_default_config(state, app_type)
|
|
}
|
|
|
|
#[cfg_attr(not(feature = "test-hooks"), doc(hidden))]
|
|
pub fn import_default_config_test_hook(
|
|
state: &AppState,
|
|
app_type: AppType,
|
|
) -> Result<bool, AppError> {
|
|
import_default_config_internal(state, app_type)
|
|
}
|
|
|
|
/// 导入当前配置为默认供应商
|
|
#[tauri::command]
|
|
pub fn import_default_config(state: State<'_, AppState>, app: String) -> Result<bool, String> {
|
|
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>,
|
|
#[allow(non_snake_case)] providerId: String, // 使用 camelCase 匹配前端
|
|
app: String,
|
|
) -> Result<crate::provider::UsageResult, String> {
|
|
let app_type = AppType::from_str(&app).map_err(|e| e.to_string())?;
|
|
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<u64>,
|
|
#[allow(non_snake_case)] apiKey: Option<String>,
|
|
#[allow(non_snake_case)] baseUrl: Option<String>,
|
|
#[allow(non_snake_case)] accessToken: Option<String>,
|
|
#[allow(non_snake_case)] userId: Option<String>,
|
|
) -> Result<crate::provider::UsageResult, String> {
|
|
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(),
|
|
)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// 读取当前生效的配置内容
|
|
#[tauri::command]
|
|
pub fn read_live_provider_settings(app: String) -> Result<serde_json::Value, String> {
|
|
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<String>,
|
|
#[allow(non_snake_case)] timeoutSecs: Option<u64>,
|
|
) -> Result<Vec<EndpointLatency>, 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<Vec<crate::settings::CustomEndpoint>, 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<ProviderSortUpdate>,
|
|
) -> Result<bool, String> {
|
|
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())
|
|
}
|
|
|
|
// ============================================================================
|
|
// 统一供应商(Universal Provider)命令
|
|
// ============================================================================
|
|
|
|
use crate::provider::UniversalProvider;
|
|
use std::collections::HashMap;
|
|
use tauri::{AppHandle, Emitter};
|
|
|
|
/// 统一供应商同步完成事件的 payload
|
|
#[derive(Clone, serde::Serialize)]
|
|
pub struct UniversalProviderSyncedEvent {
|
|
/// 操作类型: "upsert" | "delete" | "sync"
|
|
pub action: String,
|
|
/// 统一供应商 ID
|
|
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<HashMap<String, UniversalProvider>, 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<Option<UniversalProvider>, 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<bool, String> {
|
|
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<bool, String> {
|
|
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<bool, String> {
|
|
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)
|
|
}
|