From a187380d6fec3b0021cb0b671c857d172e7d386b Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 21 Jan 2026 11:27:44 +0800 Subject: [PATCH 1/9] fix(failover): switch to P1 immediately when enabling auto failover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, enabling auto failover kept using the current provider until the first failure, causing inconsistency when the current provider was not in the failover queue. When stopping proxy, the restored config would not match user expectations. New behavior: - Enable auto failover = immediately switch to queue P1 - Subsequent routing follows queue order (P1→P2→...) - Auto-add current provider to queue if queue is empty Changes: - Add switch_proxy_target() for hot-switching during proxy mode - Update provider_router to use queue order when failover enabled - Sync tray menu Auto click with the same logic - Update UI tooltips to reflect new semantics - Add tests for queue-only routing scenario --- src-tauri/src/commands/failover.rs | 59 +++++++++ src-tauri/src/proxy/provider_router.rs | 124 ++++++++++++++++-- src-tauri/src/proxy/server.rs | 12 ++ src-tauri/src/services/proxy.rs | 96 +++++++++++++- src-tauri/src/tray.rs | 35 ++++- src/components/proxy/FailoverQueueManager.tsx | 3 +- src/components/proxy/FailoverToggle.tsx | 4 +- src/components/proxy/ProxyPanel.tsx | 4 +- src/i18n/locales/en.json | 11 +- src/i18n/locales/ja.json | 11 +- src/i18n/locales/zh.json | 11 +- src/lib/query/failover.ts | 15 +++ 12 files changed, 357 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/commands/failover.rs b/src-tauri/src/commands/failover.rs index 8269f5fc3..d0aad3900 100644 --- a/src-tauri/src/commands/failover.rs +++ b/src-tauri/src/commands/failover.rs @@ -5,6 +5,8 @@ use crate::database::FailoverQueueItem; use crate::provider::Provider; use crate::store::AppState; +use std::str::FromStr; +use tauri::Emitter; /// 获取故障转移队列 #[tauri::command] @@ -84,6 +86,47 @@ pub async fn set_auto_failover_enabled( "[Failover] Setting auto_failover_enabled: app_type='{app_type}', enabled={enabled}" ); + // 强一致语义:开启故障转移后立即切到队列 P1(并确保队列非空) + // + // 说明: + // - 仅在 enabled=true 时执行“切到 P1” + // - 若队列为空,则尝试把“当前供应商”自动加入队列作为 P1,避免用户在 UI 上陷入死锁(无法先加队列再开启) + let p1_provider_id = if enabled { + let mut queue = state + .db + .get_failover_queue(&app_type) + .map_err(|e| e.to_string())?; + + if queue.is_empty() { + let app_enum = crate::app_config::AppType::from_str(&app_type) + .map_err(|_| format!("无效的应用类型: {app_type}"))?; + + let current_id = crate::settings::get_effective_current_provider(&state.db, &app_enum) + .map_err(|e| e.to_string())?; + + let Some(current_id) = current_id else { + return Err("故障转移队列为空,且未设置当前供应商,无法开启故障转移".to_string()); + }; + + state + .db + .add_to_failover_queue(&app_type, ¤t_id) + .map_err(|e| e.to_string())?; + + queue = state + .db + .get_failover_queue(&app_type) + .map_err(|e| e.to_string())?; + } + + queue + .first() + .map(|item| item.provider_id.clone()) + .ok_or_else(|| "故障转移队列为空,无法开启故障转移".to_string())? + } else { + String::new() + }; + // 读取当前配置 let mut config = state .db @@ -101,6 +144,22 @@ pub async fn set_auto_failover_enabled( .await .map_err(|e| e.to_string())?; + // 开启后立即切到 P1:更新 is_current + 本地 settings + Live 备份(接管模式下) + if enabled { + state + .proxy_service + .switch_proxy_target(&app_type, &p1_provider_id) + .await?; + + // 发射 provider-switched 事件(让前端刷新当前供应商) + let event_data = serde_json::json!({ + "appType": app_type, + "providerId": p1_provider_id, + "source": "failoverEnabled" + }); + let _ = app.emit("provider-switched", event_data); + } + // 刷新托盘菜单,确保状态同步 if let Ok(new_menu) = crate::tray::create_tray_menu(&app, &state) { if let Some(tray) = app.tray_by_id("main") { diff --git a/src-tauri/src/proxy/provider_router.rs b/src-tauri/src/proxy/provider_router.rs index 5f3a315fd..cf9f0748c 100644 --- a/src-tauri/src/proxy/provider_router.rs +++ b/src-tauri/src/proxy/provider_router.rs @@ -2,11 +2,13 @@ //! //! 负责选择和管理代理目标供应商,实现智能故障转移 +use crate::app_config::AppType; use crate::database::Database; use crate::error::AppError; use crate::provider::Provider; use crate::proxy::circuit_breaker::{AllowResult, CircuitBreaker, CircuitBreakerConfig}; use std::collections::HashMap; +use std::str::FromStr; use std::sync::Arc; use tokio::sync::RwLock; @@ -31,7 +33,7 @@ impl ProviderRouter { /// /// 返回按优先级排序的可用供应商列表: /// - 故障转移关闭时:仅返回当前供应商 - /// - 故障转移开启时:完全按照故障转移队列顺序返回,忽略当前供应商设置 + /// - 故障转移开启时:仅使用故障转移队列,按队列顺序依次尝试(P1 → P2 → ...) pub async fn select_providers(&self, app_type: &str) -> Result, AppError> { let mut result = Vec::new(); let mut total_providers = 0usize; @@ -47,12 +49,25 @@ impl ProviderRouter { }; if auto_failover_enabled { - // 故障转移开启:使用 in_failover_queue 标记的供应商,按 sort_index 排序 - let failover_providers = self.db.get_failover_providers(app_type)?; - total_providers = failover_providers.len(); + // 故障转移开启:仅按队列顺序依次尝试(P1 → P2 → ...) + let all_providers = self.db.get_all_providers(app_type)?; - for provider in failover_providers { - let circuit_key = format!("{}:{}", app_type, provider.id); + // 使用 DAO 返回的排序结果,确保和前端展示一致 + let ordered_ids: Vec = self + .db + .get_failover_queue(app_type)? + .into_iter() + .map(|item| item.provider_id) + .collect(); + + total_providers = ordered_ids.len(); + + for provider_id in ordered_ids { + let Some(provider) = all_providers.get(&provider_id).cloned() else { + continue; + }; + + let circuit_key = format!("{app_type}:{}", provider.id); let breaker = self.get_or_create_circuit_breaker(&circuit_key).await; if breaker.is_available().await { @@ -63,7 +78,16 @@ impl ProviderRouter { } } else { // 故障转移关闭:仅使用当前供应商,跳过熔断器检查 - if let Some(current_id) = self.db.get_current_provider(app_type)? { + let current_id = AppType::from_str(app_type) + .ok() + .and_then(|app_enum| { + crate::settings::get_effective_current_provider(&self.db, &app_enum) + .ok() + .flatten() + }) + .or_else(|| self.db.get_current_provider(app_type).ok().flatten()); + + if let Some(current_id) = current_id { if let Some(current) = self.db.get_provider_by_id(¤t_id, app_type)? { total_providers = 1; result.push(current); @@ -239,9 +263,53 @@ mod tests { use super::*; use crate::database::Database; use serde_json::json; + use serial_test::serial; + use std::env; + use tempfile::TempDir; + + struct TempHome { + #[allow(dead_code)] + dir: TempDir, + original_home: Option, + original_userprofile: Option, + } + + impl TempHome { + fn new() -> Self { + let dir = TempDir::new().expect("failed to create temp home"); + let original_home = env::var("HOME").ok(); + let original_userprofile = env::var("USERPROFILE").ok(); + + env::set_var("HOME", dir.path()); + env::set_var("USERPROFILE", dir.path()); + crate::settings::reload_settings().expect("reload settings"); + + Self { + dir, + original_home, + original_userprofile, + } + } + } + + impl Drop for TempHome { + fn drop(&mut self) { + match &self.original_home { + Some(value) => env::set_var("HOME", value), + None => env::remove_var("HOME"), + } + + match &self.original_userprofile { + Some(value) => env::set_var("USERPROFILE", value), + None => env::remove_var("USERPROFILE"), + } + } + } #[tokio::test] + #[serial] async fn test_provider_router_creation() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); let router = ProviderRouter::new(db); @@ -250,7 +318,9 @@ mod tests { } #[tokio::test] + #[serial] async fn test_failover_disabled_uses_current_provider() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); let provider_a = @@ -271,7 +341,9 @@ mod tests { } #[tokio::test] - async fn test_failover_enabled_uses_queue_order() { + #[serial] + async fn test_failover_enabled_uses_queue_order_ignoring_current() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); // 设置 sort_index 来控制顺序:b=1, a=2 @@ -298,13 +370,45 @@ mod tests { let providers = router.select_providers("claude").await.unwrap(); assert_eq!(providers.len(), 2); - // 按 sort_index 排序:b(1) 在前,a(2) 在后 + // 故障转移开启时:仅按队列顺序选择(忽略当前供应商) assert_eq!(providers[0].id, "b"); assert_eq!(providers[1].id, "a"); } #[tokio::test] + #[serial] + async fn test_failover_enabled_uses_queue_only_even_if_current_not_in_queue() { + let _home = TempHome::new(); + let db = Arc::new(Database::memory().unwrap()); + + let provider_a = + Provider::with_id("a".to_string(), "Provider A".to_string(), json!({}), None); + let mut provider_b = + Provider::with_id("b".to_string(), "Provider B".to_string(), json!({}), None); + provider_b.sort_index = Some(1); + + db.save_provider("claude", &provider_a).unwrap(); + db.save_provider("claude", &provider_b).unwrap(); + db.set_current_provider("claude", "a").unwrap(); + + // 只把 b 加入故障转移队列(模拟“当前供应商不在队列里”的常见配置) + db.add_to_failover_queue("claude", "b").unwrap(); + + let mut config = db.get_proxy_config_for_app("claude").await.unwrap(); + config.auto_failover_enabled = true; + db.update_proxy_config_for_app(config).await.unwrap(); + + let router = ProviderRouter::new(db.clone()); + let providers = router.select_providers("claude").await.unwrap(); + + assert_eq!(providers.len(), 1); + assert_eq!(providers[0].id, "b"); + } + + #[tokio::test] + #[serial] async fn test_select_providers_does_not_consume_half_open_permit() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); db.update_circuit_breaker_config(&CircuitBreakerConfig { @@ -345,7 +449,9 @@ mod tests { } #[tokio::test] + #[serial] async fn test_release_permit_neutral_frees_half_open_slot() { + let _home = TempHome::new(); let db = Arc::new(Database::memory().unwrap()); // 配置熔断器:1 次失败即熔断,0 秒超时立即进入 HalfOpen diff --git a/src-tauri/src/proxy/server.rs b/src-tauri/src/proxy/server.rs index bfd13ee2d..1a75083a9 100644 --- a/src-tauri/src/proxy/server.rs +++ b/src-tauri/src/proxy/server.rs @@ -190,6 +190,18 @@ impl ProxyServer { status } + /// 更新某个应用类型当前“目标供应商”(用于 UI 展示 active_targets) + /// + /// 注意:这不代表该供应商一定已经处理过请求,而是用于“热切换/启用故障转移立即切 P1” + /// 等场景下,让 UI 能立刻反映最新目标。 + pub async fn set_active_target(&self, app_type: &str, provider_id: &str, provider_name: &str) { + let mut current_providers = self.state.current_providers.write().await; + current_providers.insert( + app_type.to_string(), + (provider_id.to_string(), provider_name.to_string()), + ); + } + fn build_router(&self) -> Router { let cors = CorsLayer::new() .allow_origin(Any) diff --git a/src-tauri/src/services/proxy.rs b/src-tauri/src/services/proxy.rs index 823bbfce8..b22043ad2 100644 --- a/src-tauri/src/services/proxy.rs +++ b/src-tauri/src/services/proxy.rs @@ -1530,7 +1530,10 @@ impl ProxyService { app_type: &str, provider_id: &str, ) -> Result<(), String> { - // 更新数据库中的 is_current 标记 + // 代理模式切换供应商(热切换): + // - 更新 SSOT(数据库 is_current) + // - 同步本地 settings(设备级 current_provider_*) + // - 若该应用正处于接管模式,则同步更新 Live 备份(用于停止代理时恢复) let app_type_enum = AppType::from_str(app_type).map_err(|_| format!("无效的应用类型: {app_type}"))?; @@ -1538,6 +1541,35 @@ impl ProxyService { .set_current_provider(app_type_enum.as_str(), provider_id) .map_err(|e| format!("更新当前供应商失败: {e}"))?; + // 同步本地 settings(设备级优先) + crate::settings::set_current_provider(&app_type_enum, Some(provider_id)) + .map_err(|e| format!("更新本地当前供应商失败: {e}"))?; + + // 仅在确实处于接管状态时才更新 Live 备份,避免无接管时误写覆盖 Live + let has_backup = self + .db + .get_live_backup(app_type_enum.as_str()) + .await + .ok() + .flatten() + .is_some(); + let live_taken_over = self.detect_takeover_in_live_config_for_app(&app_type_enum); + + if let Ok(Some(provider)) = self.db.get_provider_by_id(provider_id, app_type) { + // 同步更新 Live 备份(用于 stop_with_restore 恢复) + if has_backup || live_taken_over { + self.update_live_backup_from_provider(app_type, &provider) + .await?; + } + + // 同步更新 ProxyStatus.active_targets(用于 UI 立即反映切换目标) + if let Some(server) = self.server.read().await.as_ref() { + server + .set_active_target(app_type_enum.as_str(), &provider.id, &provider.name) + .await; + } + } + log::info!("代理模式:已切换 {app_type} 的目标供应商为 {provider_id}"); Ok(()) } @@ -2058,4 +2090,66 @@ model = "gpt-5.1-codex" "should not add ANTHROPIC_AUTH_TOKEN when absent" ); } + + #[tokio::test] + #[serial] + async fn switch_proxy_target_updates_live_backup_when_taken_over() { + let _home = TempHome::new(); + crate::settings::reload_settings().expect("reload settings"); + + let db = Arc::new(Database::memory().expect("init db")); + let service = ProxyService::new(db.clone()); + + let provider_a = Provider::with_id( + "a".to_string(), + "A".to_string(), + json!({ + "env": { + "ANTHROPIC_API_KEY": "a-key" + } + }), + None, + ); + let provider_b = Provider::with_id( + "b".to_string(), + "B".to_string(), + json!({ + "env": { + "ANTHROPIC_API_KEY": "b-key" + } + }), + None, + ); + db.save_provider("claude", &provider_a) + .expect("save provider a"); + db.save_provider("claude", &provider_b) + .expect("save provider b"); + db.set_current_provider("claude", "a") + .expect("set current provider"); + + // 模拟“已接管”状态:存在 Live 备份(内容不重要,会被热切换更新) + db.save_live_backup("claude", "{\"env\":{}}") + .await + .expect("seed live backup"); + + service + .switch_proxy_target("claude", "b") + .await + .expect("switch proxy target"); + + // 断言:本地 settings 的 current provider 已同步 + assert_eq!( + crate::settings::get_current_provider(&AppType::Claude).as_deref(), + Some("b") + ); + + // 断言:Live 备份已更新为目标供应商配置(用于 stop_with_restore 恢复) + let backup = db + .get_live_backup("claude") + .await + .expect("get live backup") + .expect("backup exists"); + let expected = serde_json::to_string(&provider_b.settings_config).expect("serialize"); + assert_eq!(backup.original_config, expected); + } } diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 10b85fa0c..a3c151bf9 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -211,6 +211,28 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A if let Some(app_state) = app.try_state::() { let app_type_str = app_type.as_str(); + // 强一致语义:Auto 模式开启后立即切到队列 P1(P1→P2→...) + // 若队列为空,则尝试把“当前供应商”自动加入队列作为 P1,避免用户陷入无法开启的死锁。 + let mut queue = app_state.db.get_failover_queue(app_type_str)?; + if queue.is_empty() { + let current_id = + crate::settings::get_effective_current_provider(&app_state.db, app_type)?; + let Some(current_id) = current_id else { + return Err(AppError::Message( + "故障转移队列为空,且未设置当前供应商,无法启用 Auto 模式".to_string(), + )); + }; + app_state + .db + .add_to_failover_queue(app_type_str, ¤t_id)?; + queue = app_state.db.get_failover_queue(app_type_str)?; + } + + let p1_provider_id = queue + .first() + .map(|item| item.provider_id.clone()) + .ok_or_else(|| AppError::Message("故障转移队列为空,无法启用 Auto 模式".to_string()))?; + // 真正启用 failover:启动代理服务 + 执行接管 + 开启 auto_failover let proxy_service = &app_state.proxy_service; @@ -238,6 +260,16 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A .db .set_proxy_flags_sync(app_type_str, true, true)?; + // 3.1) 立即切到队列 P1(热切换:不写 Live,仅更新 DB/settings/备份) + if let Err(e) = futures::executor::block_on( + proxy_service.switch_proxy_target(app_type_str, &p1_provider_id), + ) { + log::error!("[Tray] Auto 模式切换到队列 P1 失败: {e}"); + return Err(AppError::Message(format!( + "Auto 模式切换到队列 P1 失败: {e}" + ))); + } + // 4) 更新托盘菜单 if let Ok(new_menu) = create_tray_menu(app, app_state.inner()) { if let Some(tray) = app.tray_by_id("main") { @@ -249,7 +281,8 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A let event_data = serde_json::json!({ "appType": app_type_str, "proxyEnabled": true, - "autoFailoverEnabled": true + "autoFailoverEnabled": true, + "providerId": p1_provider_id }); if let Err(e) = app.emit("proxy-flags-changed", event_data.clone()) { log::error!("发射 proxy-flags-changed 事件失败: {e}"); diff --git a/src/components/proxy/FailoverQueueManager.tsx b/src/components/proxy/FailoverQueueManager.tsx index 5dc7120a2..9d56475dc 100644 --- a/src/components/proxy/FailoverQueueManager.tsx +++ b/src/components/proxy/FailoverQueueManager.tsx @@ -140,7 +140,8 @@ export function FailoverQueueManager({

{t("proxy.failover.autoSwitchDescription", { - defaultValue: "开启后,请求失败时自动切换到队列中的下一个供应商", + defaultValue: + "开启后将立即切换到队列 P1,并在请求失败时自动切换到队列中的下一个供应商", })}

diff --git a/src/components/proxy/FailoverToggle.tsx b/src/components/proxy/FailoverToggle.tsx index 9ec4c0222..9e6e23121 100644 --- a/src/components/proxy/FailoverToggle.tsx +++ b/src/components/proxy/FailoverToggle.tsx @@ -39,11 +39,11 @@ export function FailoverToggle({ className, activeApp }: FailoverToggleProps) { const tooltipText = isEnabled ? t("failover.tooltip.enabled", { app: appLabel, - defaultValue: `${appLabel} 故障转移已启用\n自动切换到下一个可用供应商`, + defaultValue: `${appLabel} 故障转移已启用\n按队列优先级(P1→P2→...)选择供应商`, }) : t("failover.tooltip.disabled", { app: appLabel, - defaultValue: `启用 ${appLabel} 故障转移\n当当前供应商失败时自动切换`, + defaultValue: `启用 ${appLabel} 故障转移\n将立即切换到队列 P1,并在失败时自动切换到下一个`, }); return ( diff --git a/src/components/proxy/ProxyPanel.tsx b/src/components/proxy/ProxyPanel.tsx index 84e5858b0..eda4692a8 100644 --- a/src/components/proxy/ProxyPanel.tsx +++ b/src/components/proxy/ProxyPanel.tsx @@ -50,8 +50,8 @@ export function ProxyPanel() { } }, [globalConfig]); - // 获取所有三个应用类型的故障转移队列(不包含当前供应商) - // 当前供应商始终优先,队列仅用于失败后的备用顺序 + // 获取所有三个应用类型的故障转移队列 + // 启用自动故障转移后,将按队列优先级(P1→P2→...)选择供应商 const { data: claudeQueue = [] } = useFailoverQueue("claude"); const { data: codexQueue = [] } = useFailoverQueue("codex"); const { data: geminiQueue = [] } = useFailoverQueue("gemini"); diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index bfccd61cc..63568863d 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1145,8 +1145,8 @@ "tooltip": "Failover priority {{priority}}" }, "tooltip": { - "enabled": "{{app}} failover enabled\nAutomatically switch to next available provider", - "disabled": "Enable {{app}} failover\nAutomatically switch when current provider fails" + "enabled": "{{app}} failover enabled\nRequests follow queue priority (P1→P2→...)", + "disabled": "Enable {{app}} failover\nSwitches to queue P1 immediately, then falls back on failures" } }, "proxy": { @@ -1248,15 +1248,18 @@ }, "switchFailed": "Switch failed: {{error}}", "failover": { - "proxyRequired": "Proxy service must be started to configure failover" + "proxyRequired": "Proxy service must be started to configure failover", + "autoSwitch": "Auto Failover", + "autoSwitchDescription": "When enabled, switches to queue P1 immediately and automatically tries the next provider in the queue on failures" }, "failoverQueue": { "title": "Failover Queue", "description": "Manage failover order for each app's providers", - "info": "The current active provider always takes priority. When requests fail, the system will try other providers in queue order.", + "info": "When auto failover is enabled, requests follow the queue priority order (P1 first). On failures, the system will try the next provider in the queue.", "selectProvider": "Select a provider to add to queue", "noAvailableProviders": "No providers available to add", "empty": "Failover queue is empty. Add providers to enable automatic failover.", + "orderHint": "Queue order matches the provider list order on the Home page. Reorder providers on the Home page to change priority.", "dragHint": "Drag providers to adjust failover order. Lower numbers have higher priority.", "toggleEnabled": "Enable/Disable", "addSuccess": "Added to failover queue", diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 7784a9104..29172b74d 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1145,8 +1145,8 @@ "tooltip": "フェイルオーバー優先度 {{priority}}" }, "tooltip": { - "enabled": "{{app}} フェイルオーバーが有効\n次の利用可能なプロバイダーに自動切り替え", - "disabled": "{{app}} フェイルオーバーを有効にする\n現在のプロバイダーが失敗した場合に自動切り替え" + "enabled": "{{app}} フェイルオーバーが有効\nキューの優先度(P1→P2→...)で使用します", + "disabled": "{{app}} フェイルオーバーを有効にする\nキューの P1 に即時切替し、失敗時は次を順に試行します" } }, "proxy": { @@ -1243,15 +1243,18 @@ }, "switchFailed": "切り替えに失敗しました: {{error}}", "failover": { - "proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります" + "proxyRequired": "フェイルオーバーを設定するには、プロキシサービスを先に起動する必要があります", + "autoSwitch": "自動フェイルオーバー", + "autoSwitchDescription": "有効にするとキューの P1 に即時切り替え、リクエスト失敗時はキュー内の次のプロバイダーを自動で試行します" }, "failoverQueue": { "title": "フェイルオーバーキュー", "description": "各アプリのプロバイダーのフェイルオーバー順序を管理します", - "info": "現在アクティブなプロバイダーが常に優先されます。リクエストが失敗すると、システムはキュー順に他のプロバイダーを試行します。", + "info": "自動フェイルオーバーを有効にすると、キューの優先度順(P1 優先)でプロバイダーを使用します。失敗時はキュー内の次のプロバイダーを順に試行します。", "selectProvider": "キューに追加するプロバイダーを選択", "noAvailableProviders": "追加できるプロバイダーがありません", "empty": "フェイルオーバーキューが空です。自動フェイルオーバーを有効にするにはプロバイダーを追加してください。", + "orderHint": "キューの順序はホームのプロバイダー一覧の順序と一致します。ホームでドラッグして順序を変更できます。", "dragHint": "ドラッグでフェイルオーバー順序を調整します。番号が小さいほど優先度が高くなります。", "toggleEnabled": "有効/無効", "addSuccess": "フェイルオーバーキューに追加しました", diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index d36f1638a..8c9bb049b 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1145,8 +1145,8 @@ "tooltip": "故障转移优先级 {{priority}}" }, "tooltip": { - "enabled": "{{app}} 故障转移已启用\n自动切换到下一个可用供应商", - "disabled": "启用 {{app}} 故障转移\n当当前供应商失败时自动切换" + "enabled": "{{app}} 故障转移已启用\n按队列优先级(P1→P2→...)选择供应商", + "disabled": "启用 {{app}} 故障转移\n将立即切换到队列 P1,并在失败时自动切换到下一个" } }, "proxy": { @@ -1248,15 +1248,18 @@ }, "switchFailed": "切换失败: {{error}}", "failover": { - "proxyRequired": "需要先启动代理服务才能配置故障转移" + "proxyRequired": "需要先启动代理服务才能配置故障转移", + "autoSwitch": "自动故障转移", + "autoSwitchDescription": "开启后将立即切换到队列 P1,并在请求失败时自动切换到队列中的下一个供应商" }, "failoverQueue": { "title": "故障转移队列", "description": "管理各应用的供应商故障转移顺序", - "info": "当前激活的供应商始终优先。当请求失败时,系统会按队列顺序依次尝试其他供应商。", + "info": "启用自动故障转移后,将按队列优先级选择供应商(P1 优先)。当请求失败时,系统会按队列顺序依次尝试下一个供应商。", "selectProvider": "选择供应商添加到队列", "noAvailableProviders": "没有可添加的供应商", "empty": "故障转移队列为空。添加供应商以启用自动故障转移。", + "orderHint": "队列顺序与首页供应商列表顺序一致,可在首页拖拽调整顺序。", "dragHint": "拖拽供应商可调整故障转移顺序,序号越小优先级越高。", "toggleEnabled": "启用/禁用", "addSuccess": "已添加到故障转移队列", diff --git a/src/lib/query/failover.ts b/src/lib/query/failover.ts index 10049eb80..a2a8be7c8 100644 --- a/src/lib/query/failover.ts +++ b/src/lib/query/failover.ts @@ -268,6 +268,21 @@ export function useSetAutoFailoverEnabled() { queryClient.invalidateQueries({ queryKey: ["autoFailoverEnabled", variables.appType], }); + // 启用/关闭故障转移可能触发: + // - 立即切到队列 P1(当前供应商变化) + // - 队列为空时自动把当前供应商加入队列(队列内容变化) + queryClient.invalidateQueries({ + queryKey: ["failoverQueue", variables.appType], + }); + queryClient.invalidateQueries({ + queryKey: ["availableProvidersForFailover", variables.appType], + }); + queryClient.invalidateQueries({ + queryKey: ["providers", variables.appType], + }); + queryClient.invalidateQueries({ + queryKey: ["proxyStatus"], + }); }, }); } From dc865fbbbf903a339d540d259d88618944c60467 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 21 Jan 2026 15:46:54 +0800 Subject: [PATCH 2/9] fix(icon): auto-apply default color from metadata when color prop is not provided When ProviderIcon is used without a color prop, it now automatically fetches the defaultColor from icon metadata. This fixes the issue where Gemini app icon turns black when selected in AppSwitcher, because the component was inheriting the parent's text-foreground color. --- src/components/ProviderIcon.tsx | 17 +++++++++++++++-- src/icons/extracted/index.ts | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/components/ProviderIcon.tsx b/src/components/ProviderIcon.tsx index f76f13ff2..0348d7f94 100644 --- a/src/components/ProviderIcon.tsx +++ b/src/components/ProviderIcon.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { getIcon, hasIcon } from "@/icons/extracted"; +import { getIcon, hasIcon, getIconMetadata } from "@/icons/extracted"; import { cn } from "@/lib/utils"; interface ProviderIconProps { @@ -39,6 +39,19 @@ export const ProviderIcon: React.FC = ({ }; }, [size]); + // 获取有效颜色:优先使用传入的 color,否则从元数据获取 defaultColor + const effectiveColor = useMemo(() => { + if (color) return color; + if (icon) { + const metadata = getIconMetadata(icon); + // 只有当 defaultColor 不是 currentColor 时才使用 + if (metadata?.defaultColor && metadata.defaultColor !== "currentColor") { + return metadata.defaultColor; + } + } + return undefined; + }, [color, icon]); + // 如果有图标,显示图标 if (iconSvg) { return ( @@ -47,7 +60,7 @@ export const ProviderIcon: React.FC = ({ "inline-flex items-center justify-center flex-shrink-0", className, )} - style={{ ...sizeStyle, color }} + style={{ ...sizeStyle, color: effectiveColor }} dangerouslySetInnerHTML={{ __html: iconSvg }} /> ); diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index 1545b6e1b..405f3aca2 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -65,3 +65,5 @@ export function getIcon(name: string): string { export function hasIcon(name: string): boolean { return name.toLowerCase() in icons; } + +export { getIconMetadata } from "./metadata"; From 2d17bde790a486b663cbab3208d9c4a403f2ec75 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 21 Jan 2026 16:20:28 +0800 Subject: [PATCH 3/9] feat(skills): add baoyu-skills preset repo and auto-supplement missing defaults - Add JimLiu/baoyu-skills to default skill repositories - Change init_default_skill_repos() from "first-run only" to "supplement missing" - New preset repos will now auto-appear for existing users on upgrade --- src-tauri/src/database/dao/skills.rs | 24 ++++++++++++++++-------- src-tauri/src/services/skill.rs | 6 ++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/database/dao/skills.rs b/src-tauri/src/database/dao/skills.rs index 67df0d2c1..2254cb061 100644 --- a/src-tauri/src/database/dao/skills.rs +++ b/src-tauri/src/database/dao/skills.rs @@ -203,24 +203,32 @@ impl Database { Ok(()) } - /// 初始化默认的 Skill 仓库(首次启动时调用) + /// 初始化默认的 Skill 仓库(启动时调用,补充缺失的默认仓库) pub fn init_default_skill_repos(&self) -> Result { - // 检查是否已有仓库 + // 获取已有仓库列表 let existing = self.get_skill_repos()?; - if !existing.is_empty() { - return Ok(0); - } + let existing_keys: std::collections::HashSet<(String, String)> = existing + .iter() + .map(|r| (r.owner.clone(), r.name.clone())) + .collect(); // 获取默认仓库列表 let default_store = crate::services::skill::SkillStore::default(); let mut count = 0; + // 仅插入缺失的默认仓库 for repo in &default_store.repos { - self.save_skill_repo(repo)?; - count += 1; + let key = (repo.owner.clone(), repo.name.clone()); + if !existing_keys.contains(&key) { + self.save_skill_repo(repo)?; + count += 1; + log::info!("补充默认 Skill 仓库: {}/{}", repo.owner, repo.name); + } } - log::info!("初始化默认 Skill 仓库完成,共 {count} 个"); + if count > 0 { + log::info!("补充默认 Skill 仓库完成,新增 {count} 个"); + } Ok(count) } } diff --git a/src-tauri/src/services/skill.rs b/src-tauri/src/services/skill.rs index 78feec7e5..85b661d8d 100644 --- a/src-tauri/src/services/skill.rs +++ b/src-tauri/src/services/skill.rs @@ -128,6 +128,12 @@ impl Default for SkillStore { branch: "master".to_string(), enabled: true, }, + SkillRepo { + owner: "JimLiu".to_string(), + name: "baoyu-skills".to_string(), + branch: "main".to_string(), + enabled: true, + }, ], } } From 3677bb61f538c5facba161633e5edfd475cbb449 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 21 Jan 2026 18:02:21 +0800 Subject: [PATCH 4/9] refactor(settings): move rectifier section below failover in advanced settings --- src/components/settings/SettingsPage.tsx | 44 ++++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/settings/SettingsPage.tsx b/src/components/settings/SettingsPage.tsx index 2eaf48089..8d2a66f66 100644 --- a/src/components/settings/SettingsPage.tsx +++ b/src/components/settings/SettingsPage.tsx @@ -462,6 +462,28 @@ export function SettingsPage({ + + +
+ +
+

+ {t("settings.advanced.rectifier.title")} +

+

+ {t("settings.advanced.rectifier.description")} +

+
+
+
+ + + +
+ - - -
- -
-

- {t("settings.advanced.rectifier.title")} -

-

- {t("settings.advanced.rectifier.description")} -

-
-
-
- - - -
- Date: Wed, 21 Jan 2026 20:01:29 +0800 Subject: [PATCH 5/9] chore: release v3.10.0 --- CHANGELOG.md | 42 +++++++++++++++++++++++++++++++++++++++ README.md | 4 ++-- README_JA.md | 4 ++-- README_ZH.md | 4 ++-- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 8 files changed, 52 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af37f440b..8f8ff5481 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 --- +## [3.10.0] - 2026-01-21 + +### Feature Release + +This release introduces OpenCode support and brings improvements across proxy, usage tracking, and overall UX. + +### Added + +- **OpenCode Support** - Manage OpenCode providers, MCP servers, and Skills, with first-launch import and full internationalization (#695) +- **Global Proxy** - Add global proxy settings for outbound network requests (#596) +- **Claude Rectifier** - Add thinking signature rectifier for Claude API (#595) +- **Health Check Enhancements** - Configurable prompt and CLI-compatible requests for stream health check (#623) +- **Per-Provider Config** - Support provider-specific configuration and persistence (#663) +- **App Visibility Controls** - Show/hide apps and keep tray menu in sync (Gemini hidden by default) +- **Takeover Compact Mode** - Use a compact AppSwitcher layout when showing 3+ visible apps +- **Keyboard Shortcut** - Press `ESC` to quickly go back/close panels (#670) +- **Terminal Improvements** - Provider-specific terminal button, `fnm` path support, and safer cross-platform launching (#564) +- **WSL Tool Detection** - Detect tool versions in WSL with additional security hardening (#627) +- **Skills Presets** - Add `baoyu-skills` preset repo and auto-supplement missing default repos + +### Changed + +- **Proxy Logging** - Simplify proxy log output (#585) +- **Pricing Editor UX** - Unify pricing edit modal with `FullScreenPanel` +- **Advanced Settings Layout** - Move rectifier section below failover for better flow +- **OpenRouter Compat Mode** - Disable OpenRouter compatibility mode by default and hide UI toggle + +### Fixed + +- **Auto Failover** - Switch to P1 immediately when enabling auto failover +- **Provider Edit Dialog** - Fix stale data when reopening provider editor after save (#654) +- **Deeplink** - Support multiple endpoints and prioritize `GOOGLE_GEMINI_BASE_URL` over `GEMINI_BASE_URL` (#597) +- **MCP (WSL)** - Skip `cmd /c` wrapper for WSL target paths (#592) +- **Usage Templates** - Add variable hints and validation fixes; prevent config leaking between providers (#628) +- **Gemini Timeout Format** - Convert timeout params to Gemini CLI format (#580) +- **UI** - Fix Select dropdown rendering in `FullScreenPanel`; auto-apply default icon color when unset +- **Usage UI** - Auto-adapt usage block offset based on action buttons width (#613) +- **Provider Endpoint** - Persist endpoint auto-select state (#611) +- **Provider Form** - Reset baseUrl and apiKey states when switching presets + +--- + ## [3.9.1] - 2026-01-09 ### Bug Fix Release diff --git a/README.md b/README.md index ee5fa2867..69f858cbc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # All-in-One Assistant for Claude Code, Codex & Gemini CLI -[![Version](https://img.shields.io/badge/version-3.9.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) +[![Version](https://img.shields.io/badge/version-3.10.0-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest) @@ -52,7 +52,7 @@ This project is sponsored by Z.ai, supporting us with their GLM CODING PLAN.GLM ## Features -### Current Version: v3.9.1 | [Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.9.0-en.md) +### Current Version: v3.10.0 | [Full Changelog](CHANGELOG.md) | [Release Notes](docs/release-note-v3.9.0-en.md) **v3.8.0 Major Update (2025-11-28)** diff --git a/README_JA.md b/README_JA.md index 063559cf6..ac22f4eef 100644 --- a/README_JA.md +++ b/README_JA.md @@ -2,7 +2,7 @@ # Claude Code / Codex / Gemini CLI オールインワン・アシスタント -[![Version](https://img.shields.io/badge/version-3.9.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) +[![Version](https://img.shields.io/badge/version-3.10.0-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest) @@ -52,7 +52,7 @@ ## 特長 -### 現在のバージョン:v3.9.1 | [完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.9.0-ja.md) +### 現在のバージョン:v3.10.0 | [完全な更新履歴](CHANGELOG.md) | [リリースノート](docs/release-note-v3.9.0-ja.md) **v3.8.0 メジャーアップデート (2025-11-28)** diff --git a/README_ZH.md b/README_ZH.md index a25b86af7..b34195e49 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -2,7 +2,7 @@ # Claude Code / Codex / Gemini CLI 全方位辅助工具 -[![Version](https://img.shields.io/badge/version-3.9.1-blue.svg)](https://github.com/farion1231/cc-switch/releases) +[![Version](https://img.shields.io/badge/version-3.10.0-blue.svg)](https://github.com/farion1231/cc-switch/releases) [![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-lightgrey.svg)](https://github.com/farion1231/cc-switch/releases) [![Built with Tauri](https://img.shields.io/badge/built%20with-Tauri%202-orange.svg)](https://tauri.app/) [![Downloads](https://img.shields.io/endpoint?url=https://api.pinstudios.net/api/badges/downloads/farion1231/cc-switch/total)](https://github.com/farion1231/cc-switch/releases/latest) @@ -52,7 +52,7 @@ ## 功能特性 -### 当前版本:v3.9.1 | [完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.9.0-zh.md) +### 当前版本:v3.10.0 | [完整更新日志](CHANGELOG.md) | [发布说明](docs/release-note-v3.9.0-zh.md) **v3.8.0 重大更新(2025-11-28)** diff --git a/package.json b/package.json index fe0f9fad5..e54f2aa60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cc-switch", - "version": "3.9.1", + "version": "3.10.0", "description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI", "type": "module", "scripts": { diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index e48e0090e..a727dfbdf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -701,7 +701,7 @@ dependencies = [ [[package]] name = "cc-switch" -version = "3.9.1" +version = "3.10.0" dependencies = [ "anyhow", "async-stream", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d767a28e6..06f359de7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cc-switch" -version = "3.9.1" +version = "3.10.0" description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI" authors = ["Jason Young"] license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 5b784f1dd..dc8fd6dd4 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CC Switch", - "version": "3.9.1", + "version": "3.10.0", "identifier": "com.ccswitch.desktop", "build": { "frontendDist": "../dist", From 9736861ace863252fcc5e813b7ad2cee5f6f8504 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 21 Jan 2026 23:36:18 +0800 Subject: [PATCH 6/9] feat(icons): add new provider icons and fix SiliconFlow iconColor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add rc.svg for RightCode (optimized from 96KB to 581 bytes) - Add catcoder.svg for KAT-Coder (快手) - Add mcp.svg for Model Context Protocol - Add siliconflow.svg for SiliconFlow - Register all new icons in index.ts - Add icon config for RightCode in claude/codex presets - Add icon config for KAT-Coder in claude/opencode presets - Fix SiliconFlow iconColor from #000000 to #6E29F6 --- src/config/claudeProviderPresets.ts | 53 ++++++++++++++++++++++++++- src/config/codexProviderPresets.ts | 14 +++++++ src/config/opencodeProviderPresets.ts | 1 + src/icons/extracted/catcoder.svg | 1 + src/icons/extracted/index.ts | 4 ++ src/icons/extracted/mcp.svg | 1 + src/icons/extracted/rc.svg | 6 +++ src/icons/extracted/siliconflow.svg | 1 + 8 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 src/icons/extracted/catcoder.svg create mode 100644 src/icons/extracted/mcp.svg create mode 100644 src/icons/extracted/rc.svg create mode 100644 src/icons/extracted/siliconflow.svg diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 5eda7f8d4..49254e29d 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -100,7 +100,7 @@ export const providerPresets: ProviderPreset[] = [ iconColor: "#0F62FE", }, { - name: "Z.ai GLM", + name: "Zhipu GLM en", websiteUrl: "https://z.ai", apiKeyUrl: "https://z.ai/subscribe?ic=8JVLJQFSKB", settingsConfig: { @@ -211,6 +211,7 @@ export const providerPresets: ProviderPreset[] = [ editorValue: "", }, }, + icon: "catcoder", }, { name: "Longcat", @@ -336,6 +337,42 @@ export const providerPresets: ProviderPreset[] = [ icon: "aihubmix", iconColor: "#006FFB", }, + { + name: "SiliconFlow", + websiteUrl: "https://siliconflow.cn", + apiKeyUrl: "https://cloud.siliconflow.cn/me/account/ak", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://api.siliconflow.cn", + ANTHROPIC_AUTH_TOKEN: "", + ANTHROPIC_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1", + ANTHROPIC_DEFAULT_HAIKU_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1", + ANTHROPIC_DEFAULT_SONNET_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1", + ANTHROPIC_DEFAULT_OPUS_MODEL: "Pro/MiniMaxAI/MiniMax-M2.1", + }, + }, + category: "aggregator", + icon: "siliconflow", + iconColor: "#6E29F6", + }, + { + name: "SiliconFlow en", + websiteUrl: "https://siliconflow.com", + apiKeyUrl: "https://cloud.siliconflow.com/account/ak", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://api.siliconflow.com", + ANTHROPIC_AUTH_TOKEN: "", + ANTHROPIC_MODEL: "MiniMaxAI/MiniMax-M2.1", + ANTHROPIC_DEFAULT_HAIKU_MODEL: "MiniMaxAI/MiniMax-M2.1", + ANTHROPIC_DEFAULT_SONNET_MODEL: "MiniMaxAI/MiniMax-M2.1", + ANTHROPIC_DEFAULT_OPUS_MODEL: "MiniMaxAI/MiniMax-M2.1", + }, + }, + category: "aggregator", + icon: "siliconflow", + iconColor: "#000000", + }, { name: "DMXAPI", websiteUrl: "https://www.dmxapi.cn", @@ -412,6 +449,20 @@ export const providerPresets: ProviderPreset[] = [ icon: "aigocode", iconColor: "#5B7FFF", }, + { + name: "RightCode", + websiteUrl: "https://www.right.codes", + apiKeyUrl: "https://www.right.codes/register?aff=0bdf9bfa", + settingsConfig: { + env: { + ANTHROPIC_BASE_URL: "https://www.right.codes/claude", + ANTHROPIC_AUTH_TOKEN: "", + }, + }, + category: "third_party", + icon: "rc", + iconColor: "#E96B2C", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/codexProviderPresets.ts b/src/config/codexProviderPresets.ts index e728c8af9..6622a2cf5 100644 --- a/src/config/codexProviderPresets.ts +++ b/src/config/codexProviderPresets.ts @@ -192,6 +192,20 @@ requires_openai_auth = true`, icon: "aigocode", iconColor: "#5B7FFF", }, + { + name: "RightCode", + websiteUrl: "https://www.right.codes", + apiKeyUrl: "https://www.right.codes/register?aff=0bdf9bfa", + auth: generateThirdPartyAuth(""), + config: generateThirdPartyConfig( + "rightcode", + "https://right.codes/codex/v1", + "gpt-5.2", + ), + category: "third_party", + icon: "rc", + iconColor: "#E96B2C", + }, { name: "OpenRouter", websiteUrl: "https://openrouter.ai", diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 48014a88a..15d5cf696 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -302,6 +302,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ editorValue: "", }, }, + icon: "catcoder", }, { name: "Longcat", diff --git a/src/icons/extracted/catcoder.svg b/src/icons/extracted/catcoder.svg new file mode 100644 index 000000000..d9bf3d7c3 --- /dev/null +++ b/src/icons/extracted/catcoder.svg @@ -0,0 +1 @@ +KwaiKAT \ No newline at end of file diff --git a/src/icons/extracted/index.ts b/src/icons/extracted/index.ts index 405f3aca2..0ac9282d9 100644 --- a/src/icons/extracted/index.ts +++ b/src/icons/extracted/index.ts @@ -50,10 +50,14 @@ export const icons: Record = { zeroone: `01.AI`, zhipu: `Zhipu`, openrouter: `OpenRouter`, + rc: `RightCode`, longcat: `LongCat`, modelscope: `ModelScope`, aihubmix: `AiHubMix`, opencode: `OpenCode`, + siliconflow: `SiliconCloud`, + catcoder: `KwaiKAT`, + mcp: `ModelContextProtocol`, }; export const iconList = Object.keys(icons); diff --git a/src/icons/extracted/mcp.svg b/src/icons/extracted/mcp.svg new file mode 100644 index 000000000..5cd83a8bf --- /dev/null +++ b/src/icons/extracted/mcp.svg @@ -0,0 +1 @@ +ModelContextProtocol \ No newline at end of file diff --git a/src/icons/extracted/rc.svg b/src/icons/extracted/rc.svg new file mode 100644 index 000000000..b8089148b --- /dev/null +++ b/src/icons/extracted/rc.svg @@ -0,0 +1,6 @@ + + RightCode + + + + diff --git a/src/icons/extracted/siliconflow.svg b/src/icons/extracted/siliconflow.svg new file mode 100644 index 000000000..6b5f6d805 --- /dev/null +++ b/src/icons/extracted/siliconflow.svg @@ -0,0 +1 @@ +SiliconCloud \ No newline at end of file From 09d8c279722c44cb82a4f6692c578fb4d0b557d3 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 22 Jan 2026 00:01:32 +0800 Subject: [PATCH 7/9] feat(icons): replace MCP button icon with official MCP logo Use inline SVG for MCP icon to support currentColor inheritance, enabling proper hover color transitions. --- src/App.tsx | 4 ++-- src/components/BrandIcons.tsx | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 29be74cb7..498b29568 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -11,7 +11,6 @@ import { // Bot, // TODO: Agents 功能开发中,暂时不需要 Book, Wrench, - Server, RefreshCw, Search, Download, @@ -52,6 +51,7 @@ import UnifiedSkillsPanel from "@/components/skills/UnifiedSkillsPanel"; import { DeepLinkImportDialog } from "@/components/DeepLinkImportDialog"; import { AgentsPanel } from "@/components/agents/AgentsPanel"; import { UniversalProviderPanel } from "@/components/universal"; +import { McpIcon } from "@/components/BrandIcons"; import { Button } from "@/components/ui/button"; type View = @@ -943,7 +943,7 @@ function App() { className="text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5" title={t("mcp.title")} > - + diff --git a/src/components/BrandIcons.tsx b/src/components/BrandIcons.tsx index 54ead109d..f6a68c67a 100644 --- a/src/components/BrandIcons.tsx +++ b/src/components/BrandIcons.tsx @@ -46,3 +46,21 @@ export function GeminiIcon({ size = 16, className = "" }: IconProps) { /> ); } + +// MCP icon uses inline SVG to support currentColor for hover effects +export function McpIcon({ size = 16, className = "" }: IconProps) { + return ( + + + + + ); +} From b1f7ff4768026f9ec1db39f49b9f1d8e3693c104 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 22 Jan 2026 00:07:35 +0800 Subject: [PATCH 8/9] fix(config): remove icon config from provider preset Remove unused icon and iconColor fields from Requesty Cloud preset --- src/config/claudeProviderPresets.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/config/claudeProviderPresets.ts b/src/config/claudeProviderPresets.ts index 49254e29d..9d9904a57 100644 --- a/src/config/claudeProviderPresets.ts +++ b/src/config/claudeProviderPresets.ts @@ -459,9 +459,7 @@ export const providerPresets: ProviderPreset[] = [ ANTHROPIC_AUTH_TOKEN: "", }, }, - category: "third_party", - icon: "rc", - iconColor: "#E96B2C", + category: "third_party" }, { name: "OpenRouter", From e6b6bc18a959b9f3efbda2509b613cdd9e6a7c56 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 22 Jan 2026 10:35:43 +0800 Subject: [PATCH 9/9] feat(config): add RightCode provider and rename Z.ai GLM - Add RightCode provider preset with GPT-5.2 models - Rename "Z.ai GLM" to "Zhipu GLM en" for clarity --- src/config/opencodeProviderPresets.ts | 31 +++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/config/opencodeProviderPresets.ts b/src/config/opencodeProviderPresets.ts index 15d5cf696..99cb08bec 100644 --- a/src/config/opencodeProviderPresets.ts +++ b/src/config/opencodeProviderPresets.ts @@ -103,12 +103,12 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, { - name: "Z.ai GLM", + name: "Zhipu GLM en", websiteUrl: "https://z.ai", apiKeyUrl: "https://z.ai/subscribe?ic=8JVLJQFSKB", settingsConfig: { npm: "@ai-sdk/openai-compatible", - name: "Z.ai GLM", + name: "Zhipu GLM en", options: { baseURL: "https://api.z.ai/v1", apiKey: "", @@ -646,6 +646,33 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [ }, }, }, + { + name: "RightCode", + websiteUrl: "https://www.right.codes", + apiKeyUrl: "https://www.right.codes/register?aff=0bdf9bfa", + settingsConfig: { + npm: "@ai-sdk/openai", + name: "RightCode", + options: { + baseURL: "https://right.codes/codex/v1", + apiKey: "", + }, + models: { + "gpt-5.2": { name: "GPT-5.2" }, + "gpt-5.2-codex": { name: "GPT-5.2 Codex" }, + }, + }, + category: "third_party", + icon: "rc", + iconColor: "#E96B2C", + templateValues: { + apiKey: { + label: "API Key", + placeholder: "", + editorValue: "", + }, + }, + }, // ========== 自定义模板 ========== {