mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-25 13:45:03 +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
494 lines
16 KiB
Rust
494 lines
16 KiB
Rust
//! 请求处理器
|
||
//!
|
||
//! 处理各种API端点的HTTP请求
|
||
//!
|
||
//! 重构后的结构:
|
||
//! - 通用逻辑提取到 `handler_context` 和 `response_processor` 模块
|
||
//! - 各 handler 只保留独特的业务逻辑
|
||
//! - Claude 的格式转换逻辑保留在此文件(用于 OpenRouter 旧接口回退)
|
||
|
||
use super::{
|
||
error_mapper::{get_error_message, map_proxy_error_to_status},
|
||
handler_config::{
|
||
CLAUDE_PARSER_CONFIG, CODEX_PARSER_CONFIG, GEMINI_PARSER_CONFIG, OPENAI_PARSER_CONFIG,
|
||
},
|
||
handler_context::RequestContext,
|
||
providers::{get_adapter, streaming::create_anthropic_sse_stream, transform},
|
||
response_processor::{create_logged_passthrough_stream, process_response, SseUsageCollector},
|
||
server::ProxyState,
|
||
types::*,
|
||
usage::parser::TokenUsage,
|
||
ProxyError,
|
||
};
|
||
use crate::app_config::AppType;
|
||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||
use rust_decimal::Decimal;
|
||
use serde_json::{json, Value};
|
||
use std::str::FromStr;
|
||
|
||
// ============================================================================
|
||
// 健康检查和状态查询(简单端点)
|
||
// ============================================================================
|
||
|
||
/// 健康检查
|
||
pub async fn health_check() -> (StatusCode, Json<Value>) {
|
||
(
|
||
StatusCode::OK,
|
||
Json(json!({
|
||
"status": "healthy",
|
||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||
})),
|
||
)
|
||
}
|
||
|
||
/// 获取服务状态
|
||
pub async fn get_status(State(state): State<ProxyState>) -> Result<Json<ProxyStatus>, ProxyError> {
|
||
let status = state.status.read().await.clone();
|
||
Ok(Json(status))
|
||
}
|
||
|
||
// ============================================================================
|
||
// Claude API 处理器(包含格式转换逻辑)
|
||
// ============================================================================
|
||
|
||
/// 处理 /v1/messages 请求(Claude API)
|
||
///
|
||
/// Claude 处理器包含独特的格式转换逻辑:
|
||
/// - 过去用于 OpenRouter 的 OpenAI Chat Completions 兼容接口(Anthropic ↔ OpenAI 转换)
|
||
/// - 现在 OpenRouter 已推出 Claude Code 兼容接口,默认不再启用该转换(逻辑保留以备回退)
|
||
pub async fn handle_messages(
|
||
State(state): State<ProxyState>,
|
||
headers: axum::http::HeaderMap,
|
||
Json(body): Json<Value>,
|
||
) -> Result<axum::response::Response, ProxyError> {
|
||
let mut ctx =
|
||
RequestContext::new(&state, &body, &headers, AppType::Claude, "Claude", "claude").await?;
|
||
|
||
let is_stream = body
|
||
.get("stream")
|
||
.and_then(|s| s.as_bool())
|
||
.unwrap_or(false);
|
||
|
||
// 转发请求
|
||
let forwarder = ctx.create_forwarder(&state);
|
||
let result = match forwarder
|
||
.forward_with_retry(
|
||
&AppType::Claude,
|
||
"/v1/messages",
|
||
body.clone(),
|
||
headers,
|
||
ctx.get_providers(),
|
||
)
|
||
.await
|
||
{
|
||
Ok(result) => result,
|
||
Err(mut err) => {
|
||
if let Some(provider) = err.provider.take() {
|
||
ctx.provider = provider;
|
||
}
|
||
log_forward_error(&state, &ctx, is_stream, &err.error);
|
||
return Err(err.error);
|
||
}
|
||
};
|
||
|
||
ctx.provider = result.provider;
|
||
let response = result.response;
|
||
|
||
// 检查是否需要格式转换(OpenRouter 等中转服务)
|
||
let adapter = get_adapter(&AppType::Claude);
|
||
let needs_transform = adapter.needs_transform(&ctx.provider);
|
||
|
||
// Claude 特有:格式转换处理
|
||
if needs_transform {
|
||
return handle_claude_transform(response, &ctx, &state, &body, is_stream).await;
|
||
}
|
||
|
||
// 通用响应处理(透传模式)
|
||
process_response(response, &ctx, &state, &CLAUDE_PARSER_CONFIG).await
|
||
}
|
||
|
||
/// Claude 格式转换处理(独有逻辑)
|
||
///
|
||
/// 处理 OpenRouter 旧 OpenAI 兼容接口的回退方案(当前默认不启用)
|
||
async fn handle_claude_transform(
|
||
response: reqwest::Response,
|
||
ctx: &RequestContext,
|
||
state: &ProxyState,
|
||
_original_body: &Value,
|
||
is_stream: bool,
|
||
) -> Result<axum::response::Response, ProxyError> {
|
||
let status = response.status();
|
||
|
||
if is_stream {
|
||
// 流式响应转换 (OpenAI SSE → Anthropic SSE)
|
||
let stream = response.bytes_stream();
|
||
let sse_stream = create_anthropic_sse_stream(stream);
|
||
|
||
// 创建使用量收集器
|
||
let usage_collector = {
|
||
let state = state.clone();
|
||
let provider_id = ctx.provider.id.clone();
|
||
let model = ctx.request_model.clone();
|
||
let status_code = status.as_u16();
|
||
let start_time = ctx.start_time;
|
||
|
||
SseUsageCollector::new(start_time, move |events, first_token_ms| {
|
||
if let Some(usage) = TokenUsage::from_claude_stream_events(&events) {
|
||
let latency_ms = start_time.elapsed().as_millis() as u64;
|
||
let state = state.clone();
|
||
let provider_id = provider_id.clone();
|
||
let model = model.clone();
|
||
|
||
tokio::spawn(async move {
|
||
log_usage(
|
||
&state,
|
||
&provider_id,
|
||
"claude",
|
||
&model,
|
||
usage,
|
||
latency_ms,
|
||
first_token_ms,
|
||
true,
|
||
status_code,
|
||
)
|
||
.await;
|
||
});
|
||
} else {
|
||
log::debug!("[Claude] OpenRouter 流式响应缺少 usage 统计,跳过消费记录");
|
||
}
|
||
})
|
||
};
|
||
|
||
// 获取流式超时配置
|
||
let timeout_config = ctx.streaming_timeout_config();
|
||
|
||
let logged_stream = create_logged_passthrough_stream(
|
||
sse_stream,
|
||
"Claude/OpenRouter",
|
||
Some(usage_collector),
|
||
timeout_config,
|
||
);
|
||
|
||
let mut headers = axum::http::HeaderMap::new();
|
||
headers.insert(
|
||
"Content-Type",
|
||
axum::http::HeaderValue::from_static("text/event-stream"),
|
||
);
|
||
headers.insert(
|
||
"Cache-Control",
|
||
axum::http::HeaderValue::from_static("no-cache"),
|
||
);
|
||
headers.insert(
|
||
"Connection",
|
||
axum::http::HeaderValue::from_static("keep-alive"),
|
||
);
|
||
|
||
let body = axum::body::Body::from_stream(logged_stream);
|
||
return Ok((headers, body).into_response());
|
||
}
|
||
|
||
// 非流式响应转换 (OpenAI → Anthropic)
|
||
let response_headers = response.headers().clone();
|
||
|
||
let body_bytes = response.bytes().await.map_err(|e| {
|
||
log::error!("[Claude] 读取响应体失败: {e}");
|
||
ProxyError::ForwardFailed(format!("Failed to read response body: {e}"))
|
||
})?;
|
||
|
||
let body_str = String::from_utf8_lossy(&body_bytes);
|
||
|
||
let openai_response: Value = serde_json::from_slice(&body_bytes).map_err(|e| {
|
||
log::error!("[Claude] 解析 OpenAI 响应失败: {e}, body: {body_str}");
|
||
ProxyError::TransformError(format!("Failed to parse OpenAI response: {e}"))
|
||
})?;
|
||
|
||
let anthropic_response = transform::openai_to_anthropic(openai_response).map_err(|e| {
|
||
log::error!("[Claude] 转换响应失败: {e}");
|
||
e
|
||
})?;
|
||
|
||
// 记录使用量
|
||
if let Some(usage) = TokenUsage::from_claude_response(&anthropic_response) {
|
||
let model = anthropic_response
|
||
.get("model")
|
||
.and_then(|m| m.as_str())
|
||
.unwrap_or("unknown");
|
||
let latency_ms = ctx.latency_ms();
|
||
|
||
tokio::spawn({
|
||
let state = state.clone();
|
||
let provider_id = ctx.provider.id.clone();
|
||
let model = model.to_string();
|
||
async move {
|
||
log_usage(
|
||
&state,
|
||
&provider_id,
|
||
"claude",
|
||
&model,
|
||
usage,
|
||
latency_ms,
|
||
None,
|
||
false,
|
||
status.as_u16(),
|
||
)
|
||
.await;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 构建响应
|
||
let mut builder = axum::response::Response::builder().status(status);
|
||
|
||
for (key, value) in response_headers.iter() {
|
||
if key.as_str().to_lowercase() != "content-length"
|
||
&& key.as_str().to_lowercase() != "transfer-encoding"
|
||
{
|
||
builder = builder.header(key, value);
|
||
}
|
||
}
|
||
|
||
builder = builder.header("content-type", "application/json");
|
||
|
||
let response_body = serde_json::to_vec(&anthropic_response).map_err(|e| {
|
||
log::error!("[Claude] 序列化响应失败: {e}");
|
||
ProxyError::TransformError(format!("Failed to serialize response: {e}"))
|
||
})?;
|
||
|
||
let body = axum::body::Body::from(response_body);
|
||
builder.body(body).map_err(|e| {
|
||
log::error!("[Claude] 构建响应失败: {e}");
|
||
ProxyError::Internal(format!("Failed to build response: {e}"))
|
||
})
|
||
}
|
||
|
||
// ============================================================================
|
||
// Codex API 处理器
|
||
// ============================================================================
|
||
|
||
/// 处理 /v1/chat/completions 请求(OpenAI Chat Completions API - Codex CLI)
|
||
pub async fn handle_chat_completions(
|
||
State(state): State<ProxyState>,
|
||
headers: axum::http::HeaderMap,
|
||
Json(body): Json<Value>,
|
||
) -> Result<axum::response::Response, ProxyError> {
|
||
let mut ctx =
|
||
RequestContext::new(&state, &body, &headers, AppType::Codex, "Codex", "codex").await?;
|
||
|
||
let is_stream = body
|
||
.get("stream")
|
||
.and_then(|v| v.as_bool())
|
||
.unwrap_or(false);
|
||
|
||
let forwarder = ctx.create_forwarder(&state);
|
||
let result = match forwarder
|
||
.forward_with_retry(
|
||
&AppType::Codex,
|
||
"/v1/chat/completions",
|
||
body,
|
||
headers,
|
||
ctx.get_providers(),
|
||
)
|
||
.await
|
||
{
|
||
Ok(result) => result,
|
||
Err(mut err) => {
|
||
if let Some(provider) = err.provider.take() {
|
||
ctx.provider = provider;
|
||
}
|
||
log_forward_error(&state, &ctx, is_stream, &err.error);
|
||
return Err(err.error);
|
||
}
|
||
};
|
||
|
||
ctx.provider = result.provider;
|
||
let response = result.response;
|
||
|
||
process_response(response, &ctx, &state, &OPENAI_PARSER_CONFIG).await
|
||
}
|
||
|
||
/// 处理 /v1/responses 请求(OpenAI Responses API - Codex CLI 透传)
|
||
pub async fn handle_responses(
|
||
State(state): State<ProxyState>,
|
||
headers: axum::http::HeaderMap,
|
||
Json(body): Json<Value>,
|
||
) -> Result<axum::response::Response, ProxyError> {
|
||
let mut ctx =
|
||
RequestContext::new(&state, &body, &headers, AppType::Codex, "Codex", "codex").await?;
|
||
|
||
let is_stream = body
|
||
.get("stream")
|
||
.and_then(|v| v.as_bool())
|
||
.unwrap_or(false);
|
||
|
||
let forwarder = ctx.create_forwarder(&state);
|
||
let result = match forwarder
|
||
.forward_with_retry(
|
||
&AppType::Codex,
|
||
"/v1/responses",
|
||
body,
|
||
headers,
|
||
ctx.get_providers(),
|
||
)
|
||
.await
|
||
{
|
||
Ok(result) => result,
|
||
Err(mut err) => {
|
||
if let Some(provider) = err.provider.take() {
|
||
ctx.provider = provider;
|
||
}
|
||
log_forward_error(&state, &ctx, is_stream, &err.error);
|
||
return Err(err.error);
|
||
}
|
||
};
|
||
|
||
ctx.provider = result.provider;
|
||
let response = result.response;
|
||
|
||
process_response(response, &ctx, &state, &CODEX_PARSER_CONFIG).await
|
||
}
|
||
|
||
// ============================================================================
|
||
// Gemini API 处理器
|
||
// ============================================================================
|
||
|
||
/// 处理 Gemini API 请求(透传,包括查询参数)
|
||
pub async fn handle_gemini(
|
||
State(state): State<ProxyState>,
|
||
uri: axum::http::Uri,
|
||
headers: axum::http::HeaderMap,
|
||
Json(body): Json<Value>,
|
||
) -> Result<axum::response::Response, ProxyError> {
|
||
// Gemini 的模型名称在 URI 中
|
||
let mut ctx = RequestContext::new(&state, &body, &headers, AppType::Gemini, "Gemini", "gemini")
|
||
.await?
|
||
.with_model_from_uri(&uri);
|
||
|
||
// 提取完整的路径和查询参数
|
||
let endpoint = uri
|
||
.path_and_query()
|
||
.map(|pq| pq.as_str())
|
||
.unwrap_or(uri.path());
|
||
|
||
let is_stream = body
|
||
.get("stream")
|
||
.and_then(|v| v.as_bool())
|
||
.unwrap_or(false);
|
||
|
||
let forwarder = ctx.create_forwarder(&state);
|
||
let result = match forwarder
|
||
.forward_with_retry(
|
||
&AppType::Gemini,
|
||
endpoint,
|
||
body,
|
||
headers,
|
||
ctx.get_providers(),
|
||
)
|
||
.await
|
||
{
|
||
Ok(result) => result,
|
||
Err(mut err) => {
|
||
if let Some(provider) = err.provider.take() {
|
||
ctx.provider = provider;
|
||
}
|
||
log_forward_error(&state, &ctx, is_stream, &err.error);
|
||
return Err(err.error);
|
||
}
|
||
};
|
||
|
||
ctx.provider = result.provider;
|
||
let response = result.response;
|
||
|
||
process_response(response, &ctx, &state, &GEMINI_PARSER_CONFIG).await
|
||
}
|
||
|
||
// ============================================================================
|
||
// 使用量记录(保留用于 Claude 转换逻辑)
|
||
// ============================================================================
|
||
|
||
fn log_forward_error(
|
||
state: &ProxyState,
|
||
ctx: &RequestContext,
|
||
is_streaming: bool,
|
||
error: &ProxyError,
|
||
) {
|
||
use super::usage::logger::UsageLogger;
|
||
|
||
let logger = UsageLogger::new(&state.db);
|
||
let status_code = map_proxy_error_to_status(error);
|
||
let error_message = get_error_message(error);
|
||
let request_id = uuid::Uuid::new_v4().to_string();
|
||
|
||
if let Err(e) = logger.log_error_with_context(
|
||
request_id,
|
||
ctx.provider.id.clone(),
|
||
ctx.app_type_str.to_string(),
|
||
ctx.request_model.clone(),
|
||
status_code,
|
||
error_message,
|
||
ctx.latency_ms(),
|
||
is_streaming,
|
||
Some(ctx.session_id.clone()),
|
||
None,
|
||
) {
|
||
log::warn!("记录失败请求日志失败: {e}");
|
||
}
|
||
}
|
||
|
||
/// 记录请求使用量
|
||
#[allow(clippy::too_many_arguments)]
|
||
async fn log_usage(
|
||
state: &ProxyState,
|
||
provider_id: &str,
|
||
app_type: &str,
|
||
model: &str,
|
||
usage: TokenUsage,
|
||
latency_ms: u64,
|
||
first_token_ms: Option<u64>,
|
||
is_streaming: bool,
|
||
status_code: u16,
|
||
) {
|
||
use super::usage::logger::UsageLogger;
|
||
|
||
let logger = UsageLogger::new(&state.db);
|
||
|
||
// 获取 provider 的 cost_multiplier
|
||
let multiplier = match state.db.get_provider_by_id(provider_id, app_type) {
|
||
Ok(Some(p)) => {
|
||
if let Some(meta) = p.meta {
|
||
if let Some(cm) = meta.cost_multiplier {
|
||
Decimal::from_str(&cm).unwrap_or_else(|e| {
|
||
log::warn!(
|
||
"cost_multiplier 解析失败 (provider_id={provider_id}): {cm} - {e}"
|
||
);
|
||
Decimal::from(1)
|
||
})
|
||
} else {
|
||
Decimal::from(1)
|
||
}
|
||
} else {
|
||
Decimal::from(1)
|
||
}
|
||
}
|
||
_ => Decimal::from(1),
|
||
};
|
||
|
||
let request_id = uuid::Uuid::new_v4().to_string();
|
||
|
||
if let Err(e) = logger.log_with_calculation(
|
||
request_id,
|
||
provider_id.to_string(),
|
||
app_type.to_string(),
|
||
model.to_string(),
|
||
usage,
|
||
multiplier,
|
||
latency_ms,
|
||
first_token_ms,
|
||
status_code,
|
||
None,
|
||
None, // provider_type
|
||
is_streaming,
|
||
) {
|
||
log::warn!("[USG-001] 记录使用量失败: {e}");
|
||
}
|
||
}
|