mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +08:00
5376ea042b
* i18n: update cache terminology across all languages
- Change 'Cache Read' to 'Cache Hit' in all languages
- Change 'Cache Write' to 'Cache Creation' in all languages
- Update zh: 缓存读取 → 缓存命中, 缓存写入 → 缓存创建
- Update en: Cache Read → Cache Hit, Cache Write → Cache Creation
- Update ja: キャッシュ読取 → キャッシュヒット, キャッシュ書込 → キャッシュ作成
Affected keys: cacheReadTokens, cacheCreationTokens, cacheReadCost,
cacheWriteCost, cacheRead, cacheWrite
* feat(usage): add cache metrics to trend chart
- Add cache creation tokens visualization (orange line)
- Add cache hit tokens visualization (purple line)
- Add gradient definitions for new cache metrics
- Include cache data in hourly aggregation
- Display cache metrics alongside input/output tokens
This provides better visibility into cache usage patterns over time.
* fix(usage): fix timezone handling in datetime picker
- Add timestampToLocalDatetime() to convert Unix timestamp to local datetime
- Add localDatetimeToTimestamp() with validation for incomplete input
- Fix issue where typing hours/minutes would jump to previous day
- Validate datetime format completeness before conversion
- Use local timezone instead of UTC for datetime-local input
This resolves the issue where users couldn't fine-tune time selection
and the input would jump unexpectedly when editing hours or minutes.
* feat(usage): add auto-refresh for usage statistics
- Add 30-second auto-refresh interval for all usage queries
- Disable background refresh to save resources
- Apply to: summary, trends, provider stats, model stats, request logs
- Queries automatically update when tab is active
- Pause refresh when user switches to another tab
This keeps usage data fresh without manual refresh.
* fix(proxy): improve usage logging and cache token parsing
- Log requests even when usage parsing fails (with default values)
- Add detailed debug logging for usage metrics
- Support cache_read_input_tokens field in Codex responses
- Fallback to input_tokens_details.cached_tokens if needed
- Add test case for cached_tokens in input_tokens_details
- Ensure all requests are tracked in database for analytics
This fixes missing request logs when API responses lack usage data
and improves cache token detection across different response formats.
* style(rust): use inline format args in format! macros
- Replace format!("...", var) with format!("...{var}")
- Update universal provider ID formatting
- Update error message formatting
- Update config.toml generation in Codex provider
Fixes clippy::uninlined_format_args warnings.
* feat(proxy): enhance provider router logging
- Add debug logs for failover queue provider count
- Log circuit breaker state for each provider check
- Add logs for missing current provider scenarios
- Log when no current provider is configured
- Use inline format args for better readability
This improves debugging of provider selection and failover behavior.
* feat(database): update model pricing data
- Update Claude models to full version format (e.g. claude-opus-4-5-20251101)
- Add GPT-5.2 series model pricing (10 models)
- Add GPT-5.1 series model pricing (10 models)
- Add GPT-5 series model pricing (12 models)
- Add Gemini 3 series model pricing (2 models)
- Update Gemini 2.5 series model ID format (use dot separator)
- Unify display names by removing thinking level suffixes
* fix(usage): correct Gemini output token calculation
Fix Gemini API output token parsing to use totalTokenCount - promptTokenCount
instead of candidatesTokenCount alone. This ensures thoughtsTokenCount is
included in output statistics.
- Update from_gemini_response to calculate output from total - input
- Update from_gemini_stream_chunks with same logic for consistency
- Fix from_codex_stream_events to use adjusted token calculation
- Add test case for responses with thoughtsTokenCount
- Update existing tests to match new calculation logic
* fix(usage): correct cache token billing and add Codex format auto-detection
- Avoid double-billing cache tokens by subtracting from input before calculation
- Add smart Codex parser that auto-detects OpenAI vs Codex API format
- Extract model name from Codex responses for accurate tracking
* fix(proxy): improve takeover detection with live config check
- Add live config takeover detection for hot-switch decision
- Rebuild takeover when backup is missing or placeholder remains
- Make detect_takeover_in_live_config_for_app public
- Fix is_takeover_active to use actual takeover status
* refactor(usage): simplify model pricing lookup by removing suffix fallback
Replace complex suffix-stripping fallback with direct prefix/suffix cleanup.
Model IDs are now cleaned by removing vendor prefix (before /) and colon
suffix (after :), then matched exactly against pricing table.
* feat(database): add Chinese AI model pricing data
Add pricing for domestic AI models (CNY/1M tokens):
- Doubao-Seed-Code (ByteDance)
- DeepSeek V3/V3.1/V3.2
- Kimi K2/K2-Thinking/K2-Turbo (Moonshot)
- MiniMax M2/M2.1/M2.1-Lightning
- GLM-4.6/4.7 (Zhipu)
- Mimo V2 Flash (Xiaomi)
Also fix test case to use correct model ID and remove invalid currency column.
* refactor(proxy): improve header forwarding with blacklist approach
Change from whitelist to blacklist mode for request header forwarding.
Only skip headers that will be overridden (auth, host, content-length).
This preserves client's original headers and improves compatibility.
* fix(proxy): bypass timeout and retry configs when failover is disabled
When auto_failover_enabled is false, timeout and retry configurations
should not affect normal request flow. This change ensures:
- create_forwarder: passes 0 for all timeout/retry params when failover
is disabled, effectively bypassing these checks
- streaming_timeout_config: returns 0 for both first_byte_timeout and
idle_timeout when failover is disabled
This prevents unnecessary timeout errors and retry attempts when users
have explicitly disabled the failover feature.
* fix(proxy): handle zero value input in failover config fields
* refactor(proxy): remove retry logic and add enabled check for failover
* refactor(proxy): distinguish circuit-open from no-provider errors
* Align usage stats to sliding windows
* feat(proxy): add body and header filtering for upstream requests
* feat(proxy): enable transparent passthrough for headers
- Passthrough anthropic-beta header as-is from client
- Passthrough anthropic-version header from client
- Passthrough client IP headers (x-forwarded-for, x-real-ip) by default
- Filter private params (underscore-prefixed fields) from request body
- No database changes required
* feat(proxy): extract session ID from client requests for logging
- Add SessionIdExtractor to parse session ID from Claude/Codex requests
- Support extraction from metadata.user_id, headers, previous_response_id
- Pass session_id through RequestContext to usage logger
- Enable request correlation by session in proxy_request_logs
295 lines
9.2 KiB
Rust
295 lines
9.2 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 表读取)
|
||
// 只有当该应用已被代理接管(enabled=true)且开启了自动故障转移时才执行
|
||
let (app_enabled, auto_failover_enabled) = match db.get_proxy_config_for_app(&app_type).await {
|
||
Ok(config) => (config.enabled, config.auto_failover_enabled),
|
||
Err(e) => {
|
||
log::error!("[{app_type}] Failed to read proxy_config: {e}, defaulting to disabled");
|
||
(false, false)
|
||
}
|
||
};
|
||
|
||
if app_enabled && 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)
|
||
}
|