mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-30 02:14:43 +08:00
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
This commit is contained in:
@@ -2,18 +2,18 @@
|
||||
//!
|
||||
//! 提供获取、设置和测试全局代理的 Tauri 命令。
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::proxy::http_client;
|
||||
use crate::store::AppState;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::net::{Ipv4Addr, SocketAddrV4, TcpStream};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// 获取全局代理 URL
|
||||
///
|
||||
/// 返回当前配置的代理 URL,null 表示直连。
|
||||
#[tauri::command]
|
||||
pub fn get_global_proxy_url(db: tauri::State<'_, Arc<Database>>) -> Result<Option<String>, String> {
|
||||
db.get_global_proxy_url().map_err(|e| e.to_string())
|
||||
pub fn get_global_proxy_url(state: tauri::State<'_, AppState>) -> Result<Option<String>, String> {
|
||||
state.db.get_global_proxy_url().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// 设置全局代理 URL
|
||||
@@ -21,10 +21,7 @@ pub fn get_global_proxy_url(db: tauri::State<'_, Arc<Database>>) -> Result<Optio
|
||||
/// - 传入非空字符串:启用代理
|
||||
/// - 传入空字符串:清除代理(直连)
|
||||
#[tauri::command]
|
||||
pub fn set_global_proxy_url(
|
||||
db: tauri::State<'_, Arc<Database>>,
|
||||
url: String,
|
||||
) -> Result<(), String> {
|
||||
pub fn set_global_proxy_url(state: tauri::State<'_, AppState>, url: String) -> Result<(), String> {
|
||||
let url_opt = if url.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -32,7 +29,10 @@ pub fn set_global_proxy_url(
|
||||
};
|
||||
|
||||
// 1. 保存到数据库
|
||||
db.set_global_proxy_url(url_opt).map_err(|e| e.to_string())?;
|
||||
state
|
||||
.db
|
||||
.set_global_proxy_url(url_opt)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 2. 热更新全局 HTTP 客户端
|
||||
http_client::update_proxy(url_opt)?;
|
||||
@@ -69,14 +69,14 @@ pub async fn test_proxy_url(url: String) -> Result<ProxyTestResult, String> {
|
||||
let start = Instant::now();
|
||||
|
||||
// 构建带代理的临时客户端
|
||||
let proxy = reqwest::Proxy::all(&url).map_err(|e| format!("Invalid proxy URL: {}", e))?;
|
||||
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))?;
|
||||
.map_err(|e| format!("Failed to build client: {e}"))?;
|
||||
|
||||
// 测试连接到 api.anthropic.com
|
||||
// 使用 HEAD 请求,即使返回 401 也说明网络通了
|
||||
@@ -99,7 +99,7 @@ pub async fn test_proxy_url(url: String) -> Result<ProxyTestResult, String> {
|
||||
}
|
||||
Err(e) => {
|
||||
let latency = start.elapsed().as_millis() as u64;
|
||||
log::debug!("[GlobalProxy] Test failed: {} -> {} ({}ms)", url, e, latency);
|
||||
log::debug!("[GlobalProxy] Test failed: {url} -> {e} ({latency}ms)");
|
||||
Ok(ProxyTestResult {
|
||||
success: false,
|
||||
latency_ms: latency,
|
||||
@@ -130,3 +130,48 @@ pub struct UpstreamProxyStatus {
|
||||
/// 代理 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,
|
||||
}
|
||||
|
||||
/// 常见代理端口配置
|
||||
const PROXY_PORTS: &[(u16, &str)] = &[
|
||||
(7890, "http"), // Clash
|
||||
(7891, "socks5"), // Clash SOCKS
|
||||
(1080, "socks5"), // 通用 SOCKS5
|
||||
(8080, "http"), // 通用 HTTP
|
||||
(8888, "http"), // Charles/Fiddler
|
||||
(3128, "http"), // Squid
|
||||
(10808, "socks5"), // V2Ray
|
||||
(10809, "http"), // V2Ray HTTP
|
||||
];
|
||||
|
||||
/// 扫描本地代理
|
||||
///
|
||||
/// 检测常见端口是否有代理服务在运行。
|
||||
#[tauri::command]
|
||||
pub fn scan_local_proxies() -> Vec<DetectedProxy> {
|
||||
let mut found = Vec::new();
|
||||
|
||||
for &(port, proxy_type) 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!("{proxy_type}://127.0.0.1:{port}"),
|
||||
proxy_type: proxy_type.to_string(),
|
||||
port,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
found
|
||||
}
|
||||
|
||||
@@ -856,6 +856,7 @@ pub fn run() {
|
||||
commands::set_global_proxy_url,
|
||||
commands::test_proxy_url,
|
||||
commands::get_upstream_proxy_status,
|
||||
commands::scan_local_proxies,
|
||||
]);
|
||||
|
||||
let app = builder
|
||||
|
||||
@@ -20,7 +20,7 @@ static CURRENT_PROXY_URL: OnceCell<RwLock<Option<String>>> = OnceCell::new();
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `proxy_url` - 代理 URL,如 `http://127.0.0.1:7890` 或 `socks5://127.0.0.1:1080`
|
||||
/// 传入 None 或空字符串表示直连
|
||||
/// 传入 None 或空字符串表示直连
|
||||
pub fn init(proxy_url: Option<&str>) -> Result<(), String> {
|
||||
let effective_url = proxy_url.filter(|s| !s.trim().is_empty());
|
||||
let client = build_client(effective_url)?;
|
||||
@@ -100,6 +100,7 @@ pub fn get_current_proxy_url() -> Option<String> {
|
||||
}
|
||||
|
||||
/// 检查是否正在使用代理
|
||||
#[allow(dead_code)]
|
||||
pub fn is_proxy_enabled() -> bool {
|
||||
get_current_proxy_url().is_some()
|
||||
}
|
||||
@@ -114,9 +115,8 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
|
||||
|
||||
// 有代理地址则使用代理,否则直连
|
||||
if let Some(url) = proxy_url {
|
||||
let proxy = reqwest::Proxy::all(url).map_err(|e| {
|
||||
format!("Invalid proxy URL '{}': {}", mask_url(url), e)
|
||||
})?;
|
||||
let proxy = reqwest::Proxy::all(url)
|
||||
.map_err(|e| format!("Invalid proxy URL '{}': {}", mask_url(url), e))?;
|
||||
builder = builder.proxy(proxy);
|
||||
log::debug!("[GlobalProxy] Proxy configured: {}", mask_url(url));
|
||||
} else {
|
||||
@@ -126,7 +126,7 @@ fn build_client(proxy_url: Option<&str>) -> Result<Client, String> {
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map_err(|e| format!("Failed to build HTTP client: {}", e))
|
||||
.map_err(|e| format!("Failed to build HTTP client: {e}"))
|
||||
}
|
||||
|
||||
/// 隐藏 URL 中的敏感信息(用于日志)
|
||||
@@ -137,7 +137,10 @@ fn mask_url(url: &str) -> String {
|
||||
"{}://{}:{}",
|
||||
parsed.scheme(),
|
||||
parsed.host_str().unwrap_or("?"),
|
||||
parsed.port().map(|p| p.to_string()).unwrap_or_else(|| "?".to_string())
|
||||
parsed
|
||||
.port()
|
||||
.map(|p| p.to_string())
|
||||
.unwrap_or_else(|| "?".to_string())
|
||||
)
|
||||
} else {
|
||||
// URL 解析失败,返回部分内容
|
||||
|
||||
@@ -6,13 +6,13 @@ pub mod body_filter;
|
||||
pub mod circuit_breaker;
|
||||
pub mod error;
|
||||
pub mod error_mapper;
|
||||
pub mod http_client;
|
||||
pub(crate) mod failover_switch;
|
||||
mod forwarder;
|
||||
pub mod handler_config;
|
||||
pub mod handler_context;
|
||||
mod handlers;
|
||||
mod health;
|
||||
pub mod http_client;
|
||||
pub mod log_codes;
|
||||
pub mod model_mapper;
|
||||
pub mod provider_router;
|
||||
|
||||
@@ -152,7 +152,7 @@ pub fn sync_current_to_live(state: &AppState) -> Result<(), AppError> {
|
||||
// Skill sync
|
||||
for app_type in [AppType::Claude, AppType::Codex, AppType::Gemini] {
|
||||
if let Err(e) = crate::services::skill::SkillService::sync_to_app(&state.db, &app_type) {
|
||||
log::warn!("同步 Skill 到 {:?} 失败: {}", app_type, e);
|
||||
log::warn!("同步 Skill 到 {app_type:?} 失败: {e}");
|
||||
// Continue syncing other apps, don't abort
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fs;
|
||||
@@ -143,9 +142,7 @@ pub struct SkillMetadata {
|
||||
|
||||
// ========== SkillService ==========
|
||||
|
||||
pub struct SkillService {
|
||||
http_client: Client,
|
||||
}
|
||||
pub struct SkillService;
|
||||
|
||||
impl Default for SkillService {
|
||||
fn default() -> Self {
|
||||
@@ -155,10 +152,7 @@ impl Default for SkillService {
|
||||
|
||||
impl SkillService {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
// 使用全局 HTTP 客户端(已包含代理配置)
|
||||
http_client: crate::proxy::http_client::get(),
|
||||
}
|
||||
Self
|
||||
}
|
||||
|
||||
// ========== 路径管理 ==========
|
||||
@@ -860,7 +854,8 @@ impl SkillService {
|
||||
|
||||
/// 下载并解压 ZIP
|
||||
async fn download_and_extract(&self, url: &str, dest: &Path) -> Result<()> {
|
||||
let response = self.http_client.get(url).send().await?;
|
||||
let client = crate::proxy::http_client::get();
|
||||
let response = client.get(url).send().await?;
|
||||
if !response.status().is_success() {
|
||||
let status = response.status().as_u16().to_string();
|
||||
return Err(anyhow::anyhow!(format_skill_error(
|
||||
|
||||
Reference in New Issue
Block a user