mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +08:00
ec20ff4d8c
* feat(proxy): add error mapper for HTTP status code mapping - Add error_mapper.rs module to map ProxyError to HTTP status codes - Implement map_proxy_error_to_status() for error classification - Implement get_error_message() for user-friendly error messages - Support all error types: upstream, timeout, connection, provider failures - Include comprehensive unit tests for all mappings * feat(proxy): enhance error logging with context support - Add log_error_with_context() method for detailed error recording - Support streaming flag, session_id, and provider_type fields - Remove dead_code warning from log_error() method - Enable comprehensive error request tracking in database * feat(proxy): implement error capture and logging in all handlers - Capture and log all failed requests in handle_messages (Claude) - Capture and log all failed requests in handle_gemini (Gemini) - Capture and log all failed requests in handle_responses (Codex) - Capture and log all failed requests in handle_chat_completions (Codex) - Record error status codes, messages, and latency for all failures - Generate unique session_id for each request - Support both streaming and non-streaming error scenarios * style: fix clippy warnings and typescript errors - Add allow(dead_code) for CircuitBreaker::get_state (reserved for future) - Fix all uninlined format string warnings (27 instances) - Use inline format syntax for better readability - Fix unused import and parameter warnings in ProviderActions.tsx - Achieve zero warnings in both Rust and TypeScript * style: apply code formatting - Remove trailing whitespace in misc.rs - Add trailing comma in App.tsx - Format multi-line className in ProviderCard.tsx * feat(proxy): add settings button to proxy panel Add configuration buttons in both running and stopped states to provide easy access to proxy settings dialog. * fix(speedtest): skip client build for invalid inputs * chore(clippy): fix uninlined format args * Merge branch 'main' into feature/error-request-logging
135 lines
4.6 KiB
Rust
135 lines
4.6 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> {
|
||
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)
|
||
}
|
||
}
|