mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
e6f18ba801
* feat(proxy): extract model name from API response for accurate usage tracking - Add model field extraction in TokenUsage parsing for Claude, OpenAI, and Codex - Prioritize response model over request model in usage logging - Update model extractors to use parsed usage.model first - Add tests for model extraction in stream and non-stream responses * feat(proxy): implement streaming timeout control with validation - Add first byte timeout (0 or 1-180s) for streaming requests - Add idle timeout (0 or 60-600s) for streaming data gaps - Add non-streaming timeout (0 or 60-1800s) for total request - Implement timeout logic in response processor - Add 1800s global timeout fallback when disabled - Add database schema migration for timeout fields - Add i18n translations for timeout settings * feat(proxy): add model mapping module for provider-based model substitution - Add model_mapper.rs with ModelMapping struct to extract model configs from Provider - Support ANTHROPIC_MODEL, ANTHROPIC_REASONING_MODEL, and default models for haiku/sonnet/opus - Implement thinking mode detection for reasoning model priority - Include comprehensive unit tests for all mapping scenarios * fix(proxy): bypass circuit breaker for single provider scenario When failover is disabled (single provider), circuit breaker open state would block all requests causing poor UX. Now bypasses circuit breaker check in this scenario. Also integrates model mapping into request flow. * feat(ui): add reasoning model field to Claude provider form Add ANTHROPIC_REASONING_MODEL configuration field for Claude providers, allowing users to specify a dedicated model for thinking/reasoning tasks. * feat(proxy): add openrouter_compat_mode for optional format conversion Add configurable OpenRouter compatibility mode that enables Anthropic to OpenAI format conversion. When enabled, rewrites endpoint to /v1/chat/completions and transforms request/response formats. Defaults to enabled for OpenRouter. * feat(ui): add OpenRouter compatibility mode toggle Add UI toggle for OpenRouter providers to enable/disable compatibility mode which uses OpenAI Chat Completions format with SSE conversion. * feat(stream-check): use provider-configured model for health checks Extract model from provider's settings_config (ANTHROPIC_MODEL, GEMINI_MODEL, or Codex config.toml) instead of always using default test models. * refactor(ui): remove timeout settings from AutoFailoverConfigPanel Remove streaming/non-streaming timeout configuration from failover panel as these settings have been moved to a dedicated location. * refactor(database): migrate proxy_config to per-app three-row structure Replace singleton proxy_config table with app_type primary key structure, allowing independent proxy settings for Claude, Codex, and Gemini. Add GlobalProxyConfig queries and per-app config management in DAO layer. * feat(proxy): add GlobalProxyConfig and AppProxyConfig types Add new type definitions for the refactored proxy configuration: - GlobalProxyConfig: shared settings (enabled, address, port, logging) - AppProxyConfig: per-app settings (failover, timeouts, circuit breaker) * refactor(proxy): update service layer for per-app config structure Adapt proxy service, handler context, and provider router to use the new per-app configuration model. Read enabled/timeout settings from proxy_config table instead of settings table. * feat(commands): add global and per-app proxy config commands Add new Tauri commands for the refactored proxy configuration: - get_global_proxy_config / update_global_proxy_config - get_proxy_config_for_app / update_proxy_config_for_app Update startup restore logic to read from proxy_config table. * feat(api): add frontend API and Query hooks for proxy config Add TypeScript wrappers and TanStack Query hooks for: - Global proxy config (address, port, logging) - Per-app proxy config (failover, timeouts, circuit breaker) - Proxy takeover status management * refactor(ui): redesign proxy panel with inline config controls Replace ProxySettingsDialog with inline controls in ProxyPanel. Add per-app takeover switches and global address/port settings. Simplify AutoFailoverConfigPanel by removing timeout settings. * feat(i18n): add proxy takeover translations and update types Add i18n strings for proxy takeover status in zh/en/ja. Update TypeScript types for GlobalProxyConfig and AppProxyConfig. * refactor(proxy): load circuit breaker config per-app instead of globally Extract app_type from router key and read circuit breaker settings from the corresponding proxy_config row for each application.
296 lines
9.1 KiB
Rust
296 lines
9.1 KiB
Rust
//! 代理服务相关的 Tauri 命令
|
||
//!
|
||
//! 提供前端调用的 API 接口
|
||
|
||
use crate::proxy::types::*;
|
||
use crate::proxy::{CircuitBreakerConfig, CircuitBreakerStats};
|
||
use crate::store::AppState;
|
||
|
||
/// 启动代理服务器(仅启动服务,不接管 Live 配置)
|
||
#[tauri::command]
|
||
pub async fn start_proxy_server(
|
||
state: tauri::State<'_, AppState>,
|
||
) -> Result<ProxyServerInfo, String> {
|
||
state.proxy_service.start().await
|
||
}
|
||
|
||
/// 停止代理服务器(恢复 Live 配置)
|
||
#[tauri::command]
|
||
pub async fn stop_proxy_with_restore(state: tauri::State<'_, AppState>) -> Result<(), String> {
|
||
state.proxy_service.stop_with_restore().await
|
||
}
|
||
|
||
/// 获取各应用接管状态
|
||
#[tauri::command]
|
||
pub async fn get_proxy_takeover_status(
|
||
state: tauri::State<'_, AppState>,
|
||
) -> Result<ProxyTakeoverStatus, String> {
|
||
state.proxy_service.get_takeover_status().await
|
||
}
|
||
|
||
/// 为指定应用开启/关闭接管
|
||
#[tauri::command]
|
||
pub async fn set_proxy_takeover_for_app(
|
||
state: tauri::State<'_, AppState>,
|
||
app_type: String,
|
||
enabled: bool,
|
||
) -> Result<(), String> {
|
||
state
|
||
.proxy_service
|
||
.set_takeover_for_app(&app_type, enabled)
|
||
.await
|
||
}
|
||
|
||
/// 获取代理服务器状态
|
||
#[tauri::command]
|
||
pub async fn get_proxy_status(state: tauri::State<'_, AppState>) -> Result<ProxyStatus, String> {
|
||
state.proxy_service.get_status().await
|
||
}
|
||
|
||
/// 获取代理配置
|
||
#[tauri::command]
|
||
pub async fn get_proxy_config(state: tauri::State<'_, AppState>) -> Result<ProxyConfig, String> {
|
||
state.proxy_service.get_config().await
|
||
}
|
||
|
||
/// 更新代理配置
|
||
#[tauri::command]
|
||
pub async fn update_proxy_config(
|
||
state: tauri::State<'_, AppState>,
|
||
config: ProxyConfig,
|
||
) -> Result<(), String> {
|
||
state.proxy_service.update_config(&config).await
|
||
}
|
||
|
||
// ==================== Global & Per-App Config ====================
|
||
|
||
/// 获取全局代理配置
|
||
///
|
||
/// 返回统一的全局配置字段(代理开关、监听地址、端口、日志开关)
|
||
#[tauri::command]
|
||
pub async fn get_global_proxy_config(
|
||
state: tauri::State<'_, AppState>,
|
||
) -> Result<GlobalProxyConfig, String> {
|
||
let db = &state.db;
|
||
db.get_global_proxy_config()
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 更新全局代理配置
|
||
///
|
||
/// 更新统一的全局配置字段,会同时更新三行(claude/codex/gemini)
|
||
#[tauri::command]
|
||
pub async fn update_global_proxy_config(
|
||
state: tauri::State<'_, AppState>,
|
||
config: GlobalProxyConfig,
|
||
) -> Result<(), String> {
|
||
let db = &state.db;
|
||
db.update_global_proxy_config(config)
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 获取指定应用的代理配置
|
||
///
|
||
/// 返回应用级配置(enabled、auto_failover、超时、熔断器等)
|
||
#[tauri::command]
|
||
pub async fn get_proxy_config_for_app(
|
||
state: tauri::State<'_, AppState>,
|
||
app_type: String,
|
||
) -> Result<AppProxyConfig, String> {
|
||
let db = &state.db;
|
||
db.get_proxy_config_for_app(&app_type)
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 更新指定应用的代理配置
|
||
///
|
||
/// 更新应用级配置(enabled、auto_failover、超时、熔断器等)
|
||
#[tauri::command]
|
||
pub async fn update_proxy_config_for_app(
|
||
state: tauri::State<'_, AppState>,
|
||
config: AppProxyConfig,
|
||
) -> Result<(), String> {
|
||
let db = &state.db;
|
||
db.update_proxy_config_for_app(config)
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 检查代理服务器是否正在运行
|
||
#[tauri::command]
|
||
pub async fn is_proxy_running(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||
Ok(state.proxy_service.is_running().await)
|
||
}
|
||
|
||
/// 检查是否处于 Live 接管模式
|
||
#[tauri::command]
|
||
pub async fn is_live_takeover_active(state: tauri::State<'_, AppState>) -> Result<bool, String> {
|
||
state.proxy_service.is_takeover_active().await
|
||
}
|
||
|
||
/// 代理模式下切换供应商(热切换)
|
||
#[tauri::command]
|
||
pub async fn switch_proxy_provider(
|
||
state: tauri::State<'_, AppState>,
|
||
app_type: String,
|
||
provider_id: String,
|
||
) -> Result<(), String> {
|
||
state
|
||
.proxy_service
|
||
.switch_proxy_target(&app_type, &provider_id)
|
||
.await
|
||
}
|
||
|
||
// ==================== 故障转移相关命令 ====================
|
||
|
||
/// 获取供应商健康状态
|
||
#[tauri::command]
|
||
pub async fn get_provider_health(
|
||
state: tauri::State<'_, AppState>,
|
||
provider_id: String,
|
||
app_type: String,
|
||
) -> Result<ProviderHealth, String> {
|
||
let db = &state.db;
|
||
db.get_provider_health(&provider_id, &app_type)
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 重置熔断器
|
||
///
|
||
/// 重置后会检查是否应该切回队列中优先级更高的供应商:
|
||
/// 1. 检查自动故障转移是否开启
|
||
/// 2. 如果恢复的供应商在队列中优先级更高(queue_order 更小),则自动切换
|
||
#[tauri::command]
|
||
pub async fn reset_circuit_breaker(
|
||
app_handle: tauri::AppHandle,
|
||
state: tauri::State<'_, AppState>,
|
||
provider_id: String,
|
||
app_type: String,
|
||
) -> Result<(), String> {
|
||
// 1. 重置数据库健康状态
|
||
let db = &state.db;
|
||
db.update_provider_health(&provider_id, &app_type, true, None)
|
||
.await
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
// 2. 如果代理正在运行,重置内存中的熔断器状态
|
||
state
|
||
.proxy_service
|
||
.reset_provider_circuit_breaker(&provider_id, &app_type)
|
||
.await?;
|
||
|
||
// 3. 检查是否应该切回优先级更高的供应商(从 proxy_config 表读取)
|
||
let auto_failover_enabled = match db.get_proxy_config_for_app(&app_type).await {
|
||
Ok(config) => config.auto_failover_enabled,
|
||
Err(e) => {
|
||
log::error!(
|
||
"[{app_type}] Failed to read proxy_config for auto_failover_enabled: {e}, defaulting to disabled"
|
||
);
|
||
false
|
||
}
|
||
};
|
||
|
||
if auto_failover_enabled && state.proxy_service.is_running().await {
|
||
// 获取当前供应商 ID
|
||
let current_id = db
|
||
.get_current_provider(&app_type)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
if let Some(current_id) = current_id {
|
||
// 获取故障转移队列
|
||
let queue = db
|
||
.get_failover_queue(&app_type)
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
// 找到恢复的供应商和当前供应商在队列中的位置(使用 sort_index)
|
||
let restored_order = queue
|
||
.iter()
|
||
.find(|item| item.provider_id == provider_id)
|
||
.and_then(|item| item.sort_index);
|
||
|
||
let current_order = queue
|
||
.iter()
|
||
.find(|item| item.provider_id == current_id)
|
||
.and_then(|item| item.sort_index);
|
||
|
||
// 如果恢复的供应商优先级更高(sort_index 更小),则切换
|
||
if let (Some(restored), Some(current)) = (restored_order, current_order) {
|
||
if restored < current {
|
||
log::info!(
|
||
"[Recovery] 供应商 {provider_id} 已恢复且优先级更高 (P{restored} vs P{current}),自动切换"
|
||
);
|
||
|
||
// 获取供应商名称用于日志和事件
|
||
let provider_name = db
|
||
.get_all_providers(&app_type)
|
||
.ok()
|
||
.and_then(|providers| providers.get(&provider_id).map(|p| p.name.clone()))
|
||
.unwrap_or_else(|| provider_id.clone());
|
||
|
||
// 创建故障转移切换管理器并执行切换
|
||
let switch_manager =
|
||
crate::proxy::failover_switch::FailoverSwitchManager::new(db.clone());
|
||
if let Err(e) = switch_manager
|
||
.try_switch(Some(&app_handle), &app_type, &provider_id, &provider_name)
|
||
.await
|
||
{
|
||
log::error!("[Recovery] 自动切换失败: {e}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 获取熔断器配置
|
||
#[tauri::command]
|
||
pub async fn get_circuit_breaker_config(
|
||
state: tauri::State<'_, AppState>,
|
||
) -> Result<CircuitBreakerConfig, String> {
|
||
let db = &state.db;
|
||
db.get_circuit_breaker_config()
|
||
.await
|
||
.map_err(|e| e.to_string())
|
||
}
|
||
|
||
/// 更新熔断器配置
|
||
#[tauri::command]
|
||
pub async fn update_circuit_breaker_config(
|
||
state: tauri::State<'_, AppState>,
|
||
config: CircuitBreakerConfig,
|
||
) -> Result<(), String> {
|
||
let db = &state.db;
|
||
|
||
// 1. 更新数据库配置
|
||
db.update_circuit_breaker_config(&config)
|
||
.await
|
||
.map_err(|e| e.to_string())?;
|
||
|
||
// 2. 如果代理正在运行,热更新内存中的熔断器配置
|
||
state
|
||
.proxy_service
|
||
.update_circuit_breaker_configs(config)
|
||
.await?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 获取熔断器统计信息(仅当代理服务器运行时)
|
||
#[tauri::command]
|
||
pub async fn get_circuit_breaker_stats(
|
||
state: tauri::State<'_, AppState>,
|
||
provider_id: String,
|
||
app_type: String,
|
||
) -> Result<Option<CircuitBreakerStats>, String> {
|
||
// 这个功能需要访问运行中的代理服务器的内存状态
|
||
// 目前先返回 None,后续可以通过 ProxyService 暴露接口来实现
|
||
let _ = (state, provider_id, app_type);
|
||
Ok(None)
|
||
}
|