mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 21:54:58 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6889e1d248 | |||
| 771bef16f5 |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cc-switch",
|
||||
"version": "3.9.0-2",
|
||||
"version": "3.9.0-1",
|
||||
"description": "All-in-One Assistant for Claude Code, Codex & Gemini CLI",
|
||||
"scripts": {
|
||||
"dev": "pnpm tauri dev",
|
||||
|
||||
Generated
+1
-1
@@ -695,7 +695,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc-switch"
|
||||
version = "3.9.0-2"
|
||||
version = "3.9.0-1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cc-switch"
|
||||
version = "3.9.0-2"
|
||||
version = "3.9.0-1"
|
||||
description = "All-in-One Assistant for Claude Code, Codex & Gemini CLI"
|
||||
authors = ["Jason Young"]
|
||||
license = "MIT"
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::store::AppState;
|
||||
pub async fn start_proxy_server(
|
||||
state: tauri::State<'_, AppState>,
|
||||
) -> Result<ProxyServerInfo, String> {
|
||||
state.proxy_service.start().await
|
||||
state.proxy_service.start(true).await
|
||||
}
|
||||
|
||||
/// 启动代理服务器(带 Live 配置接管)
|
||||
|
||||
@@ -13,8 +13,6 @@ use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
const CC_SWITCH_SQL_EXPORT_HEADER: &str = "-- CC Switch SQLite 导出";
|
||||
|
||||
impl Database {
|
||||
/// 导出为 SQLite 兼容的 SQL 文本
|
||||
pub fn export_sql(&self, target_path: &Path) -> Result<(), AppError> {
|
||||
@@ -38,8 +36,7 @@ impl Database {
|
||||
}
|
||||
|
||||
let sql_raw = fs::read_to_string(source_path).map_err(|e| AppError::io(source_path, e))?;
|
||||
let sql_content = sql_raw.trim_start_matches('\u{feff}');
|
||||
Self::validate_cc_switch_sql_export(sql_content)?;
|
||||
let sql_content = Self::sanitize_import_sql(&sql_raw);
|
||||
|
||||
// 导入前备份现有数据库
|
||||
let backup_path = self.backup_database_file()?;
|
||||
@@ -54,7 +51,7 @@ impl Database {
|
||||
Connection::open(&temp_path).map_err(|e| AppError::Database(e.to_string()))?;
|
||||
|
||||
temp_conn
|
||||
.execute_batch(sql_content)
|
||||
.execute_batch(&sql_content)
|
||||
.map_err(|e| AppError::Database(format!("执行 SQL 导入失败: {e}")))?;
|
||||
|
||||
// 补齐缺失表/索引并进行基础校验
|
||||
@@ -96,17 +93,26 @@ impl Database {
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn validate_cc_switch_sql_export(sql: &str) -> Result<(), AppError> {
|
||||
let trimmed = sql.trim_start();
|
||||
if trimmed.starts_with(CC_SWITCH_SQL_EXPORT_HEADER) {
|
||||
return Ok(());
|
||||
/// 移除 SQLite 保留对象相关语句(如 sqlite_sequence),避免导入报错
|
||||
fn sanitize_import_sql(sql: &str) -> String {
|
||||
let mut cleaned = String::new();
|
||||
let lower_keyword = "sqlite_sequence";
|
||||
|
||||
for stmt in sql.split(';') {
|
||||
let trimmed = stmt.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.to_ascii_lowercase().contains(lower_keyword) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cleaned.push_str(trimmed);
|
||||
cleaned.push_str(";\n");
|
||||
}
|
||||
|
||||
Err(AppError::localized(
|
||||
"backup.sql.invalid_format",
|
||||
"仅支持导入由 CC Switch 导出的 SQL 备份文件。",
|
||||
"Only SQL backups exported by CC Switch are supported.",
|
||||
))
|
||||
cleaned
|
||||
}
|
||||
|
||||
/// 生成一致性快照备份,返回备份文件路径(不存在主库时返回 None)
|
||||
@@ -123,15 +129,8 @@ impl Database {
|
||||
|
||||
fs::create_dir_all(&backup_dir).map_err(|e| AppError::io(&backup_dir, e))?;
|
||||
|
||||
let base_id = format!("db_backup_{}", Utc::now().format("%Y%m%d_%H%M%S"));
|
||||
let mut backup_id = base_id.clone();
|
||||
let mut backup_path = backup_dir.join(format!("{backup_id}.db"));
|
||||
let mut counter = 1;
|
||||
while backup_path.exists() {
|
||||
backup_id = format!("{base_id}_{counter}");
|
||||
backup_path = backup_dir.join(format!("{backup_id}.db"));
|
||||
counter += 1;
|
||||
}
|
||||
let backup_id = format!("db_backup_{}", Utc::now().format("%Y%m%d_%H%M%S"));
|
||||
let backup_path = backup_dir.join(format!("{backup_id}.db"));
|
||||
|
||||
{
|
||||
let conn = lock_conn!(self.conn);
|
||||
|
||||
@@ -16,18 +16,19 @@ impl Database {
|
||||
let result = {
|
||||
let conn = lock_conn!(self.conn);
|
||||
conn.query_row(
|
||||
"SELECT listen_address, listen_port, max_retries,
|
||||
"SELECT enabled, listen_address, listen_port, max_retries,
|
||||
request_timeout, enable_logging, live_takeover_active
|
||||
FROM proxy_config WHERE id = 1",
|
||||
[],
|
||||
|row| {
|
||||
Ok(ProxyConfig {
|
||||
listen_address: row.get(0)?,
|
||||
listen_port: row.get::<_, i32>(1)? as u16,
|
||||
max_retries: row.get::<_, i32>(2)? as u8,
|
||||
request_timeout: row.get::<_, i32>(3)? as u64,
|
||||
enable_logging: row.get::<_, i32>(4)? != 0,
|
||||
live_takeover_active: row.get::<_, i32>(5).unwrap_or(0) != 0,
|
||||
enabled: row.get::<_, i32>(0)? != 0,
|
||||
listen_address: row.get(1)?,
|
||||
listen_port: row.get::<_, i32>(2)? as u16,
|
||||
max_retries: row.get::<_, i32>(3)? as u8,
|
||||
request_timeout: row.get::<_, i32>(4)? as u64,
|
||||
enable_logging: row.get::<_, i32>(5)? != 0,
|
||||
live_takeover_active: row.get::<_, i32>(6).unwrap_or(0) != 0,
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -56,7 +57,7 @@ impl Database {
|
||||
COALESCE((SELECT created_at FROM proxy_config WHERE id = 1), datetime('now')),
|
||||
datetime('now'))",
|
||||
rusqlite::params![
|
||||
0, // 已移除自动启用逻辑,保留列但固定为 0
|
||||
if config.enabled { 1 } else { 0 },
|
||||
config.listen_address,
|
||||
config.listen_port as i32,
|
||||
config.max_retries as i32,
|
||||
|
||||
+68
-34
@@ -524,12 +524,12 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
|
||||
// 异常退出恢复
|
||||
// 异常退出恢复 + 自动启动代理服务器
|
||||
let app_handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let state = app_handle.state::<AppState>();
|
||||
|
||||
// 检查是否有 Live 备份(表示上次异常退出时可能处于接管状态)
|
||||
// 检查是否有 Live 备份(表示上次有接管状态)
|
||||
let has_backups = match state.db.has_any_live_backup().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
@@ -537,15 +537,55 @@ pub fn run() {
|
||||
false
|
||||
}
|
||||
};
|
||||
// 检查 Live 配置是否仍处于被接管状态(包含占位符)
|
||||
let live_taken_over = state.proxy_service.detect_takeover_in_live_configs();
|
||||
|
||||
if has_backups || live_taken_over {
|
||||
log::warn!("检测到上次异常退出(存在接管残留),正在恢复 Live 配置...");
|
||||
if let Err(e) = state.proxy_service.recover_from_crash().await {
|
||||
log::error!("恢复 Live 配置失败: {e}");
|
||||
// 获取代理配置
|
||||
let proxy_config = match state.db.get_proxy_config().await {
|
||||
Ok(config) => Some(config),
|
||||
Err(e) => {
|
||||
log::error!("启动时获取代理配置失败: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
if has_backups {
|
||||
// 有备份说明上次退出时有接管状态
|
||||
// 检查 Live 配置是否仍处于被接管状态(包含占位符)
|
||||
let live_taken_over = state.proxy_service.detect_takeover_in_live_configs();
|
||||
|
||||
if live_taken_over {
|
||||
// Live 配置仍是接管状态,尝试重新启动代理服务以恢复接管
|
||||
log::info!("检测到上次接管状态,正在重新启动代理服务...");
|
||||
match state.proxy_service.start(false).await {
|
||||
Ok(info) => {
|
||||
log::info!("代理服务器已恢复启动: {}:{}", info.address, info.port);
|
||||
}
|
||||
Err(e) => {
|
||||
// 启动失败,恢复 Live 配置
|
||||
log::error!("恢复代理服务失败: {e},正在恢复 Live 配置...");
|
||||
if let Err(e) = state.proxy_service.recover_from_crash().await {
|
||||
log::error!("恢复 Live 配置失败: {e}");
|
||||
} else {
|
||||
log::info!("Live 配置已恢复");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::info!("Live 配置已恢复");
|
||||
// Live 配置已经是正常状态,清理残留备份
|
||||
log::info!("Live 配置已是正常状态,清理残留备份...");
|
||||
if let Err(e) = state.db.delete_all_live_backups().await {
|
||||
log::warn!("清理残留 Live 备份失败: {e}");
|
||||
}
|
||||
}
|
||||
} else if let Some(config) = proxy_config {
|
||||
// 没有备份,检查是否需要自动启动代理服务器(总开关)
|
||||
if config.enabled {
|
||||
log::info!("代理服务配置为启用,正在启动...");
|
||||
match state.proxy_service.start(true).await {
|
||||
Ok(info) => {
|
||||
log::info!("代理服务器自动启动成功: {}:{}", info.address, info.port)
|
||||
}
|
||||
Err(e) => log::error!("代理服务器自动启动失败: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -811,33 +851,27 @@ pub async fn cleanup_before_exit(app_handle: &tauri::AppHandle) {
|
||||
if let Some(state) = app_handle.try_state::<store::AppState>() {
|
||||
let proxy_service = &state.proxy_service;
|
||||
|
||||
// 退出时也需要兜底:代理可能已崩溃/未运行,但 Live 接管残留仍在(占位符/备份)。
|
||||
let has_backups = match state.db.has_any_live_backup().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
log::error!("退出时检查 Live 备份失败: {e}");
|
||||
false
|
||||
}
|
||||
};
|
||||
let live_taken_over = proxy_service.detect_takeover_in_live_configs();
|
||||
let needs_restore = has_backups || live_taken_over;
|
||||
|
||||
if needs_restore {
|
||||
log::info!("检测到接管残留,开始恢复 Live 配置...");
|
||||
if let Err(e) = proxy_service.stop_with_restore().await {
|
||||
log::error!("退出时恢复 Live 配置失败: {e}");
|
||||
} else {
|
||||
log::info!("已恢复 Live 配置");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 非接管模式:代理在运行则仅停止代理
|
||||
// 检查代理是否在运行
|
||||
if proxy_service.is_running().await {
|
||||
log::info!("检测到代理服务器正在运行,开始停止...");
|
||||
if let Err(e) = proxy_service.stop().await {
|
||||
log::error!("退出时停止代理失败: {e}");
|
||||
log::info!("检测到代理服务器正在运行,开始清理...");
|
||||
|
||||
// 检查是否处于 Live 接管模式
|
||||
if let Ok(is_takeover) = state.db.is_live_takeover_active().await {
|
||||
if is_takeover {
|
||||
// 接管模式:停止并恢复配置
|
||||
if let Err(e) = proxy_service.stop_with_restore().await {
|
||||
log::error!("退出时恢复 Live 配置失败: {e}");
|
||||
} else {
|
||||
log::info!("已恢复 Live 配置");
|
||||
}
|
||||
} else {
|
||||
// 非接管模式:仅停止代理
|
||||
if let Err(e) = proxy_service.stop().await {
|
||||
log::error!("退出时停止代理失败: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("代理服务器清理完成");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ pub struct RequestForwarder {
|
||||
}
|
||||
|
||||
impl RequestForwarder {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
router: Arc<ProviderRouter>,
|
||||
timeout_secs: u64,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! 重构后的结构:
|
||||
//! - 通用逻辑提取到 `handler_context` 和 `response_processor` 模块
|
||||
//! - 各 handler 只保留独特的业务逻辑
|
||||
//! - Claude 的格式转换逻辑保留在此文件(用于 OpenRouter 旧接口回退)
|
||||
//! - Claude 的格式转换逻辑保留在此文件(独有功能)
|
||||
|
||||
use super::{
|
||||
error_mapper::{get_error_message, map_proxy_error_to_status},
|
||||
@@ -54,8 +54,8 @@ pub async fn get_status(State(state): State<ProxyState>) -> Result<Json<ProxySta
|
||||
/// 处理 /v1/messages 请求(Claude API)
|
||||
///
|
||||
/// Claude 处理器包含独特的格式转换逻辑:
|
||||
/// - 过去用于 OpenRouter 的 OpenAI Chat Completions 兼容接口(Anthropic ↔ OpenAI 转换)
|
||||
/// - 现在 OpenRouter 已推出 Claude Code 兼容接口,默认不再启用该转换(逻辑保留以备回退)
|
||||
/// - 当使用 OpenRouter 等中转服务时,需要将 Anthropic 格式转换为 OpenAI 格式
|
||||
/// - 响应需要从 OpenAI 格式转回 Anthropic 格式
|
||||
pub async fn handle_messages(
|
||||
State(state): State<ProxyState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
@@ -112,7 +112,7 @@ pub async fn handle_messages(
|
||||
|
||||
/// Claude 格式转换处理(独有逻辑)
|
||||
///
|
||||
/// 处理 OpenRouter 旧 OpenAI 兼容接口的回退方案(当前默认不启用)
|
||||
/// 处理 OpenRouter 等需要格式转换的中转服务
|
||||
async fn handle_claude_transform(
|
||||
response: reqwest::Response,
|
||||
ctx: &RequestContext,
|
||||
|
||||
@@ -87,7 +87,7 @@ pub trait ProviderAdapter: Send + Sync {
|
||||
/// 是否需要格式转换
|
||||
///
|
||||
/// 默认返回 `false`(透传模式)。
|
||||
/// 仅当供应商需要格式转换时(如 Claude + OpenRouter 旧 OpenAI 兼容接口)才返回 `true`。
|
||||
/// 仅当供应商需要格式转换时(如 Claude + OpenRouter)才返回 `true`。
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `provider` - Provider 配置
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! ## 认证模式
|
||||
//! - **Claude**: Anthropic 官方 API (x-api-key + anthropic-version)
|
||||
//! - **ClaudeAuth**: 中转服务 (仅 Bearer 认证,无 x-api-key)
|
||||
//! - **OpenRouter**: 已支持 Claude Code 兼容接口,默认透传(保留旧转换逻辑备用)
|
||||
//! - **OpenRouter**: 需要 Anthropic ↔ OpenAI 格式转换
|
||||
|
||||
use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType};
|
||||
use crate::provider::Provider;
|
||||
@@ -28,8 +28,10 @@ impl ClaudeAdapter {
|
||||
/// - Claude: 默认 Anthropic 官方
|
||||
pub fn provider_type(&self, provider: &Provider) -> ProviderType {
|
||||
// 检测 OpenRouter
|
||||
if self.is_openrouter(provider) {
|
||||
return ProviderType::OpenRouter;
|
||||
if let Ok(base_url) = self.extract_base_url(provider) {
|
||||
if base_url.contains("openrouter.ai") {
|
||||
return ProviderType::OpenRouter;
|
||||
}
|
||||
}
|
||||
|
||||
// 检测 ClaudeAuth (仅 Bearer 认证)
|
||||
@@ -85,14 +87,6 @@ impl ClaudeAdapter {
|
||||
log::debug!("[Claude] 使用 ANTHROPIC_AUTH_TOKEN");
|
||||
return Some(key.to_string());
|
||||
}
|
||||
if let Some(key) = env
|
||||
.get("ANTHROPIC_API_KEY")
|
||||
.and_then(|v| v.as_str())
|
||||
.filter(|s| !s.is_empty())
|
||||
{
|
||||
log::debug!("[Claude] 使用 ANTHROPIC_API_KEY");
|
||||
return Some(key.to_string());
|
||||
}
|
||||
// OpenRouter key
|
||||
if let Some(key) = env
|
||||
.get("OPENROUTER_API_KEY")
|
||||
@@ -192,17 +186,12 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
}
|
||||
|
||||
fn build_url(&self, base_url: &str, endpoint: &str) -> String {
|
||||
// NOTE:
|
||||
// 过去 OpenRouter 只有 OpenAI Chat Completions 兼容接口,需要把 Claude 的 `/v1/messages`
|
||||
// 映射到 `/v1/chat/completions`,并做 Anthropic ↔ OpenAI 的格式转换。
|
||||
//
|
||||
// 现在 OpenRouter 已推出 Claude Code 兼容接口,因此默认直接透传 endpoint。
|
||||
// 如需回退旧逻辑,可恢复下面这段分支:
|
||||
//
|
||||
// if base_url.contains("openrouter.ai") {
|
||||
// return format!("{}/v1/chat/completions", base_url.trim_end_matches('/'));
|
||||
// }
|
||||
// OpenRouter 使用 /v1/chat/completions
|
||||
if base_url.contains("openrouter.ai") {
|
||||
return format!("{}/v1/chat/completions", base_url.trim_end_matches('/'));
|
||||
}
|
||||
|
||||
// Anthropic 直连
|
||||
format!(
|
||||
"{}/{}",
|
||||
base_url.trim_end_matches('/'),
|
||||
@@ -218,25 +207,19 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
.header("x-api-key", &auth.api_key)
|
||||
.header("anthropic-version", "2023-06-01"),
|
||||
// ClaudeAuth 中转服务: 仅 Bearer,无 x-api-key
|
||||
AuthStrategy::ClaudeAuth => request
|
||||
.header("Authorization", format!("Bearer {}", auth.api_key))
|
||||
.header("anthropic-version", "2023-06-01"),
|
||||
AuthStrategy::ClaudeAuth => {
|
||||
request.header("Authorization", format!("Bearer {}", auth.api_key))
|
||||
}
|
||||
// OpenRouter: Bearer
|
||||
AuthStrategy::Bearer => request
|
||||
.header("Authorization", format!("Bearer {}", auth.api_key))
|
||||
.header("anthropic-version", "2023-06-01"),
|
||||
AuthStrategy::Bearer => {
|
||||
request.header("Authorization", format!("Bearer {}", auth.api_key))
|
||||
}
|
||||
_ => request,
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_transform(&self, _provider: &Provider) -> bool {
|
||||
// NOTE:
|
||||
// OpenRouter 已推出 Claude Code 兼容接口(可直接处理 `/v1/messages`),默认不再启用
|
||||
// Anthropic ↔ OpenAI 的格式转换。
|
||||
//
|
||||
// 如果未来需要回退到旧的 OpenAI Chat Completions 方案,可恢复下面这行:
|
||||
// self.is_openrouter(_provider)
|
||||
false
|
||||
fn needs_transform(&self, provider: &Provider) -> bool {
|
||||
self.is_openrouter(provider)
|
||||
}
|
||||
|
||||
fn transform_request(
|
||||
@@ -301,21 +284,6 @@ mod tests {
|
||||
assert_eq!(auth.strategy, AuthStrategy::Anthropic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_auth_anthropic_api_key() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let provider = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
"ANTHROPIC_API_KEY": "sk-ant-test-key"
|
||||
}
|
||||
}));
|
||||
|
||||
let auth = adapter.extract_auth(&provider).unwrap();
|
||||
assert_eq!(auth.api_key, "sk-ant-test-key");
|
||||
assert_eq!(auth.strategy, AuthStrategy::Anthropic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_auth_openrouter() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
@@ -410,7 +378,7 @@ mod tests {
|
||||
fn test_build_url_openrouter() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let url = adapter.build_url("https://openrouter.ai/api", "/v1/messages");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/messages");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/chat/completions");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -429,6 +397,6 @@ mod tests {
|
||||
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api"
|
||||
}
|
||||
}));
|
||||
assert!(!adapter.needs_transform(&openrouter_provider));
|
||||
assert!(adapter.needs_transform(&openrouter_provider));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,21 +48,17 @@ pub enum ProviderType {
|
||||
Gemini,
|
||||
/// Google Gemini CLI (OAuth Bearer)
|
||||
GeminiCli,
|
||||
/// OpenRouter(已支持 Claude Code 兼容接口,默认透传;保留旧转换逻辑备用)
|
||||
/// OpenRouter (需要 Anthropic ↔ OpenAI 格式转换)
|
||||
OpenRouter,
|
||||
}
|
||||
|
||||
impl ProviderType {
|
||||
/// 是否需要格式转换
|
||||
///
|
||||
/// 过去 OpenRouter 需要将 Anthropic 格式转换为 OpenAI 格式;
|
||||
/// 现在默认关闭转换(因为 OpenRouter 已支持 Claude Code 兼容接口)。
|
||||
/// OpenRouter 需要将 Anthropic 格式转换为 OpenAI 格式
|
||||
#[allow(dead_code)]
|
||||
pub fn needs_transform(&self) -> bool {
|
||||
match self {
|
||||
ProviderType::OpenRouter => false,
|
||||
_ => false,
|
||||
}
|
||||
matches!(self, ProviderType::OpenRouter)
|
||||
}
|
||||
|
||||
/// 获取默认端点
|
||||
@@ -219,7 +215,7 @@ mod tests {
|
||||
assert!(!ProviderType::Codex.needs_transform());
|
||||
assert!(!ProviderType::Gemini.needs_transform());
|
||||
assert!(!ProviderType::GeminiCli.needs_transform());
|
||||
assert!(!ProviderType::OpenRouter.needs_transform());
|
||||
assert!(ProviderType::OpenRouter.needs_transform());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize};
|
||||
/// 代理服务器配置
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProxyConfig {
|
||||
/// 是否启用代理服务
|
||||
pub enabled: bool,
|
||||
/// 监听地址
|
||||
pub listen_address: String,
|
||||
/// 监听端口
|
||||
@@ -21,6 +23,7 @@ pub struct ProxyConfig {
|
||||
impl Default for ProxyConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
listen_address: "127.0.0.1".to_string(),
|
||||
listen_port: 15721, // 使用较少占用的高位端口
|
||||
max_retries: 3,
|
||||
|
||||
+97
-456
@@ -8,7 +8,6 @@ use crate::database::Database;
|
||||
use crate::provider::Provider;
|
||||
use crate::proxy::server::ProxyServer;
|
||||
use crate::proxy::types::*;
|
||||
use crate::services::provider::write_live_snapshot;
|
||||
use serde_json::{json, Value};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
@@ -42,16 +41,31 @@ impl ProxyService {
|
||||
}
|
||||
|
||||
/// 启动代理服务器
|
||||
pub async fn start(&self) -> Result<ProxyServerInfo, String> {
|
||||
///
|
||||
/// - `persist_enabled = true`:将 `proxy_config.enabled` 持久化为启用(用于“总开关”)
|
||||
/// - `persist_enabled = false`:仅在当前进程启动代理服务(用于“按 App 接管”自动启动)
|
||||
pub async fn start(&self, persist_enabled: bool) -> Result<ProxyServerInfo, String> {
|
||||
// 1. 获取配置
|
||||
let config = self
|
||||
let mut config = self
|
||||
.db
|
||||
.get_proxy_config()
|
||||
.await
|
||||
.map_err(|e| format!("获取代理配置失败: {e}"))?;
|
||||
|
||||
// 2. 仅在需要时持久化 enabled(避免“按 App 接管”自动启动时误打开总开关)
|
||||
if persist_enabled {
|
||||
config.enabled = true;
|
||||
}
|
||||
|
||||
// 3. 若已在运行:确保持久化状态(如需要)并返回当前信息
|
||||
if let Some(server) = self.server.read().await.as_ref() {
|
||||
if persist_enabled {
|
||||
self.db
|
||||
.update_proxy_config(config)
|
||||
.await
|
||||
.map_err(|e| format!("保存代理配置失败: {e}"))?;
|
||||
}
|
||||
|
||||
let status = server.get_status().await;
|
||||
return Ok(ProxyServerInfo {
|
||||
address: status.address,
|
||||
@@ -72,6 +86,14 @@ impl ProxyService {
|
||||
// 5. 保存服务器实例
|
||||
*self.server.write().await = Some(server);
|
||||
|
||||
// 6. 持久化 enabled 状态(仅总开关)
|
||||
if persist_enabled {
|
||||
self.db
|
||||
.update_proxy_config(config)
|
||||
.await
|
||||
.map_err(|e| format!("保存代理配置失败: {e}"))?;
|
||||
}
|
||||
|
||||
log::info!("代理服务器已启动: {}:{}", info.address, info.port);
|
||||
Ok(info)
|
||||
}
|
||||
@@ -116,7 +138,7 @@ impl ProxyService {
|
||||
}
|
||||
|
||||
// 5. 启动代理服务器
|
||||
match self.start().await {
|
||||
match self.start(true).await {
|
||||
Ok(info) => Ok(info),
|
||||
Err(e) => {
|
||||
// 启动失败,恢复原始配置
|
||||
@@ -165,16 +187,16 @@ impl ProxyService {
|
||||
|
||||
/// 为指定应用开启/关闭 Live 接管
|
||||
///
|
||||
/// - 开启:自动启动代理服务,仅接管当前 app 的 Live 配置
|
||||
/// - 关闭:仅恢复当前 app 的 Live 配置;若无其它接管,则自动停止代理服务
|
||||
/// - 开启:自动启动代理服务(不影响总开关持久化),仅接管当前 app 的 Live 配置
|
||||
/// - 关闭:仅恢复当前 app 的 Live 配置;若总开关未开启且无其它接管,则自动停止代理服务
|
||||
pub async fn set_takeover_for_app(&self, app_type: &str, enabled: bool) -> Result<(), String> {
|
||||
let app = AppType::from_str(app_type).map_err(|e| format!("无效的应用类型: {e}"))?;
|
||||
let app_type_str = app.as_str();
|
||||
|
||||
if enabled {
|
||||
// 1) 代理服务未运行则自动启动
|
||||
// 1) 代理服务未运行则自动启动(不持久化总开关)
|
||||
if !self.is_running().await {
|
||||
self.start().await?;
|
||||
self.start(false).await?;
|
||||
}
|
||||
|
||||
// 2) 已接管则直接返回(幂等)
|
||||
@@ -200,17 +222,8 @@ impl ProxyService {
|
||||
// 5) 写入接管配置(仅当前 app)
|
||||
if let Err(e) = self.takeover_live_config_strict(&app).await {
|
||||
log::error!("{app_type_str} 接管 Live 配置失败,尝试恢复: {e}");
|
||||
match self.restore_live_config_for_app(&app).await {
|
||||
Ok(()) => {
|
||||
// 恢复成功才清理备份,避免失败场景下丢失唯一可回滚来源
|
||||
let _ = self.db.delete_live_backup(app_type_str).await;
|
||||
}
|
||||
Err(restore_err) => {
|
||||
log::error!(
|
||||
"{app_type_str} 恢复 Live 配置失败,将保留备份以便下次启动恢复: {restore_err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
let _ = self.restore_live_config_for_app(&app).await;
|
||||
let _ = self.db.delete_live_backup(app_type_str).await;
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
@@ -239,7 +252,7 @@ impl ProxyService {
|
||||
.await
|
||||
.map_err(|e| format!("删除 {app_type_str} Live 备份失败: {e}"))?;
|
||||
|
||||
// 3) 若无其它接管,更新旧标志,并停止代理服务
|
||||
// 3) 若无其它接管,更新旧标志,并在总开关未开启时停止代理服务
|
||||
let has_any_backup = self
|
||||
.db
|
||||
.has_any_live_backup()
|
||||
@@ -248,7 +261,13 @@ impl ProxyService {
|
||||
if !has_any_backup {
|
||||
let _ = self.db.set_live_takeover_active(false).await;
|
||||
|
||||
if self.is_running().await {
|
||||
let master_enabled = self
|
||||
.db
|
||||
.get_proxy_config()
|
||||
.await
|
||||
.map_err(|e| format!("获取代理配置失败: {e}"))?
|
||||
.enabled;
|
||||
if !master_enabled && self.is_running().await {
|
||||
// 此时没有任何 app 处于接管状态,停止服务即可
|
||||
let _ = self.stop().await;
|
||||
}
|
||||
@@ -312,35 +331,34 @@ impl ProxyService {
|
||||
|
||||
match env_obj {
|
||||
Some(obj) => {
|
||||
obj.insert(token_key.to_string(), json!(token));
|
||||
// ANTHROPIC_AUTH_TOKEN 与 ANTHROPIC_API_KEY 视为同义字段,保持一致
|
||||
if token_key == "ANTHROPIC_AUTH_TOKEN"
|
||||
|| token_key == "ANTHROPIC_API_KEY"
|
||||
{
|
||||
let mut updated = false;
|
||||
if obj.contains_key("ANTHROPIC_AUTH_TOKEN") {
|
||||
obj.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(token),
|
||||
);
|
||||
updated = true;
|
||||
}
|
||||
if obj.contains_key("ANTHROPIC_API_KEY") {
|
||||
obj.insert(
|
||||
"ANTHROPIC_API_KEY".to_string(),
|
||||
json!(token),
|
||||
);
|
||||
updated = true;
|
||||
}
|
||||
if !updated {
|
||||
obj.insert(token_key.to_string(), json!(token));
|
||||
}
|
||||
} else {
|
||||
obj.insert(token_key.to_string(), json!(token));
|
||||
obj.insert(
|
||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||
json!(token),
|
||||
);
|
||||
obj.insert(
|
||||
"ANTHROPIC_API_KEY".to_string(),
|
||||
json!(token),
|
||||
);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// 至少写入一份可用的 Token
|
||||
provider.settings_config["env"] =
|
||||
json!({ token_key: token });
|
||||
provider.settings_config["env"] = json!({
|
||||
token_key: token
|
||||
});
|
||||
if token_key == "ANTHROPIC_AUTH_TOKEN"
|
||||
|| token_key == "ANTHROPIC_API_KEY"
|
||||
{
|
||||
provider.settings_config["env"]
|
||||
["ANTHROPIC_AUTH_TOKEN"] = json!(token);
|
||||
provider.settings_config["env"]["ANTHROPIC_API_KEY"] =
|
||||
json!(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,6 +495,12 @@ impl ProxyService {
|
||||
.await
|
||||
.map_err(|e| format!("停止代理服务器失败: {e}"))?;
|
||||
|
||||
// 将 enabled 设为 false,避免下次启动时自动开启
|
||||
if let Ok(mut config) = self.db.get_proxy_config().await {
|
||||
config.enabled = false;
|
||||
let _ = self.db.update_proxy_config(config).await;
|
||||
}
|
||||
|
||||
log::info!("代理服务器已停止");
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -489,6 +513,15 @@ impl ProxyService {
|
||||
// 1. 停止代理服务器(即使未运行也继续执行恢复逻辑)
|
||||
if let Err(e) = self.stop().await {
|
||||
log::warn!("停止代理服务器失败(将继续恢复 Live 配置): {e}");
|
||||
|
||||
// stop() 只有在 server 实例存在时才会把 enabled 设为 false;
|
||||
// 这里兜底确保“总开关关闭”能落盘关闭状态。
|
||||
if let Ok(mut config) = self.db.get_proxy_config().await {
|
||||
if config.enabled {
|
||||
config.enabled = false;
|
||||
let _ = self.db.update_proxy_config(config).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 恢复原始 Live 配置
|
||||
@@ -876,270 +909,30 @@ impl ProxyService {
|
||||
|
||||
/// 恢复原始 Live 配置
|
||||
async fn restore_live_configs(&self) -> Result<(), String> {
|
||||
let mut errors = Vec::new();
|
||||
|
||||
for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
||||
if let Err(e) = self
|
||||
.restore_live_config_for_app_with_fallback(&app_type)
|
||||
.await
|
||||
{
|
||||
errors.push(e);
|
||||
}
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(errors.join(";"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn restore_live_config_for_app_with_fallback(
|
||||
&self,
|
||||
app_type: &AppType,
|
||||
) -> Result<(), String> {
|
||||
let app_type_str = app_type.as_str();
|
||||
|
||||
// 1) 优先从 Live 备份恢复(这是“原始 Live”的唯一可靠来源)
|
||||
let backup = self
|
||||
.db
|
||||
.get_live_backup(app_type_str)
|
||||
.await
|
||||
.map_err(|e| format!("获取 {app_type_str} Live 备份失败: {e}"))?;
|
||||
if let Some(backup) = backup {
|
||||
// Claude
|
||||
if let Ok(Some(backup)) = self.db.get_live_backup("claude").await {
|
||||
let config: Value = serde_json::from_str(&backup.original_config)
|
||||
.map_err(|e| format!("解析 {app_type_str} 备份失败: {e}"))?;
|
||||
self.write_live_config_for_app(app_type, &config)?;
|
||||
log::info!("{app_type_str} Live 配置已从备份恢复");
|
||||
return Ok(());
|
||||
.map_err(|e| format!("解析 Claude 备份失败: {e}"))?;
|
||||
self.write_claude_live(&config)?;
|
||||
log::info!("Claude Live 配置已恢复");
|
||||
}
|
||||
|
||||
// 2) 兜底:备份缺失,但 Live 仍包含接管占位符(异常退出/历史 bug 场景)
|
||||
if !self.detect_takeover_in_live_config_for_app(app_type) {
|
||||
return Ok(());
|
||||
// Codex
|
||||
if let Ok(Some(backup)) = self.db.get_live_backup("codex").await {
|
||||
let config: Value = serde_json::from_str(&backup.original_config)
|
||||
.map_err(|e| format!("解析 Codex 备份失败: {e}"))?;
|
||||
self.write_codex_live(&config)?;
|
||||
log::info!("Codex Live 配置已恢复");
|
||||
}
|
||||
|
||||
// 2.1) 优先从 SSOT(当前供应商)重建 Live(比“清理字段”更可用)
|
||||
match self.restore_live_from_ssot_for_app(app_type) {
|
||||
Ok(true) => {
|
||||
log::info!("{app_type_str} Live 配置已从 SSOT 恢复(无备份兜底)");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(false) => {
|
||||
log::warn!(
|
||||
"{app_type_str} Live 备份缺失,且无法从 SSOT 恢复,将尝试清理接管占位符"
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"{app_type_str} Live 备份缺失,SSOT 恢复失败,将尝试清理接管占位符: {e}"
|
||||
);
|
||||
}
|
||||
// Gemini
|
||||
if let Ok(Some(backup)) = self.db.get_live_backup("gemini").await {
|
||||
let config: Value = serde_json::from_str(&backup.original_config)
|
||||
.map_err(|e| format!("解析 Gemini 备份失败: {e}"))?;
|
||||
self.write_gemini_live(&config)?;
|
||||
log::info!("Gemini Live 配置已恢复");
|
||||
}
|
||||
|
||||
// 2.2) 最后兜底:尽力清理占位符与本地代理地址,避免长期卡在代理占位符状态
|
||||
self.cleanup_takeover_placeholders_in_live_for_app(app_type)?;
|
||||
log::info!("{app_type_str} Live 接管占位符已清理(无备份兜底)");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_live_config_for_app(&self, app_type: &AppType, config: &Value) -> Result<(), String> {
|
||||
match app_type {
|
||||
AppType::Claude => self.write_claude_live(config),
|
||||
AppType::Codex => self.write_codex_live(config),
|
||||
AppType::Gemini => self.write_gemini_live(config),
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_takeover_in_live_config_for_app(&self, app_type: &AppType) -> bool {
|
||||
match app_type {
|
||||
AppType::Claude => match self.read_claude_live() {
|
||||
Ok(config) => Self::is_claude_live_taken_over(&config),
|
||||
Err(_) => false,
|
||||
},
|
||||
AppType::Codex => match self.read_codex_live() {
|
||||
Ok(config) => Self::is_codex_live_taken_over(&config),
|
||||
Err(_) => false,
|
||||
},
|
||||
AppType::Gemini => match self.read_gemini_live() {
|
||||
Ok(config) => Self::is_gemini_live_taken_over(&config),
|
||||
Err(_) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 当 Live 备份缺失时,尝试用 SSOT(当前供应商)写回 Live,以解除占位符接管。
|
||||
///
|
||||
/// 返回值:
|
||||
/// - Ok(true):已成功写回
|
||||
/// - Ok(false):缺少当前供应商/供应商不存在,无法写回
|
||||
fn restore_live_from_ssot_for_app(&self, app_type: &AppType) -> Result<bool, String> {
|
||||
let current_id = crate::settings::get_effective_current_provider(&self.db, app_type)
|
||||
.map_err(|e| format!("获取 {app_type:?} 当前供应商失败: {e}"))?;
|
||||
|
||||
let Some(current_id) = current_id else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let providers = self
|
||||
.db
|
||||
.get_all_providers(app_type.as_str())
|
||||
.map_err(|e| format!("读取 {app_type:?} 供应商列表失败: {e}"))?;
|
||||
|
||||
let Some(provider) = providers.get(¤t_id) else {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
write_live_snapshot(app_type, provider)
|
||||
.map_err(|e| format!("写入 {app_type:?} Live 配置失败: {e}"))?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn cleanup_takeover_placeholders_in_live_for_app(
|
||||
&self,
|
||||
app_type: &AppType,
|
||||
) -> Result<(), String> {
|
||||
match app_type {
|
||||
AppType::Claude => self.cleanup_claude_takeover_placeholders_in_live(),
|
||||
AppType::Codex => self.cleanup_codex_takeover_placeholders_in_live(),
|
||||
AppType::Gemini => self.cleanup_gemini_takeover_placeholders_in_live(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_local_proxy_url(url: &str) -> bool {
|
||||
let url = url.trim();
|
||||
if !url.starts_with("http://") {
|
||||
return false;
|
||||
}
|
||||
let rest = &url["http://".len()..];
|
||||
rest.starts_with("127.0.0.1")
|
||||
|| rest.starts_with("localhost")
|
||||
|| rest.starts_with("0.0.0.0")
|
||||
|| rest.starts_with("[::1]")
|
||||
|| rest.starts_with("[::]")
|
||||
|| rest.starts_with("::1")
|
||||
|| rest.starts_with("::")
|
||||
}
|
||||
|
||||
fn cleanup_claude_takeover_placeholders_in_live(&self) -> Result<(), String> {
|
||||
let mut config = self.read_claude_live()?;
|
||||
|
||||
let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
for key in [
|
||||
"ANTHROPIC_AUTH_TOKEN",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
] {
|
||||
if env.get(key).and_then(|v| v.as_str()) == Some(PROXY_TOKEN_PLACEHOLDER) {
|
||||
env.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
if env
|
||||
.get("ANTHROPIC_BASE_URL")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(Self::is_local_proxy_url)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
env.remove("ANTHROPIC_BASE_URL");
|
||||
}
|
||||
|
||||
self.write_claude_live(&config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn cleanup_codex_takeover_placeholders_in_live(&self) -> Result<(), String> {
|
||||
let mut config = self.read_codex_live()?;
|
||||
|
||||
if let Some(auth) = config.get_mut("auth").and_then(|v| v.as_object_mut()) {
|
||||
if auth.get("OPENAI_API_KEY").and_then(|v| v.as_str()) == Some(PROXY_TOKEN_PLACEHOLDER)
|
||||
{
|
||||
auth.remove("OPENAI_API_KEY");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(cfg_str) = config.get("config").and_then(|v| v.as_str()) {
|
||||
let updated = Self::remove_local_toml_base_url(cfg_str);
|
||||
config["config"] = json!(updated);
|
||||
}
|
||||
|
||||
self.write_codex_live(&config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_local_toml_base_url(toml_str: &str) -> String {
|
||||
use toml_edit::DocumentMut;
|
||||
|
||||
let mut doc = match toml_str.parse::<DocumentMut>() {
|
||||
Ok(doc) => doc,
|
||||
Err(_) => return toml_str.to_string(),
|
||||
};
|
||||
|
||||
let model_provider = doc
|
||||
.get("model_provider")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(str::to_string);
|
||||
|
||||
if let Some(provider_key) = model_provider {
|
||||
if let Some(model_providers) = doc
|
||||
.get_mut("model_providers")
|
||||
.and_then(|v| v.as_table_mut())
|
||||
{
|
||||
if let Some(provider_table) = model_providers
|
||||
.get_mut(provider_key.as_str())
|
||||
.and_then(|v| v.as_table_mut())
|
||||
{
|
||||
let should_remove = provider_table
|
||||
.get("base_url")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(Self::is_local_proxy_url)
|
||||
.unwrap_or(false);
|
||||
if should_remove {
|
||||
provider_table.remove("base_url");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:清理顶层 base_url(仅当它看起来像本地代理地址)
|
||||
let should_remove_root = doc
|
||||
.get("base_url")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(Self::is_local_proxy_url)
|
||||
.unwrap_or(false);
|
||||
if should_remove_root {
|
||||
doc.as_table_mut().remove("base_url");
|
||||
}
|
||||
|
||||
doc.to_string()
|
||||
}
|
||||
|
||||
fn cleanup_gemini_takeover_placeholders_in_live(&self) -> Result<(), String> {
|
||||
let mut config = self.read_gemini_live()?;
|
||||
|
||||
let Some(env) = config.get_mut("env").and_then(|v| v.as_object_mut()) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if env.get("GEMINI_API_KEY").and_then(|v| v.as_str()) == Some(PROXY_TOKEN_PLACEHOLDER) {
|
||||
env.remove("GEMINI_API_KEY");
|
||||
}
|
||||
|
||||
if env
|
||||
.get("GOOGLE_GEMINI_BASE_URL")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(Self::is_local_proxy_url)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
env.remove("GOOGLE_GEMINI_BASE_URL");
|
||||
}
|
||||
|
||||
self.write_gemini_live(&config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1153,7 +946,7 @@ impl ProxyService {
|
||||
|
||||
/// 从异常退出中恢复(启动时调用)
|
||||
///
|
||||
/// 检测到 Live 备份残留时调用此方法。
|
||||
/// 检测到 live_takeover_active=true 但代理未运行时调用此方法。
|
||||
/// 会恢复 Live 配置、清除接管标志、删除备份。
|
||||
pub async fn recover_from_crash(&self) -> Result<(), String> {
|
||||
// 1. 恢复 Live 配置
|
||||
@@ -1177,7 +970,7 @@ impl ProxyService {
|
||||
|
||||
/// 检测 Live 配置是否处于“被接管”的残留状态
|
||||
///
|
||||
/// 用于兜底处理:当数据库备份缺失但 Live 文件已经写成代理占位符时,
|
||||
/// 用于兜底处理:当数据库标志未写入成功(或旧版本遗留)但 Live 文件已经写成代理占位符时,
|
||||
/// 启动流程可以据此触发恢复逻辑。
|
||||
pub fn detect_takeover_in_live_configs(&self) -> bool {
|
||||
if let Ok(config) = self.read_claude_live() {
|
||||
@@ -1462,8 +1255,9 @@ impl ProxyService {
|
||||
.await
|
||||
.map_err(|e| format!("获取代理配置失败: {e}"))?;
|
||||
|
||||
// 保存到数据库(保持 live_takeover_active 状态不变)
|
||||
// 保存到数据库(保持 enabled 和 live_takeover_active 状态不变)
|
||||
let mut new_config = config.clone();
|
||||
new_config.enabled = previous.enabled;
|
||||
new_config.live_takeover_active = previous.live_takeover_active;
|
||||
|
||||
self.db
|
||||
@@ -1576,47 +1370,6 @@ impl ProxyService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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());
|
||||
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_toml_base_url_updates_active_model_provider_base_url() {
|
||||
@@ -1679,116 +1432,4 @@ model = "gpt-5.1-codex"
|
||||
|
||||
assert_eq!(base_url, new_url);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn sync_claude_token_does_not_add_anthropic_api_key() {
|
||||
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 = Provider::with_id(
|
||||
"p1".to_string(),
|
||||
"P1".to_string(),
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
"ANTHROPIC_AUTH_TOKEN": "stale"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
db.save_provider("claude", &provider)
|
||||
.expect("save provider");
|
||||
db.set_current_provider("claude", "p1")
|
||||
.expect("set current provider");
|
||||
|
||||
let live_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "fresh"
|
||||
}
|
||||
});
|
||||
|
||||
service
|
||||
.sync_live_config_to_provider(&AppType::Claude, &live_config)
|
||||
.await
|
||||
.expect("sync");
|
||||
|
||||
let updated = db
|
||||
.get_provider_by_id("p1", "claude")
|
||||
.expect("get provider")
|
||||
.expect("provider exists");
|
||||
let env = updated
|
||||
.settings_config
|
||||
.get("env")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("env object");
|
||||
|
||||
assert_eq!(
|
||||
env.get("ANTHROPIC_AUTH_TOKEN").and_then(|v| v.as_str()),
|
||||
Some("fresh")
|
||||
);
|
||||
assert!(
|
||||
!env.contains_key("ANTHROPIC_API_KEY"),
|
||||
"should not add ANTHROPIC_API_KEY when absent"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn sync_claude_token_respects_existing_api_key_field() {
|
||||
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 = Provider::with_id(
|
||||
"p1".to_string(),
|
||||
"P1".to_string(),
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.anthropic.com",
|
||||
"ANTHROPIC_API_KEY": "stale"
|
||||
}
|
||||
}),
|
||||
None,
|
||||
);
|
||||
db.save_provider("claude", &provider)
|
||||
.expect("save provider");
|
||||
db.set_current_provider("claude", "p1")
|
||||
.expect("set current provider");
|
||||
|
||||
let live_config = json!({
|
||||
"env": {
|
||||
"ANTHROPIC_AUTH_TOKEN": "fresh"
|
||||
}
|
||||
});
|
||||
|
||||
service
|
||||
.sync_live_config_to_provider(&AppType::Claude, &live_config)
|
||||
.await
|
||||
.expect("sync");
|
||||
|
||||
let updated = db
|
||||
.get_provider_by_id("p1", "claude")
|
||||
.expect("get provider")
|
||||
.expect("provider exists");
|
||||
let env = updated
|
||||
.settings_config
|
||||
.get("env")
|
||||
.and_then(|v| v.as_object())
|
||||
.expect("env object");
|
||||
|
||||
assert_eq!(
|
||||
env.get("ANTHROPIC_API_KEY").and_then(|v| v.as_str()),
|
||||
Some("fresh")
|
||||
);
|
||||
assert!(
|
||||
!env.contains_key("ANTHROPIC_AUTH_TOKEN"),
|
||||
"should not add ANTHROPIC_AUTH_TOKEN when absent"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "CC Switch",
|
||||
"version": "3.9.0-2",
|
||||
"version": "3.9.0-1",
|
||||
"identifier": "com.ccswitch.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../dist",
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"titleBarStyle": "Visible",
|
||||
"minWidth": 900,
|
||||
"minHeight": 600
|
||||
"titleBarStyle": "Visible"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1003,76 +1003,3 @@ fn export_sql_returns_error_for_invalid_path() {
|
||||
other => panic!("expected IoContext or Io error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_sql_rejects_non_cc_switch_backup() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
let state = create_test_state().expect("create test state");
|
||||
|
||||
let import_path = home.join("not-cc-switch.sql");
|
||||
fs::write(&import_path, "CREATE TABLE x (id INTEGER);").expect("write import sql");
|
||||
|
||||
let err = state
|
||||
.db
|
||||
.import_sql(&import_path)
|
||||
.expect_err("non-cc-switch sql should be rejected");
|
||||
|
||||
match err {
|
||||
AppError::Localized { key, .. } => {
|
||||
assert_eq!(key, "backup.sql.invalid_format");
|
||||
}
|
||||
other => panic!("expected Localized error, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_sql_accepts_cc_switch_exported_backup() {
|
||||
let _guard = test_mutex().lock().expect("acquire test mutex");
|
||||
reset_test_fs();
|
||||
let home = ensure_test_home();
|
||||
|
||||
// Create a database with some data and export it.
|
||||
let mut config = MultiAppConfig::default();
|
||||
{
|
||||
let manager = config
|
||||
.get_manager_mut(&AppType::Claude)
|
||||
.expect("claude manager");
|
||||
manager.current = "test-provider".to_string();
|
||||
manager.providers.insert(
|
||||
"test-provider".to_string(),
|
||||
Provider::with_id(
|
||||
"test-provider".to_string(),
|
||||
"Test Provider".to_string(),
|
||||
json!({"env": {"ANTHROPIC_API_KEY": "test-key"}}),
|
||||
None,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
let state = create_test_state_with_config(&config).expect("create test state");
|
||||
let export_path = home.join("cc-switch-export.sql");
|
||||
state
|
||||
.db
|
||||
.export_sql(&export_path)
|
||||
.expect("export should succeed");
|
||||
|
||||
// Reset database, then import into a fresh one.
|
||||
reset_test_fs();
|
||||
let state = create_test_state().expect("create test state");
|
||||
state
|
||||
.db
|
||||
.import_sql(&export_path)
|
||||
.expect("import should succeed");
|
||||
|
||||
let providers = state
|
||||
.db
|
||||
.get_all_providers(AppType::Claude.as_str())
|
||||
.expect("load providers");
|
||||
assert!(
|
||||
providers.contains_key("test-provider"),
|
||||
"imported providers should contain test-provider"
|
||||
);
|
||||
}
|
||||
|
||||
+8
-17
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Plus,
|
||||
Settings,
|
||||
@@ -48,7 +47,6 @@ type View = "providers" | "settings" | "prompts" | "skills" | "mcp" | "agents";
|
||||
|
||||
function App() {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [activeApp, setActiveApp] = useState<AppId>("claude");
|
||||
const [currentView, setCurrentView] = useState<View>("providers");
|
||||
@@ -70,6 +68,12 @@ function App() {
|
||||
const { isRunning: isProxyRunning, takeoverStatus } = useProxyStatus();
|
||||
// 当前应用的代理是否开启
|
||||
const isCurrentAppTakeoverActive = takeoverStatus?.[activeApp] || false;
|
||||
// 任意应用的代理是否开启
|
||||
const isTakeoverActive =
|
||||
takeoverStatus?.claude ||
|
||||
takeoverStatus?.codex ||
|
||||
takeoverStatus?.gemini ||
|
||||
false;
|
||||
|
||||
// 获取供应商列表,当代理服务运行时自动刷新
|
||||
const { data, isLoading, refetch } = useProvidersQuery(activeApp, {
|
||||
@@ -272,20 +276,7 @@ function App() {
|
||||
|
||||
// 导入配置成功后刷新
|
||||
const handleImportSuccess = async () => {
|
||||
try {
|
||||
// 导入会影响所有应用的供应商数据:刷新所有 providers 缓存
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: ["providers"],
|
||||
refetchType: "all",
|
||||
});
|
||||
await queryClient.refetchQueries({
|
||||
queryKey: ["providers"],
|
||||
type: "all",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[App] Failed to refresh providers after import", error);
|
||||
await refetch();
|
||||
}
|
||||
await refetch();
|
||||
try {
|
||||
await providersApi.updateTrayMenu();
|
||||
} catch (error) {
|
||||
@@ -599,7 +590,7 @@ function App() {
|
||||
}}
|
||||
onSubmit={handleEditProvider}
|
||||
appId={activeApp}
|
||||
isProxyTakeover={isProxyRunning && isCurrentAppTakeoverActive}
|
||||
isProxyTakeover={isProxyRunning && isTakeoverActive}
|
||||
/>
|
||||
|
||||
{usageProvider && (
|
||||
|
||||
@@ -24,11 +24,11 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex bg-muted rounded-xl p-1 gap-1">
|
||||
<div className="inline-flex bg-muted rounded-lg p-1 gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSwitch("claude")}
|
||||
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
className={`group inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeApp === "claude"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
|
||||
@@ -50,7 +50,7 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSwitch("codex")}
|
||||
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
className={`group inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeApp === "codex"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
|
||||
@@ -72,7 +72,7 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSwitch("gemini")}
|
||||
className={`group inline-flex items-center gap-2 px-3 h-8 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
className={`group inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeApp === "gemini"
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
|
||||
|
||||
@@ -26,11 +26,8 @@ import { useTranslation } from "react-i18next";
|
||||
import type { TFunction } from "i18next";
|
||||
import type { ProxyConfig } from "@/types/proxy";
|
||||
|
||||
// 表单数据类型(仅包含可编辑字段)
|
||||
type ProxyConfigForm = Pick<
|
||||
ProxyConfig,
|
||||
"listen_address" | "listen_port" | "max_retries" | "request_timeout" | "enable_logging"
|
||||
>;
|
||||
// 表单数据类型(不包含 enabled 字段,该字段由后端自动管理)
|
||||
type ProxyConfigForm = Omit<ProxyConfig, "enabled">;
|
||||
|
||||
const createProxyConfigSchema = (t: TFunction) => {
|
||||
const requestTimeoutSchema = z
|
||||
@@ -123,18 +120,19 @@ export function ProxySettingsDialog({
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
form.reset({
|
||||
listen_address: config.listen_address,
|
||||
listen_port: config.listen_port,
|
||||
max_retries: config.max_retries,
|
||||
request_timeout: config.request_timeout,
|
||||
enable_logging: config.enable_logging,
|
||||
...config,
|
||||
});
|
||||
}
|
||||
}, [config, form]);
|
||||
|
||||
const onSubmit = async (data: ProxyConfigForm) => {
|
||||
try {
|
||||
await updateConfig(data);
|
||||
// 添加 enabled 字段(从当前配置中获取,保持不变)
|
||||
const configToSave: ProxyConfig = {
|
||||
...data,
|
||||
enabled: config?.enabled ?? true,
|
||||
};
|
||||
await updateConfig(configToSave);
|
||||
closePanel();
|
||||
} catch (error) {
|
||||
console.error("Save config failed:", error);
|
||||
|
||||
@@ -50,41 +50,42 @@ export function ProxyToggle({ className, activeApp }: ProxyToggleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"p-1 rounded-xl transition-all",
|
||||
"flex items-center gap-2 px-3 py-1.5 rounded-lg transition-all cursor-default",
|
||||
takeoverEnabled
|
||||
? "bg-emerald-500/10 border border-emerald-500/30"
|
||||
: "bg-muted/50 hover:bg-muted",
|
||||
className,
|
||||
)}
|
||||
title={tooltipText}
|
||||
>
|
||||
<div className="flex items-center gap-2 px-2 h-8 rounded-md cursor-default">
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Radio
|
||||
className={cn(
|
||||
"h-4 w-4 transition-colors",
|
||||
takeoverEnabled
|
||||
? "text-emerald-500 animate-pulse"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
{isPending ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Radio
|
||||
className={cn(
|
||||
"text-sm font-medium transition-colors select-none",
|
||||
"h-4 w-4 transition-colors",
|
||||
takeoverEnabled
|
||||
? "text-emerald-600 dark:text-emerald-400"
|
||||
? "text-emerald-500 animate-pulse"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
Proxy
|
||||
</span>
|
||||
<Switch
|
||||
checked={takeoverEnabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="ml-1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={cn(
|
||||
"text-sm font-medium transition-colors select-none",
|
||||
takeoverEnabled
|
||||
? "text-emerald-600 dark:text-emerald-400"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
Proxy
|
||||
</span>
|
||||
<Switch
|
||||
checked={takeoverEnabled}
|
||||
onCheckedChange={handleToggle}
|
||||
disabled={isPending}
|
||||
className="ml-1"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -185,8 +185,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
category: "aggregator",
|
||||
icon: "modelscope",
|
||||
iconColor: "#624AFF",
|
||||
},
|
||||
{
|
||||
name: "KAT-Coder",
|
||||
@@ -230,8 +228,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
},
|
||||
},
|
||||
category: "cn_official",
|
||||
icon: "longcat",
|
||||
iconColor: "#29E154",
|
||||
},
|
||||
{
|
||||
name: "MiniMax",
|
||||
@@ -334,8 +330,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
// 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖
|
||||
endpointCandidates: ["https://aihubmix.com", "https://api.aihubmix.com"],
|
||||
category: "aggregator",
|
||||
icon: "aihubmix",
|
||||
iconColor: "#006FFB",
|
||||
},
|
||||
{
|
||||
name: "DMXAPI",
|
||||
@@ -350,8 +344,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
// 请求地址候选(用于地址管理/测速),用户可自行选择/覆盖
|
||||
endpointCandidates: ["https://www.dmxapi.cn", "https://api.dmxapi.cn"],
|
||||
category: "aggregator",
|
||||
isPartner: true, // 合作伙伴
|
||||
partnerPromotionKey: "dmxapi", // 促销信息 i18n key
|
||||
},
|
||||
{
|
||||
name: "PackyCode",
|
||||
@@ -389,6 +381,6 @@ export const providerPresets: ProviderPreset[] = [
|
||||
},
|
||||
category: "aggregator",
|
||||
icon: "openrouter",
|
||||
iconColor: "#6566F1",
|
||||
iconColor: "#6366F1",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -131,8 +131,6 @@ requires_openai_auth = true`,
|
||||
"gpt-5.1-codex",
|
||||
),
|
||||
endpointCandidates: ["https://www.dmxapi.cn/v1"],
|
||||
isPartner: true, // 合作伙伴
|
||||
partnerPromotionKey: "dmxapi", // 促销信息 i18n key
|
||||
},
|
||||
{
|
||||
name: "PackyCode",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { settingsApi } from "@/lib/api";
|
||||
@@ -39,6 +39,15 @@ export function useImportExport(
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [backupId, setBackupId] = useState<string | null>(null);
|
||||
const [isImporting, setIsImporting] = useState(false);
|
||||
const successTimerRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (successTimerRef.current) {
|
||||
window.clearTimeout(successTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelectedFile("");
|
||||
@@ -96,10 +105,6 @@ export function useImportExport(
|
||||
}
|
||||
|
||||
setBackupId(result.backupId ?? null);
|
||||
// 导入成功后立即触发外部刷新(与 live 同步结果解耦)
|
||||
// - 避免 sync 失败时 UI 不刷新
|
||||
// - 避免依赖 setTimeout(组件卸载会取消)
|
||||
void onImportSuccess?.();
|
||||
|
||||
const syncResult = await syncCurrentProvidersLiveSafe();
|
||||
if (syncResult.ok) {
|
||||
@@ -110,6 +115,10 @@ export function useImportExport(
|
||||
}),
|
||||
{ closeButton: true },
|
||||
);
|
||||
|
||||
successTimerRef.current = window.setTimeout(() => {
|
||||
void onImportSuccess?.();
|
||||
}, 1500);
|
||||
} else {
|
||||
console.error(
|
||||
"[useImportExport] Failed to sync live config",
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"themeDark": "Dark",
|
||||
"themeSystem": "System",
|
||||
"importExport": "SQL Import/Export",
|
||||
"importExportHint": "Import or export database SQL backups for migration or restore (import supports only backups exported by CC Switch).",
|
||||
"importExportHint": "Import or export database SQL backups for migration or restore.",
|
||||
"exportConfig": "Export SQL Backup",
|
||||
"selectConfigFile": "Select SQL File",
|
||||
"noFileSelected": "No configuration file selected.",
|
||||
@@ -166,7 +166,7 @@
|
||||
"selectFileFailed": "Please choose a valid SQL backup file",
|
||||
"configCorrupted": "SQL file may be corrupted or invalid",
|
||||
"backupId": "Backup ID",
|
||||
"autoReload": "Data refreshed",
|
||||
"autoReload": "Data will refresh automatically in 2 seconds...",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"languageOptionJapanese": "日本語",
|
||||
@@ -273,8 +273,7 @@
|
||||
"zhipu": "Zhipu GLM is an official partner of CC Switch. Use this link to top up and get a 10% discount",
|
||||
"packycode": "PackyCode is an official partner of CC Switch. Register using this link and enter \"cc-switch\" promo code during recharge to get 10% off",
|
||||
"minimax_cn": "MiniMax Coding Plan Special Offer, Starter from ¥9.9",
|
||||
"minimax_en": "MiniMax Coding Plan Black Friday, Starter is now $2/mo (80% OFF!)",
|
||||
"dmxapi": "Claude Code exclusive model 66% OFF now!"
|
||||
"minimax_en": "MiniMax Coding Plan Black Friday, Starter is now $2/mo (80% OFF!)"
|
||||
},
|
||||
"parameterConfig": "Parameter Config - {{name}} *",
|
||||
"mainModel": "Main Model (optional)",
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"themeDark": "ダーク",
|
||||
"themeSystem": "システム",
|
||||
"importExport": "SQL インポート/エクスポート",
|
||||
"importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします(インポートは CC Switch がエクスポートしたバックアップのみ対応)。",
|
||||
"importExportHint": "移行や復元用にデータベースの SQL バックアップをインポート/エクスポートします。",
|
||||
"exportConfig": "SQL バックアップをエクスポート",
|
||||
"selectConfigFile": "SQL ファイルを選択",
|
||||
"noFileSelected": "ファイルが選択されていません。",
|
||||
@@ -166,7 +166,7 @@
|
||||
"selectFileFailed": "有効な SQL バックアップファイルを選択してください",
|
||||
"configCorrupted": "SQL ファイルが壊れているか形式が無効な可能性があります",
|
||||
"backupId": "バックアップ ID",
|
||||
"autoReload": "データを更新しました",
|
||||
"autoReload": "2 秒後に自動で再読み込みします...",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"languageOptionJapanese": "日本語",
|
||||
@@ -273,8 +273,7 @@
|
||||
"zhipu": "Zhipu GLM は CC Switch の公式パートナーです。リンク経由でチャージすると 10% 割引",
|
||||
"packycode": "PackyCode は CC Switch の公式パートナーです。登録後チャージ時に \"cc-switch\" を入力すると 10% オフ",
|
||||
"minimax_cn": "MiniMax Coding Plan 特別価格、Starter ¥9.9 から",
|
||||
"minimax_en": "MiniMax Coding Plan Black Friday、Starter が月額 $2(80% OFF)",
|
||||
"dmxapi": "Claude Code 専用モデル 66% OFF 実施中!"
|
||||
"minimax_en": "MiniMax Coding Plan Black Friday、Starter が月額 $2(80% OFF)"
|
||||
},
|
||||
"parameterConfig": "パラメーター設定 - {{name}} *",
|
||||
"mainModel": "メインモデル(任意)",
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
"themeDark": "深色",
|
||||
"themeSystem": "跟随系统",
|
||||
"importExport": "SQL 导入导出",
|
||||
"importExportHint": "导入/导出数据库 SQL 备份(仅支持导入由 CC Switch 导出的备份),便于备份或迁移。",
|
||||
"importExportHint": "导入/导出数据库 SQL 备份,便于备份或迁移。",
|
||||
"exportConfig": "导出 SQL 备份",
|
||||
"selectConfigFile": "选择 SQL 文件",
|
||||
"noFileSelected": "尚未选择配置文件。",
|
||||
@@ -166,7 +166,7 @@
|
||||
"selectFileFailed": "请选择有效的 SQL 备份文件",
|
||||
"configCorrupted": "SQL 文件可能已损坏或格式不正确",
|
||||
"backupId": "备份ID",
|
||||
"autoReload": "数据已刷新",
|
||||
"autoReload": "数据将在2秒后自动刷新...",
|
||||
"languageOptionChinese": "中文",
|
||||
"languageOptionEnglish": "English",
|
||||
"languageOptionJapanese": "日本語",
|
||||
@@ -273,8 +273,7 @@
|
||||
"zhipu": "智谱 GLM 是 CC Switch 的官方合作伙伴,使用此链接充值可以获得9折优惠",
|
||||
"packycode": "PackyCode 是 CC Switch 的官方合作伙伴,使用此链接注册并在充值时填写 \"cc-switch\" 优惠码,可以享受9折优惠",
|
||||
"minimax_cn": "MiniMax Coding Plan 特惠,Starter 套餐 9.9 元起",
|
||||
"minimax_en": "MiniMax Coding Plan 黑五特惠,Starter 套餐现仅 $2/月(2折优惠!)",
|
||||
"dmxapi": "Claude Code 专属模型 3.4 折优惠进行中!"
|
||||
"minimax_en": "MiniMax Coding Plan 黑五特惠,Starter 套餐现仅 $2/月(2折优惠!)"
|
||||
},
|
||||
"parameterConfig": "参数配置 - {{name}} *",
|
||||
"mainModel": "主模型 (可选)",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -45,10 +45,6 @@ export const icons: Record<string, string> = {
|
||||
yi: `<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>Yi</title><path d="M18.62 13.927c.611 0 1.107.505 1.107 1.128v5.817c0 .623-.496 1.128-1.108 1.128a1.118 1.118 0 01-1.108-1.128v-5.817c0-.623.496-1.128 1.108-1.128zM16.59 3.052a1.094 1.094 0 011.562-.129c.466.404.522 1.116.126 1.59l-5.938 7.111v9.147c0 .624-.496 1.129-1.108 1.129a1.118 1.118 0 01-1.108-1.129v-9.477l.003-.088.01-.087c.015-.232.102-.462.261-.654l6.192-7.413zM2.906 2.256a1.094 1.094 0 011.559.157l4.387 5.45a1.142 1.142 0 01-.155 1.587 1.094 1.094 0 01-1.559-.157l-4.387-5.45a1.144 1.144 0 01.06-1.498l.095-.09z"></path><ellipse cx="20.146" cy="10.692" fill="#00FF25" rx="1.354" ry="1.379"></ellipse></svg>`,
|
||||
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>`,
|
||||
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>`,
|
||||
};
|
||||
|
||||
export const iconList = Object.keys(icons);
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 819 B |
@@ -303,34 +303,6 @@ export const iconMetadata: Record<string, IconMetadata> = {
|
||||
keywords: ["chatglm", "glm"],
|
||||
defaultColor: "#0F62FE",
|
||||
},
|
||||
openrouter: {
|
||||
name: "openrouter",
|
||||
displayName: "OpenRouter",
|
||||
category: "ai-provider",
|
||||
keywords: ["openrouter", "router", "aggregator"],
|
||||
defaultColor: "#6566F1",
|
||||
},
|
||||
longcat: {
|
||||
name: "longcat",
|
||||
displayName: "LongCat",
|
||||
category: "ai-provider",
|
||||
keywords: ["longcat", "long", "cat"],
|
||||
defaultColor: "#29E154",
|
||||
},
|
||||
modelscope: {
|
||||
name: "modelscope",
|
||||
displayName: "ModelScope",
|
||||
category: "ai-provider",
|
||||
keywords: ["modelscope", "alibaba", "scope"],
|
||||
defaultColor: "#624AFF",
|
||||
},
|
||||
aihubmix: {
|
||||
name: "aihubmix",
|
||||
displayName: "AiHubMix",
|
||||
category: "ai-provider",
|
||||
keywords: ["aihubmix", "hub", "mix", "aggregator"],
|
||||
defaultColor: "#006FFB",
|
||||
},
|
||||
};
|
||||
|
||||
export function getIconMetadata(name: string): IconMetadata | undefined {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 632 B |
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 906 B |
@@ -1,4 +1,5 @@
|
||||
export interface ProxyConfig {
|
||||
enabled: boolean;
|
||||
listen_address: string;
|
||||
listen_port: number;
|
||||
max_retries: number;
|
||||
|
||||
@@ -110,6 +110,12 @@ describe("useImportExport Hook", () => {
|
||||
expect(result.current.status).toBe("success");
|
||||
expect(result.current.backupId).toBe("backup-123");
|
||||
expect(toastSuccessMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Skip delay to execute callback
|
||||
await act(async () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
});
|
||||
|
||||
expect(onImportSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user