Files
CC-Switch/src-tauri/src/commands/global_proxy.rs
T
Dex Miller 8b92982112 Feature/global proxy (#596)
* 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
2026-01-13 10:55:53 +08:00

248 lines
7.5 KiB
Rust

//! 全局出站代理相关命令
//!
//! 提供获取、设置和测试全局代理的 Tauri 命令。
use crate::proxy::http_client;
use crate::store::AppState;
use serde::Serialize;
use std::net::{Ipv4Addr, SocketAddrV4, TcpStream};
use std::time::{Duration, Instant};
/// 获取全局代理 URL
///
/// 返回当前配置的代理 URL,null 表示直连。
#[tauri::command]
pub fn get_global_proxy_url(state: tauri::State<'_, AppState>) -> Result<Option<String>, String> {
let result = state.db.get_global_proxy_url().map_err(|e| e.to_string())?;
log::debug!(
"[GlobalProxy] [GP-010] Read from database: {}",
result
.as_ref()
.map(|u| http_client::mask_url(u))
.unwrap_or_else(|| "None".to_string())
);
Ok(result)
}
/// 设置全局代理 URL
///
/// - 传入非空字符串:启用代理
/// - 传入空字符串:清除代理(直连)
///
/// 执行顺序:先验证 → 写 DB → 再应用
/// 这样确保 DB 写失败时不会出现运行态与持久化不一致的问题
#[tauri::command]
pub fn set_global_proxy_url(state: tauri::State<'_, AppState>, url: String) -> Result<(), String> {
// 调试:显示接收到的 URL 信息(不包含敏感内容)
let has_auth = url.contains('@') && (url.starts_with("http://") || url.starts_with("socks"));
log::debug!(
"[GlobalProxy] [GP-011] Received URL: length={}, has_auth={}",
url.len(),
has_auth
);
let url_opt = if url.trim().is_empty() {
None
} else {
Some(url.as_str())
};
// 1. 先验证代理配置是否有效(不应用)
http_client::validate_proxy(url_opt)?;
// 2. 验证成功后保存到数据库
state
.db
.set_global_proxy_url(url_opt)
.map_err(|e| e.to_string())?;
// 3. DB 写入成功后再应用到运行态
http_client::apply_proxy(url_opt)?;
log::info!(
"[GlobalProxy] [GP-009] Configuration updated: {}",
url_opt
.map(http_client::mask_url)
.unwrap_or_else(|| "direct connection".to_string())
);
Ok(())
}
/// 代理测试结果
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProxyTestResult {
/// 是否连接成功
pub success: bool,
/// 延迟(毫秒)
pub latency_ms: u64,
/// 错误信息
pub error: Option<String>,
}
/// 测试代理连接
///
/// 通过指定的代理 URL 发送测试请求,返回连接结果和延迟。
/// 使用多个测试目标,任一成功即认为代理可用。
#[tauri::command]
pub async fn test_proxy_url(url: String) -> Result<ProxyTestResult, String> {
if url.trim().is_empty() {
return Err("Proxy URL is empty".to_string());
}
let start = Instant::now();
// 构建带代理的临时客户端
let proxy = reqwest::Proxy::all(&url).map_err(|e| format!("Invalid proxy URL: {e}"))?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(10))
.connect_timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to build client: {e}"))?;
// 使用多个测试目标,提高兼容性
// 优先使用 httpbin(专门用于 HTTP 测试),回退到其他公共端点
let test_urls = [
"https://httpbin.org/get",
"https://www.google.com",
"https://api.anthropic.com",
];
let mut last_error = None;
for test_url in test_urls {
match client.head(test_url).send().await {
Ok(resp) => {
let latency = start.elapsed().as_millis() as u64;
log::debug!(
"[GlobalProxy] Test successful: {} -> {} via {} ({}ms)",
http_client::mask_url(&url),
test_url,
resp.status(),
latency
);
return Ok(ProxyTestResult {
success: true,
latency_ms: latency,
error: None,
});
}
Err(e) => {
log::debug!("[GlobalProxy] Test to {test_url} failed: {e}");
last_error = Some(e);
}
}
}
// 所有测试目标都失败
let latency = start.elapsed().as_millis() as u64;
let error_msg = last_error
.map(|e| e.to_string())
.unwrap_or_else(|| "All test targets failed".to_string());
log::debug!(
"[GlobalProxy] Test failed: {} -> {} ({}ms)",
http_client::mask_url(&url),
error_msg,
latency
);
Ok(ProxyTestResult {
success: false,
latency_ms: latency,
error: Some(error_msg),
})
}
/// 获取当前出站代理状态
///
/// 返回当前是否启用了出站代理以及代理 URL。
#[tauri::command]
pub fn get_upstream_proxy_status() -> UpstreamProxyStatus {
let url = http_client::get_current_proxy_url();
UpstreamProxyStatus {
enabled: url.is_some(),
proxy_url: url,
}
}
/// 出站代理状态信息
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UpstreamProxyStatus {
/// 是否启用代理
pub enabled: bool,
/// 代理 URL
pub proxy_url: Option<String>,
}
/// 检测到的代理信息
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DetectedProxy {
/// 代理 URL
pub url: String,
/// 代理类型 (http/socks5)
pub proxy_type: String,
/// 端口
pub port: u16,
}
/// 常见代理端口配置
/// 格式:(端口, 主要类型, 是否同时支持 http 和 socks5)
/// 对于 mixed 端口,会同时返回两种协议供用户选择
const PROXY_PORTS: &[(u16, &str, bool)] = &[
(7890, "http", true), // Clash (mixed mode)
(7891, "socks5", false), // Clash SOCKS only
(1080, "socks5", false), // 通用 SOCKS5
(8080, "http", false), // 通用 HTTP
(8888, "http", false), // Charles/Fiddler
(3128, "http", false), // Squid
(10808, "socks5", false), // V2Ray SOCKS
(10809, "http", false), // V2Ray HTTP
];
/// 扫描本地代理
///
/// 检测常见端口是否有代理服务在运行。
/// 使用异步任务避免阻塞 UI 线程。
#[tauri::command]
pub async fn scan_local_proxies() -> Vec<DetectedProxy> {
// 使用 spawn_blocking 避免阻塞主线程
tokio::task::spawn_blocking(|| {
let mut found = Vec::new();
for &(port, primary_type, is_mixed) in PROXY_PORTS {
let addr = SocketAddrV4::new(Ipv4Addr::LOCALHOST, port);
if TcpStream::connect_timeout(&addr.into(), Duration::from_millis(100)).is_ok() {
// 添加主要类型
found.push(DetectedProxy {
url: format!("{primary_type}://127.0.0.1:{port}"),
proxy_type: primary_type.to_string(),
port,
});
// 对于 mixed 端口,同时添加另一种协议
if is_mixed {
let alt_type = if primary_type == "http" {
"socks5"
} else {
"http"
};
found.push(DetectedProxy {
url: format!("{alt_type}://127.0.0.1:{port}"),
proxy_type: alt_type.to_string(),
port,
});
}
}
}
found
})
.await
.unwrap_or_default()
}