mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +08:00
feat: add Universal Provider feature (#348)
* 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>
This commit is contained in:
@@ -690,3 +690,140 @@ pub struct ProviderSortUpdate {
|
||||
#[serde(rename = "sortIndex")]
|
||||
pub sort_index: usize,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 统一供应商(Universal Provider)服务方法
|
||||
// ============================================================================
|
||||
|
||||
use crate::provider::UniversalProvider;
|
||||
use std::collections::HashMap;
|
||||
|
||||
impl ProviderService {
|
||||
/// 获取所有统一供应商
|
||||
pub fn list_universal(
|
||||
state: &AppState,
|
||||
) -> Result<HashMap<String, UniversalProvider>, AppError> {
|
||||
state.db.get_all_universal_providers()
|
||||
}
|
||||
|
||||
/// 获取单个统一供应商
|
||||
pub fn get_universal(
|
||||
state: &AppState,
|
||||
id: &str,
|
||||
) -> Result<Option<UniversalProvider>, AppError> {
|
||||
state.db.get_universal_provider(id)
|
||||
}
|
||||
|
||||
/// 添加或更新统一供应商(不自动同步,需手动调用 sync_universal_to_apps)
|
||||
pub fn upsert_universal(
|
||||
state: &AppState,
|
||||
provider: UniversalProvider,
|
||||
) -> Result<bool, AppError> {
|
||||
// 保存统一供应商
|
||||
state.db.save_universal_provider(&provider)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 删除统一供应商
|
||||
pub fn delete_universal(state: &AppState, id: &str) -> Result<bool, AppError> {
|
||||
// 获取统一供应商(用于删除生成的子供应商)
|
||||
let provider = state.db.get_universal_provider(id)?;
|
||||
|
||||
// 删除统一供应商
|
||||
state.db.delete_universal_provider(id)?;
|
||||
|
||||
// 删除生成的子供应商
|
||||
if let Some(p) = provider {
|
||||
if p.apps.claude {
|
||||
let claude_id = format!("universal-claude-{}", id);
|
||||
let _ = state.db.delete_provider("claude", &claude_id);
|
||||
}
|
||||
if p.apps.codex {
|
||||
let codex_id = format!("universal-codex-{}", id);
|
||||
let _ = state.db.delete_provider("codex", &codex_id);
|
||||
}
|
||||
if p.apps.gemini {
|
||||
let gemini_id = format!("universal-gemini-{}", id);
|
||||
let _ = state.db.delete_provider("gemini", &gemini_id);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 同步统一供应商到各应用
|
||||
pub fn sync_universal_to_apps(state: &AppState, id: &str) -> Result<bool, AppError> {
|
||||
let provider = state
|
||||
.db
|
||||
.get_universal_provider(id)?
|
||||
.ok_or_else(|| AppError::Message(format!("统一供应商 {} 不存在", id)))?;
|
||||
|
||||
// 同步到 Claude
|
||||
if let Some(mut claude_provider) = provider.to_claude_provider() {
|
||||
// 合并已有配置
|
||||
if let Some(existing) = state.db.get_provider_by_id(&claude_provider.id, "claude")? {
|
||||
let mut merged = existing.settings_config.clone();
|
||||
Self::merge_json(&mut merged, &claude_provider.settings_config);
|
||||
claude_provider.settings_config = merged;
|
||||
}
|
||||
state.db.save_provider("claude", &claude_provider)?;
|
||||
} else {
|
||||
// 如果禁用了 Claude,删除对应的子供应商
|
||||
let claude_id = format!("universal-claude-{}", id);
|
||||
let _ = state.db.delete_provider("claude", &claude_id);
|
||||
}
|
||||
|
||||
// 同步到 Codex
|
||||
if let Some(mut codex_provider) = provider.to_codex_provider() {
|
||||
// 合并已有配置
|
||||
if let Some(existing) = state.db.get_provider_by_id(&codex_provider.id, "codex")? {
|
||||
let mut merged = existing.settings_config.clone();
|
||||
Self::merge_json(&mut merged, &codex_provider.settings_config);
|
||||
codex_provider.settings_config = merged;
|
||||
}
|
||||
state.db.save_provider("codex", &codex_provider)?;
|
||||
} else {
|
||||
let codex_id = format!("universal-codex-{}", id);
|
||||
let _ = state.db.delete_provider("codex", &codex_id);
|
||||
}
|
||||
|
||||
// 同步到 Gemini
|
||||
if let Some(mut gemini_provider) = provider.to_gemini_provider() {
|
||||
// 合并已有配置
|
||||
if let Some(existing) = state.db.get_provider_by_id(&gemini_provider.id, "gemini")? {
|
||||
let mut merged = existing.settings_config.clone();
|
||||
Self::merge_json(&mut merged, &gemini_provider.settings_config);
|
||||
gemini_provider.settings_config = merged;
|
||||
}
|
||||
state.db.save_provider("gemini", &gemini_provider)?;
|
||||
} else {
|
||||
let gemini_id = format!("universal-gemini-{}", id);
|
||||
let _ = state.db.delete_provider("gemini", &gemini_id);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// 递归合并 JSON:base 为底,patch 覆盖同名字段
|
||||
fn merge_json(base: &mut serde_json::Value, patch: &serde_json::Value) {
|
||||
use serde_json::Value;
|
||||
|
||||
match (base, patch) {
|
||||
(Value::Object(base_map), Value::Object(patch_map)) => {
|
||||
for (k, v_patch) in patch_map {
|
||||
match base_map.get_mut(k) {
|
||||
Some(v_base) => Self::merge_json(v_base, v_patch),
|
||||
None => {
|
||||
base_map.insert(k.clone(), v_patch.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 其它类型:直接覆盖
|
||||
(base_val, patch_val) => {
|
||||
*base_val = patch_val.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user