mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-27 08:14:33 +08:00
Feat/provider individual config (#663)
* refactor(ui): simplify UpdateBadge to minimal dot indicator * feat(provider): add individual test and proxy config for providers Add support for provider-specific model test and proxy configurations: - Add ProviderTestConfig and ProviderProxyConfig types in Rust and TypeScript - Create ProviderAdvancedConfig component with collapsible panels - Update stream_check service to merge provider config with global config - Proxy config UI follows global proxy style (single URL input) Provider-level configs stored in meta field, no database schema changes needed. * feat(ui): add failover toggle and improve proxy controls - Add FailoverToggle component with slide animation - Simplify ProxyToggle style to match FailoverToggle - Add usage statistics button when proxy is active - Fix i18n parameter passing for failover messages - Add missing failover translation keys (inQueue, addQueue, priority) - Replace AboutSection icon with app logo * fix(proxy): support system proxy fallback and provider-level proxy config - Remove no_proxy() calls in http_client.rs to allow system proxy fallback - Add get_for_provider() to build HTTP client with provider-specific proxy - Update forwarder.rs and stream_check.rs to use provider proxy config - Fix EditProviderDialog.tsx to include provider.meta in useMemo deps - Add useEffect in ProviderAdvancedConfig.tsx to sync expand state Fixes #636 Fixes #583 * fix(ui): sync toast theme with app setting * feat(settings): add log config management Fixes #612 Fixes #514 * fix(proxy): increase request body size limit to 200MB Fixes #666 * docs(proxy): update timeout config descriptions and defaults Fixes #612 * fix(proxy): filter x-goog-api-key header to prevent duplication * fix(proxy): prevent proxy recursion when system proxy points to localhost Detect if HTTP_PROXY, HTTPS_PROXY, or ALL_PROXY environment variables point to loopback addresses (localhost, 127.0.0.1), and bypass system proxy in such cases to avoid infinite request loops. * fix(i18n): add providerAdvanced i18n keys and fix failover toast parameter - Add providerAdvanced.* i18n keys to en.json, zh.json, and ja.json - Fix failover toggleFailed toast to pass detail parameter - Remove Chinese fallback text from UI for English/Japanese users * fix(tray): restore tray-provider events and enable Auto failover properly - Emit provider-switched event on tray provider click (backward compatibility) - Auto button now: starts proxy, takes over live config, enables failover * fix(log): enable dynamic log level and single file mode - Initialize log at Trace level for dynamic adjustment - Change rotation strategy to KeepSome(1) for single file - Set max file size to 1GB - Delete old log file on startup for clean start * fix(tray): fix clippy uninlined format args warning Use inline format arguments: {app_type_str} instead of {} * fix(provider): allow typing :// in endpoint URL inputs Change input type from "url" to "text" to prevent browser URL validation from blocking :// input. Closes #681 * fix(stream-check): use Gemini native streaming API format - Change endpoint from OpenAI-compatible to native streamGenerateContent - Add alt=sse parameter for SSE format response - Use x-goog-api-key header instead of Bearer token - Convert request body to Gemini contents/parts format * feat(proxy): add request logging for debugging Add debug logs for outgoing requests including URL and body content with byte size, matching the existing response logging format. * fix(log): prevent usize underflow in KeepSome rotation strategy KeepSome(n) internally computes n-2, so n=1 causes underflow. Use KeepSome(2) as the minimum safe value.
This commit is contained in:
@@ -28,6 +28,7 @@ const HEADER_BLACKLIST: &[&str] = &[
|
||||
// 认证类(会被覆盖)
|
||||
"authorization",
|
||||
"x-api-key",
|
||||
"x-goog-api-key",
|
||||
// 连接类(由 HTTP 客户端管理)
|
||||
"host",
|
||||
"content-length",
|
||||
@@ -585,8 +586,9 @@ impl RequestForwarder {
|
||||
// 默认使用空白名单,过滤所有 _ 前缀字段
|
||||
let filtered_body = filter_private_params_with_whitelist(request_body, &[]);
|
||||
|
||||
// 每次请求时获取最新的全局 HTTP 客户端(支持热更新代理配置)
|
||||
let client = super::http_client::get();
|
||||
// 获取 HTTP 客户端:优先使用供应商单独代理配置,否则使用全局客户端
|
||||
let proxy_config = provider.meta.as_ref().and_then(|m| m.proxy_config.as_ref());
|
||||
let client = super::http_client::get_for_provider(proxy_config);
|
||||
let mut request = client.post(&url);
|
||||
|
||||
// 只有当 timeout > 0 时才设置请求超时
|
||||
@@ -662,6 +664,17 @@ impl RequestForwarder {
|
||||
request = request.header("anthropic-version", version_str);
|
||||
}
|
||||
|
||||
// 输出请求信息日志
|
||||
let tag = adapter.name();
|
||||
log::debug!("[{tag}] >>> 请求 URL: {url}");
|
||||
if let Ok(body_str) = serde_json::to_string(&filtered_body) {
|
||||
log::debug!(
|
||||
"[{tag}] >>> 请求体内容 ({}字节): {}",
|
||||
body_str.len(),
|
||||
body_str
|
||||
);
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
let response = request.json(&filtered_body).send().await.map_err(|e| {
|
||||
if e.is_timeout() {
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
//! 提供支持全局代理配置的 HTTP 客户端。
|
||||
//! 所有需要发送 HTTP 请求的模块都应使用此模块提供的客户端。
|
||||
|
||||
use crate::provider::ProviderProxyConfig;
|
||||
use once_cell::sync::OnceCell;
|
||||
use reqwest::Client;
|
||||
use std::env;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::RwLock;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -155,23 +158,15 @@ pub fn update_proxy(proxy_url: Option<&str>) -> Result<(), String> {
|
||||
|
||||
/// 获取全局 HTTP 客户端
|
||||
///
|
||||
/// 返回配置了代理的客户端(如果已配置代理),否则返回直连客户端。
|
||||
/// 返回配置了代理的客户端(如果已配置代理),否则返回跟随系统代理的客户端。
|
||||
pub fn get() -> Client {
|
||||
GLOBAL_CLIENT
|
||||
.get()
|
||||
.and_then(|lock| lock.read().ok())
|
||||
.map(|c| c.clone())
|
||||
.unwrap_or_else(|| {
|
||||
// 如果还没初始化,创建一个默认客户端(配置与 build_client 一致)
|
||||
log::warn!("[GlobalProxy] [GP-004] Client not initialized, using fallback");
|
||||
Client::builder()
|
||||
.timeout(Duration::from_secs(600))
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.pool_max_idle_per_host(10)
|
||||
.tcp_keepalive(Duration::from_secs(60))
|
||||
.no_proxy()
|
||||
.build()
|
||||
.unwrap_or_default()
|
||||
build_client(None).unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -199,7 +194,7 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
|
||||
.pool_max_idle_per_host(10)
|
||||
.tcp_keepalive(Duration::from_secs(60));
|
||||
|
||||
// 有代理地址则使用代理,否则直连
|
||||
// 有代理地址则使用代理,否则跟随系统代理
|
||||
if let Some(url) = proxy_url {
|
||||
// 先验证 URL 格式和 scheme
|
||||
let parsed = url::Url::parse(url)
|
||||
@@ -219,8 +214,16 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
|
||||
builder = builder.proxy(proxy);
|
||||
log::debug!("[GlobalProxy] Proxy configured: {}", mask_url(url));
|
||||
} else {
|
||||
builder = builder.no_proxy();
|
||||
log::debug!("[GlobalProxy] Direct connection (no proxy)");
|
||||
// 未设置全局代理时,让 reqwest 自动检测系统代理(环境变量)
|
||||
// 若系统代理指向本机,禁用系统代理避免自环
|
||||
if system_proxy_points_to_loopback() {
|
||||
builder = builder.no_proxy();
|
||||
log::warn!(
|
||||
"[GlobalProxy] System proxy points to localhost, bypassing to avoid recursion"
|
||||
);
|
||||
} else {
|
||||
log::debug!("[GlobalProxy] Following system proxy (no explicit proxy configured)");
|
||||
}
|
||||
}
|
||||
|
||||
builder
|
||||
@@ -228,6 +231,50 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
|
||||
.map_err(|e| format!("Failed to build HTTP client: {e}"))
|
||||
}
|
||||
|
||||
fn system_proxy_points_to_loopback() -> bool {
|
||||
const KEYS: [&str; 6] = [
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
];
|
||||
|
||||
KEYS.iter()
|
||||
.filter_map(|key| env::var(key).ok())
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
.any(|value| proxy_points_to_loopback(&value))
|
||||
}
|
||||
|
||||
fn proxy_points_to_loopback(value: &str) -> bool {
|
||||
fn host_is_loopback(host: &str) -> bool {
|
||||
if host.eq_ignore_ascii_case("localhost") {
|
||||
return true;
|
||||
}
|
||||
host.parse::<IpAddr>()
|
||||
.map(|ip| ip.is_loopback())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
if let Ok(parsed) = url::Url::parse(value) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
return host_is_loopback(host);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let with_scheme = format!("http://{value}");
|
||||
if let Ok(parsed) = url::Url::parse(&with_scheme) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
return host_is_loopback(host);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// 隐藏 URL 中的敏感信息(用于日志)
|
||||
pub fn mask_url(url: &str) -> String {
|
||||
if let Ok(parsed) = url::Url::parse(url) {
|
||||
@@ -247,9 +294,109 @@ pub fn mask_url(url: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据供应商单独代理配置构建代理 URL
|
||||
///
|
||||
/// 将 ProviderProxyConfig 转换为代理 URL 字符串
|
||||
fn build_proxy_url_from_config(config: &ProviderProxyConfig) -> Option<String> {
|
||||
let proxy_type = config.proxy_type.as_deref().unwrap_or("http");
|
||||
let host = config.proxy_host.as_deref()?;
|
||||
let port = config.proxy_port?;
|
||||
|
||||
// 构建带认证的代理 URL
|
||||
if let (Some(username), Some(password)) = (&config.proxy_username, &config.proxy_password) {
|
||||
if !username.is_empty() && !password.is_empty() {
|
||||
return Some(format!(
|
||||
"{proxy_type}://{username}:{password}@{host}:{port}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(format!("{proxy_type}://{host}:{port}"))
|
||||
}
|
||||
|
||||
/// 根据供应商单独代理配置构建 HTTP 客户端
|
||||
///
|
||||
/// 如果供应商配置了单独代理(enabled = true),则使用该代理构建客户端;
|
||||
/// 否则返回 None,调用方应使用全局客户端。
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `proxy_config` - 供应商的代理配置
|
||||
///
|
||||
/// # Returns
|
||||
/// 如果配置有效则返回 Some(Client),否则返回 None
|
||||
pub fn build_client_for_provider(proxy_config: Option<&ProviderProxyConfig>) -> Option<Client> {
|
||||
let config = proxy_config.filter(|c| c.enabled)?;
|
||||
|
||||
let proxy_url = build_proxy_url_from_config(config)?;
|
||||
|
||||
log::debug!(
|
||||
"[ProviderProxy] Building client with proxy: {}",
|
||||
mask_url(&proxy_url)
|
||||
);
|
||||
|
||||
// 构建带代理的客户端
|
||||
let proxy = match reqwest::Proxy::all(&proxy_url) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"[ProviderProxy] Failed to create proxy from '{}': {}",
|
||||
mask_url(&proxy_url),
|
||||
e
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match Client::builder()
|
||||
.timeout(Duration::from_secs(600))
|
||||
.connect_timeout(Duration::from_secs(30))
|
||||
.pool_max_idle_per_host(10)
|
||||
.tcp_keepalive(Duration::from_secs(60))
|
||||
.proxy(proxy)
|
||||
.build()
|
||||
{
|
||||
Ok(client) => {
|
||||
log::info!(
|
||||
"[ProviderProxy] Client built with proxy: {}",
|
||||
mask_url(&proxy_url)
|
||||
);
|
||||
Some(client)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("[ProviderProxy] Failed to build client: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取供应商专用的 HTTP 客户端
|
||||
///
|
||||
/// 优先使用供应商单独代理配置,如果未启用则返回全局客户端。
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `proxy_config` - 供应商的代理配置
|
||||
///
|
||||
/// # Returns
|
||||
/// 返回适合该供应商的 HTTP 客户端
|
||||
pub fn get_for_provider(proxy_config: Option<&ProviderProxyConfig>) -> Client {
|
||||
// 优先使用供应商单独代理
|
||||
if let Some(client) = build_client_for_provider(proxy_config) {
|
||||
return client;
|
||||
}
|
||||
|
||||
// 回退到全局客户端
|
||||
get()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
fn env_lock() -> &'static Mutex<()> {
|
||||
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
LOCK.get_or_init(|| Mutex::new(()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mask_url() {
|
||||
@@ -298,4 +445,40 @@ mod tests {
|
||||
let result = build_client(Some("invalid-scheme://127.0.0.1:7890"));
|
||||
assert!(result.is_err(), "Should reject invalid proxy scheme");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proxy_points_to_loopback() {
|
||||
assert!(proxy_points_to_loopback("http://127.0.0.1:7890"));
|
||||
assert!(proxy_points_to_loopback("socks5://localhost:1080"));
|
||||
assert!(proxy_points_to_loopback("127.0.0.1:7890"));
|
||||
assert!(!proxy_points_to_loopback("http://192.168.1.10:7890"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_proxy_points_to_loopback() {
|
||||
let _guard = env_lock().lock().unwrap();
|
||||
|
||||
let keys = [
|
||||
"HTTP_PROXY",
|
||||
"http_proxy",
|
||||
"HTTPS_PROXY",
|
||||
"https_proxy",
|
||||
"ALL_PROXY",
|
||||
"all_proxy",
|
||||
];
|
||||
|
||||
for key in &keys {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
|
||||
std::env::set_var("HTTP_PROXY", "http://127.0.0.1:7890");
|
||||
assert!(system_proxy_points_to_loopback());
|
||||
|
||||
std::env::set_var("HTTP_PROXY", "http://10.0.0.2:7890");
|
||||
assert!(!system_proxy_points_to_loopback());
|
||||
|
||||
for key in &keys {
|
||||
std::env::remove_var(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,6 @@ pub fn create_anthropic_sse_stream(
|
||||
}
|
||||
|
||||
if let Ok(chunk) = serde_json::from_str::<OpenAIStreamChunk>(data) {
|
||||
// 仅在 DEBUG 级别简短记录 SSE 事件
|
||||
log::debug!("[Claude/OpenRouter] <<< SSE chunk received");
|
||||
|
||||
if message_id.is_none() {
|
||||
|
||||
@@ -12,6 +12,7 @@ use super::{
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use bytes::Bytes;
|
||||
use futures::stream::{Stream, StreamExt};
|
||||
use reqwest::header::HeaderMap;
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
@@ -47,6 +48,12 @@ pub async fn handle_streaming(
|
||||
parser_config: &UsageParserConfig,
|
||||
) -> Response {
|
||||
let status = response.status();
|
||||
log::debug!(
|
||||
"[{}] 已接收上游流式响应: status={}, headers={}",
|
||||
ctx.tag,
|
||||
status.as_u16(),
|
||||
format_headers(response.headers())
|
||||
);
|
||||
let mut builder = axum::response::Response::builder().status(status);
|
||||
|
||||
// 复制响应头
|
||||
@@ -94,6 +101,19 @@ pub async fn handle_non_streaming(
|
||||
log::error!("[{}] 读取响应失败: {e}", ctx.tag);
|
||||
ProxyError::ForwardFailed(format!("Failed to read response body: {e}"))
|
||||
})?;
|
||||
log::debug!(
|
||||
"[{}] 已接收上游响应体: status={}, bytes={}, headers={}",
|
||||
ctx.tag,
|
||||
status.as_u16(),
|
||||
body_bytes.len(),
|
||||
format_headers(&response_headers)
|
||||
);
|
||||
|
||||
log::debug!(
|
||||
"[{}] 上游响应体内容: {}",
|
||||
ctx.tag,
|
||||
String::from_utf8_lossy(&body_bytes)
|
||||
);
|
||||
|
||||
// 解析并记录使用量
|
||||
if let Ok(json_value) = serde_json::from_slice::<Value>(&body_bytes) {
|
||||
@@ -470,6 +490,12 @@ pub fn create_logged_passthrough_stream(
|
||||
|
||||
match chunk_result {
|
||||
Some(Ok(bytes)) => {
|
||||
if is_first_chunk {
|
||||
log::debug!(
|
||||
"[{tag}] 已接收上游流式首包: bytes={}",
|
||||
bytes.len()
|
||||
);
|
||||
}
|
||||
is_first_chunk = false;
|
||||
let text = String::from_utf8_lossy(&bytes);
|
||||
buffer.push_str(&text);
|
||||
@@ -488,13 +514,9 @@ pub fn create_logged_passthrough_stream(
|
||||
if let Some(c) = &collector {
|
||||
c.push(json_value.clone()).await;
|
||||
}
|
||||
log::debug!(
|
||||
"[{}] <<< SSE 事件: {}",
|
||||
tag,
|
||||
data.chars().take(100).collect::<String>()
|
||||
);
|
||||
log::debug!("[{tag}] <<< SSE 事件: {data}");
|
||||
} else {
|
||||
log::debug!("[{tag}] <<< SSE 数据: {}", data.chars().take(100).collect::<String>());
|
||||
log::debug!("[{tag}] <<< SSE 数据: {data}");
|
||||
}
|
||||
} else {
|
||||
log::debug!("[{tag}] <<< SSE: [DONE]");
|
||||
@@ -523,3 +545,14 @@ pub fn create_logged_passthrough_stream(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_headers(headers: &HeaderMap) -> String {
|
||||
headers
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
let value_str = value.to_str().unwrap_or("<non-utf8>");
|
||||
format!("{key}={value_str}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::{
|
||||
};
|
||||
use crate::database::Database;
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
@@ -224,6 +225,8 @@ impl ProxyServer {
|
||||
// Gemini API (支持带前缀和不带前缀)
|
||||
.route("/v1beta/*path", post(handlers::handle_gemini))
|
||||
.route("/gemini/v1beta/*path", post(handlers::handle_gemini))
|
||||
// 提高默认请求体大小限制(避免 413 Payload Too Large)
|
||||
.layer(DefaultBodyLimit::max(200 * 1024 * 1024))
|
||||
.layer(cors)
|
||||
.with_state(self.state.clone())
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@ pub struct ProxyConfig {
|
||||
/// 是否正在接管 Live 配置
|
||||
#[serde(default)]
|
||||
pub live_takeover_active: bool,
|
||||
/// 流式首字超时(秒)- 等待首个数据块的最大时间
|
||||
/// 流式首字超时(秒)- 等待首个数据块的最大时间,范围 1-120 秒,默认 60 秒
|
||||
#[serde(default = "default_streaming_first_byte_timeout")]
|
||||
pub streaming_first_byte_timeout: u64,
|
||||
/// 流式静默超时(秒)- 两个数据块之间的最大间隔
|
||||
/// 流式静默超时(秒)- 两个数据块之间的最大间隔,范围 60-600 秒,填 0 禁用(防止中途卡住)
|
||||
#[serde(default = "default_streaming_idle_timeout")]
|
||||
pub streaming_idle_timeout: u64,
|
||||
/// 非流式总超时(秒)- 非流式请求的总超时时间
|
||||
/// 非流式总超时(秒)- 非流式请求的总超时时间,范围 60-1200 秒,默认 600 秒(10 分钟)
|
||||
#[serde(default = "default_non_streaming_timeout")]
|
||||
pub non_streaming_timeout: u64,
|
||||
}
|
||||
@@ -221,6 +221,50 @@ fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn default_log_level() -> String {
|
||||
"info".to_string()
|
||||
}
|
||||
|
||||
/// 日志配置
|
||||
///
|
||||
/// 存储在 settings 表的 log_config 字段中(JSON 格式)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LogConfig {
|
||||
/// 总开关:是否启用日志
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
/// 日志级别: error, warn, info, debug, trace
|
||||
#[serde(default = "default_log_level")]
|
||||
pub level: String,
|
||||
}
|
||||
|
||||
impl Default for LogConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
level: "info".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LogConfig {
|
||||
/// 将配置转换为 log::LevelFilter
|
||||
pub fn to_level_filter(&self) -> log::LevelFilter {
|
||||
if !self.enabled {
|
||||
return log::LevelFilter::Off;
|
||||
}
|
||||
match self.level.to_lowercase().as_str() {
|
||||
"error" => log::LevelFilter::Error,
|
||||
"warn" => log::LevelFilter::Warn,
|
||||
"info" => log::LevelFilter::Info,
|
||||
"debug" => log::LevelFilter::Debug,
|
||||
"trace" => log::LevelFilter::Trace,
|
||||
_ => log::LevelFilter::Info,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -254,4 +298,60 @@ mod tests {
|
||||
assert!(!config.enabled);
|
||||
assert!(!config.request_thinking_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_config_default() {
|
||||
let config = LogConfig::default();
|
||||
assert!(config.enabled);
|
||||
assert_eq!(config.level, "info");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_config_serde_default() {
|
||||
let json = "{}";
|
||||
let config: LogConfig = serde_json::from_str(json).unwrap();
|
||||
assert!(config.enabled);
|
||||
assert_eq!(config.level, "info");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_config_to_level_filter() {
|
||||
let mut config = LogConfig::default();
|
||||
|
||||
config.level = "error".to_string();
|
||||
assert_eq!(config.to_level_filter(), log::LevelFilter::Error);
|
||||
|
||||
config.level = "warn".to_string();
|
||||
assert_eq!(config.to_level_filter(), log::LevelFilter::Warn);
|
||||
|
||||
config.level = "info".to_string();
|
||||
assert_eq!(config.to_level_filter(), log::LevelFilter::Info);
|
||||
|
||||
config.level = "debug".to_string();
|
||||
assert_eq!(config.to_level_filter(), log::LevelFilter::Debug);
|
||||
|
||||
config.level = "trace".to_string();
|
||||
assert_eq!(config.to_level_filter(), log::LevelFilter::Trace);
|
||||
|
||||
// 无效级别回退到 info
|
||||
config.level = "invalid".to_string();
|
||||
assert_eq!(config.to_level_filter(), log::LevelFilter::Info);
|
||||
|
||||
// 禁用时返回 Off
|
||||
config.enabled = false;
|
||||
config.level = "debug".to_string();
|
||||
assert_eq!(config.to_level_filter(), log::LevelFilter::Off);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_log_config_serde_roundtrip() {
|
||||
let config = LogConfig {
|
||||
enabled: true,
|
||||
level: "debug".to_string(),
|
||||
};
|
||||
let json = serde_json::to_string(&config).unwrap();
|
||||
let parsed: LogConfig = serde_json::from_str(&json).unwrap();
|
||||
assert!(parsed.enabled);
|
||||
assert_eq!(parsed.level, "debug");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user