mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +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
150 lines
5.2 KiB
Rust
150 lines
5.2 KiB
Rust
//! 故障转移切换模块
|
||
//!
|
||
//! 处理故障转移成功后的供应商切换逻辑,包括:
|
||
//! - 去重控制(避免多个请求同时触发)
|
||
//! - 数据库更新
|
||
//! - 托盘菜单更新
|
||
//! - 前端事件发射
|
||
//! - Live 备份更新
|
||
|
||
use crate::database::Database;
|
||
use crate::error::AppError;
|
||
use std::collections::HashSet;
|
||
use std::str::FromStr;
|
||
use std::sync::Arc;
|
||
use tauri::{Emitter, Manager};
|
||
use tokio::sync::RwLock;
|
||
|
||
/// 故障转移切换管理器
|
||
///
|
||
/// 负责处理故障转移成功后的供应商切换,确保 UI 能够直观反映当前使用的供应商。
|
||
#[derive(Clone)]
|
||
pub struct FailoverSwitchManager {
|
||
/// 正在处理中的切换(key = "app_type:provider_id")
|
||
pending_switches: Arc<RwLock<HashSet<String>>>,
|
||
db: Arc<Database>,
|
||
}
|
||
|
||
impl FailoverSwitchManager {
|
||
pub fn new(db: Arc<Database>) -> Self {
|
||
Self {
|
||
pending_switches: Arc::new(RwLock::new(HashSet::new())),
|
||
db,
|
||
}
|
||
}
|
||
|
||
/// 尝试执行故障转移切换
|
||
///
|
||
/// 如果相同的切换已在进行中,则跳过;否则执行切换逻辑。
|
||
///
|
||
/// # Returns
|
||
/// - `Ok(true)` - 切换成功执行
|
||
/// - `Ok(false)` - 切换已在进行中,跳过
|
||
/// - `Err(e)` - 切换过程中发生错误
|
||
pub async fn try_switch(
|
||
&self,
|
||
app_handle: Option<&tauri::AppHandle>,
|
||
app_type: &str,
|
||
provider_id: &str,
|
||
provider_name: &str,
|
||
) -> Result<bool, AppError> {
|
||
let switch_key = format!("{app_type}:{provider_id}");
|
||
|
||
// 去重检查:如果相同切换已在进行中,跳过
|
||
{
|
||
let mut pending = self.pending_switches.write().await;
|
||
if pending.contains(&switch_key) {
|
||
log::debug!("[Failover] 切换已在进行中,跳过: {app_type} -> {provider_id}");
|
||
return Ok(false);
|
||
}
|
||
pending.insert(switch_key.clone());
|
||
}
|
||
|
||
// 执行切换(确保最后清理 pending 标记)
|
||
let result = self
|
||
.do_switch(app_handle, app_type, provider_id, provider_name)
|
||
.await;
|
||
|
||
// 清理 pending 标记
|
||
{
|
||
let mut pending = self.pending_switches.write().await;
|
||
pending.remove(&switch_key);
|
||
}
|
||
|
||
result
|
||
}
|
||
|
||
async fn do_switch(
|
||
&self,
|
||
app_handle: Option<&tauri::AppHandle>,
|
||
app_type: &str,
|
||
provider_id: &str,
|
||
provider_name: &str,
|
||
) -> Result<bool, AppError> {
|
||
// 检查该应用是否已被代理接管(enabled=true)
|
||
// 只有被接管的应用才允许执行故障转移切换
|
||
let app_enabled = match self.db.get_proxy_config_for_app(app_type).await {
|
||
Ok(config) => config.enabled,
|
||
Err(e) => {
|
||
log::warn!("[Failover] 无法读取 {app_type} 配置: {e},跳过切换");
|
||
return Ok(false);
|
||
}
|
||
};
|
||
|
||
if !app_enabled {
|
||
log::info!("[Failover] {app_type} 未被代理接管(enabled=false),跳过切换");
|
||
return Ok(false);
|
||
}
|
||
|
||
log::info!("[Failover] 开始切换供应商: {app_type} -> {provider_name} ({provider_id})");
|
||
|
||
// 1. 更新数据库 is_current
|
||
self.db.set_current_provider(app_type, provider_id)?;
|
||
|
||
// 2. 更新本地 settings(设备级)
|
||
let app_type_enum = crate::app_config::AppType::from_str(app_type)
|
||
.map_err(|_| AppError::Message(format!("无效的应用类型: {app_type}")))?;
|
||
crate::settings::set_current_provider(&app_type_enum, Some(provider_id))?;
|
||
|
||
// 3. 更新托盘菜单和发射事件
|
||
if let Some(app) = app_handle {
|
||
// 更新托盘菜单
|
||
if let Some(app_state) = app.try_state::<crate::store::AppState>() {
|
||
// 更新 Live 备份(确保代理停止时恢复正确配置)
|
||
if let Ok(Some(provider)) = self.db.get_provider_by_id(provider_id, app_type) {
|
||
if let Err(e) = app_state
|
||
.proxy_service
|
||
.update_live_backup_from_provider(app_type, &provider)
|
||
.await
|
||
{
|
||
log::warn!("[Failover] 更新 Live 备份失败: {e}");
|
||
}
|
||
}
|
||
|
||
// 重建托盘菜单
|
||
if let Ok(new_menu) = crate::tray::create_tray_menu(app, app_state.inner()) {
|
||
if let Some(tray) = app.tray_by_id("main") {
|
||
if let Err(e) = tray.set_menu(Some(new_menu)) {
|
||
log::error!("[Failover] 更新托盘菜单失败: {e}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 发射事件到前端
|
||
let event_data = serde_json::json!({
|
||
"appType": app_type,
|
||
"providerId": provider_id,
|
||
"source": "failover" // 标识来源是故障转移
|
||
});
|
||
if let Err(e) = app.emit("provider-switched", event_data) {
|
||
log::error!("[Failover] 发射供应商切换事件失败: {e}");
|
||
}
|
||
}
|
||
|
||
log::info!("[Failover] 供应商切换完成: {app_type} -> {provider_name} ({provider_id})");
|
||
|
||
Ok(true)
|
||
}
|
||
}
|