mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-06-16 13:34:04 +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
208 lines
6.9 KiB
Rust
208 lines
6.9 KiB
Rust
use axum::{
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
Json,
|
|
};
|
|
use serde_json::json;
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum ProxyError {
|
|
#[error("服务器已在运行")]
|
|
AlreadyRunning,
|
|
|
|
#[error("服务器未运行")]
|
|
NotRunning,
|
|
|
|
#[error("地址绑定失败: {0}")]
|
|
BindFailed(String),
|
|
|
|
#[error("停止超时")]
|
|
StopTimeout,
|
|
|
|
#[error("停止失败: {0}")]
|
|
StopFailed(String),
|
|
|
|
#[error("请求转发失败: {0}")]
|
|
ForwardFailed(String),
|
|
|
|
#[error("无可用的Provider")]
|
|
NoAvailableProvider,
|
|
|
|
#[error("所有供应商已熔断,无可用渠道")]
|
|
AllProvidersCircuitOpen,
|
|
|
|
#[error("未配置供应商")]
|
|
NoProvidersConfigured,
|
|
|
|
#[allow(dead_code)]
|
|
#[error("Provider不健康: {0}")]
|
|
ProviderUnhealthy(String),
|
|
|
|
#[error("上游错误 (状态码 {status}): {body:?}")]
|
|
UpstreamError { status: u16, body: Option<String> },
|
|
|
|
#[error("超过最大重试次数")]
|
|
MaxRetriesExceeded,
|
|
|
|
#[error("数据库错误: {0}")]
|
|
DatabaseError(String),
|
|
|
|
#[error("配置错误: {0}")]
|
|
ConfigError(String),
|
|
|
|
#[allow(dead_code)]
|
|
#[error("格式转换错误: {0}")]
|
|
TransformError(String),
|
|
|
|
#[allow(dead_code)]
|
|
#[error("无效的请求: {0}")]
|
|
InvalidRequest(String),
|
|
|
|
#[error("超时: {0}")]
|
|
Timeout(String),
|
|
|
|
/// 流式响应空闲超时
|
|
#[allow(dead_code)]
|
|
#[error("流式响应空闲超时: {0}秒无数据")]
|
|
StreamIdleTimeout(u64),
|
|
|
|
/// 认证错误
|
|
#[allow(dead_code)]
|
|
#[error("认证失败: {0}")]
|
|
AuthError(String),
|
|
|
|
#[allow(dead_code)]
|
|
#[error("内部错误: {0}")]
|
|
Internal(String),
|
|
}
|
|
|
|
impl IntoResponse for ProxyError {
|
|
fn into_response(self) -> Response {
|
|
let (status, body) = match &self {
|
|
ProxyError::UpstreamError {
|
|
status: upstream_status,
|
|
body: upstream_body,
|
|
} => {
|
|
let http_status =
|
|
StatusCode::from_u16(*upstream_status).unwrap_or(StatusCode::BAD_GATEWAY);
|
|
|
|
// 尝试解析上游响应体为 JSON,如果失败则包装为字符串
|
|
let error_body = if let Some(body_str) = upstream_body {
|
|
if let Ok(json_body) = serde_json::from_str::<serde_json::Value>(body_str) {
|
|
// 上游返回的是 JSON,直接透传
|
|
json_body
|
|
} else {
|
|
// 上游返回的不是 JSON,包装为错误消息
|
|
json!({
|
|
"error": {
|
|
"message": body_str,
|
|
"type": "upstream_error",
|
|
}
|
|
})
|
|
}
|
|
} else {
|
|
json!({
|
|
"error": {
|
|
"message": format!("Upstream error (status {})", upstream_status),
|
|
"type": "upstream_error",
|
|
}
|
|
})
|
|
};
|
|
|
|
(http_status, error_body)
|
|
}
|
|
_ => {
|
|
let (http_status, message) = match &self {
|
|
ProxyError::AlreadyRunning => (StatusCode::CONFLICT, self.to_string()),
|
|
ProxyError::NotRunning => (StatusCode::SERVICE_UNAVAILABLE, self.to_string()),
|
|
ProxyError::BindFailed(_) => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
|
|
}
|
|
ProxyError::StopTimeout => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
|
|
}
|
|
ProxyError::StopFailed(_) => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
|
|
}
|
|
ProxyError::ForwardFailed(_) => (StatusCode::BAD_GATEWAY, self.to_string()),
|
|
ProxyError::NoAvailableProvider => {
|
|
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
|
|
}
|
|
ProxyError::AllProvidersCircuitOpen => {
|
|
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
|
|
}
|
|
ProxyError::NoProvidersConfigured => {
|
|
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
|
|
}
|
|
ProxyError::ProviderUnhealthy(_) => {
|
|
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
|
|
}
|
|
ProxyError::MaxRetriesExceeded => {
|
|
(StatusCode::SERVICE_UNAVAILABLE, self.to_string())
|
|
}
|
|
ProxyError::DatabaseError(_) => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
|
|
}
|
|
ProxyError::ConfigError(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
|
ProxyError::TransformError(_) => {
|
|
(StatusCode::UNPROCESSABLE_ENTITY, self.to_string())
|
|
}
|
|
ProxyError::InvalidRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
|
|
ProxyError::Timeout(_) => (StatusCode::GATEWAY_TIMEOUT, self.to_string()),
|
|
ProxyError::StreamIdleTimeout(_) => {
|
|
(StatusCode::GATEWAY_TIMEOUT, self.to_string())
|
|
}
|
|
ProxyError::AuthError(_) => (StatusCode::UNAUTHORIZED, self.to_string()),
|
|
ProxyError::Internal(_) => {
|
|
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string())
|
|
}
|
|
ProxyError::UpstreamError { .. } => unreachable!(),
|
|
};
|
|
|
|
let error_body = json!({
|
|
"error": {
|
|
"message": message,
|
|
"type": "proxy_error",
|
|
}
|
|
});
|
|
|
|
(http_status, error_body)
|
|
}
|
|
};
|
|
|
|
(status, Json(body)).into_response()
|
|
}
|
|
}
|
|
|
|
/// 错误分类
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ErrorCategory {
|
|
/// 可重试错误(网络问题、5xx)
|
|
Retryable, // 网络超时、5xx 错误
|
|
/// 不可重试错误(4xx、认证失败)
|
|
NonRetryable, // 认证失败、参数错误、4xx 错误
|
|
#[allow(dead_code)]
|
|
ClientAbort, // 客户端主动中断
|
|
}
|
|
|
|
/// 判断错误是否可重试
|
|
#[allow(dead_code)]
|
|
pub fn categorize_error(error: &reqwest::Error) -> ErrorCategory {
|
|
if error.is_timeout() || error.is_connect() {
|
|
return ErrorCategory::Retryable;
|
|
}
|
|
|
|
if let Some(status) = error.status() {
|
|
if status.is_server_error() {
|
|
ErrorCategory::Retryable
|
|
} else if status.is_client_error() {
|
|
ErrorCategory::NonRetryable
|
|
} else {
|
|
ErrorCategory::Retryable
|
|
}
|
|
} else {
|
|
ErrorCategory::Retryable
|
|
}
|
|
}
|