mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-26 14:35:22 +08:00
6dd809701b
* 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
254 lines
9.0 KiB
Rust
254 lines
9.0 KiB
Rust
//! HTTP代理服务器
|
|
//!
|
|
//! 基于Axum的HTTP服务器,处理代理请求
|
|
|
|
use super::{
|
|
failover_switch::FailoverSwitchManager, handlers, log_codes::srv as log_srv,
|
|
provider_router::ProviderRouter, types::*, ProxyError,
|
|
};
|
|
use crate::database::Database;
|
|
use axum::{
|
|
routing::{get, post},
|
|
Router,
|
|
};
|
|
use std::net::SocketAddr;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{oneshot, RwLock};
|
|
use tokio::task::JoinHandle;
|
|
use tower_http::cors::{Any, CorsLayer};
|
|
|
|
/// 代理服务器状态(共享)
|
|
#[derive(Clone)]
|
|
pub struct ProxyState {
|
|
pub db: Arc<Database>,
|
|
pub config: Arc<RwLock<ProxyConfig>>,
|
|
pub status: Arc<RwLock<ProxyStatus>>,
|
|
pub start_time: Arc<RwLock<Option<std::time::Instant>>>,
|
|
/// 每个应用类型当前使用的 provider (app_type -> (provider_id, provider_name))
|
|
pub current_providers: Arc<RwLock<std::collections::HashMap<String, (String, String)>>>,
|
|
/// 共享的 ProviderRouter(持有熔断器状态,跨请求保持)
|
|
pub provider_router: Arc<ProviderRouter>,
|
|
/// AppHandle,用于发射事件和更新托盘菜单
|
|
pub app_handle: Option<tauri::AppHandle>,
|
|
/// 故障转移切换管理器
|
|
pub failover_manager: Arc<FailoverSwitchManager>,
|
|
}
|
|
|
|
/// 代理HTTP服务器
|
|
pub struct ProxyServer {
|
|
config: ProxyConfig,
|
|
state: ProxyState,
|
|
shutdown_tx: Arc<RwLock<Option<oneshot::Sender<()>>>>,
|
|
/// 服务器任务句柄,用于等待服务器实际关闭
|
|
server_handle: Arc<RwLock<Option<JoinHandle<()>>>>,
|
|
}
|
|
|
|
impl ProxyServer {
|
|
pub fn new(
|
|
config: ProxyConfig,
|
|
db: Arc<Database>,
|
|
app_handle: Option<tauri::AppHandle>,
|
|
) -> Self {
|
|
// 创建共享的 ProviderRouter(熔断器状态将跨所有请求保持)
|
|
let provider_router = Arc::new(ProviderRouter::new(db.clone()));
|
|
// 创建故障转移切换管理器
|
|
let failover_manager = Arc::new(FailoverSwitchManager::new(db.clone()));
|
|
|
|
let state = ProxyState {
|
|
db,
|
|
config: Arc::new(RwLock::new(config.clone())),
|
|
status: Arc::new(RwLock::new(ProxyStatus::default())),
|
|
start_time: Arc::new(RwLock::new(None)),
|
|
current_providers: Arc::new(RwLock::new(std::collections::HashMap::new())),
|
|
provider_router,
|
|
app_handle,
|
|
failover_manager,
|
|
};
|
|
|
|
Self {
|
|
config,
|
|
state,
|
|
shutdown_tx: Arc::new(RwLock::new(None)),
|
|
server_handle: Arc::new(RwLock::new(None)),
|
|
}
|
|
}
|
|
|
|
pub async fn start(&self) -> Result<ProxyServerInfo, ProxyError> {
|
|
// 检查是否已在运行
|
|
if self.shutdown_tx.read().await.is_some() {
|
|
return Err(ProxyError::AlreadyRunning);
|
|
}
|
|
|
|
let addr: SocketAddr =
|
|
format!("{}:{}", self.config.listen_address, self.config.listen_port)
|
|
.parse()
|
|
.map_err(|e| ProxyError::BindFailed(format!("无效的地址: {e}")))?;
|
|
|
|
// 创建关闭通道
|
|
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
|
|
|
// 构建路由
|
|
let app = self.build_router();
|
|
|
|
// 绑定监听器
|
|
let listener = tokio::net::TcpListener::bind(&addr)
|
|
.await
|
|
.map_err(|e| ProxyError::BindFailed(e.to_string()))?;
|
|
|
|
log::info!("[{}] 代理服务器启动于 {addr}", log_srv::STARTED);
|
|
|
|
// 保存关闭句柄
|
|
*self.shutdown_tx.write().await = Some(shutdown_tx);
|
|
|
|
// 更新状态
|
|
let mut status = self.state.status.write().await;
|
|
status.running = true;
|
|
status.address = self.config.listen_address.clone();
|
|
status.port = self.config.listen_port;
|
|
drop(status);
|
|
|
|
// 记录启动时间
|
|
*self.state.start_time.write().await = Some(std::time::Instant::now());
|
|
|
|
// 启动服务器
|
|
let state = self.state.clone();
|
|
let handle = tokio::spawn(async move {
|
|
axum::serve(listener, app)
|
|
.with_graceful_shutdown(async {
|
|
shutdown_rx.await.ok();
|
|
})
|
|
.await
|
|
.ok();
|
|
|
|
// 服务器停止后更新状态
|
|
state.status.write().await.running = false;
|
|
*state.start_time.write().await = None;
|
|
});
|
|
|
|
// 保存服务器任务句柄
|
|
*self.server_handle.write().await = Some(handle);
|
|
|
|
Ok(ProxyServerInfo {
|
|
address: self.config.listen_address.clone(),
|
|
port: self.config.listen_port,
|
|
started_at: chrono::Utc::now().to_rfc3339(),
|
|
})
|
|
}
|
|
|
|
pub async fn stop(&self) -> Result<(), ProxyError> {
|
|
// 1. 发送关闭信号
|
|
if let Some(tx) = self.shutdown_tx.write().await.take() {
|
|
let _ = tx.send(());
|
|
} else {
|
|
return Err(ProxyError::NotRunning);
|
|
}
|
|
|
|
// 2. 等待服务器任务结束(带 5 秒超时保护)
|
|
if let Some(handle) = self.server_handle.write().await.take() {
|
|
match tokio::time::timeout(std::time::Duration::from_secs(5), handle).await {
|
|
Ok(Ok(())) => {
|
|
log::info!("[{}] 代理服务器已完全停止", log_srv::STOPPED);
|
|
Ok(())
|
|
}
|
|
Ok(Err(e)) => {
|
|
log::warn!("[{}] 代理服务器任务异常终止: {e}", log_srv::TASK_ERROR);
|
|
Err(ProxyError::StopFailed(e.to_string()))
|
|
}
|
|
Err(_) => {
|
|
log::warn!(
|
|
"[{}] 代理服务器停止超时(5秒),强制继续",
|
|
log_srv::STOP_TIMEOUT
|
|
);
|
|
Err(ProxyError::StopTimeout)
|
|
}
|
|
}
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub async fn get_status(&self) -> ProxyStatus {
|
|
let mut status = self.state.status.read().await.clone();
|
|
|
|
// 计算运行时间
|
|
if let Some(start) = *self.state.start_time.read().await {
|
|
status.uptime_seconds = start.elapsed().as_secs();
|
|
}
|
|
|
|
// 从 current_providers HashMap 获取每个应用类型当前正在使用的 provider
|
|
let current_providers = self.state.current_providers.read().await;
|
|
status.active_targets = current_providers
|
|
.iter()
|
|
.map(|(app_type, (provider_id, provider_name))| ActiveTarget {
|
|
app_type: app_type.clone(),
|
|
provider_id: provider_id.clone(),
|
|
provider_name: provider_name.clone(),
|
|
})
|
|
.collect();
|
|
|
|
status
|
|
}
|
|
|
|
fn build_router(&self) -> Router {
|
|
let cors = CorsLayer::new()
|
|
.allow_origin(Any)
|
|
.allow_methods(Any)
|
|
.allow_headers(Any);
|
|
|
|
Router::new()
|
|
// 健康检查
|
|
.route("/health", get(handlers::health_check))
|
|
.route("/status", get(handlers::get_status))
|
|
// Claude API (支持带前缀和不带前缀两种格式)
|
|
.route("/v1/messages", post(handlers::handle_messages))
|
|
.route("/claude/v1/messages", post(handlers::handle_messages))
|
|
// OpenAI Chat Completions API (Codex CLI,支持带前缀和不带前缀)
|
|
.route("/chat/completions", post(handlers::handle_chat_completions))
|
|
.route(
|
|
"/v1/chat/completions",
|
|
post(handlers::handle_chat_completions),
|
|
)
|
|
.route(
|
|
"/v1/v1/chat/completions",
|
|
post(handlers::handle_chat_completions),
|
|
)
|
|
.route(
|
|
"/codex/v1/chat/completions",
|
|
post(handlers::handle_chat_completions),
|
|
)
|
|
// OpenAI Responses API (Codex CLI,支持带前缀和不带前缀)
|
|
.route("/responses", post(handlers::handle_responses))
|
|
.route("/v1/responses", post(handlers::handle_responses))
|
|
.route("/v1/v1/responses", post(handlers::handle_responses))
|
|
.route("/codex/v1/responses", post(handlers::handle_responses))
|
|
// Gemini API (支持带前缀和不带前缀)
|
|
.route("/v1beta/*path", post(handlers::handle_gemini))
|
|
.route("/gemini/v1beta/*path", post(handlers::handle_gemini))
|
|
.layer(cors)
|
|
.with_state(self.state.clone())
|
|
}
|
|
|
|
/// 在不重启服务的情况下更新运行时配置
|
|
pub async fn apply_runtime_config(&self, config: &ProxyConfig) {
|
|
*self.state.config.write().await = config.clone();
|
|
}
|
|
|
|
/// 热更新熔断器配置
|
|
///
|
|
/// 将新配置应用到所有已创建的熔断器实例
|
|
pub async fn update_circuit_breaker_configs(
|
|
&self,
|
|
config: super::circuit_breaker::CircuitBreakerConfig,
|
|
) {
|
|
self.state.provider_router.update_all_configs(config).await;
|
|
}
|
|
|
|
/// 重置指定 Provider 的熔断器
|
|
pub async fn reset_provider_circuit_breaker(&self, provider_id: &str, app_type: &str) {
|
|
self.state
|
|
.provider_router
|
|
.reset_provider_breaker(provider_id, app_type)
|
|
.await;
|
|
}
|
|
}
|