mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-04-08 15:10:34 +08:00
* refactor(proxy): simplify logging for better readability - Delete 17 verbose debug logs from handlers, streaming, and response_processor - Convert excessive INFO logs to DEBUG level for internal processing details - Add 2 critical INFO logs in forwarder.rs for failover scenarios: - Log when switching to next provider after failure - Log when all providers have been exhausted - Fix clippy uninlined_format_args warning This reduces log noise while maintaining visibility into key user-facing decisions. * fix: replace unsafe unwrap() calls with proper error handling - database/dao/mcp.rs: Use map_err for serde_json serialization - database/dao/providers.rs: Use map_err for settings_config and meta serialization - commands/misc.rs: Use expect() for compile-time regex pattern - services/prompt.rs: Use unwrap_or_default() for SystemTime - deeplink/provider.rs: Replace unwrap() with is_none_or pattern for Option checks Reduces potential panic points from 26 to 1 (static regex init, safe). * refactor(proxy): simplify verbose logging output - Remove response JSON full output logging in response_processor - Remove per-request INFO logs in provider_router (failover status, provider selection) - Change model mapping log from INFO to DEBUG - Change usage logging failure from INFO to WARN - Remove redundant debug logs for circuit breaker operations Reduces log noise significantly while preserving important warnings and errors. * feat(proxy): add structured log codes for i18n support Add error code system to proxy module logs for multi-language support: - CB-001~006: Circuit breaker state transitions and triggers - SRV-001~004: Proxy server lifecycle events - FWD-001~002: Request forwarding and failover - FO-001~005: Failover switch operations - USG-001~002: Usage logging errors Log format: [CODE] Chinese message Frontend/log tools can map codes to any language. New file: src/proxy/log_codes.rs - centralized code definitions * chore: bump version to 3.9.1 * style: format code with prettier and rustfmt * fix(ui): allow number inputs to be fully cleared before saving - Convert numeric state to string type for controlled inputs - Use isNaN() check instead of || fallback to allow 0 values - Apply fix to ProxyPanel, CircuitBreakerConfigPanel, AutoFailoverConfigPanel, and ModelTestConfigPanel * feat(pricing): support @ separator in model name matching - Refactor model name cleaning into chained method calls - Add @ to - replacement (e.g., gpt-5.2-codex@low → gpt-5.2-codex-low) - Add test case for @ separator matching * fix(proxy): improve validation and error handling in proxy config panels - Add StopTimeout/StopFailed error types for proper stop() error reporting - Replace silent clamp with validation-and-block in config panels - Add listenAddress format validation in ProxyPanel - Use log_codes constants instead of hardcoded strings - Use once_cell::Lazy for regex precompilation * fix(proxy): harden error handling and input validation - Handle RwLock poisoning in settings.rs with unwrap_or_else - Add fallback for dirs::home_dir() in config modules - Normalize localhost to 127.0.0.1 in ProxyPanel - Format IPv6 addresses with brackets for valid URLs - Strict port validation with pure digit regex - Treat NaN as validation failure in config panels - Log warning on cost_multiplier parse failure - Align timeoutSeconds range to [0, 300] across all panels
148 lines
5.1 KiB
Rust
148 lines
5.1 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!("[FO-002] 无法读取 {app_type} 配置: {e},跳过切换");
|
||
return Ok(false);
|
||
}
|
||
};
|
||
|
||
if !app_enabled {
|
||
log::debug!("[Failover] {app_type} 未启用代理,跳过切换");
|
||
return Ok(false);
|
||
}
|
||
|
||
log::info!("[FO-001] 切换: {app_type} → {provider_name}");
|
||
|
||
// 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!("[FO-003] 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}");
|
||
}
|
||
}
|
||
|
||
Ok(true)
|
||
}
|
||
}
|