mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
8b92982112
* 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 * feat(proxy): add global proxy settings support Add ability to configure a global HTTP/HTTPS proxy for all outbound requests including provider API calls, speed tests, and stream checks. * 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 * feat(proxy): add local proxy auto-scan and fix hot-reload - Add scan_local_proxies command to detect common proxy ports - Fix SkillService not using updated proxy after hot-reload - Move global proxy settings to advanced tab - Add error handling for scan failures * fix(proxy): allow localhost input in proxy address field * fix(proxy): restore request timeout and fix proxy hot-reload issues - Add URL scheme validation in build_client (http/https/socks5/socks5h) - Restore per-request timeout for speedtest, stream_check, usage_script, forwarder - Fix set_global_proxy_url to validate before persisting to DB - Mask proxy credentials in all log outputs - Fix forwarder hot-reload by fetching client on each request * style: format code with prettier * fix(proxy): improve global proxy stability and error handling - Fix RwLock silent failures with explicit error propagation - Handle init() duplicate calls gracefully with warning log - Align fallback client config with build_client settings - Make scan_local_proxies async to avoid UI blocking - Add mixed mode support for Clash 7890 port (http+socks5) - Use multiple test targets for better proxy connectivity test - Clear invalid proxy config on init failure - Restore timeout constraints in usage_script - Fix mask_url output for URLs without port - Add structured error codes [GP-001 to GP-009] * feat(proxy): add username/password authentication support - Add separate username and password input fields - Implement password visibility toggle with eye icon - Add clear button to reset all proxy fields - Auto-extract auth info from saved URL and merge on save - Update i18n translations (zh/en/ja) * fix(proxy): fix double encoding issue in proxy auth and add debug logs - Remove encodeURIComponent in mergeAuth() since URL object's username/password setters already do percent-encoding automatically - Add GP-010 debug log for database read operations - Add GP-011 debug log to track incoming URL info (length, has_auth) - Fix username.trim() in fallback branch for consistent behavior
188 lines
5.9 KiB
Rust
188 lines
5.9 KiB
Rust
use futures::future::join_all;
|
|
use reqwest::{Client, Url};
|
|
use serde::Serialize;
|
|
use std::time::Instant;
|
|
|
|
use crate::error::AppError;
|
|
|
|
const DEFAULT_TIMEOUT_SECS: u64 = 8;
|
|
const MAX_TIMEOUT_SECS: u64 = 30;
|
|
const MIN_TIMEOUT_SECS: u64 = 2;
|
|
|
|
/// 端点测速结果
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct EndpointLatency {
|
|
pub url: String,
|
|
pub latency: Option<u128>,
|
|
pub status: Option<u16>,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
/// 网络测速相关业务
|
|
pub struct SpeedtestService;
|
|
|
|
impl SpeedtestService {
|
|
/// 测试一组端点的响应延迟。
|
|
pub async fn test_endpoints(
|
|
urls: Vec<String>,
|
|
timeout_secs: Option<u64>,
|
|
) -> Result<Vec<EndpointLatency>, AppError> {
|
|
if urls.is_empty() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let mut results: Vec<Option<EndpointLatency>> = vec![None; urls.len()];
|
|
let mut valid_targets = Vec::new();
|
|
|
|
for (idx, raw_url) in urls.into_iter().enumerate() {
|
|
let trimmed = raw_url.trim().to_string();
|
|
|
|
if trimmed.is_empty() {
|
|
results[idx] = Some(EndpointLatency {
|
|
url: raw_url,
|
|
latency: None,
|
|
status: None,
|
|
error: Some("URL 不能为空".to_string()),
|
|
});
|
|
continue;
|
|
}
|
|
|
|
match Url::parse(&trimmed) {
|
|
Ok(parsed_url) => valid_targets.push((idx, trimmed, parsed_url)),
|
|
Err(err) => {
|
|
results[idx] = Some(EndpointLatency {
|
|
url: trimmed,
|
|
latency: None,
|
|
status: None,
|
|
error: Some(format!("URL 无效: {err}")),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if valid_targets.is_empty() {
|
|
return Ok(results.into_iter().flatten().collect::<Vec<_>>());
|
|
}
|
|
|
|
let timeout = Self::sanitize_timeout(timeout_secs);
|
|
let (client, request_timeout) = Self::build_client(timeout)?;
|
|
|
|
let tasks = valid_targets.into_iter().map(|(idx, trimmed, parsed_url)| {
|
|
let client = client.clone();
|
|
async move {
|
|
// 先进行一次热身请求,忽略结果,仅用于复用连接/绕过首包惩罚。
|
|
let _ = client
|
|
.get(parsed_url.clone())
|
|
.timeout(request_timeout)
|
|
.send()
|
|
.await;
|
|
|
|
// 第二次请求开始计时,并将其作为结果返回。
|
|
let start = Instant::now();
|
|
let latency = match client.get(parsed_url).timeout(request_timeout).send().await {
|
|
Ok(resp) => EndpointLatency {
|
|
url: trimmed,
|
|
latency: Some(start.elapsed().as_millis()),
|
|
status: Some(resp.status().as_u16()),
|
|
error: None,
|
|
},
|
|
Err(err) => {
|
|
let status = err.status().map(|s| s.as_u16());
|
|
let error_message = if err.is_timeout() {
|
|
"请求超时".to_string()
|
|
} else if err.is_connect() {
|
|
"连接失败".to_string()
|
|
} else {
|
|
err.to_string()
|
|
};
|
|
|
|
EndpointLatency {
|
|
url: trimmed,
|
|
latency: None,
|
|
status,
|
|
error: Some(error_message),
|
|
}
|
|
}
|
|
};
|
|
|
|
(idx, latency)
|
|
}
|
|
});
|
|
|
|
for (idx, latency) in join_all(tasks).await {
|
|
results[idx] = Some(latency);
|
|
}
|
|
|
|
Ok(results.into_iter().flatten().collect::<Vec<_>>())
|
|
}
|
|
|
|
fn build_client(timeout_secs: u64) -> Result<(Client, std::time::Duration), AppError> {
|
|
// 使用全局 HTTP 客户端(已包含代理配置)
|
|
// 返回 timeout Duration 供请求级别使用
|
|
let timeout = std::time::Duration::from_secs(timeout_secs);
|
|
Ok((crate::proxy::http_client::get(), timeout))
|
|
}
|
|
|
|
fn sanitize_timeout(timeout_secs: Option<u64>) -> u64 {
|
|
let secs = timeout_secs.unwrap_or(DEFAULT_TIMEOUT_SECS);
|
|
secs.clamp(MIN_TIMEOUT_SECS, MAX_TIMEOUT_SECS)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn sanitize_timeout_clamps_values() {
|
|
assert_eq!(
|
|
SpeedtestService::sanitize_timeout(Some(1)),
|
|
MIN_TIMEOUT_SECS
|
|
);
|
|
assert_eq!(
|
|
SpeedtestService::sanitize_timeout(Some(999)),
|
|
MAX_TIMEOUT_SECS
|
|
);
|
|
assert_eq!(
|
|
SpeedtestService::sanitize_timeout(Some(10)),
|
|
10.clamp(MIN_TIMEOUT_SECS, MAX_TIMEOUT_SECS)
|
|
);
|
|
assert_eq!(
|
|
SpeedtestService::sanitize_timeout(None),
|
|
DEFAULT_TIMEOUT_SECS
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_endpoints_handles_empty_list() {
|
|
let result =
|
|
tauri::async_runtime::block_on(SpeedtestService::test_endpoints(Vec::new(), Some(5)))
|
|
.expect("empty list should succeed");
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_endpoints_reports_invalid_url() {
|
|
let result = tauri::async_runtime::block_on(SpeedtestService::test_endpoints(
|
|
vec!["not a url".into(), "".into()],
|
|
None,
|
|
))
|
|
.expect("invalid inputs should still succeed");
|
|
|
|
assert_eq!(result.len(), 2);
|
|
assert!(
|
|
result[0]
|
|
.error
|
|
.as_deref()
|
|
.unwrap_or_default()
|
|
.starts_with("URL 无效"),
|
|
"invalid url should yield parse error"
|
|
);
|
|
assert_eq!(
|
|
result[1].error.as_deref(),
|
|
Some("URL 不能为空"),
|
|
"empty url should report validation error"
|
|
);
|
|
}
|
|
}
|