Merge branch 'main' into feat/provider-chat-completions

This commit is contained in:
YoVinchen
2026-01-22 11:08:20 +08:00
34 changed files with 597 additions and 75 deletions
+42
View File
@@ -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
+2 -2
View File
@@ -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)**
+2 -2
View File
@@ -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)**
+2 -2
View File
@@ -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**
+1 -1
View File
@@ -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": {
+1 -1
View File
@@ -701,7 +701,7 @@ dependencies = [
[[package]]
name = "cc-switch"
version = "3.9.1"
version = "3.10.0"
dependencies = [
"anyhow",
"async-stream",
+1 -1
View File
@@ -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"
+59
View File
@@ -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, &current_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") {
+16 -8
View File
@@ -203,24 +203,32 @@ impl Database {
Ok(())
}
/// 初始化默认的 Skill 仓库(首次启动时调用)
/// 初始化默认的 Skill 仓库(启动时调用,补充缺失的默认仓库
pub fn init_default_skill_repos(&self) -> Result<usize, AppError> {
// 检查是否已有仓库
// 获取已有仓库列表
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)
}
}
+115 -9
View File
@@ -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<Vec<Provider>, 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<String> = 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(&current_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<String>,
original_userprofile: Option<String>,
}
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
+12
View File
@@ -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)
+95 -1
View File
@@ -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);
}
}
+6
View File
@@ -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,
},
],
}
}
+34 -1
View File
@@ -211,6 +211,28 @@ fn handle_auto_click(app: &tauri::AppHandle, app_type: &AppType) -> Result<(), A
if let Some(app_state) = app.try_state::<AppState>() {
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, &current_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}");
+1 -1
View File
@@ -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",
+2 -2
View File
@@ -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")}
>
<Server className="w-4 h-4" />
<McpIcon size={16} />
</Button>
</div>
+18
View File
@@ -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 (
<svg
fill="currentColor"
fillRule="evenodd"
height={size}
width={size}
className={className}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z" />
<path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z" />
</svg>
);
}
+15 -2
View File
@@ -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<ProviderIconProps> = ({
};
}, [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<ProviderIconProps> = ({
"inline-flex items-center justify-center flex-shrink-0",
className,
)}
style={{ ...sizeStyle, color }}
style={{ ...sizeStyle, color: effectiveColor }}
dangerouslySetInnerHTML={{ __html: iconSvg }}
/>
);
@@ -140,7 +140,8 @@ export function FailoverQueueManager({
</div>
<p className="text-xs text-muted-foreground">
{t("proxy.failover.autoSwitchDescription", {
defaultValue: "开启后,请求失败时自动切换到队列中的下一个供应商",
defaultValue:
"开启后将立即切换到队列 P1,并在请求失败时自动切换到队列中的下一个供应商",
})}
</p>
</div>
+2 -2
View File
@@ -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 (
+2 -2
View File
@@ -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");
+22 -22
View File
@@ -462,6 +462,28 @@ export function SettingsPage({
</AccordionContent>
</AccordionItem>
<AccordionItem
value="rectifier"
className="rounded-xl glass-card overflow-hidden"
>
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
<div className="flex items-center gap-3">
<Zap className="h-5 w-5 text-purple-500" />
<div className="text-left">
<h3 className="text-base font-semibold">
{t("settings.advanced.rectifier.title")}
</h3>
<p className="text-sm text-muted-foreground font-normal">
{t("settings.advanced.rectifier.description")}
</p>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
<RectifierConfigPanel />
</AccordionContent>
</AccordionItem>
<AccordionItem
value="test"
className="rounded-xl glass-card overflow-hidden"
@@ -560,28 +582,6 @@ export function SettingsPage({
</AccordionContent>
</AccordionItem>
<AccordionItem
value="rectifier"
className="rounded-xl glass-card overflow-hidden"
>
<AccordionTrigger className="px-6 py-4 hover:no-underline hover:bg-muted/50 data-[state=open]:bg-muted/50">
<div className="flex items-center gap-3">
<Zap className="h-5 w-5 text-purple-500" />
<div className="text-left">
<h3 className="text-base font-semibold">
{t("settings.advanced.rectifier.title")}
</h3>
<p className="text-sm text-muted-foreground font-normal">
{t("settings.advanced.rectifier.description")}
</p>
</div>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6 pt-4 border-t border-border/50">
<RectifierConfigPanel />
</AccordionContent>
</AccordionItem>
<AccordionItem
value="logConfig"
className="rounded-xl glass-card overflow-hidden"
+50 -1
View File
@@ -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,18 @@ 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"
},
{
name: "OpenRouter",
websiteUrl: "https://openrouter.ai",
+14
View File
@@ -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",
+30 -2
View File
@@ -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: "",
@@ -302,6 +302,7 @@ export const opencodeProviderPresets: OpenCodeProviderPreset[] = [
editorValue: "",
},
},
icon: "catcoder",
},
{
name: "Longcat",
@@ -645,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: "",
},
},
},
// ========== 自定义模板 ==========
{
+7 -4
View File
@@ -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",
+7 -4
View File
@@ -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": "フェイルオーバーキューに追加しました",
+7 -4
View File
@@ -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": "已添加到故障转移队列",
+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>KwaiKAT</title><path d="M20.42 19.311h3.418V1l-6.781 4.177-6.778-4.111.026 7.868h3.418l-.026-2.222 3.42 2.035 3.303-2.035v12.6z"></path><path d="M3.064 10.734c2.784-2.07 6.942-2.394 9.941.907l.01.01.01.013 9.16 12.24h-3.84l-7.69-10.217c-1.63-1.737-3.891-1.689-5.515-.638-1.624 1.05-2.563 3.073-1.548 5.28 1.494 3.246 6.152 3.275 7.725.108l.032-.064 2.02 2.629c-2.98 3.968-9.329 3.926-12.165-.552-2.395-3.78-.926-7.645 1.86-9.716z"></path></svg>

After

Width:  |  Height:  |  Size: 608 B

+6
View File
@@ -50,10 +50,14 @@ export const icons: Record<string, string> = {
zeroone: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>01.AI</title><path d="M5.246 12c0 .837-.086 1.554-.257 2.151-.172.598-.45 1.055-.837 1.373-.386.317-.898.476-1.534.476-.901 0-1.563-.353-1.985-1.059C.211 14.235 0 13.255 0 12c0-.837.086-1.554.257-2.151.172-.598.45-1.055.832-1.373C1.472 8.16 1.981 8 2.618 8c.894 0 1.555.351 1.985 1.053.429.702.643 1.685.643 2.947zm-3.883 0c0 .956.09 1.668.273 2.134.183.467.51.7.982.7.465 0 .792-.23.981-.694.19-.463.285-1.176.285-2.14 0-.956-.095-1.668-.285-2.134-.19-.467-.516-.7-.981-.7-.472 0-.8.233-.982.7-.182.466-.273 1.178-.273 2.134zm8.52 3.771H8.517l.011-6.295-1.823.324V8.571l2.04-.457h1.136v7.657zm2.497-1.6h.543c.3 0 .543.256.543.572v.571a.558.558 0 01-.543.572h-.543a.558.558 0 01-.543-.572v-.571c0-.316.243-.572.543-.572zm10.317-6.057H24v7.772h-1.303V8.114zm-3.692 0l2.606 7.772h-1.303l-.69-2.058h-3.073l-.69 2.058h-1.303l2.606-7.772h1.847zm.191 4.457l-1.115-3.323-1.114 3.323h2.23z"></path></svg>`,
zhipu: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Zhipu</title><path d="M11.991 23.503a.24.24 0 00-.244.248.24.24 0 00.244.249.24.24 0 00.245-.249.24.24 0 00-.22-.247l-.025-.001zM9.671 5.365a1.697 1.697 0 011.099 2.132l-.071.172-.016.04-.018.054c-.07.16-.104.32-.104.498-.035.71.47 1.279 1.186 1.314h.366c1.309.053 2.338 1.173 2.286 2.523-.052 1.332-1.152 2.38-2.478 2.327h-.174c-.715.018-1.274.64-1.239 1.368 0 .124.018.23.053.337.209.373.54.658.96.8.75.23 1.517-.125 1.9-.782l.018-.035c.402-.64 1.17-.96 1.92-.711.854.284 1.378 1.226 1.099 2.167a1.661 1.661 0 01-2.077 1.102 1.711 1.711 0 01-.907-.711l-.017-.035c-.2-.323-.463-.58-.851-.711l-.056-.018a1.646 1.646 0 00-1.954.746 1.66 1.66 0 01-1.065.764 1.677 1.677 0 01-1.989-1.279c-.209-.906.332-1.83 1.257-2.043a1.51 1.51 0 01.296-.035h.018c.68-.071 1.151-.622 1.116-1.333a1.307 1.307 0 00-.227-.693 2.515 2.515 0 01-.366-1.403 2.39 2.39 0 01.366-1.208c.14-.195.21-.444.227-.693.018-.71-.506-1.261-1.186-1.332l-.07-.018a1.43 1.43 0 01-.299-.07l-.05-.019a1.7 1.7 0 01-1.047-2.114 1.68 1.68 0 012.094-1.101zm-5.575 10.11c.26-.264.639-.367.994-.27.355.096.633.379.728.74.095.362-.007.748-.267 1.013-.402.41-1.053.41-1.455 0a1.062 1.062 0 010-1.482zm14.845-.294c.359-.09.738.024.992.297.254.274.344.665.237 1.025-.107.36-.396.634-.756.718-.551.128-1.1-.22-1.23-.781a1.05 1.05 0 01.757-1.26zm-.064-4.39c.314.32.49.753.49 1.206 0 .452-.176.886-.49 1.206-.315.32-.74.5-1.185.5-.444 0-.87-.18-1.184-.5a1.727 1.727 0 010-2.412 1.654 1.654 0 012.369 0zm-11.243.163c.364.484.447 1.128.218 1.691a1.665 1.665 0 01-2.188.923c-.855-.36-1.26-1.358-.907-2.228a1.68 1.68 0 011.33-1.038c.593-.08 1.183.169 1.547.652zm11.545-4.221c.368 0 .708.2.892.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.892.524c-.568 0-1.03-.47-1.03-1.048 0-.579.462-1.048 1.03-1.048zm-14.358 0c.368 0 .707.2.891.524.184.324.184.724 0 1.048a1.026 1.026 0 01-.891.524c-.569 0-1.03-.47-1.03-1.048 0-.579.461-1.048 1.03-1.048zm10.031-1.475c.925 0 1.675.764 1.675 1.706s-.75 1.705-1.675 1.705-1.674-.763-1.674-1.705c0-.942.75-1.706 1.674-1.706zm-2.626-.684c.362-.082.653-.356.761-.718a1.062 1.062 0 00-.238-1.028 1.017 1.017 0 00-.996-.294c-.547.14-.881.7-.752 1.257.13.558.675.907 1.225.783zm0 16.876c.359-.087.644-.36.75-.72a1.062 1.062 0 00-.237-1.019 1.018 1.018 0 00-.985-.301 1.037 1.037 0 00-.762.717c-.108.361-.017.754.239 1.028.245.263.606.377.953.305l.043-.01zM17.19 3.5a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64a.631.631 0 00-.628.64c0 .355.28.64.628.64zm-10.38 0a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64a.631.631 0 00-.628.64c0 .355.279.64.628.64zm-5.182 7.852a.631.631 0 00-.628.64c0 .354.28.639.628.639a.63.63 0 00.627-.606l.001-.034a.62.62 0 00-.628-.64zm5.182 9.13a.631.631 0 00-.628.64c0 .355.279.64.628.64a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm10.38.018a.631.631 0 00-.628.64c0 .355.28.64.628.64a.631.631 0 00.628-.64c0-.355-.279-.64-.628-.64zm5.182-9.148a.631.631 0 00-.628.64c0 .354.279.639.628.639a.631.631 0 00.628-.64c0-.355-.28-.64-.628-.64zm-.384-4.992a.24.24 0 00.244-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249c0 .142.122.249.244.249zM11.991.497a.24.24 0 00.245-.248A.24.24 0 0011.99 0a.24.24 0 00-.244.249c0 .133.108.236.223.247l.021.001zM2.011 6.36a.24.24 0 00.245-.249.24.24 0 00-.244-.249.24.24 0 00-.244.249.24.24 0 00.244.249zm0 11.263a.24.24 0 00-.243.248.24.24 0 00.244.249.24.24 0 00.244-.249.252.252 0 00-.244-.248zm19.995-.018a.24.24 0 00-.245.248.24.24 0 00.245.25.24.24 0 00.244-.25.252.252 0 00-.244-.248z" fill="#3859FF" fill-rule="nonzero"></path></svg>`,
openrouter: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenRouter</title><path d="M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z"></path></svg>`,
rc: `<svg viewBox="0 0 24 24" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg"><title>RightCode</title><path d="M3 4h6c2.2 0 4 1.8 4 4 0 1.5-.8 2.8-2 3.5L14 17h-3l-2.5-5H5v5H3V4zm2 2v4h4c1.1 0 2-.9 2-2s-.9-2-2-2H5z" fill="#E96B2C"/><path d="M21 8.5c0-2.5-2-4.5-4.5-4.5-1.5 0-2.8.7-3.6 1.8l1.5 1.2c.5-.7 1.2-1 2.1-1 1.4 0 2.5 1.1 2.5 2.5v.5h-1c-2.5 0-4.5 1.3-4.5 3.5 0 1.9 1.6 3.5 3.5 3.5 1.2 0 2.3-.6 3-1.5V16h2V8.5zM17 14c-1.1 0-2-.7-2-1.5s.9-1.5 2-1.5h2v1c0 1.1-.9 2-2 2z" fill="#E96B2C"/><rect x="3" y="18" width="18" height="2" rx="1" fill="#E96B2C"/></svg>`,
longcat: `<svg fill="currentColor" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>LongCat</title><path clip-rule="evenodd" d="M.507 19.883a.507.507 0 01-.489-.642L4.29 3.745a1.013 1.013 0 011.533-.578l5.622 3.687a1.013 1.013 0 001.11 0L18.2 3.165a1.013 1.013 0 011.532.58l4.25 15.497a.506.506 0 01-.49.64H18.07a6.297 6.297 0 001.53-4.115v-.177a6.09 6.09 0 00-1.513-4.017l-.697-3.495a.438.438 0 00-.694-.266L14.07 9.781a.748.748 0 01-.654.121 5.156 5.156 0 00-2.833 0 .746.746 0 01-.653-.121L7.302 7.81a.435.435 0 00-.688.269l-.675 3.652a5.36 5.36 0 00-1.539 3.76v.333c0 1.474.527 2.9 1.488 4.02l.032.038H.507z" fill="#29E154" fill-rule="evenodd"></path><path d="M9.213 16.843h1.52v-3.546h-1.29l-.23 3.546zm5.573 0h-1.52v-3.546h1.29l.23 3.546z"></path></svg>`,
modelscope: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelScope</title><path d="M0 7.967h2.667v2.667H0zM8 10.633h2.667V13.3H8z" fill="#36CED0"></path><path d="M0 10.633h2.667V13.3H0zM2.667 13.3h2.666v2.667H8v2.666H2.667V13.3zM2.667 5.3H8v2.667H5.333v2.666H2.667V5.3zM10.667 13.3h2.667v2.667h-2.667z" fill="#624AFF"></path><path d="M24 7.967h-2.667v2.667H24zM16 10.633h-2.667V13.3H16z" fill="#36CED0"></path><path d="M24 10.633h-2.667V13.3H24zM21.333 13.3h-2.666v2.667H16v2.666h5.333V13.3zM21.333 5.3H16v2.667h2.667v2.666h2.666V5.3z" fill="#624AFF"></path></svg>`,
aihubmix: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>AiHubMix</title><path d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12z" fill="#006FFB"></path><path clip-rule="evenodd" d="M11.24 8.393c.095-.644.302-1.47.624-2.48L12 5.496l.136.417c.322 1.01.53 1.836.624 2.48.071.472.071 1.072 0 1.8-.072.731-.072 1.336 0 1.814.106.7.426 1.281.96 1.744a2.795 2.795 0 001.89.708 2.78 2.78 0 002.034-.84c.56-.559.842-1.234.848-2.024.003-.7.075-1.472.216-2.316.069-.422.14-.775.21-1.06l.095-.384.168.356a7.862 7.862 0 01.76 3.244v.16a7.84 7.84 0 01-.624 3.089 7.952 7.952 0 01-4.228 4.228 7.841 7.841 0 01-3.089.623 7.84 7.84 0 01-3.089-.623 7.952 7.952 0 01-4.228-4.228 7.84 7.84 0 01-.623-3.09v-.159a7.862 7.862 0 01.759-3.244l.169-.356.093.385c.072.284.143.637.211 1.059.141.844.213 1.616.216 2.316.006.79.29 1.465.848 2.024.563.56 1.241.84 2.035.84.715 0 1.345-.236 1.889-.708a2.79 2.79 0 00.96-1.744c.073-.478.073-1.083 0-1.814-.071-.728-.071-1.328 0-1.8zm.76 9.694c1.097 0 2.125-.26 3.085-.778a6.379 6.379 0 001.77-1.399c.063-.07-.01-.178-.101-.153-.37.1-.75.15-1.144.15a4.236 4.236 0 01-2.18-.59 4.253 4.253 0 01-1.35-1.233.099.099 0 00-.16 0 4.253 4.253 0 01-1.35 1.232 4.236 4.236 0 01-2.18.591c-.393 0-.774-.05-1.143-.15-.091-.025-.165.083-.102.153a6.38 6.38 0 001.77 1.399c.96.518 1.988.778 3.085.778z" fill="#fff" fill-rule="evenodd"></path></svg>`,
opencode: `<svg height="1em" width="1em" style="flex:none;line-height:1" viewBox="0 0 240 300" xmlns="http://www.w3.org/2000/svg"><title>OpenCode</title><g clip-path="url(#clip0_1401_86274)"><mask id="mask0_1401_86274" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="240" height="300"><path d="M240 0H0V300H240V0Z" fill="white"/></mask><g mask="url(#mask0_1401_86274)"><path d="M180 240H60V120H180V240Z" fill="#CFCECD"/><path d="M180 60H60V240H180V60ZM240 300H0V0H240V300Z" fill="#211E1E"/></g></g><defs><clipPath id="clip0_1401_86274"><rect width="240" height="300" fill="white"/></clipPath></defs></svg>`,
siliconflow: `<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z" fill="#6E29F6" fill-rule="evenodd"></path></svg>`,
catcoder: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>KwaiKAT</title><path d="M20.42 19.311h3.418V1l-6.781 4.177-6.778-4.111.026 7.868h3.418l-.026-2.222 3.42 2.035 3.303-2.035v12.6z"></path><path d="M3.064 10.734c2.784-2.07 6.942-2.394 9.941.907l.01.01.01.013 9.16 12.24h-3.84l-7.69-10.217c-1.63-1.737-3.891-1.689-5.515-.638-1.624 1.05-2.563 3.073-1.548 5.28 1.494 3.246 6.152 3.275 7.725.108l.032-.064 2.02 2.629c-2.98 3.968-9.329 3.926-12.165-.552-2.395-3.78-.926-7.645 1.86-9.716z"></path></svg>`,
mcp: `<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>`,
};
export const iconList = Object.keys(icons);
@@ -65,3 +69,5 @@ export function getIcon(name: string): string {
export function hasIcon(name: string): boolean {
return name.toLowerCase() in icons;
}
export { getIconMetadata } from "./metadata";
+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>ModelContextProtocol</title><path d="M15.688 2.343a2.588 2.588 0 00-3.61 0l-9.626 9.44a.863.863 0 01-1.203 0 .823.823 0 010-1.18l9.626-9.44a4.313 4.313 0 016.016 0 4.116 4.116 0 011.204 3.54 4.3 4.3 0 013.609 1.18l.05.05a4.115 4.115 0 010 5.9l-8.706 8.537a.274.274 0 000 .393l1.788 1.754a.823.823 0 010 1.18.863.863 0 01-1.203 0l-1.788-1.753a1.92 1.92 0 010-2.754l8.706-8.538a2.47 2.47 0 000-3.54l-.05-.049a2.588 2.588 0 00-3.607-.003l-7.172 7.034-.002.002-.098.097a.863.863 0 01-1.204 0 .823.823 0 010-1.18l7.273-7.133a2.47 2.47 0 00-.003-3.537z"></path><path d="M14.485 4.703a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a4.115 4.115 0 000 5.9 4.314 4.314 0 006.016 0l7.12-6.982a.823.823 0 000-1.18.863.863 0 00-1.204 0l-7.119 6.982a2.588 2.588 0 01-3.61 0 2.47 2.47 0 010-3.54l7.12-6.982z"></path></svg>

After

Width:  |  Height:  |  Size: 978 B

+6
View File
@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" width="1em" height="1em" xmlns="http://www.w3.org/2000/svg">
<title>RightCode</title>
<path d="M3 4h6c2.2 0 4 1.8 4 4 0 1.5-.8 2.8-2 3.5L14 17h-3l-2.5-5H5v5H3V4zm2 2v4h4c1.1 0 2-.9 2-2s-.9-2-2-2H5z" fill="#E96B2C"/>
<path d="M21 8.5c0-2.5-2-4.5-4.5-4.5-1.5 0-2.8.7-3.6 1.8l1.5 1.2c.5-.7 1.2-1 2.1-1 1.4 0 2.5 1.1 2.5 2.5v.5h-1c-2.5 0-4.5 1.3-4.5 3.5 0 1.9 1.6 3.5 3.5 3.5 1.2 0 2.3-.6 3-1.5V16h2V8.5zM17 14c-1.1 0-2-.7-2-1.5s.9-1.5 2-1.5h2v1c0 1.1-.9 2-2 2z" fill="#E96B2C"/>
<rect x="3" y="18" width="18" height="2" rx="1" fill="#E96B2C"/>
</svg>

After

Width:  |  Height:  |  Size: 581 B

+1
View File
@@ -0,0 +1 @@
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>SiliconCloud</title><path clip-rule="evenodd" d="M22.956 6.521H12.522c-.577 0-1.044.468-1.044 1.044v3.13c0 .577-.466 1.044-1.043 1.044H1.044c-.577 0-1.044.467-1.044 1.044v4.174C0 17.533.467 18 1.044 18h10.434c.577 0 1.044-.467 1.044-1.043v-3.13c0-.578.466-1.044 1.043-1.044h9.391c.577 0 1.044-.467 1.044-1.044V7.565c0-.576-.467-1.044-1.044-1.044z" fill="#6E29F6" fill-rule="evenodd"></path></svg>

After

Width:  |  Height:  |  Size: 520 B

+15
View File
@@ -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"],
});
},
});
}