From 5fde5dcc7cb3a0d7c34c6db00c1a620e481446a7 Mon Sep 17 00:00:00 2001 From: wonfen Date: Sat, 21 Jun 2025 21:48:39 +0800 Subject: [PATCH 01/15] feat: implement async proxy lookup and optimize system/auto proxy retrieval logic --- src-tauri/src/cmd/network.rs | 12 +- src-tauri/src/core/async_proxy_query.rs | 430 +++++++++++++++++++++++ src-tauri/src/core/event_driven_proxy.rs | 61 +--- src-tauri/src/core/mod.rs | 1 + 4 files changed, 451 insertions(+), 53 deletions(-) create mode 100644 src-tauri/src/core/async_proxy_query.rs diff --git a/src-tauri/src/cmd/network.rs b/src-tauri/src/cmd/network.rs index 23a9ded664..d3b438b725 100644 --- a/src-tauri/src/cmd/network.rs +++ b/src-tauri/src/cmd/network.rs @@ -1,18 +1,15 @@ use super::CmdResult; -use crate::core::EventDrivenProxyManager; +use crate::core::{async_proxy_query::AsyncProxyQuery, EventDrivenProxyManager}; use crate::wrap_err; use network_interface::NetworkInterface; use serde_yaml::Mapping; -use sysproxy::Sysproxy; -use tokio::task::spawn_blocking; /// get the system proxy #[tauri::command] pub async fn get_sys_proxy() -> CmdResult { - let current = spawn_blocking(Sysproxy::get_system_proxy) - .await - .map_err(|e| format!("Failed to spawn blocking task for sysproxy: {}", e))? - .map_err(|e| format!("Failed to get system proxy: {}", e))?; + log::debug!(target: "app", "异步获取系统代理配置"); + + let current = AsyncProxyQuery::get_system_proxy().await; let mut map = Mapping::new(); map.insert("enable".into(), current.enable.into()); @@ -22,6 +19,7 @@ pub async fn get_sys_proxy() -> CmdResult { ); map.insert("bypass".into(), current.bypass.into()); + log::debug!(target: "app", "返回系统代理配置: enable={}, {}:{}", current.enable, current.host, current.port); Ok(map) } diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs new file mode 100644 index 0000000000..b03c0d6881 --- /dev/null +++ b/src-tauri/src/core/async_proxy_query.rs @@ -0,0 +1,430 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use tokio::process::Command; +use tokio::time::{timeout, Duration}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AsyncAutoproxy { + pub enable: bool, + pub url: String, +} + +impl Default for AsyncAutoproxy { + fn default() -> Self { + Self { + enable: false, + url: String::new(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AsyncSysproxy { + pub enable: bool, + pub host: String, + pub port: u16, + pub bypass: String, +} + +impl Default for AsyncSysproxy { + fn default() -> Self { + Self { + enable: false, + host: "127.0.0.1".to_string(), + port: 7890, + bypass: String::new(), + } + } +} + +pub struct AsyncProxyQuery; + +impl AsyncProxyQuery { + /// 异步获取自动代理配置(PAC) + pub async fn get_auto_proxy() -> AsyncAutoproxy { + match timeout(Duration::from_secs(3), Self::get_auto_proxy_impl()).await { + Ok(Ok(proxy)) => { + log::debug!(target: "app", "异步获取自动代理成功: enable={}, url={}", proxy.enable, proxy.url); + proxy + } + Ok(Err(e)) => { + log::warn!(target: "app", "异步获取自动代理失败: {}", e); + AsyncAutoproxy::default() + } + Err(_) => { + log::warn!(target: "app", "异步获取自动代理超时"); + AsyncAutoproxy::default() + } + } + } + + /// 异步获取系统代理配置 + pub async fn get_system_proxy() -> AsyncSysproxy { + match timeout(Duration::from_secs(3), Self::get_system_proxy_impl()).await { + Ok(Ok(proxy)) => { + log::debug!(target: "app", "异步获取系统代理成功: enable={}, {}:{}", proxy.enable, proxy.host, proxy.port); + proxy + } + Ok(Err(e)) => { + log::warn!(target: "app", "异步获取系统代理失败: {}", e); + AsyncSysproxy::default() + } + Err(_) => { + log::warn!(target: "app", "异步获取系统代理超时"); + AsyncSysproxy::default() + } + } + } + + #[cfg(target_os = "windows")] + async fn get_auto_proxy_impl() -> Result { + // Windows: 使用 netsh winhttp show proxy 命令 + let output = Command::new("netsh") + .args(&["winhttp", "show", "proxy"]) + .output() + .await?; + + if !output.status.success() { + return Ok(AsyncAutoproxy::default()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + log::debug!(target: "app", "netsh output: {}", stdout); + + // 解析输出,查找 PAC 配置 + for line in stdout.lines() { + let line = line.trim(); + if line.starts_with("代理自动配置脚本") || line.starts_with("Proxy auto-config script") + { + // 修复:正确解析包含冒号的URL + // 格式: "代理自动配置脚本 : http://127.0.0.1:11233/commands/pac" + // 或: "Proxy auto-config script : http://127.0.0.1:11233/commands/pac" + if let Some(colon_pos) = line.find(" : ") { + let url = line[colon_pos + 3..].trim(); + if !url.is_empty() && url != "(none)" && url != "无" { + log::debug!(target: "app", "解析到PAC URL: {}", url); + return Ok(AsyncAutoproxy { + enable: true, + url: url.to_string(), + }); + } + } else if let Some(colon_pos) = line.find(':') { + // 兼容其他可能的格式 + let url = line[colon_pos + 1..].trim(); + // 确保这不是URL中的协议部分 + if url.starts_with("http") && !url.is_empty() && url != "(none)" && url != "无" + { + log::debug!(target: "app", "解析到PAC URL (fallback): {}", url); + return Ok(AsyncAutoproxy { + enable: true, + url: url.to_string(), + }); + } + } + } + } + + log::debug!(target: "app", "未找到有效的PAC配置"); + Ok(AsyncAutoproxy::default()) + } + + #[cfg(target_os = "macos")] + async fn get_auto_proxy_impl() -> Result { + // macOS: 使用 scutil --proxy 命令 + let output = Command::new("scutil").args(&["--proxy"]).output().await?; + + if !output.status.success() { + return Ok(AsyncAutoproxy::default()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + log::debug!(target: "app", "scutil output: {}", stdout); + + let mut pac_enabled = false; + let mut pac_url = String::new(); + + // 解析 scutil 输出 + for line in stdout.lines() { + let line = line.trim(); + if line.contains("ProxyAutoConfigEnable") && line.contains("1") { + pac_enabled = true; + } else if line.contains("ProxyAutoConfigURLString") { + // 修复:正确解析包含冒号的URL + // 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac" + if let Some(colon_pos) = line.find(" : ") { + pac_url = line[colon_pos + 3..].trim().to_string(); + } + } + } + + log::debug!(target: "app", "解析结果: pac_enabled={}, pac_url={}", pac_enabled, pac_url); + + Ok(AsyncAutoproxy { + enable: pac_enabled && !pac_url.is_empty(), + url: pac_url, + }) + } + + #[cfg(target_os = "linux")] + async fn get_auto_proxy_impl() -> Result { + // Linux: 检查环境变量和GNOME设置 + + // 首先检查环境变量 + if let Ok(auto_proxy) = std::env::var("auto_proxy") { + if !auto_proxy.is_empty() { + return Ok(AsyncAutoproxy { + enable: true, + url: auto_proxy, + }); + } + } + + // 尝试使用 gsettings 获取 GNOME 代理设置 + let output = Command::new("gsettings") + .args(&["get", "org.gnome.system.proxy", "mode"]) + .output() + .await; + + if let Ok(output) = output { + if output.status.success() { + let mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if mode.contains("auto") { + // 获取 PAC URL + let pac_output = Command::new("gsettings") + .args(&["get", "org.gnome.system.proxy", "autoconfig-url"]) + .output() + .await; + + if let Ok(pac_output) = pac_output { + if pac_output.status.success() { + let pac_url = String::from_utf8_lossy(&pac_output.stdout) + .trim() + .trim_matches('\'') + .trim_matches('"') + .to_string(); + + if !pac_url.is_empty() { + return Ok(AsyncAutoproxy { + enable: true, + url: pac_url, + }); + } + } + } + } + } + } + + Ok(AsyncAutoproxy::default()) + } + + #[cfg(target_os = "windows")] + async fn get_system_proxy_impl() -> Result { + let output = Command::new("netsh") + .args(&["winhttp", "show", "proxy"]) + .output() + .await?; + + if !output.status.success() { + return Ok(AsyncSysproxy::default()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + log::debug!(target: "app", "netsh proxy output: {}", stdout); + + let mut proxy_enabled = false; + let mut proxy_server = String::new(); + let mut bypass_list = String::new(); + + for line in stdout.lines() { + let line = line.trim(); + if line.starts_with("代理服务器") || line.starts_with("Proxy Server") { + if let Some(server_part) = line.split(':').nth(1) { + let server = server_part.trim(); + if !server.is_empty() && server != "(none)" && server != "无" { + proxy_server = server.to_string(); + proxy_enabled = true; + } + } + } else if line.starts_with("绕过列表") || line.starts_with("Bypass List") { + if let Some(bypass_part) = line.split(':').nth(1) { + bypass_list = bypass_part.trim().to_string(); + } + } + } + + if proxy_enabled && !proxy_server.is_empty() { + // 解析服务器地址和端口 + let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') { + let host = proxy_server[..colon_pos].to_string(); + let port = proxy_server[colon_pos + 1..].parse::().unwrap_or(8080); + (host, port) + } else { + (proxy_server, 8080) + }; + + Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: bypass_list, + }) + } else { + Ok(AsyncSysproxy::default()) + } + } + + #[cfg(target_os = "macos")] + async fn get_system_proxy_impl() -> Result { + let output = Command::new("scutil").args(&["--proxy"]).output().await?; + + if !output.status.success() { + return Ok(AsyncSysproxy::default()); + } + + let stdout = String::from_utf8_lossy(&output.stdout); + log::debug!(target: "app", "scutil proxy output: {}", stdout); + + let mut http_enabled = false; + let mut http_host = String::new(); + let mut http_port = 8080u16; + let mut exceptions = Vec::new(); + + for line in stdout.lines() { + let line = line.trim(); + if line.contains("HTTPEnable") && line.contains("1") { + http_enabled = true; + } else if line.contains("HTTPProxy") && !line.contains("Port") { + if let Some(host_part) = line.split(':').nth(1) { + http_host = host_part.trim().to_string(); + } + } else if line.contains("HTTPPort") { + if let Some(port_part) = line.split(':').nth(1) { + if let Ok(port) = port_part.trim().parse::() { + http_port = port; + } + } + } else if line.contains("ExceptionsList") { + // 解析异常列表 + if let Some(list_part) = line.split(':').nth(1) { + let list = list_part.trim(); + if !list.is_empty() { + exceptions.push(list.to_string()); + } + } + } + } + + Ok(AsyncSysproxy { + enable: http_enabled && !http_host.is_empty(), + host: http_host, + port: http_port, + bypass: exceptions.join(","), + }) + } + + #[cfg(target_os = "linux")] + async fn get_system_proxy_impl() -> Result { + // Linux: 检查环境变量和桌面环境设置 + + // 首先检查环境变量 + if let Ok(http_proxy) = std::env::var("http_proxy") { + if let Ok(proxy_info) = Self::parse_proxy_url(&http_proxy) { + return Ok(proxy_info); + } + } + + if let Ok(https_proxy) = std::env::var("https_proxy") { + if let Ok(proxy_info) = Self::parse_proxy_url(&https_proxy) { + return Ok(proxy_info); + } + } + + // 尝试使用 gsettings 获取 GNOME 代理设置 + let mode_output = Command::new("gsettings") + .args(&["get", "org.gnome.system.proxy", "mode"]) + .output() + .await; + + if let Ok(mode_output) = mode_output { + if mode_output.status.success() { + let mode = String::from_utf8_lossy(&mode_output.stdout) + .trim() + .to_string(); + if mode.contains("manual") { + // 获取HTTP代理设置 + let host_result = Command::new("gsettings") + .args(&["get", "org.gnome.system.proxy.http", "host"]) + .output() + .await; + + let port_result = Command::new("gsettings") + .args(&["get", "org.gnome.system.proxy.http", "port"]) + .output() + .await; + + if let (Ok(host_output), Ok(port_output)) = (host_result, port_result) { + if host_output.status.success() && port_output.status.success() { + let host = String::from_utf8_lossy(&host_output.stdout) + .trim() + .trim_matches('\'') + .trim_matches('"') + .to_string(); + + let port = String::from_utf8_lossy(&port_output.stdout) + .trim() + .parse::() + .unwrap_or(8080); + + if !host.is_empty() { + return Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: String::new(), + }); + } + } + } + } + } + } + + Ok(AsyncSysproxy::default()) + } + + #[cfg(target_os = "linux")] + fn parse_proxy_url(proxy_url: &str) -> Result { + // 解析形如 "http://proxy.example.com:8080" 的URL + let url = proxy_url.trim(); + + // 移除协议前缀 + let url = if url.starts_with("http://") { + &url[7..] + } else if url.starts_with("https://") { + &url[8..] + } else { + url + }; + + // 解析主机和端口 + let (host, port) = if let Some(colon_pos) = url.rfind(':') { + let host = url[..colon_pos].to_string(); + let port = url[colon_pos + 1..].parse::().unwrap_or(8080); + (host, port) + } else { + (url.to_string(), 8080) + }; + + if host.is_empty() { + return Err(anyhow!("无效的代理URL")); + } + + Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: std::env::var("no_proxy").unwrap_or_default(), + }) + } +} diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index e7a22c661c..28f9a095cc 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -4,6 +4,7 @@ use tokio::sync::{mpsc, oneshot}; use tokio::time::{sleep, timeout, Duration}; use crate::config::{Config, IVerge}; +use crate::core::async_proxy_query::AsyncProxyQuery; use crate::logging_error; use crate::utils::logging::Type; use once_cell::sync::Lazy; @@ -393,56 +394,24 @@ impl EventDrivenProxyManager { } async fn get_auto_proxy_with_timeout() -> Autoproxy { - let result = timeout( - Duration::from_secs(2), - tokio::task::spawn_blocking(|| Autoproxy::get_auto_proxy()), - ) - .await; - - match result { - Ok(Ok(Ok(proxy))) => proxy, - Ok(Ok(Err(e))) => { - log::warn!(target: "app", "获取自动代理失败: {}", e); - Autoproxy { - enable: false, - url: "".to_string(), - } - } - Ok(Err(e)) => { - log::error!(target: "app", "spawn_blocking失败: {}", e); - Autoproxy { - enable: false, - url: "".to_string(), - } - } - Err(_) => { - log::warn!(target: "app", "获取自动代理超时"); - Autoproxy { - enable: false, - url: "".to_string(), - } - } + let async_proxy = AsyncProxyQuery::get_auto_proxy().await; + + // 转换为兼容的结构 + Autoproxy { + enable: async_proxy.enable, + url: async_proxy.url, } } async fn get_sys_proxy_with_timeout() -> Sysproxy { - let result = timeout( - Duration::from_secs(2), - tokio::task::spawn_blocking(|| Sysproxy::get_system_proxy()), - ) - .await; - - match result { - Ok(Ok(Ok(proxy))) => proxy, - _ => { - log::warn!(target: "app", "获取系统代理失败或超时"); - Sysproxy { - enable: false, - host: "127.0.0.1".to_string(), - port: 7890, - bypass: "".to_string(), - } - } + let async_proxy = AsyncProxyQuery::get_system_proxy().await; + + // 转换为兼容的结构 + Sysproxy { + enable: async_proxy.enable, + host: async_proxy.host, + port: async_proxy.port, + bypass: async_proxy.bypass, } } diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 5cc2fa4492..044abaf9c5 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,4 @@ +pub mod async_proxy_query; pub mod backup; #[allow(clippy::module_inception)] mod core; From 41fc13cfe24a3d0c89c380396185faf32a9265cf Mon Sep 17 00:00:00 2001 From: wonfen Date: Sat, 21 Jun 2025 22:39:12 +0800 Subject: [PATCH 02/15] fix: format & update --- UPDATELOG.md | 1 + src-tauri/src/core/async_proxy_query.rs | 15 +++------------ src-tauri/src/core/core.rs | 12 ++++++------ src-tauri/src/core/event_driven_proxy.rs | 2 +- 4 files changed, 11 insertions(+), 19 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index ae48099dcd..2cf97bb57d 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -12,6 +12,7 @@ ### 🚀 优化改进 - 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死 +- 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死 ## v2.3.1 diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs index b03c0d6881..a82790448e 100644 --- a/src-tauri/src/core/async_proxy_query.rs +++ b/src-tauri/src/core/async_proxy_query.rs @@ -3,21 +3,12 @@ use serde::{Deserialize, Serialize}; use tokio::process::Command; use tokio::time::{timeout, Duration}; -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AsyncAutoproxy { pub enable: bool, pub url: String, } -impl Default for AsyncAutoproxy { - fn default() -> Self { - Self { - enable: false, - url: String::new(), - } - } -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AsyncSysproxy { pub enable: bool, @@ -131,7 +122,7 @@ impl AsyncProxyQuery { #[cfg(target_os = "macos")] async fn get_auto_proxy_impl() -> Result { // macOS: 使用 scutil --proxy 命令 - let output = Command::new("scutil").args(&["--proxy"]).output().await?; + let output = Command::new("scutil").args(["--proxy"]).output().await?; if !output.status.success() { return Ok(AsyncAutoproxy::default()); @@ -276,7 +267,7 @@ impl AsyncProxyQuery { #[cfg(target_os = "macos")] async fn get_system_proxy_impl() -> Result { - let output = Command::new("scutil").args(&["--proxy"]).output().await?; + let output = Command::new("scutil").args(["--proxy"]).output().await?; if !output.status.success() { return Ok(AsyncSysproxy::default()); diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index 12623176dd..c1d7e2e80b 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -524,7 +524,7 @@ impl CoreManager { ) -> Result<(Vec, String)> { let output = if cfg!(windows) { tokio::process::Command::new("tasklist") - .args(&[ + .args([ "/FI", &format!("IMAGENAME eq {}", process_name), "/FO", @@ -569,7 +569,7 @@ impl CoreManager { } } else { // Unix系统直接解析PID列表 - for pid_str in stdout.trim().split_whitespace() { + for pid_str in stdout.split_whitespace() { if let Ok(pid) = pid_str.parse::() { pids.push(pid); } @@ -592,14 +592,14 @@ impl CoreManager { let success = if cfg!(windows) { tokio::process::Command::new("taskkill") - .args(&["/F", "/PID", &pid.to_string()]) + .args(["/F", "/PID", &pid.to_string()]) .output() .await .map(|output| output.status.success()) .unwrap_or(false) } else { tokio::process::Command::new("kill") - .args(&["-9", &pid.to_string()]) + .args(["-9", &pid.to_string()]) .output() .await .map(|output| output.status.success()) @@ -649,12 +649,12 @@ impl CoreManager { async fn is_process_running(&self, pid: u32) -> Result { let output = if cfg!(windows) { tokio::process::Command::new("tasklist") - .args(&["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"]) + .args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"]) .output() .await? } else { tokio::process::Command::new("ps") - .args(&["-p", &pid.to_string()]) + .args(["-p", &pid.to_string()]) .output() .await? }; diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index 28f9a095cc..97cd28531f 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -85,7 +85,7 @@ struct ProxyConfig { guard_enabled: bool, } -static PROXY_MANAGER: Lazy = Lazy::new(|| EventDrivenProxyManager::new()); +static PROXY_MANAGER: Lazy = Lazy::new(EventDrivenProxyManager::new); impl EventDrivenProxyManager { pub fn global() -> &'static EventDrivenProxyManager { From 3d8b2cf35fcced4ed5a84dae1180b83a243e968e Mon Sep 17 00:00:00 2001 From: Tunglies Date: Sun, 22 Jun 2025 01:16:57 +0800 Subject: [PATCH 03/15] fix: improve error handling for application handle retrieval (#3860) * fix: improve error handling for application handle retrieval * fix: correct argument passing for command execution and improve URL stripping logic --- src-tauri/src/core/async_proxy_query.rs | 24 +++++++++++++----------- src-tauri/src/core/event_driven_proxy.rs | 6 +++--- src-tauri/src/core/sysopt.rs | 1 + 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs index a82790448e..b26a4951eb 100644 --- a/src-tauri/src/core/async_proxy_query.rs +++ b/src-tauri/src/core/async_proxy_query.rs @@ -1,3 +1,5 @@ +#[cfg(target_os = "linux")] +use anyhow::anyhow; use anyhow::Result; use serde::{Deserialize, Serialize}; use tokio::process::Command; @@ -71,7 +73,7 @@ impl AsyncProxyQuery { async fn get_auto_proxy_impl() -> Result { // Windows: 使用 netsh winhttp show proxy 命令 let output = Command::new("netsh") - .args(&["winhttp", "show", "proxy"]) + .args(["winhttp", "show", "proxy"]) .output() .await?; @@ -172,7 +174,7 @@ impl AsyncProxyQuery { // 尝试使用 gsettings 获取 GNOME 代理设置 let output = Command::new("gsettings") - .args(&["get", "org.gnome.system.proxy", "mode"]) + .args(["get", "org.gnome.system.proxy", "mode"]) .output() .await; @@ -182,7 +184,7 @@ impl AsyncProxyQuery { if mode.contains("auto") { // 获取 PAC URL let pac_output = Command::new("gsettings") - .args(&["get", "org.gnome.system.proxy", "autoconfig-url"]) + .args(["get", "org.gnome.system.proxy", "autoconfig-url"]) .output() .await; @@ -212,7 +214,7 @@ impl AsyncProxyQuery { #[cfg(target_os = "windows")] async fn get_system_proxy_impl() -> Result { let output = Command::new("netsh") - .args(&["winhttp", "show", "proxy"]) + .args(["winhttp", "show", "proxy"]) .output() .await?; @@ -333,7 +335,7 @@ impl AsyncProxyQuery { // 尝试使用 gsettings 获取 GNOME 代理设置 let mode_output = Command::new("gsettings") - .args(&["get", "org.gnome.system.proxy", "mode"]) + .args(["get", "org.gnome.system.proxy", "mode"]) .output() .await; @@ -345,12 +347,12 @@ impl AsyncProxyQuery { if mode.contains("manual") { // 获取HTTP代理设置 let host_result = Command::new("gsettings") - .args(&["get", "org.gnome.system.proxy.http", "host"]) + .args(["get", "org.gnome.system.proxy.http", "host"]) .output() .await; let port_result = Command::new("gsettings") - .args(&["get", "org.gnome.system.proxy.http", "port"]) + .args(["get", "org.gnome.system.proxy.http", "port"]) .output() .await; @@ -390,10 +392,10 @@ impl AsyncProxyQuery { let url = proxy_url.trim(); // 移除协议前缀 - let url = if url.starts_with("http://") { - &url[7..] - } else if url.starts_with("https://") { - &url[8..] + let url = if let Some(stripped) = url.strip_prefix("http://") { + stripped + } else if let Some(stripped) = url.strip_prefix("https://") { + stripped } else { url }; diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index 97cd28531f..6813dd1e06 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -530,9 +530,9 @@ impl EventDrivenProxyManager { use tauri_plugin_shell::ShellExt; let app_handle = match Handle::global().app_handle() { - Ok(handle) => handle, - Err(e) => { - log::error!(target: "app", "获取应用句柄失败: {}", e); + Some(handle) => handle, + None => { + log::error!(target: "app", "获取应用句柄失败"); return; } }; diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 71cca516e7..4cca93f366 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -9,6 +9,7 @@ use crate::{ use anyhow::Result; use once_cell::sync::OnceCell; use std::sync::Arc; +#[cfg(not(target_os = "windows"))] use sysproxy::{Autoproxy, Sysproxy}; use tauri::async_runtime::Mutex as TokioMutex; use tauri_plugin_autostart::ManagerExt; From 4b860ba897b80c619e74ed926a300e6d59627399 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:24:25 +0800 Subject: [PATCH 04/15] chore(deps): update npm dependencies (#3831) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 12 ++--- pnpm-lock.yaml | 136 ++++++++++++++++++++++++------------------------- 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index 6106f1395d..715a6fc213 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,10 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@juggle/resize-observer": "^3.4.0", - "@mui/icons-material": "^7.1.1", - "@mui/lab": "7.0.0-beta.13", - "@mui/material": "^7.1.1", - "@mui/x-data-grid": "^8.5.2", + "@mui/icons-material": "^7.1.2", + "@mui/lab": "7.0.0-beta.14", + "@mui/material": "^7.1.2", + "@mui/x-data-grid": "^8.5.3", "@tauri-apps/api": "2.5.0", "@tauri-apps/plugin-clipboard-manager": "^2.2.3", "@tauri-apps/plugin-dialog": "^2.2.2", @@ -62,7 +62,7 @@ "monaco-editor": "^0.52.2", "monaco-yaml": "^5.4.0", "nanoid": "^5.1.5", - "peggy": "^5.0.3", + "peggy": "^5.0.4", "react": "19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "19.1.0", @@ -99,7 +99,7 @@ "prettier": "^3.5.3", "pretty-quick": "^4.2.2", "sass": "^1.89.2", - "terser": "^5.43.0", + "terser": "^5.43.1", "typescript": "^5.8.3", "vite": "^6.3.5", "vite-plugin-monaco-editor": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 078f7d22b8..026505cbde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,17 +27,17 @@ importers: specifier: ^3.4.0 version: 3.4.0 '@mui/icons-material': - specifier: ^7.1.1 - version: 7.1.1(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + specifier: ^7.1.2 + version: 7.1.2(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/lab': - specifier: 7.0.0-beta.13 - version: 7.0.0-beta.13(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 7.0.0-beta.14 + version: 7.0.0-beta.14(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/material': - specifier: ^7.1.1 - version: 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^7.1.2 + version: 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/x-data-grid': - specifier: ^8.5.2 - version: 8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: ^8.5.3 + version: 8.5.3(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@tauri-apps/api': specifier: 2.5.0 version: 2.5.0 @@ -120,8 +120,8 @@ importers: specifier: ^5.1.5 version: 5.1.5 peggy: - specifier: ^5.0.3 - version: 5.0.3 + specifier: ^5.0.4 + version: 5.0.4 react: specifier: 19.1.0 version: 19.1.0 @@ -191,10 +191,10 @@ importers: version: 19.1.6(@types/react@19.1.8) '@vitejs/plugin-legacy': specifier: ^6.1.1 - version: 6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) + version: 6.1.1(terser@5.43.1)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) '@vitejs/plugin-react': specifier: 4.5.2 - version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) + version: 4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) adm-zip: specifier: ^0.5.16 version: 0.5.16 @@ -226,20 +226,20 @@ importers: specifier: ^1.89.2 version: 1.89.2 terser: - specifier: ^5.43.0 - version: 5.43.0 + specifier: ^5.43.1 + version: 5.43.1 typescript: specifier: ^5.8.3 version: 5.8.3 vite: specifier: ^6.3.5 - version: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + version: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) vite-plugin-monaco-editor: specifier: ^1.1.0 version: 1.1.0(monaco-editor@0.52.2) vite-plugin-svgr: specifier: ^4.3.0 - version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)) + version: 4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)) packages: @@ -1023,27 +1023,27 @@ packages: '@kurkle/color@0.3.4': resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@mui/core-downloads-tracker@7.1.1': - resolution: {integrity: sha512-yBckQs4aQ8mqukLnPC6ivIRv6guhaXi8snVl00VtyojBbm+l6VbVhyTSZ68Abcx7Ah8B+GZhrB7BOli+e+9LkQ==} + '@mui/core-downloads-tracker@7.1.2': + resolution: {integrity: sha512-0gLO1PvbJwSYe5ji021tGj6HFqrtEPMGKK4L1zWwRbhzrWWUumUJvMvJUsIgWQIYQsgOnhq9k2Fc1BxLGHDsAg==} - '@mui/icons-material@7.1.1': - resolution: {integrity: sha512-X37+Yc8QpEnl0sYmz+WcLFy2dWgNRzbswDzLPXG7QU1XDVlP5TPp1HXjdmCupOWLL/I9m1fyhcyZl8/HPpp/Cg==} + '@mui/icons-material@7.1.2': + resolution: {integrity: sha512-slqJByDub7Y1UcokrM17BoMBMvn8n7daXFXVoTv0MEH5k3sHjmsH8ql/Mt3s9vQ20cORDr83UZ448TEGcbrXtw==} engines: {node: '>=14.0.0'} peerDependencies: - '@mui/material': ^7.1.1 + '@mui/material': ^7.1.2 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': optional: true - '@mui/lab@7.0.0-beta.13': - resolution: {integrity: sha512-wLSeePenug3+/kek4cFMIF3QZVC2fHt2Z3O3HwOFvakgErmT39WltYsNpWNojCnXUqcIExUp9xNW0Wk+tJShgA==} + '@mui/lab@7.0.0-beta.14': + resolution: {integrity: sha512-pn+ZvylDcBKQOo17oa/PhtIA/UFQFq8RvpN+r/jHrztz/CjMDju2CWBne0txvQ5JIS8uTIGp2/IsTa7II1g5wg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 '@emotion/styled': ^11.3.0 - '@mui/material': ^7.1.1 + '@mui/material': ^7.1.2 '@mui/material-pigment-css': ^7.1.1 '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -1058,8 +1058,8 @@ packages: '@types/react': optional: true - '@mui/material@7.1.1': - resolution: {integrity: sha512-mTpdmdZCaHCGOH3SrYM41+XKvNL0iQfM9KlYgpSjgadXx/fEKhhvOktxm8++Xw6FFeOHoOiV+lzOI8X1rsv71A==} + '@mui/material@7.1.2': + resolution: {integrity: sha512-Z5PYKkA6Kd8vS04zKxJNpwuvt6IoMwqpbidV7RCrRQQKwczIwcNcS8L6GnN4pzFYfEs+N9v6co27DmG07rcnoA==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.5.0 @@ -1135,8 +1135,8 @@ packages: '@types/react': optional: true - '@mui/x-data-grid@8.5.2': - resolution: {integrity: sha512-4KzawLZqRKp3KcGKsTDVz7zkEjACllQD5Zb8ds1QKlA6C3/oIoSU7PsemFLj+RL3rT5aORsLMBl97/egQ5tUhA==} + '@mui/x-data-grid@8.5.3': + resolution: {integrity: sha512-rA+de5yre16KFIGKRBUwb8kYIdn7SPPrZsBy1P3QxisqhC+Wz2AQg/W6WWv71aFHwplmGwsFUjU6d47Fy/wvXg==} engines: {node: '>=14.0.0'} peerDependencies: '@emotion/react': ^11.9.0 @@ -1151,8 +1151,8 @@ packages: '@emotion/styled': optional: true - '@mui/x-internals@8.5.2': - resolution: {integrity: sha512-5YhB2AekK7G8d0YrAjg3WNf0uy3V73JD98WNxJhbIlCraQgl8QOQzr2zNO7MAf/X7mZQtjpjuAsiG3+gI2NVyg==} + '@mui/x-internals@8.5.3': + resolution: {integrity: sha512-ImCg4E3DT3XoDIZO0pNCbB7iw14N+YCFY3J1V28POwCD7P2f3HSIz4jwzM006oYxI6bqeE6LMfpdPRDW6s6dQw==} engines: {node: '>=14.0.0'} peerDependencies: '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 @@ -1288,8 +1288,8 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@peggyjs/from-mem@2.0.0': - resolution: {integrity: sha512-f+pL/s2DiT+2dxwheSoJT0P/KJy/s0klzE+ZqRdXHlkeyFk/DpKtyjLZIiA79kx56g3oEPA8Zu9EzEKzAwuvhw==} + '@peggyjs/from-mem@2.0.1': + resolution: {integrity: sha512-5dAPJsLrb3KQahPb8kUqg9nGS2dKlMC4vCB3dMWoZIRqmPrNbBt6P6jidczFBoz+2EbFXBxXi0o9BUpEPHoD+g==} engines: {node: '>=20'} '@pkgr/core@0.2.7': @@ -2512,8 +2512,8 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - peggy@5.0.3: - resolution: {integrity: sha512-QErYmLjj/ehiNNJRqx2qb36hzkanuascpMqREs2RQqaXhU3cflIRScP/u2BoobIfu/FaeI3GGxNB/vFX/Ar9lg==} + peggy@5.0.4: + resolution: {integrity: sha512-NMRm2w2irCFbiOaejvcDEyn+DMUaGd8s4RT1ztj9Kr/kR367pziIvmjqJ0OFqcAg+LqT5tPGsW96MNT5gUNdUw==} engines: {node: '>=20'} hasBin: true @@ -2717,8 +2717,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} hasBin: true @@ -2746,8 +2746,8 @@ packages: sockette@2.0.6: resolution: {integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==} - source-map-generator@2.0.0: - resolution: {integrity: sha512-4KomB7QsJti7dFBAVF6SXHzuCNQauk4gE2CummcqPzl+eJqXz1CkkiBdVXXW3g8VGh23bxcdEVACOzrxpIqnUg==} + source-map-generator@2.0.1: + resolution: {integrity: sha512-AtEu86XavXC2HD/bQVQoDbovnTRZE/0QMAAHn6RxALAoLOM3a47IG06TpsJK23BnCmjbTYNLwx2vUhgzRYgGMA==} engines: {node: '>=20'} source-map-js@1.2.1: @@ -2815,8 +2815,8 @@ packages: resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} engines: {node: '>=18'} - terser@5.43.0: - resolution: {integrity: sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==} + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} engines: {node: '>=10'} hasBin: true @@ -3943,20 +3943,20 @@ snapshots: '@kurkle/color@0.3.4': {} - '@mui/core-downloads-tracker@7.1.1': {} + '@mui/core-downloads-tracker@7.1.2': {} - '@mui/icons-material@7.1.1(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/icons-material@7.1.2(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react: 19.1.0 optionalDependencies: '@types/react': 19.1.8 - '@mui/lab@7.0.0-beta.13(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/lab@7.0.0-beta.14(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/types': 7.4.3(@types/react@19.1.8) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) @@ -3969,10 +3969,10 @@ snapshots: '@emotion/styled': 11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@types/react': 19.1.8 - '@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/core-downloads-tracker': 7.1.1 + '@mui/core-downloads-tracker': 7.1.2 '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/types': 7.4.3(@types/react@19.1.8) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) @@ -4046,13 +4046,13 @@ snapshots: optionalDependencies: '@types/react': 19.1.8 - '@mui/x-data-grid@8.5.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + '@mui/x-data-grid@8.5.3(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@mui/material@7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 - '@mui/material': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@mui/material': 7.1.2(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) '@mui/utils': 7.1.1(@types/react@19.1.8)(react@19.1.0) - '@mui/x-internals': 8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) + '@mui/x-internals': 8.5.3(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) clsx: 2.1.1 prop-types: 15.8.1 react: 19.1.0 @@ -4064,7 +4064,7 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@mui/x-internals@8.5.2(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': + '@mui/x-internals@8.5.3(@mui/system@7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0)': dependencies: '@babel/runtime': 7.27.6 '@mui/system': 7.1.1(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0))(@types/react@19.1.8)(react@19.1.0) @@ -4193,9 +4193,9 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@peggyjs/from-mem@2.0.0': + '@peggyjs/from-mem@2.0.1': dependencies: - semver: 7.7.1 + semver: 7.7.2 '@pkgr/core@0.2.7': {} @@ -4507,7 +4507,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-legacy@6.1.1(terser@5.43.0)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))': + '@vitejs/plugin-legacy@6.1.1(terser@5.43.1)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1))': dependencies: '@babel/core': 7.27.4 '@babel/preset-env': 7.27.2(@babel/core@7.27.4) @@ -4517,12 +4517,12 @@ snapshots: magic-string: 0.30.17 regenerator-runtime: 0.14.1 systemjs: 6.15.1 - terser: 5.43.0 - vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + terser: 5.43.1 + vite: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1))': + '@vitejs/plugin-react@4.5.2(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1))': dependencies: '@babel/core': 7.27.4 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.27.4) @@ -4530,7 +4530,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.11 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + vite: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) transitivePeerDependencies: - supports-color @@ -5499,11 +5499,11 @@ snapshots: path-type@4.0.0: {} - peggy@5.0.3: + peggy@5.0.4: dependencies: - '@peggyjs/from-mem': 2.0.0 + '@peggyjs/from-mem': 2.0.1 commander: 14.0.0 - source-map-generator: 2.0.0 + source-map-generator: 2.0.1 picocolors@1.1.1: {} @@ -5726,7 +5726,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.1: {} + semver@7.7.2: {} server-only@0.0.1: {} @@ -5747,7 +5747,7 @@ snapshots: sockette@2.0.6: {} - source-map-generator@2.0.0: {} + source-map-generator@2.0.1: {} source-map-js@1.2.1: {} @@ -5818,7 +5818,7 @@ snapshots: mkdirp: 3.0.1 yallist: 5.0.0 - terser@5.43.0: + terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.6 acorn: 8.14.1 @@ -5930,18 +5930,18 @@ snapshots: dependencies: monaco-editor: 0.52.2 - vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1)): + vite-plugin-svgr@4.3.0(rollup@4.40.2)(typescript@5.8.3)(vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1)): dependencies: '@rollup/pluginutils': 5.1.4(rollup@4.40.2) '@svgr/core': 8.1.0(typescript@5.8.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.8.3)) - vite: 6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1) + vite: 6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@6.3.5(sass@1.89.2)(terser@5.43.0)(yaml@2.7.1): + vite@6.3.5(sass@1.89.2)(terser@5.43.1)(yaml@2.7.1): dependencies: esbuild: 0.25.4 fdir: 6.4.4(picomatch@4.0.2) @@ -5952,7 +5952,7 @@ snapshots: optionalDependencies: fsevents: 2.3.3 sass: 1.89.2 - terser: 5.43.0 + terser: 5.43.1 yaml: 2.7.1 void-elements@3.1.0: {} From 9b1c66030663c762cf59ec155bbcd828e95392b1 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 22 Jun 2025 15:31:59 +0800 Subject: [PATCH 05/15] feat: add new monochrome tray icons for system and tunnel --- src-tauri/icons/tray-icon-sys-mono-new.ico | Bin 0 -> 113296 bytes src-tauri/icons/tray-icon-tun-mono-new.ico | Bin 0 -> 109633 bytes src-tauri/src/core/tray/mod.rs | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 src-tauri/icons/tray-icon-sys-mono-new.ico create mode 100644 src-tauri/icons/tray-icon-tun-mono-new.ico diff --git a/src-tauri/icons/tray-icon-sys-mono-new.ico b/src-tauri/icons/tray-icon-sys-mono-new.ico new file mode 100644 index 0000000000000000000000000000000000000000..d28cc7cbccaa6feaf061a522d16f68405cf07223 GIT binary patch literal 113296 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 literal 0 HcmV?d00001 diff --git a/src-tauri/icons/tray-icon-tun-mono-new.ico b/src-tauri/icons/tray-icon-tun-mono-new.ico new file mode 100644 index 0000000000000000000000000000000000000000..d580583abdfe0bbb60e6d7d50de67b7f73b4ecb6 GIT binary patch literal 109633 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 Date: Sun, 22 Jun 2025 15:31:59 +0800 Subject: [PATCH 06/15] feat: add new monochrome tray icons for system and tunnel --- UPDATELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index 2cf97bb57d..c43f559ce8 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -3,11 +3,12 @@ ### 🐞 修复问题 - 修复系统代理端口不同步问题 -- 修复 自定义 `css` 背景图无法生效问题 +- 修复自定义 `css` 背景图无法生效问题 ### ✨ 新增功能 - `sidecar` 模式下清理多余的内核进程,防止运行出现异常 +- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测) ### 🚀 优化改进 From a92872c8317974350007880927fdbb37b7da35bf Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 22 Jun 2025 15:42:01 +0800 Subject: [PATCH 07/15] fix: resolve race condition freeze on rapid tray icon clicks in lightweight mode --- UPDATELOG.md | 1 + src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/core/hotkey.rs | 90 +++---------------- src-tauri/src/core/tray/mod.rs | 42 +++++++-- src-tauri/src/feat/window.rs | 37 +++++++- src-tauri/src/module/lightweight.rs | 27 +++++- src-tauri/src/utils/resolve.rs | 10 ++- src-tauri/src/utils/window_manager.rs | 124 +++++++++++++++++++++++--- 9 files changed, 229 insertions(+), 104 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index c43f559ce8..973785cc11 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -4,6 +4,7 @@ - 修复系统代理端口不同步问题 - 修复自定义 `css` 背景图无法生效问题 +- 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题 ### ✨ 新增功能 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 173f7a46f6..659f5dabc6 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1078,6 +1078,7 @@ dependencies = [ "reqwest", "reqwest_dav", "runas", + "scopeguard", "serde", "serde_json", "serde_yaml", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 6908bba03c..843ce20514 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -80,6 +80,7 @@ gethostname = "1.0.2" hmac = "0.12.1" sha2 = "0.10.9" hex = "0.4.3" +scopeguard = "1.2.0" [target.'cfg(windows)'.dependencies] runas = "=1.2.0" diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 070a049bbe..33c8fa965c 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -1,10 +1,6 @@ use crate::{ - config::Config, - core::handle, - feat, logging, logging_error, - module::lightweight::entry_lightweight_mode, - process::AsyncHandler, - utils::{logging::Type, resolve}, + config::Config, core::handle, feat, logging, logging_error, + module::lightweight::entry_lightweight_mode, utils::logging::Type, }; use anyhow::{bail, Result}; use once_cell::sync::OnceCell; @@ -14,7 +10,7 @@ use tauri::Manager; use tauri_plugin_global_shortcut::{Code, GlobalShortcutExt, ShortcutState}; pub struct Hotkey { - current: Arc>>, // 保存当前的热键设置 + current: Arc>>, } impl Hotkey { @@ -38,7 +34,6 @@ impl Hotkey { enable_global_hotkey ); - // 如果全局热键被禁用,则不注册热键 if !enable_global_hotkey { return Ok(()); } @@ -153,76 +148,14 @@ impl Hotkey { "=== Hotkey Dashboard Window Operation Start ===" ); - // 检查是否在轻量模式下,如果是,需要同步处理 - if crate::module::lightweight::is_in_lightweight_mode() { - logging!( - info, - Type::Hotkey, - true, - "In lightweight mode, calling open_or_close_dashboard directly" - ); - crate::feat::open_or_close_dashboard(); - } else { - AsyncHandler::spawn(move || async move { - logging!( - debug, - Type::Hotkey, - true, - "Toggle dashboard window visibility (async)" - ); + logging!( + info, + Type::Hotkey, + true, + "Using unified WindowManager for hotkey operation (bypass debounce)" + ); - // 检查窗口是否存在 - if let Some(window) = handle::Handle::global().get_window() { - // 如果窗口可见,则隐藏 - match window.is_visible() { - Ok(visible) => { - if visible { - logging!( - info, - Type::Window, - true, - "Window is visible, hiding it" - ); - let _ = window.hide(); - } else { - // 如果窗口不可见,则显示 - logging!( - info, - Type::Window, - true, - "Window is hidden, showing it" - ); - if window.is_minimized().unwrap_or(false) { - let _ = window.unminimize(); - } - let _ = window.show(); - let _ = window.set_focus(); - } - } - Err(e) => { - logging!( - warn, - Type::Window, - true, - "Failed to check window visibility: {}", - e - ); - let _ = window.show(); - let _ = window.set_focus(); - } - } - } else { - // 如果窗口不存在,创建一个新窗口 - logging!( - info, - Type::Window, - true, - "Window does not exist, creating a new one" - ); - resolve::create_window(true); - } - }); - } + crate::feat::open_or_close_dashboard_hotkey(); logging!( debug, @@ -261,10 +194,8 @@ impl Hotkey { } } } else { - // 直接执行函数,不做任何状态检查 logging!(debug, Type::Hotkey, "Executing function directly"); - // 获取全局热键状态 let is_enable_global_hotkey = Config::verge() .latest() .enable_global_hotkey @@ -274,7 +205,6 @@ impl Hotkey { f(); } else { use crate::utils::window_manager::WindowManager; - // 非轻量模式且未启用全局热键时,只在窗口可见且有焦点的情况下响应热键 let is_visible = WindowManager::is_main_window_visible(); let is_focused = WindowManager::is_main_window_focused(); diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index de10715e04..98c46004e1 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -39,6 +39,29 @@ use super::handle; #[derive(Clone)] struct TrayState {} +// 托盘点击防抖机制 +static TRAY_CLICK_DEBOUNCE: OnceCell> = OnceCell::new(); +const TRAY_CLICK_DEBOUNCE_MS: u64 = 300; + +fn get_tray_click_debounce() -> &'static Mutex { + TRAY_CLICK_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) +} + +fn should_handle_tray_click() -> bool { + let debounce_lock = get_tray_click_debounce(); + let mut last_click = debounce_lock.lock(); + let now = Instant::now(); + + if now.duration_since(*last_click) >= Duration::from_millis(TRAY_CLICK_DEBOUNCE_MS) { + *last_click = now; + true + } else { + log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms", + now.duration_since(*last_click).as_millis()); + false + } +} + #[cfg(target_os = "macos")] pub struct Tray { pub speed_rate: Arc>>, @@ -664,6 +687,11 @@ impl Tray { .. } = event { + // 添加防抖检查,防止快速连击 + if !should_handle_tray_click() { + return; + } + match tray_event.as_str() { "system_proxy" => feat::toggle_system_proxy(), "tun_mode" => feat::toggle_tun_mode(None), @@ -949,12 +977,15 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { "open_window" => { use crate::utils::window_manager::WindowManager; log::info!(target: "app", "托盘菜单点击: 打开窗口"); - // 如果在轻量模式中,先退出轻量模式 + + if !should_handle_tray_click() { + return; + } + if crate::module::lightweight::is_in_lightweight_mode() { log::info!(target: "app", "当前在轻量模式,正在退出"); crate::module::lightweight::exit_lightweight_mode(); } - // 使用统一的窗口管理器显示窗口 let result = WindowManager::show_main_window(); log::info!(target: "app", "窗口显示结果: {:?}", result); } @@ -977,7 +1008,10 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { "restart_clash" => feat::restart_clash_core(), "restart_app" => feat::restart_app(), "entry_lightweight_mode" => { - // 处理轻量模式的切换 + if !should_handle_tray_click() { + return; + } + let was_lightweight = crate::module::lightweight::is_in_lightweight_mode(); if was_lightweight { crate::module::lightweight::exit_lightweight_mode(); @@ -985,7 +1019,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { crate::module::lightweight::entry_lightweight_mode(); } - // 退出轻量模式后显示主窗口 if was_lightweight { use crate::utils::window_manager::WindowManager; let result = WindowManager::show_main_window(); @@ -1002,7 +1035,6 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { _ => {} } - // 统一调用状态更新 if let Err(e) = Tray::global().update_all_states() { log::warn!(target: "app", "更新托盘状态失败: {}", e); } diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index 4ac7505818..9d43ff4d56 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -11,11 +11,43 @@ use crate::{ /// Open or close the dashboard window #[allow(dead_code)] pub fn open_or_close_dashboard() { + open_or_close_dashboard_internal(false) +} + +/// Open or close the dashboard window (hotkey call, dispatched to main thread) +#[allow(dead_code)] +pub fn open_or_close_dashboard_hotkey() { + open_or_close_dashboard_internal(true) +} + +/// Internal implementation for opening/closing dashboard +fn open_or_close_dashboard_internal(bypass_debounce: bool) { + use crate::process::AsyncHandler; use crate::utils::window_manager::WindowManager; - log::info!(target: "app", "Attempting to open/close dashboard"); + log::info!(target: "app", "Attempting to open/close dashboard (绕过防抖: {})", bypass_debounce); - // 检查是否在轻量模式下 + // 热键调用调度到主线程执行,避免 WebView 创建死锁 + if bypass_debounce { + log::info!(target: "app", "热键调用,调度到主线程执行窗口操作"); + + AsyncHandler::spawn(move || async move { + log::info!(target: "app", "主线程中执行热键窗口操作"); + + if crate::module::lightweight::is_in_lightweight_mode() { + log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); + crate::module::lightweight::exit_lightweight_mode(); + log::info!(target: "app", "Creating new window after exiting lightweight mode"); + let result = WindowManager::show_main_window(); + log::info!(target: "app", "Window operation result: {:?}", result); + return; + } + + let result = WindowManager::toggle_main_window(); + log::info!(target: "app", "Window toggle result: {:?}", result); + }); + return; + } if crate::module::lightweight::is_in_lightweight_mode() { log::info!(target: "app", "Currently in lightweight mode, exiting lightweight mode"); crate::module::lightweight::exit_lightweight_mode(); @@ -25,7 +57,6 @@ pub fn open_or_close_dashboard() { return; } - // 使用统一的窗口管理器切换窗口状态 let result = WindowManager::toggle_main_window(); log::info!(target: "app", "Window toggle result: {:?}", result); } diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index b6ebed7cf5..f93cc170c2 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -13,11 +13,17 @@ use crate::AppHandleManager; use anyhow::{Context, Result}; use delay_timer::prelude::TaskBuilder; -use std::sync::Mutex; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Mutex, +}; use tauri::{Listener, Manager}; const LIGHT_WEIGHT_TASK_UID: &str = "light_weight_task"; +// 添加退出轻量模式的锁,防止并发调用 +static EXITING_LIGHTWEIGHT: AtomicBool = AtomicBool::new(false); + fn with_lightweight_status(f: F) -> R where F: FnOnce(&mut LightWeightState) -> R, @@ -131,6 +137,25 @@ pub fn entry_lightweight_mode() { // 添加从轻量模式恢复的函数 pub fn exit_lightweight_mode() { + // 使用原子操作检查是否已经在退出过程中,防止并发调用 + if EXITING_LIGHTWEIGHT + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_err() + { + logging!( + info, + Type::Lightweight, + true, + "轻量模式退出操作已在进行中,跳过重复调用" + ); + return; + } + + // 使用defer确保无论如何都会重置标志 + let _guard = scopeguard::guard((), |_| { + EXITING_LIGHTWEIGHT.store(false, Ordering::SeqCst); + }); + // 确保当前确实处于轻量模式才执行退出操作 if !is_in_lightweight_mode() { logging!(info, Type::Lightweight, true, "当前不在轻量模式,无需退出"); diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index 8e1c0ca7ac..dada58ec65 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -13,6 +13,7 @@ use anyhow::{bail, Result}; use once_cell::sync::OnceCell; use parking_lot::{Mutex, RwLock}; use percent_encoding::percent_decode_str; +use scopeguard; use serde_yaml::Mapping; use std::{ sync::Arc, @@ -337,6 +338,12 @@ pub fn create_window(is_show: bool) -> bool { *creating = (true, Instant::now()); + // ScopeGuard 确保创建状态重置,防止 webview 卡死 + let _guard = scopeguard::guard(creating, |mut creating_guard| { + *creating_guard = (false, Instant::now()); + logging!(debug, Type::Window, true, "[ScopeGuard] 窗口创建状态已重置"); + }); + match tauri::WebviewWindowBuilder::new( &handle::Handle::global().app_handle().unwrap(), "main", /* the unique window label */ @@ -419,8 +426,6 @@ pub fn create_window(is_show: bool) -> bool { Ok(newly_created_window) => { logging!(debug, Type::Window, true, "主窗口实例创建成功"); - *creating = (false, Instant::now()); - update_ui_ready_stage(UiReadyStage::NotStarted); AsyncHandler::spawn(move || async move { @@ -534,7 +539,6 @@ pub fn create_window(is_show: bool) -> bool { } Err(e) => { logging!(error, Type::Window, true, "主窗口构建失败: {}", e); - *creating = (false, Instant::now()); // Reset the creating state if window creation failed false } } diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index d00656f647..c06cb5190f 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -4,6 +4,14 @@ use tauri::{Manager, WebviewWindow, Wry}; #[cfg(target_os = "macos")] use crate::AppHandleManager; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use scopeguard; +use std::{ + sync::atomic::{AtomicBool, Ordering}, + time::{Duration, Instant}, +}; + /// 窗口操作结果 #[derive(Debug, Clone, Copy, PartialEq)] pub enum WindowOperationResult { @@ -34,6 +42,45 @@ pub enum WindowState { NotExist, } +// 窗口操作防抖机制 +static WINDOW_OPERATION_DEBOUNCE: OnceCell> = OnceCell::new(); +static WINDOW_OPERATION_IN_PROGRESS: AtomicBool = AtomicBool::new(false); +const WINDOW_OPERATION_DEBOUNCE_MS: u64 = 500; + +fn get_window_operation_debounce() -> &'static Mutex { + WINDOW_OPERATION_DEBOUNCE.get_or_init(|| Mutex::new(Instant::now() - Duration::from_secs(1))) +} + +fn should_handle_window_operation() -> bool { + if WINDOW_OPERATION_IN_PROGRESS.load(Ordering::Acquire) { + log::warn!(target: "app", "[防抖] 窗口操作已在进行中,跳过重复调用"); + return false; + } + + let debounce_lock = get_window_operation_debounce(); + let mut last_operation = debounce_lock.lock(); + let now = Instant::now(); + let elapsed = now.duration_since(*last_operation); + + log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", + elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + + if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { + *last_operation = now; + WINDOW_OPERATION_IN_PROGRESS.store(true, Ordering::Release); + log::info!(target: "app", "[防抖] 窗口操作被允许执行"); + true + } else { + log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", + elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); + false + } +} + +fn finish_window_operation() { + WINDOW_OPERATION_IN_PROGRESS.store(false, Ordering::Release); +} + /// 统一的窗口管理器 pub struct WindowManager; @@ -65,6 +112,14 @@ impl WindowManager { /// 智能显示主窗口 pub fn show_main_window() -> WindowOperationResult { + // 防抖检查 + if !should_handle_window_operation() { + return WindowOperationResult::NoAction; + } + let _guard = scopeguard::guard((), |_| { + finish_window_operation(); + }); + logging!(info, Type::Window, true, "开始智能显示主窗口"); logging!( debug, @@ -80,8 +135,11 @@ impl WindowManager { WindowState::NotExist => { logging!(info, Type::Window, true, "窗口不存在,创建新窗口"); if Self::create_new_window() { + logging!(info, Type::Window, true, "窗口创建成功"); + std::thread::sleep(std::time::Duration::from_millis(100)); WindowOperationResult::Created } else { + logging!(warn, Type::Window, true, "窗口创建失败"); WindowOperationResult::Failed } } @@ -91,6 +149,16 @@ impl WindowManager { } WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { if let Some(window) = Self::get_main_window() { + let state_after_check = Self::get_main_window_state(); + if state_after_check == WindowState::VisibleFocused { + logging!( + info, + Type::Window, + true, + "窗口在检查期间已变为可见和有焦点状态" + ); + return WindowOperationResult::NoAction; + } Self::activate_window(&window) } else { WindowOperationResult::Failed @@ -101,6 +169,14 @@ impl WindowManager { /// 切换主窗口显示状态(显示/隐藏) pub fn toggle_main_window() -> WindowOperationResult { + // 防抖检查 + if !should_handle_window_operation() { + return WindowOperationResult::NoAction; + } + let _guard = scopeguard::guard((), |_| { + finish_window_operation(); + }); + logging!(info, Type::Window, true, "开始切换主窗口显示状态"); let current_state = Self::get_main_window_state(); @@ -108,37 +184,61 @@ impl WindowManager { info, Type::Window, true, - "当前窗口状态: {:?}", - current_state + "当前窗口状态: {:?} | 详细状态: {}", + current_state, + Self::get_window_status_info() ); match current_state { WindowState::NotExist => { // 窗口不存在,创建新窗口 + logging!(info, Type::Window, true, "窗口不存在,将创建新窗口"); + // 由于已经有防抖保护,直接调用内部方法 if Self::create_new_window() { WindowOperationResult::Created } else { WindowOperationResult::Failed } } - WindowState::VisibleFocused => { - // 窗口可见且有焦点,隐藏它 - if let Some(window) = Self::get_main_window() { - if window.hide().is_ok() { - logging!(info, Type::Window, true, "窗口已隐藏"); - WindowOperationResult::Hidden + WindowState::VisibleFocused | WindowState::VisibleUnfocused => { + logging!( + info, + Type::Window, + true, + "窗口可见(焦点状态: {}),将隐藏窗口", + if current_state == WindowState::VisibleFocused { + "有焦点" } else { - WindowOperationResult::Failed + "无焦点" + } + ); + if let Some(window) = Self::get_main_window() { + match window.hide() { + Ok(_) => { + logging!(info, Type::Window, true, "窗口已成功隐藏"); + WindowOperationResult::Hidden + } + Err(e) => { + logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e); + WindowOperationResult::Failed + } } } else { + logging!(warn, Type::Window, true, "无法获取窗口实例"); WindowOperationResult::Failed } } - WindowState::VisibleUnfocused | WindowState::Minimized | WindowState::Hidden => { - // 窗口存在但不可见或无焦点,激活它 + WindowState::Minimized | WindowState::Hidden => { + logging!( + info, + Type::Window, + true, + "窗口存在但被隐藏或最小化,将激活窗口" + ); if let Some(window) = Self::get_main_window() { Self::activate_window(&window) } else { + logging!(warn, Type::Window, true, "无法获取窗口实例"); WindowOperationResult::Failed } } @@ -251,7 +351,7 @@ impl WindowManager { .unwrap_or(false) } - /// 创建新窗口现有的实现 + /// 创建新窗口,防抖避免重复调用 fn create_new_window() -> bool { use crate::utils::resolve; resolve::create_window(true) From e7461fccabe35c4014be7bf63b672e0924cd7652 Mon Sep 17 00:00:00 2001 From: Tunglies Date: Sun, 22 Jun 2025 16:28:06 +0800 Subject: [PATCH 08/15] refactor: remove unused macOS tray speed display and improve tray icon handling (#3862) * refactor: remove unused macOS tray speed display and improve tray icon handling * refactor: disable macOS specific logging during core initialization * feat: add linux elevator function to determine privilege escalation command --- UPDATELOG.md | 4 + src-tauri/src/core/core.rs | 7 +- src-tauri/src/core/tray/mod.rs | 216 +---------- src-tauri/src/core/tray/speed_rate.rs | 335 ------------------ src-tauri/src/module/mihomo.rs | 30 +- src-tauri/src/utils/help.rs | 49 --- src/components/setting/mods/layout-viewer.tsx | 4 +- 7 files changed, 14 insertions(+), 631 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index 973785cc11..5472ddc2d9 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -16,6 +16,10 @@ - 优化重构订阅切换逻辑,可以随时中断载入过程,防止卡死 - 引入事件驱动代理管理器,优化代理配置更新逻辑,防止卡死 +### 🗑️ 移除内容 + +- 移除了 macOS tray 图标显示网络速率 + ## v2.3.1 ### 🐞 修复问题 diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index c1d7e2e80b..bc2d54d1aa 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -1,5 +1,3 @@ -#[cfg(target_os = "macos")] -use crate::core::tray::Tray; use crate::{ config::*, core::{ @@ -980,9 +978,8 @@ impl CoreManager { } logging!(trace, Type::Core, "Initied core logic completed"); - #[cfg(target_os = "macos")] - logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); - + // #[cfg(target_os = "macos")] + // logging_error!(Type::Core, true, Tray::global().subscribe_traffic().await); Ok(()) } diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 98c46004e1..0fb4334400 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -12,15 +12,7 @@ use crate::{ }; use anyhow::Result; -#[cfg(target_os = "macos")] -use futures::StreamExt; use parking_lot::Mutex; -#[cfg(target_os = "macos")] -use parking_lot::RwLock; -#[cfg(target_os = "macos")] -pub use speed_rate::{SpeedRate, Traffic}; -#[cfg(target_os = "macos")] -use std::sync::Arc; use std::{ fs, sync::atomic::{AtomicBool, Ordering}, @@ -31,8 +23,6 @@ use tauri::{ tray::{MouseButton, MouseButtonState, TrayIconEvent}, AppHandle, Wry, }; -#[cfg(target_os = "macos")] -use tokio::sync::broadcast; use super::handle; @@ -64,10 +54,6 @@ fn should_handle_tray_click() -> bool { #[cfg(target_os = "macos")] pub struct Tray { - pub speed_rate: Arc>>, - shutdown_tx: Arc>>>, - is_subscribed: Arc>, - pub rate_cache: Arc>>, last_menu_update: Mutex>, menu_updating: AtomicBool, } @@ -187,10 +173,6 @@ impl Tray { #[cfg(target_os = "macos")] return TRAY.get_or_init(|| Tray { - speed_rate: Arc::new(Mutex::new(None)), - shutdown_tx: Arc::new(RwLock::new(None)), - is_subscribed: Arc::new(RwLock::new(false)), - rate_cache: Arc::new(Mutex::new(None)), last_menu_update: Mutex::new(None), menu_updating: AtomicBool::new(false), }); @@ -203,11 +185,6 @@ impl Tray { } pub fn init(&self) -> Result<()> { - #[cfg(target_os = "macos")] - { - let mut speed_rate = self.speed_rate.lock(); - *speed_rate = Some(SpeedRate::new()); - } Ok(()) } @@ -314,7 +291,7 @@ impl Tray { /// 更新托盘图标 #[cfg(target_os = "macos")] - pub fn update_icon(&self, rate: Option) -> Result<()> { + pub fn update_icon(&self, _rate: Option) -> Result<()> { let app_handle = match handle::Handle::global().app_handle() { Some(handle) => handle, None => { @@ -335,55 +312,18 @@ impl Tray { let system_mode = verge.enable_system_proxy.as_ref().unwrap_or(&false); let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false); - let (is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { + let (_is_custom_icon, icon_bytes) = match (*system_mode, *tun_mode) { (true, true) => TrayState::get_tun_tray_icon(), (true, false) => TrayState::get_sysproxy_tray_icon(), (false, true) => TrayState::get_tun_tray_icon(), (false, false) => TrayState::get_common_tray_icon(), }; - let enable_tray_speed = verge.enable_tray_speed.unwrap_or(false); - let enable_tray_icon = verge.enable_tray_icon.unwrap_or(true); let colorful = verge.tray_icon.clone().unwrap_or("monochrome".to_string()); let is_colorful = colorful == "colorful"; - if !enable_tray_speed { - let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); - let _ = tray.set_icon_as_template(!is_colorful); - return Ok(()); - } - - let rate = if let Some(rate) = rate { - Some(rate) - } else { - let guard = self.speed_rate.lock(); - if let Some(guard) = guard.as_ref() { - if let Some(rate) = guard.get_curent_rate() { - Some(rate) - } else { - Some(Rate::default()) - } - } else { - Some(Rate::default()) - } - }; - - let mut rate_guard = self.rate_cache.lock(); - if *rate_guard != rate { - *rate_guard = rate; - - let bytes = if enable_tray_icon { - Some(icon_bytes) - } else { - None - }; - - let rate = rate_guard.as_ref(); - if let Ok(rate_bytes) = SpeedRate::add_speed_text(is_custom_icon, bytes, rate) { - let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&rate_bytes)?)); - let _ = tray.set_icon_as_template(!is_custom_icon && !is_colorful); - } - } + let _ = tray.set_icon(Some(tauri::image::Image::from_bytes(&icon_bytes)?)); + let _ = tray.set_icon_as_template(!is_colorful); Ok(()) } @@ -498,155 +438,9 @@ impl Tray { Ok(()) } - /// 订阅流量数据 - #[cfg(target_os = "macos")] - pub async fn subscribe_traffic(&self) -> Result<()> { - log::info!(target: "app", "subscribe traffic"); - - // 如果已经订阅,先取消订阅 - if *self.is_subscribed.read() { - self.unsubscribe_traffic(); - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - } - - let (shutdown_tx, shutdown_rx) = broadcast::channel(3); - *self.shutdown_tx.write() = Some(shutdown_tx); - *self.is_subscribed.write() = true; - - let speed_rate = Arc::clone(&self.speed_rate); - let is_subscribed = Arc::clone(&self.is_subscribed); - - // 使用单线程防止阻塞主线程 - std::thread::Builder::new() - .name("traffic-monitor".into()) - .spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("Failed to build tokio runtime for traffic monitor"); - // 在单独的运行时中执行异步任务 - rt.block_on(async move { - let mut shutdown = shutdown_rx; - let speed_rate = speed_rate.clone(); - let is_subscribed = is_subscribed.clone(); - let mut consecutive_errors = 0; - let max_consecutive_errors = 5; - - let mut interval = tokio::time::interval(std::time::Duration::from_secs(10)); - - 'outer: loop { - if !*is_subscribed.read() { - log::info!(target: "app", "Traffic subscription has been cancelled"); - break; - } - - match tokio::time::timeout( - std::time::Duration::from_secs(5), - Traffic::get_traffic_stream() - ).await { - Ok(stream_result) => { - match stream_result { - Ok(mut stream) => { - consecutive_errors = 0; - - loop { - tokio::select! { - traffic_result = stream.next() => { - match traffic_result { - Some(Ok(traffic)) => { - if let Ok(Some(rate)) = tokio::time::timeout( - std::time::Duration::from_millis(50), - async { - let guard = speed_rate.try_lock(); - if let Some(guard) = guard { - if let Some(sr) = guard.as_ref() { - sr.update_and_check_changed(traffic.up, traffic.down) - } else { - None - } - } else { - None - } - } - ).await { - let _ = tokio::time::timeout( - std::time::Duration::from_millis(100), - async { let _ = Tray::global().update_icon(Some(rate)); } - ).await; - } - }, - Some(Err(e)) => { - log::error!(target: "app", "Traffic stream error: {}", e); - consecutive_errors += 1; - if consecutive_errors >= max_consecutive_errors { - log::error!(target: "app", "Too many errors, reconnecting traffic stream"); - break; - } - }, - None => { - log::info!(target: "app", "Traffic stream ended, reconnecting"); - break; - } - } - }, - _ = shutdown.recv() => { - log::info!(target: "app", "Received shutdown signal for traffic stream"); - break 'outer; - }, - _ = interval.tick() => { - if !*is_subscribed.read() { - log::info!(target: "app", "Traffic monitor detected subscription cancelled"); - break 'outer; - } - log::debug!(target: "app", "Traffic subscription periodic health check"); - }, - _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => { - log::info!(target: "app", "Traffic stream max active time reached, reconnecting"); - break; - } - } - } - }, - Err(e) => { - log::error!(target: "app", "Failed to get traffic stream: {}", e); - consecutive_errors += 1; - if consecutive_errors >= max_consecutive_errors { - log::error!(target: "app", "Too many consecutive errors, pausing traffic monitoring"); - tokio::time::sleep(std::time::Duration::from_secs(30)).await; - consecutive_errors = 0; - } else { - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - } - } - } - }, - Err(_) => { - log::error!(target: "app", "Traffic stream initialization timed out"); - tokio::time::sleep(std::time::Duration::from_secs(2)).await; - } - } - - if !*is_subscribed.read() { - break; - } - } - log::info!(target: "app", "Traffic subscription thread terminated"); - }); - }) - .expect("Failed to spawn traffic monitor thread"); - - Ok(()) - } - /// 取消订阅 traffic 数据 #[cfg(target_os = "macos")] - pub fn unsubscribe_traffic(&self) { - log::info!(target: "app", "unsubscribe traffic"); - *self.is_subscribed.write() = false; - if let Some(tx) = self.shutdown_tx.write().take() { - drop(tx); - } - } + pub fn unsubscribe_traffic(&self) {} pub fn create_tray_from_handle(&self, app_handle: &AppHandle) -> Result<()> { log::info!(target: "app", "正在从AppHandle创建系统托盘"); diff --git a/src-tauri/src/core/tray/speed_rate.rs b/src-tauri/src/core/tray/speed_rate.rs index 06cbf61014..8b13789179 100644 --- a/src-tauri/src/core/tray/speed_rate.rs +++ b/src-tauri/src/core/tray/speed_rate.rs @@ -1,336 +1 @@ -use crate::{ - module::mihomo::{MihomoManager, Rate}, - utils::help::format_bytes_speed, -}; -use ab_glyph::FontArc; -use anyhow::Result; -use futures::Stream; -use image::{GenericImageView, Rgba, RgbaImage}; -use imageproc::drawing::draw_text_mut; -use parking_lot::Mutex; -use std::{io::Cursor, sync::Arc}; -use tokio_tungstenite::tungstenite::http; -use tungstenite::client::IntoClientRequest; -#[derive(Debug, Clone)] -pub struct SpeedRate { - rate: Arc>, - last_update: Arc>, -} -impl SpeedRate { - pub fn new() -> Self { - Self { - rate: Arc::new(Mutex::new((Rate::default(), Rate::default()))), - last_update: Arc::new(Mutex::new(std::time::Instant::now())), - } - } - - pub fn update_and_check_changed(&self, up: u64, down: u64) -> Option { - let mut rates = self.rate.lock(); - let mut last_update = self.last_update.lock(); - let now = std::time::Instant::now(); - - // 限制更新频率为每秒最多2次(500ms) - if now.duration_since(*last_update).as_millis() < 500 { - return None; - } - - let (current, previous) = &mut *rates; - - // Avoid unnecessary float conversions for small value checks - let should_update = if current.up < 1000 && down < 1000 { - // For small values, always update to ensure accuracy - current.up != up || current.down != down - } else { - // For larger values, use integer math to check for >5% change - // Multiply by 20 instead of dividing by 0.05 to avoid floating point - let up_threshold = current.up / 20; - let down_threshold = current.down / 20; - - (up > current.up && up - current.up > up_threshold) - || (up < current.up && current.up - up > up_threshold) - || (down > current.down && down - current.down > down_threshold) - || (down < current.down && current.down - down > down_threshold) - }; - - if !should_update { - return None; - } - - *previous = current.clone(); - current.up = up; - current.down = down; - *last_update = now; - - if previous != current { - Some(current.clone()) - } else { - None - } - } - - pub fn get_curent_rate(&self) -> Option { - let rates = self.rate.lock(); - let (current, _) = &*rates; - Some(current.clone()) - } - - // 分离图标加载和速率渲染 - pub fn add_speed_text( - is_custom_icon: bool, - icon_bytes: Option>, - rate: Option<&Rate>, - ) -> Result> { - let rate = rate.unwrap_or(&Rate { up: 0, down: 0 }); - - let (mut icon_width, mut icon_height) = (0, 256); - let icon_image = if let Some(bytes) = icon_bytes.clone() { - let icon_image = image::load_from_memory(&bytes)?; - icon_width = icon_image.width(); - icon_height = icon_image.height(); - icon_image - } else { - // 返回一个空的 RGBA 图像 - image::DynamicImage::new_rgba8(0, 0) - }; - - let total_width = match (is_custom_icon, icon_bytes.is_some()) { - (true, true) => 510, - (true, false) => 740, - (false, false) => 740, - (false, true) => icon_width + 740, - }; - - // println!( - // "icon_height: {}, icon_wight: {}, total_width: {}", - // icon_height, icon_width, total_width - // ); - - // 创建新的透明画布 - let mut combined_image = RgbaImage::new(total_width, icon_height); - - // 将原始图标绘制到新画布的左侧 - if icon_bytes.is_some() { - for y in 0..icon_height { - for x in 0..icon_width { - let pixel = icon_image.get_pixel(x, y); - combined_image.put_pixel(x, y, pixel); - } - } - } - - let is_colorful = if let Some(bytes) = icon_bytes.clone() { - !crate::utils::help::is_monochrome_image_from_bytes(&bytes).unwrap_or(false) - } else { - false - }; - - // 选择文本颜色 - let (text_color, shadow_color) = if is_colorful { - ( - Rgba([144u8, 144u8, 144u8, 255u8]), - // Rgba([255u8, 255u8, 255u8, 128u8]), - Rgba([0u8, 0u8, 0u8, 128u8]), - ) - // ( - // Rgba([160u8, 160u8, 160u8, 255u8]), - // // Rgba([255u8, 255u8, 255u8, 128u8]), - // Rgba([0u8, 0u8, 0u8, 255u8]), - // ) - } else { - ( - Rgba([255u8, 255u8, 255u8, 255u8]), - Rgba([0u8, 0u8, 0u8, 128u8]), - ) - }; - // 减小字体大小以适应文本区域 - let font_data = include_bytes!("../../../assets/fonts/SF-Pro.ttf"); - let font = FontArc::try_from_vec(font_data.to_vec()).unwrap(); - let font_size = icon_height as f32 * 0.6; // 稍微减小字体 - let scale = ab_glyph::PxScale::from(font_size); - - // 使用更简洁的速率格式 - let up_text = format!("↑ {}", format_bytes_speed(rate.up)); - let down_text = format!("↓ {}", format_bytes_speed(rate.down)); - - // For test rate display - // let down_text = format!("↓ {}", format_bytes_speed(102 * 1020 * 1024)); - - // 计算文本位置,确保垂直间距合适 - // 修改文本位置为居右显示 - // 计算右对齐的文本位置 - // let up_text_width = imageproc::drawing::text_size(scale, &font, &up_text).0 as u32; - // let down_text_width = imageproc::drawing::text_size(scale, &font, &down_text).0 as u32; - // let up_text_x = total_width - up_text_width; - // let down_text_x = total_width - down_text_width; - - // 计算左对齐的文本位置 - let (up_text_x, down_text_x) = { - if is_custom_icon || icon_bytes.is_some() { - let text_left_offset = 30; - let left_begin = icon_width + text_left_offset; - (left_begin, left_begin) - } else { - (icon_width, icon_width) - } - }; - - // 优化垂直位置,使速率显示的高度和上下间距正好等于图标大小 - let text_height = font_size as i32; - let total_text_height = text_height * 2; - let up_y = (icon_height as i32 - total_text_height) / 2; - let down_y = up_y + text_height; - - // 绘制速率文本(先阴影后文字) - let shadow_offset = 1; - - // 绘制上行速率 - draw_text_mut( - &mut combined_image, - shadow_color, - up_text_x as i32 + shadow_offset, - up_y + shadow_offset, - scale, - &font, - &up_text, - ); - draw_text_mut( - &mut combined_image, - text_color, - up_text_x as i32, - up_y, - scale, - &font, - &up_text, - ); - - // 绘制下行速率 - draw_text_mut( - &mut combined_image, - shadow_color, - down_text_x as i32 + shadow_offset, - down_y + shadow_offset, - scale, - &font, - &down_text, - ); - draw_text_mut( - &mut combined_image, - text_color, - down_text_x as i32, - down_y, - scale, - &font, - &down_text, - ); - - // 将结果转换为 PNG 数据 - let mut bytes = Vec::new(); - combined_image.write_to(&mut Cursor::new(&mut bytes), image::ImageFormat::Png)?; - Ok(bytes) - } -} - -#[derive(Debug, Clone)] -pub struct Traffic { - pub up: u64, - pub down: u64, -} - -impl Traffic { - pub async fn get_traffic_stream() -> Result>> - { - use futures::{ - future::FutureExt, - stream::{self, StreamExt}, - }; - use std::time::Duration; - - // 先处理错误和超时情况 - let stream = Box::pin( - stream::unfold((), move |_| async move { - 'retry: loop { - log::info!(target: "app", "establishing traffic websocket connection"); - let (url, token) = MihomoManager::get_traffic_ws_url(); - let mut request = match url.into_client_request() { - Ok(req) => req, - Err(e) => { - log::error!(target: "app", "failed to create websocket request: {}", e); - tokio::time::sleep(Duration::from_secs(2)).await; - continue 'retry; - } - }; - - request.headers_mut().insert(http::header::AUTHORIZATION, token); - - match tokio::time::timeout(Duration::from_secs(3), - tokio_tungstenite::connect_async(request) - ).await { - Ok(Ok((ws_stream, _))) => { - log::info!(target: "app", "traffic websocket connection established"); - // 设置流超时控制 - let traffic_stream = ws_stream - .take_while(|msg| { - let continue_stream = msg.is_ok(); - async move { continue_stream }.boxed() - }) - .filter_map(|msg| async move { - match msg { - Ok(msg) => { - if !msg.is_text() { - return None; - } - - match tokio::time::timeout( - Duration::from_millis(200), - async { msg.into_text() } - ).await { - Ok(Ok(text)) => { - match serde_json::from_str::(&text) { - Ok(json) => { - let up = json["up"].as_u64().unwrap_or(0); - let down = json["down"].as_u64().unwrap_or(0); - Some(Ok(Traffic { up, down })) - }, - Err(e) => { - log::warn!(target: "app", "traffic json parse error: {} for {}", e, text); - None - } - } - }, - Ok(Err(e)) => { - log::warn!(target: "app", "traffic text conversion error: {}", e); - None - }, - Err(_) => { - log::warn!(target: "app", "traffic text processing timeout"); - None - } - } - }, - Err(e) => { - log::error!(target: "app", "traffic websocket error: {}", e); - None - } - } - }); - - return Some((traffic_stream, ())); - }, - Ok(Err(e)) => { - log::error!(target: "app", "traffic websocket connection failed: {}", e); - }, - Err(_) => { - log::error!(target: "app", "traffic websocket connection timed out"); - } - } - - tokio::time::sleep(Duration::from_secs(2)).await; - } - }) - .flatten(), - ); - - Ok(stream) - } -} diff --git a/src-tauri/src/module/mihomo.rs b/src-tauri/src/module/mihomo.rs index 0fc40d31f3..37eb57a752 100644 --- a/src-tauri/src/module/mihomo.rs +++ b/src-tauri/src/module/mihomo.rs @@ -4,8 +4,6 @@ use once_cell::sync::Lazy; use parking_lot::{Mutex, RwLock}; use std::time::{Duration, Instant}; use tauri::http::HeaderMap; -#[cfg(target_os = "macos")] -use tauri::http::HeaderValue; // 缓存的最大有效期(5秒) const CACHE_TTL: Duration = Duration::from_secs(5); @@ -106,31 +104,5 @@ impl MihomoManager { Some((server, headers)) } - // 提供默认值的版本,避免在connection_info为None时panic - #[cfg(target_os = "macos")] - fn get_clash_client_info_or_default() -> (String, HeaderMap) { - Self::get_clash_client_info().unwrap_or_else(|| { - let mut headers = HeaderMap::new(); - headers.insert("Content-Type", "application/json".parse().unwrap()); - ("http://127.0.0.1:9090".to_string(), headers) - }) - } - - #[cfg(target_os = "macos")] - pub fn get_traffic_ws_url() -> (String, HeaderValue) { - let (url, headers) = MihomoManager::get_clash_client_info_or_default(); - let ws_url = url.replace("http://", "ws://") + "/traffic"; - let auth = headers - .get("Authorization") - .map(|val| val.to_str().unwrap_or("").to_string()) - .unwrap_or_default(); - - // 创建默认的空HeaderValue而不是使用unwrap_or_default - let token = match HeaderValue::from_str(&auth) { - Ok(v) => v, - Err(_) => HeaderValue::from_static(""), - }; - - (ws_url, token) - } + // 已移除未使用的 get_clash_client_info_or_default 和 get_traffic_ws_url 方法 } diff --git a/src-tauri/src/utils/help.rs b/src-tauri/src/utils/help.rs index 0df0573bee..4c30b0f1a8 100644 --- a/src-tauri/src/utils/help.rs +++ b/src-tauri/src/utils/help.rs @@ -125,19 +125,6 @@ pub fn open_file(_: tauri::AppHandle, path: PathBuf) -> Result<()> { Ok(()) } -#[cfg(target_os = "macos")] -pub fn is_monochrome_image_from_bytes(data: &[u8]) -> anyhow::Result { - let img = image::load_from_memory(data)?; - let rgb_img = img.to_rgb8(); - - for pixel in rgb_img.pixels() { - if pixel[0] != pixel[1] || pixel[1] != pixel[2] { - return Ok(false); - } - } - Ok(true) -} - #[cfg(target_os = "linux")] pub fn linux_elevator() -> String { use std::process::Command; @@ -176,39 +163,3 @@ macro_rules! t { } }; } - -/// 将字节数转换为可读的流量字符串 -/// 支持 B/s、KB/s、MB/s、GB/s 的自动转换 -/// -/// # Examples -/// ```not_run -/// format_bytes_speed(1000) // returns "1000B/s" -/// format_bytes_speed(1024) // returns "1.0KB/s" -/// format_bytes_speed(1024 * 1024) // returns "1.0MB/s" -/// ``` -/// ``` -#[cfg(target_os = "macos")] -pub fn format_bytes_speed(speed: u64) -> String { - const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"]; - let mut size = speed as f64; - let mut unit_index = 0; - - while size >= 1000.0 && unit_index < UNITS.len() - 1 { - size /= 1024.0; - unit_index += 1; - } - - format!("{:.1}{}/s", size, UNITS[unit_index]) -} - -#[cfg(target_os = "macos")] -#[test] -fn test_format_bytes_speed() { - assert_eq!(format_bytes_speed(0), "0.0B/s"); - assert_eq!(format_bytes_speed(1023), "1.0KB/s"); - assert_eq!(format_bytes_speed(1024), "1.0KB/s"); - assert_eq!(format_bytes_speed(1024 * 1024), "1.0MB/s"); - assert_eq!(format_bytes_speed(1024 * 1024 * 1024), "1.0GB/s"); - assert_eq!(format_bytes_speed(1024 * 500), "500.0KB/s"); - assert_eq!(format_bytes_speed(1024 * 1024 * 2), "2.0MB/s"); -} diff --git a/src/components/setting/mods/layout-viewer.tsx b/src/components/setting/mods/layout-viewer.tsx index 55425e459c..2b498b76ee 100644 --- a/src/components/setting/mods/layout-viewer.tsx +++ b/src/components/setting/mods/layout-viewer.tsx @@ -209,7 +209,7 @@ export const LayoutViewer = forwardRef((props, ref) => { )} - {OS === "macos" && ( + {/* {OS === "macos" && ( ((props, ref) => { - )} + )} */} {OS === "macos" && ( From f6b5524e0e25d2c6217b5d01305e396a5d537a3f Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 22 Jun 2025 18:45:38 +0800 Subject: [PATCH 09/15] refactor: replace shell command check with WinAPI call --- src-tauri/Cargo.toml | 2 + src-tauri/src/core/core.rs | 220 +++++++++++++++++++++++-------------- 2 files changed, 138 insertions(+), 84 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 843ce20514..7b04a3b4e1 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -94,6 +94,8 @@ winapi = { version = "0.3.9", features = [ "errhandlingapi", "minwindef", "winerror", + "tlhelp32", + "processthreadsapi", ] } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index bc2d54d1aa..4c4589823d 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -514,70 +514,102 @@ impl CoreManager { Ok(()) } - /// 根据进程名查找进程PID列表 + /// 根据进程名查找进程PID列 async fn find_processes_by_name( &self, process_name: String, _target: &str, ) -> Result<(Vec, String)> { - let output = if cfg!(windows) { - tokio::process::Command::new("tasklist") - .args([ - "/FI", - &format!("IMAGENAME eq {}", process_name), - "/FO", - "CSV", - "/NH", - ]) - .output() - .await? - } else if cfg!(target_os = "macos") { - tokio::process::Command::new("pgrep") - .arg(&process_name) - .output() - .await? - } else { - // Linux - tokio::process::Command::new("pidof") - .arg(&process_name) - .output() - .await? - }; + #[cfg(windows)] + { + use std::mem; + use winapi::um::handleapi::CloseHandle; + use winapi::um::tlhelp32::{ + CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W, + TH32CS_SNAPPROCESS, + }; + use winapi::um::winnt::HANDLE; - if !output.status.success() { - return Ok((Vec::new(), process_name)); - } + let process_name_clone = process_name.clone(); + let pids = tokio::task::spawn_blocking(move || -> Result> { + let mut pids = Vec::new(); - let stdout = String::from_utf8_lossy(&output.stdout); - let mut pids = Vec::new(); - - if cfg!(windows) { - // 解析CSV格式输出: "进程名","PID","会话名","会话#","内存使用" - for line in stdout.lines() { - if !line.is_empty() && line.contains(&process_name) { - let fields: Vec<&str> = line.split(',').collect(); - if fields.len() >= 2 { - // 移除引号并解析PID - let pid_str = fields[1].trim_matches('"'); - if let Ok(pid) = pid_str.parse::() { - pids.push(pid); + unsafe { + // 创建进程快照 + let snapshot: HANDLE = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if snapshot == winapi::um::handleapi::INVALID_HANDLE_VALUE { + return Err(anyhow::anyhow!("Failed to create process snapshot")); + } + + let mut pe32: PROCESSENTRY32W = mem::zeroed(); + pe32.dwSize = mem::size_of::() as u32; + + // 获取第一个进程 + if Process32FirstW(snapshot, &mut pe32) != 0 { + loop { + // 将宽字符转换为String + let end_pos = pe32 + .szExeFile + .iter() + .position(|&x| x == 0) + .unwrap_or(pe32.szExeFile.len()); + let exe_file = String::from_utf16_lossy(&pe32.szExeFile[..end_pos]); + + // 检查进程名是否匹配 + if exe_file.eq_ignore_ascii_case(&process_name_clone) { + pids.push(pe32.th32ProcessID); + } + if Process32NextW(snapshot, &mut pe32) == 0 { + break; + } } } + + // 关闭句柄 + CloseHandle(snapshot); } + + Ok(pids) + }) + .await??; + + Ok((pids, process_name)) + } + + #[cfg(not(windows))] + { + let output = if cfg!(target_os = "macos") { + tokio::process::Command::new("pgrep") + .arg(&process_name) + .output() + .await? + } else { + // Linux + tokio::process::Command::new("pidof") + .arg(&process_name) + .output() + .await? + }; + + if !output.status.success() { + return Ok((Vec::new(), process_name)); } - } else { + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut pids = Vec::new(); + // Unix系统直接解析PID列表 for pid_str in stdout.split_whitespace() { if let Ok(pid) = pid_str.parse::() { pids.push(pid); } } - } - Ok((pids, process_name)) + Ok((pids, process_name)) + } } - /// 终止进程并验证结果 + /// 终止进程并验证结果 - 使用Windows API直接终止,更优雅高效 async fn kill_process_with_verification(&self, pid: u32, process_name: String) -> bool { logging!( info, @@ -588,14 +620,30 @@ impl CoreManager { pid ); - let success = if cfg!(windows) { - tokio::process::Command::new("taskkill") - .args(["/F", "/PID", &pid.to_string()]) - .output() - .await - .map(|output| output.status.success()) - .unwrap_or(false) - } else { + #[cfg(windows)] + let success = { + use winapi::um::handleapi::CloseHandle; + use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess}; + use winapi::um::winnt::{HANDLE, PROCESS_TERMINATE}; + + tokio::task::spawn_blocking(move || -> bool { + unsafe { + let process_handle: HANDLE = OpenProcess(PROCESS_TERMINATE, 0, pid); + if process_handle.is_null() { + return false; + } + let result = TerminateProcess(process_handle, 1); + CloseHandle(process_handle); + + result != 0 + } + }) + .await + .unwrap_or(false) + }; + + #[cfg(not(windows))] + let success = { tokio::process::Command::new("kill") .args(["-9", &pid.to_string()]) .output() @@ -643,34 +691,49 @@ impl CoreManager { } } - /// 检查进程是否仍在运行 + /// Windows API检查进程 async fn is_process_running(&self, pid: u32) -> Result { - let output = if cfg!(windows) { - tokio::process::Command::new("tasklist") - .args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"]) - .output() - .await? - } else { - tokio::process::Command::new("ps") + #[cfg(windows)] + { + use winapi::shared::minwindef::DWORD; + use winapi::um::handleapi::CloseHandle; + use winapi::um::processthreadsapi::GetExitCodeProcess; + use winapi::um::processthreadsapi::OpenProcess; + use winapi::um::winnt::{HANDLE, PROCESS_QUERY_INFORMATION}; + + let result = tokio::task::spawn_blocking(move || -> Result { + unsafe { + let process_handle: HANDLE = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid); + if process_handle.is_null() { + return Ok(false); + } + let mut exit_code: DWORD = 0; + let result = GetExitCodeProcess(process_handle, &mut exit_code); + CloseHandle(process_handle); + + if result == 0 { + return Ok(false); + } + Ok(exit_code == 259) + } + }) + .await?; + + result + } + + #[cfg(not(windows))] + { + let output = tokio::process::Command::new("ps") .args(["-p", &pid.to_string()]) .output() - .await? - }; + .await?; - Ok(output.status.success() && !output.stdout.is_empty()) + Ok(output.status.success() && !output.stdout.is_empty()) + } } async fn start_core_by_sidecar(&self) -> Result<()> { - if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { - logging!( - warn, - Type::Core, - true, - "清理多余 mihomo 进程时发生错误: {}", - e - ); - } - logging!(trace, Type::Core, true, "Running core by sidecar"); let config_file = &Config::generate_file(ConfigType::Run)?; let app_handle = handle::Handle::global() @@ -1033,17 +1096,6 @@ impl CoreManager { pub async fn restart_core(&self) -> Result<()> { self.stop_core().await?; - // 在重启时也清理多余的 mihomo 进程 - if let Err(e) = self.cleanup_orphaned_mihomo_processes().await { - logging!( - warn, - Type::Core, - true, - "重启时清理多余 mihomo 进程失败: {}", - e - ); - } - self.start_core().await?; Ok(()) } From bdfc383a1858f61f30bec588c9c04bb3940dc15d Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 22 Jun 2025 21:05:50 +0800 Subject: [PATCH 10/15] refactor: enhance Windows proxy retrieval by using WinAPI for registry access --- src-tauri/Cargo.toml | 2 + src-tauri/src/core/async_proxy_query.rs | 288 ++++++++++++++++------- src-tauri/src/core/event_driven_proxy.rs | 25 +- src-tauri/src/utils/autostart.rs | 4 +- 4 files changed, 215 insertions(+), 104 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7b04a3b4e1..cd95b11b81 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -96,6 +96,8 @@ winapi = { version = "0.3.9", features = [ "winerror", "tlhelp32", "processthreadsapi", + "winhttp", + "winreg", ] } [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src-tauri/src/core/async_proxy_query.rs b/src-tauri/src/core/async_proxy_query.rs index b26a4951eb..5fabaf217d 100644 --- a/src-tauri/src/core/async_proxy_query.rs +++ b/src-tauri/src/core/async_proxy_query.rs @@ -1,10 +1,12 @@ -#[cfg(target_os = "linux")] -use anyhow::anyhow; use anyhow::Result; use serde::{Deserialize, Serialize}; -use tokio::process::Command; use tokio::time::{timeout, Duration}; +#[cfg(target_os = "linux")] +use anyhow::anyhow; +#[cfg(not(target_os = "windows"))] +use tokio::process::Command; + #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AsyncAutoproxy { pub enable: bool, @@ -71,54 +73,96 @@ impl AsyncProxyQuery { #[cfg(target_os = "windows")] async fn get_auto_proxy_impl() -> Result { - // Windows: 使用 netsh winhttp show proxy 命令 - let output = Command::new("netsh") - .args(["winhttp", "show", "proxy"]) - .output() - .await?; + // Windows: 从注册表读取PAC配置 + tokio::task::spawn_blocking(move || -> Result { + Self::get_pac_config_from_registry() + }) + .await? + } - if !output.status.success() { - return Ok(AsyncAutoproxy::default()); - } + #[cfg(target_os = "windows")] + fn get_pac_config_from_registry() -> Result { + use std::ptr; + use winapi::shared::minwindef::{DWORD, HKEY}; + use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; + use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; + + unsafe { + let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" + .encode_utf16() + .collect::>(); + + let mut hkey: HKEY = ptr::null_mut(); + let result = + RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); + + if result != 0 { + log::debug!(target: "app", "无法打开注册表项"); + return Ok(AsyncAutoproxy::default()); + } - let stdout = String::from_utf8_lossy(&output.stdout); - log::debug!(target: "app", "netsh output: {}", stdout); + // 1. 检查自动配置是否启用 (AutoConfigURL 存在且不为空即表示启用) + let auto_config_url_name = "AutoConfigURL\0".encode_utf16().collect::>(); + let mut url_buffer = vec![0u16; 1024]; + let mut url_buffer_size: DWORD = (url_buffer.len() * 2) as DWORD; + let mut url_value_type: DWORD = 0; + + let url_query_result = RegQueryValueExW( + hkey, + auto_config_url_name.as_ptr(), + ptr::null_mut(), + &mut url_value_type, + url_buffer.as_mut_ptr() as *mut u8, + &mut url_buffer_size, + ); + + let mut pac_url = String::new(); + if url_query_result == 0 && url_value_type == REG_SZ && url_buffer_size > 0 { + let end_pos = url_buffer + .iter() + .position(|&x| x == 0) + .unwrap_or(url_buffer.len()); + pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]); + log::debug!(target: "app", "从注册表读取到PAC URL: {}", pac_url); + } - // 解析输出,查找 PAC 配置 - for line in stdout.lines() { - let line = line.trim(); - if line.starts_with("代理自动配置脚本") || line.starts_with("Proxy auto-config script") - { - // 修复:正确解析包含冒号的URL - // 格式: "代理自动配置脚本 : http://127.0.0.1:11233/commands/pac" - // 或: "Proxy auto-config script : http://127.0.0.1:11233/commands/pac" - if let Some(colon_pos) = line.find(" : ") { - let url = line[colon_pos + 3..].trim(); - if !url.is_empty() && url != "(none)" && url != "无" { - log::debug!(target: "app", "解析到PAC URL: {}", url); - return Ok(AsyncAutoproxy { - enable: true, - url: url.to_string(), - }); - } - } else if let Some(colon_pos) = line.find(':') { - // 兼容其他可能的格式 - let url = line[colon_pos + 1..].trim(); - // 确保这不是URL中的协议部分 - if url.starts_with("http") && !url.is_empty() && url != "(none)" && url != "无" - { - log::debug!(target: "app", "解析到PAC URL (fallback): {}", url); - return Ok(AsyncAutoproxy { - enable: true, - url: url.to_string(), - }); - } + // 2. 检查自动检测设置是否启用 + let auto_detect_name = "AutoDetect\0".encode_utf16().collect::>(); + let mut auto_detect: DWORD = 0; + let mut detect_buffer_size: DWORD = 4; + let mut detect_value_type: DWORD = 0; + + let detect_query_result = RegQueryValueExW( + hkey, + auto_detect_name.as_ptr(), + ptr::null_mut(), + &mut detect_value_type, + &mut auto_detect as *mut DWORD as *mut u8, + &mut detect_buffer_size, + ); + + RegCloseKey(hkey); + + // PAC 启用的条件:AutoConfigURL 不为空,或 AutoDetect 被启用 + let pac_enabled = !pac_url.is_empty() + || (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0); + + if pac_enabled { + log::debug!(target: "app", "PAC配置启用: URL={}, AutoDetect={}", pac_url, auto_detect); + + if pac_url.is_empty() && auto_detect != 0 { + pac_url = "auto-detect".to_string(); } + + Ok(AsyncAutoproxy { + enable: true, + url: pac_url, + }) + } else { + log::debug!(target: "app", "PAC配置未启用"); + Ok(AsyncAutoproxy::default()) } } - - log::debug!(target: "app", "未找到有效的PAC配置"); - Ok(AsyncAutoproxy::default()) } #[cfg(target_os = "macos")] @@ -142,7 +186,7 @@ impl AsyncProxyQuery { if line.contains("ProxyAutoConfigEnable") && line.contains("1") { pac_enabled = true; } else if line.contains("ProxyAutoConfigURLString") { - // 修复:正确解析包含冒号的URL + // 正确解析包含冒号的URL // 格式: "ProxyAutoConfigURLString : http://127.0.0.1:11233/commands/pac" if let Some(colon_pos) = line.find(" : ") { pac_url = line[colon_pos + 3..].trim().to_string(); @@ -213,57 +257,121 @@ impl AsyncProxyQuery { #[cfg(target_os = "windows")] async fn get_system_proxy_impl() -> Result { - let output = Command::new("netsh") - .args(["winhttp", "show", "proxy"]) - .output() - .await?; + // Windows: 使用注册表直接读取代理设置 + tokio::task::spawn_blocking(move || -> Result { + Self::get_system_proxy_from_registry() + }) + .await? + } - if !output.status.success() { - return Ok(AsyncSysproxy::default()); - } + #[cfg(target_os = "windows")] + fn get_system_proxy_from_registry() -> Result { + use std::ptr; + use winapi::shared::minwindef::{DWORD, HKEY}; + use winapi::um::winnt::{KEY_READ, REG_DWORD, REG_SZ}; + use winapi::um::winreg::{RegCloseKey, RegOpenKeyExW, RegQueryValueExW, HKEY_CURRENT_USER}; + + unsafe { + let key_path = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\0" + .encode_utf16() + .collect::>(); + + let mut hkey: HKEY = ptr::null_mut(); + let result = + RegOpenKeyExW(HKEY_CURRENT_USER, key_path.as_ptr(), 0, KEY_READ, &mut hkey); + + if result != 0 { + return Ok(AsyncSysproxy::default()); + } - let stdout = String::from_utf8_lossy(&output.stdout); - log::debug!(target: "app", "netsh proxy output: {}", stdout); + // 检查代理是否启用 + let proxy_enable_name = "ProxyEnable\0".encode_utf16().collect::>(); + let mut proxy_enable: DWORD = 0; + let mut buffer_size: DWORD = 4; + let mut value_type: DWORD = 0; + + let enable_result = RegQueryValueExW( + hkey, + proxy_enable_name.as_ptr(), + ptr::null_mut(), + &mut value_type, + &mut proxy_enable as *mut DWORD as *mut u8, + &mut buffer_size, + ); + + if enable_result != 0 || value_type != REG_DWORD || proxy_enable == 0 { + RegCloseKey(hkey); + return Ok(AsyncSysproxy::default()); + } - let mut proxy_enabled = false; - let mut proxy_server = String::new(); - let mut bypass_list = String::new(); + // 读取代理服务器设置 + let proxy_server_name = "ProxyServer\0".encode_utf16().collect::>(); + let mut buffer = vec![0u16; 1024]; + let mut buffer_size: DWORD = (buffer.len() * 2) as DWORD; + let mut value_type: DWORD = 0; + + let server_result = RegQueryValueExW( + hkey, + proxy_server_name.as_ptr(), + ptr::null_mut(), + &mut value_type, + buffer.as_mut_ptr() as *mut u8, + &mut buffer_size, + ); + + let mut proxy_server = String::new(); + if server_result == 0 && value_type == REG_SZ && buffer_size > 0 { + let end_pos = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len()); + proxy_server = String::from_utf16_lossy(&buffer[..end_pos]); + } - for line in stdout.lines() { - let line = line.trim(); - if line.starts_with("代理服务器") || line.starts_with("Proxy Server") { - if let Some(server_part) = line.split(':').nth(1) { - let server = server_part.trim(); - if !server.is_empty() && server != "(none)" && server != "无" { - proxy_server = server.to_string(); - proxy_enabled = true; - } - } - } else if line.starts_with("绕过列表") || line.starts_with("Bypass List") { - if let Some(bypass_part) = line.split(':').nth(1) { - bypass_list = bypass_part.trim().to_string(); - } + // 读取代理绕过列表 + let proxy_override_name = "ProxyOverride\0".encode_utf16().collect::>(); + let mut bypass_buffer = vec![0u16; 1024]; + let mut bypass_buffer_size: DWORD = (bypass_buffer.len() * 2) as DWORD; + let mut bypass_value_type: DWORD = 0; + + let override_result = RegQueryValueExW( + hkey, + proxy_override_name.as_ptr(), + ptr::null_mut(), + &mut bypass_value_type, + bypass_buffer.as_mut_ptr() as *mut u8, + &mut bypass_buffer_size, + ); + + let mut bypass_list = String::new(); + if override_result == 0 && bypass_value_type == REG_SZ && bypass_buffer_size > 0 { + let end_pos = bypass_buffer + .iter() + .position(|&x| x == 0) + .unwrap_or(bypass_buffer.len()); + bypass_list = String::from_utf16_lossy(&bypass_buffer[..end_pos]); } - } - if proxy_enabled && !proxy_server.is_empty() { - // 解析服务器地址和端口 - let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') { - let host = proxy_server[..colon_pos].to_string(); - let port = proxy_server[colon_pos + 1..].parse::().unwrap_or(8080); - (host, port) + RegCloseKey(hkey); + + if !proxy_server.is_empty() { + // 解析服务器地址和端口 + let (host, port) = if let Some(colon_pos) = proxy_server.rfind(':') { + let host = proxy_server[..colon_pos].to_string(); + let port = proxy_server[colon_pos + 1..].parse::().unwrap_or(8080); + (host, port) + } else { + (proxy_server, 8080) + }; + + log::debug!(target: "app", "从注册表读取到代理设置: {}:{}, bypass: {}", host, port, bypass_list); + + Ok(AsyncSysproxy { + enable: true, + host, + port, + bypass: bypass_list, + }) } else { - (proxy_server, 8080) - }; - - Ok(AsyncSysproxy { - enable: true, - host, - port, - bypass: bypass_list, - }) - } else { - Ok(AsyncSysproxy::default()) + Ok(AsyncSysproxy::default()) + } } } diff --git a/src-tauri/src/core/event_driven_proxy.rs b/src-tauri/src/core/event_driven_proxy.rs index 6813dd1e06..09bbc165d9 100644 --- a/src-tauri/src/core/event_driven_proxy.rs +++ b/src-tauri/src/core/event_driven_proxy.rs @@ -526,16 +526,10 @@ impl EventDrivenProxyManager { #[cfg(target_os = "windows")] async fn execute_sysproxy_command(args: &[&str]) { - use crate::{core::handle::Handle, utils::dirs}; - use tauri_plugin_shell::ShellExt; - - let app_handle = match Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::error!(target: "app", "获取应用句柄失败"); - return; - } - }; + use crate::utils::dirs; + #[allow(unused_imports)] // creation_flags必须 + use std::os::windows::process::CommandExt; + use tokio::process::Command; let binary_path = match dirs::service_path() { Ok(path) => path, @@ -551,10 +545,9 @@ impl EventDrivenProxyManager { return; } - let shell = app_handle.shell(); - let output = shell - .command(sysproxy_exe.as_path().to_str().unwrap()) + let output = Command::new(sysproxy_exe) .args(args) + .creation_flags(0x08000000) // CREATE_NO_WINDOW - 隐藏窗口 .output() .await; @@ -562,6 +555,12 @@ impl EventDrivenProxyManager { Ok(output) => { if !output.status.success() { log::error!(target: "app", "执行sysproxy命令失败: {:?}", args); + let stderr = String::from_utf8_lossy(&output.stderr); + if !stderr.is_empty() { + log::error!(target: "app", "sysproxy错误输出: {}", stderr); + } + } else { + log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args); } } Err(e) => { diff --git a/src-tauri/src/utils/autostart.rs b/src-tauri/src/utils/autostart.rs index 565f715051..df04b8100a 100644 --- a/src-tauri/src/utils/autostart.rs +++ b/src-tauri/src/utils/autostart.rs @@ -4,7 +4,7 @@ use anyhow::{anyhow, Result}; use log::info; #[cfg(target_os = "windows")] -use std::{fs, path::Path, path::PathBuf}; +use std::{fs, os::windows::process::CommandExt, path::Path, path::PathBuf}; /// Windows 下的开机启动文件夹路径 #[cfg(target_os = "windows")] @@ -59,6 +59,8 @@ pub fn create_shortcut() -> Result<()> { let output = std::process::Command::new("powershell") .args(["-Command", &powershell_command]) + // 隐藏 PowerShell 窗口 + .creation_flags(0x08000000) // CREATE_NO_WINDOW .output() .map_err(|e| anyhow!("执行 PowerShell 命令失败: {}", e))?; From fee08f3826af5dce0d9e328fb7a3888e8d0f307d Mon Sep 17 00:00:00 2001 From: wonfen Date: Sun, 22 Jun 2025 23:19:11 +0800 Subject: [PATCH 11/15] fix: correct address display error caused by async system proxy retrieval --- src/components/home/clash-info-card.tsx | 13 ++- .../setting/mods/sysproxy-viewer.tsx | 36 ++++++- src/providers/app-data-provider.tsx | 95 ++++++++++++++----- 3 files changed, 113 insertions(+), 31 deletions(-) diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index 029f174c82..dcdd22a8e5 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -17,7 +17,7 @@ const formatUptime = (uptimeMs: number) => { export const ClashInfoCard = () => { const { t } = useTranslation(); const { version: clashVersion } = useClash(); - const { clashConfig, sysproxy, rules, uptime } = useAppData(); + const { clashConfig, rules, uptime, systemProxyAddress } = useAppData(); // 使用useMemo缓存格式化后的uptime,避免频繁计算 const formattedUptime = useMemo(() => formatUptime(uptime), [uptime]); @@ -42,7 +42,7 @@ export const ClashInfoCard = () => { {t("System Proxy Address")} - {sysproxy?.server || "-"} + {systemProxyAddress} @@ -74,7 +74,14 @@ export const ClashInfoCard = () => { ); - }, [clashConfig, clashVersion, t, formattedUptime, rules.length, sysproxy]); + }, [ + clashConfig, + clashVersion, + t, + formattedUptime, + rules.length, + systemProxyAddress, + ]); return ( ((props, ref) => { } }; + const { systemProxyAddress } = useAppData(); + + // 为当前状态计算系统代理地址 + const getSystemProxyAddress = useMemo(() => { + if (!clashConfig) return "-"; + + const isPacMode = value.pac ?? false; + + if (isPacMode) { + const host = value.proxy_host || "127.0.0.1"; + const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897; + return `${host}:${port}`; + } else { + return systemProxyAddress; + } + }, [ + value.pac, + value.proxy_host, + verge?.verge_mixed_port, + clashConfig, + systemProxyAddress, + ]); + const getCurrentPacUrl = useMemo(() => { + const host = value.proxy_host || "127.0.0.1"; + // 根据环境判断PAC端口 + const port = import.meta.env.DEV ? 11233 : 33331; + return `http://${host}:${port}/commands/pac`; + }, [value.proxy_host]); + useImperativeHandle(ref, () => ({ open: () => { setOpen(true); @@ -417,7 +447,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { {t("Server Addr")} - {sysproxy?.server ? sysproxy.server : t("Not available")} + {getSystemProxyAddress} @@ -425,7 +455,9 @@ export const SysproxyViewer = forwardRef((props, ref) => { {value.pac && ( {t("PAC URL")} - {autoproxy?.url || "-"} + + {getCurrentPacUrl || "-"} + )} diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 166b8057b4..0fee8a817b 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -1,5 +1,12 @@ -import { createContext, useContext, useMemo, useEffect } from "react"; -import useSWR from "swr"; +import React, { + createContext, + useContext, + useEffect, + useMemo, + useState, +} from "react"; +import { useVerge } from "@/hooks/use-verge"; +import useSWR, { mutate } from "swr"; import useSWRSubscription from "swr/subscription"; import { getProxies, @@ -37,6 +44,8 @@ interface AppDataContextType { }; traffic: { up: number; down: number }; memory: { inuse: number }; + systemProxyAddress: string; + refreshProxy: () => Promise; refreshClashConfig: () => Promise; refreshRules: () => Promise; @@ -55,8 +64,9 @@ export const AppDataProvider = ({ }: { children: React.ReactNode; }) => { - const { clashInfo } = useClashInfo(); const pageVisible = useVisibility(); + const { clashInfo } = useClashInfo(); + const { verge } = useVerge(); // 基础数据 - 中频率更新 (5秒) const { data: proxiesData, mutate: refreshProxy } = useSWR( @@ -508,8 +518,39 @@ export const AppDataProvider = ({ }; // 聚合所有数据 - const value = useMemo( - () => ({ + const value = useMemo(() => { + // 计算系统代理地址 + const calculateSystemProxyAddress = () => { + if (!verge || !clashConfig) return "-"; + + const isPacMode = verge.proxy_auto_config ?? false; + + if (isPacMode) { + // PAC模式:显示我们期望设置的代理地址 + const proxyHost = verge.proxy_host || "127.0.0.1"; + const proxyPort = + verge.verge_mixed_port || clashConfig["mixed-port"] || 7897; + return `${proxyHost}:${proxyPort}`; + } else { + // HTTP代理模式:优先使用系统地址,但如果格式不正确则使用期望地址 + const systemServer = sysproxy?.server; + if ( + systemServer && + systemServer !== "-" && + !systemServer.startsWith(":") + ) { + return systemServer; + } else { + // 系统地址无效,返回期望的代理地址 + const proxyHost = verge.proxy_host || "127.0.0.1"; + const proxyPort = + verge.verge_mixed_port || clashConfig["mixed-port"] || 7897; + return `${proxyHost}:${proxyPort}`; + } + } + }; + + return { // 数据 proxies: proxiesData, clashConfig, @@ -534,6 +575,8 @@ export const AppDataProvider = ({ traffic: trafficData, memory: memoryData, + systemProxyAddress: calculateSystemProxyAddress(), + // 刷新方法 refreshProxy, refreshClashConfig, @@ -542,27 +585,27 @@ export const AppDataProvider = ({ refreshProxyProviders, refreshRuleProviders, refreshAll, - }), - [ - proxiesData, - clashConfig, - rulesData, - sysproxy, - runningMode, - uptimeData, - connectionsData, - trafficData, - memoryData, - proxyProviders, - ruleProviders, - refreshProxy, - refreshClashConfig, - refreshRules, - refreshSysproxy, - refreshProxyProviders, - refreshRuleProviders, - ], - ); + }; + }, [ + proxiesData, + clashConfig, + rulesData, + sysproxy, + runningMode, + uptimeData, + connectionsData, + trafficData, + memoryData, + proxyProviders, + ruleProviders, + verge, + refreshProxy, + refreshClashConfig, + refreshRules, + refreshSysproxy, + refreshProxyProviders, + refreshRuleProviders, + ]); return ( {children} From 628de70e89fb06a21ed187adfe9dba445803cbe6 Mon Sep 17 00:00:00 2001 From: wonfen Date: Mon, 23 Jun 2025 00:06:47 +0800 Subject: [PATCH 12/15] chore: remove unused imports --- package.json | 8 +- pnpm-lock.yaml | 145 +----------------- src/components/home/clash-mode-card.tsx | 2 +- .../home/enhanced-traffic-stats.tsx | 2 +- src/components/home/ip-info-card.tsx | 9 +- src/components/layout/layout-traffic.tsx | 2 +- .../profile/proxies-editor-viewer.tsx | 2 +- .../profile/rules-editor-viewer.tsx | 2 +- src/components/proxy/use-render-list.ts | 2 +- .../setting/mods/controller-viewer.tsx | 2 +- src/components/setting/setting-system.tsx | 3 +- .../shared/ProxyControlSwitches.tsx | 4 - src/hooks/use-log-data.ts | 6 - src/pages/_layout.tsx | 1 - src/pages/connections.tsx | 5 +- src/pages/home.tsx | 6 +- src/pages/profiles.tsx | 2 +- src/providers/app-data-provider.tsx | 10 +- src/services/api.ts | 1 - src/services/global-log-service.ts | 3 +- src/utils/uri-parser.ts | 2 +- 21 files changed, 25 insertions(+), 194 deletions(-) diff --git a/package.json b/package.json index 715a6fc213..3c720f8e00 100644 --- a/package.json +++ b/package.json @@ -45,24 +45,21 @@ "@tauri-apps/plugin-shell": "2.2.2", "@tauri-apps/plugin-updater": "2.8.1", "@tauri-apps/plugin-window-state": "^2.2.3", - "@types/d3-shape": "^3.1.7", "@types/json-schema": "^7.0.15", + "json-schema": "^0.4.0", "ahooks": "^3.8.5", "axios": "^1.10.0", "chart.js": "^4.5.0", "cli-color": "^2.0.4", - "d3-shape": "^3.2.0", "dayjs": "1.11.13", "foxact": "^0.2.49", "glob": "^11.0.3", "i18next": "^25.2.1", - "js-base64": "^3.7.7", "js-yaml": "^4.1.0", "lodash-es": "^4.17.21", "monaco-editor": "^0.52.2", "monaco-yaml": "^5.4.0", "nanoid": "^5.1.5", - "peggy": "^5.0.4", "react": "19.1.0", "react-chartjs-2": "^5.3.0", "react-dom": "19.1.0", @@ -82,7 +79,6 @@ "devDependencies": { "@actions/github": "^6.0.1", "@tauri-apps/cli": "2.5.0", - "@types/js-cookie": "^3.0.6", "@types/js-yaml": "^4.0.9", "@types/lodash-es": "^4.17.12", "@types/react": "19.1.8", @@ -93,11 +89,9 @@ "commander": "^14.0.0", "cross-env": "^7.0.3", "https-proxy-agent": "^7.0.6", - "husky": "^9.1.7", "meta-json-schema": "^1.19.10", "node-fetch": "^3.3.2", "prettier": "^3.5.3", - "pretty-quick": "^4.2.2", "sass": "^1.89.2", "terser": "^5.43.1", "typescript": "^5.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 026505cbde..568e04c6d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,9 +68,6 @@ importers: '@tauri-apps/plugin-window-state': specifier: ^2.2.3 version: 2.2.3 - '@types/d3-shape': - specifier: ^3.1.7 - version: 3.1.7 '@types/json-schema': specifier: ^7.0.15 version: 7.0.15 @@ -86,9 +83,6 @@ importers: cli-color: specifier: ^2.0.4 version: 2.0.4 - d3-shape: - specifier: ^3.2.0 - version: 3.2.0 dayjs: specifier: 1.11.13 version: 1.11.13 @@ -101,12 +95,12 @@ importers: i18next: specifier: ^25.2.1 version: 25.2.1(typescript@5.8.3) - js-base64: - specifier: ^3.7.7 - version: 3.7.7 js-yaml: specifier: ^4.1.0 version: 4.1.0 + json-schema: + specifier: ^0.4.0 + version: 0.4.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -119,9 +113,6 @@ importers: nanoid: specifier: ^5.1.5 version: 5.1.5 - peggy: - specifier: ^5.0.4 - version: 5.0.4 react: specifier: 19.1.0 version: 19.1.0 @@ -174,9 +165,6 @@ importers: '@tauri-apps/cli': specifier: 2.5.0 version: 2.5.0 - '@types/js-cookie': - specifier: ^3.0.6 - version: 3.0.6 '@types/js-yaml': specifier: ^4.0.9 version: 4.0.9 @@ -207,9 +195,6 @@ importers: https-proxy-agent: specifier: ^7.0.6 version: 7.0.6 - husky: - specifier: ^9.1.7 - version: 9.1.7 meta-json-schema: specifier: ^1.19.10 version: 1.19.10 @@ -219,9 +204,6 @@ importers: prettier: specifier: ^3.5.3 version: 3.5.3 - pretty-quick: - specifier: ^4.2.2 - version: 4.2.2(prettier@3.5.3) sass: specifier: ^1.89.2 version: 1.89.2 @@ -1288,14 +1270,6 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@peggyjs/from-mem@2.0.1': - resolution: {integrity: sha512-5dAPJsLrb3KQahPb8kUqg9nGS2dKlMC4vCB3dMWoZIRqmPrNbBt6P6jidczFBoz+2EbFXBxXi0o9BUpEPHoD+g==} - engines: {node: '>=20'} - - '@pkgr/core@0.2.7': - resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} - engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} @@ -1592,12 +1566,6 @@ packages: '@types/babel__traverse@7.20.7': resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} - '@types/d3-path@3.1.1': - resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} - - '@types/d3-shape@3.1.7': - resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} - '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} @@ -1610,9 +1578,6 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/js-cookie@3.0.6': - resolution: {integrity: sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==} - '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -1875,14 +1840,6 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - d3-path@3.1.0: - resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} - engines: {node: '>=12'} - - d3-shape@3.2.0: - resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} - engines: {node: '>=12'} - d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -2132,11 +2089,6 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - husky@9.1.7: - resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} - engines: {node: '>=18'} - hasBin: true - i18next@25.2.1: resolution: {integrity: sha512-+UoXK5wh+VlE1Zy5p6MjcvctHXAhRwQKCxiJD8noKZzIXmnAX8gdHX5fLPA3MEVxEN4vbZkQFy8N0LyD9tUqPw==} peerDependencies: @@ -2145,10 +2097,6 @@ packages: typescript: optional: true - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - immutable@5.1.2: resolution: {integrity: sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==} @@ -2211,9 +2159,6 @@ packages: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} - js-base64@3.7.7: - resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} - js-cookie@3.0.5: resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} engines: {node: '>=14'} @@ -2238,6 +2183,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -2435,10 +2383,6 @@ packages: peerDependencies: monaco-editor: '>=0.36' - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2512,11 +2456,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - peggy@5.0.4: - resolution: {integrity: sha512-NMRm2w2irCFbiOaejvcDEyn+DMUaGd8s4RT1ztj9Kr/kR367pziIvmjqJ0OFqcAg+LqT5tPGsW96MNT5gUNdUw==} - engines: {node: '>=20'} - hasBin: true - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2537,13 +2476,6 @@ packages: engines: {node: '>=14'} hasBin: true - pretty-quick@4.2.2: - resolution: {integrity: sha512-uAh96tBW1SsD34VhhDmWuEmqbpfYc/B3j++5MC/6b3Cb8Ow7NJsvKFhg0eoGu2xXX+o9RkahkTK6sUdd8E7g5w==} - engines: {node: '>=14'} - hasBin: true - peerDependencies: - prettier: ^3.0.0 - prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2717,11 +2649,6 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} - engines: {node: '>=10'} - hasBin: true - server-only@0.0.1: resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==} @@ -2746,10 +2673,6 @@ packages: sockette@2.0.6: resolution: {integrity: sha512-W6iG8RGV6Zife3Cj+FhuyHV447E6fqFM2hKmnaQrTvg3OydINV3Msj3WPFbX76blUlUxvQSMMMdrJxce8NqI5Q==} - source-map-generator@2.0.1: - resolution: {integrity: sha512-AtEu86XavXC2HD/bQVQoDbovnTRZE/0QMAAHn6RxALAoLOM3a47IG06TpsJK23BnCmjbTYNLwx2vUhgzRYgGMA==} - engines: {node: '>=20'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2824,9 +2747,6 @@ packages: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - tinyglobby@0.2.13: resolution: {integrity: sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==} engines: {node: '>=12.0.0'} @@ -4193,12 +4113,6 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true - '@peggyjs/from-mem@2.0.1': - dependencies: - semver: 7.7.2 - - '@pkgr/core@0.2.7': {} - '@popperjs/core@2.11.8': {} '@rolldown/pluginutils@1.0.0-beta.11': {} @@ -4447,12 +4361,6 @@ snapshots: dependencies: '@babel/types': 7.27.6 - '@types/d3-path@3.1.1': {} - - '@types/d3-shape@3.1.7': - dependencies: - '@types/d3-path': 3.1.1 - '@types/debug@4.1.12': dependencies: '@types/ms': 2.1.0 @@ -4467,8 +4375,6 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/js-cookie@3.0.6': {} - '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': {} @@ -4728,12 +4634,6 @@ snapshots: csstype@3.1.3: {} - d3-path@3.1.0: {} - - d3-shape@3.2.0: - dependencies: - d3-path: 3.1.0 - d@1.0.2: dependencies: es5-ext: 0.10.64 @@ -5020,16 +4920,12 @@ snapshots: transitivePeerDependencies: - supports-color - husky@9.1.7: {} - i18next@25.2.1(typescript@5.8.3): dependencies: '@babel/runtime': 7.27.6 optionalDependencies: typescript: 5.8.3 - ignore@7.0.5: {} - immutable@5.1.2: {} import-fresh@3.3.1: @@ -5081,8 +4977,6 @@ snapshots: dependencies: '@isaacs/cliui': 8.0.2 - js-base64@3.7.7: {} - js-cookie@3.0.5: {} js-tokens@4.0.0: {} @@ -5097,6 +4991,8 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema@0.4.0: {} + json5@2.2.3: {} jsonc-parser@3.3.1: {} @@ -5429,8 +5325,6 @@ snapshots: vscode-uri: 3.1.0 yaml: 2.7.1 - mri@1.2.0: {} - ms@2.1.3: {} nanoid@3.3.11: {} @@ -5499,12 +5393,6 @@ snapshots: path-type@4.0.0: {} - peggy@5.0.4: - dependencies: - '@peggyjs/from-mem': 2.0.1 - commander: 14.0.0 - source-map-generator: 2.0.1 - picocolors@1.1.1: {} picomatch@2.3.1: @@ -5520,17 +5408,6 @@ snapshots: prettier@3.5.3: {} - pretty-quick@4.2.2(prettier@3.5.3): - dependencies: - '@pkgr/core': 0.2.7 - ignore: 7.0.5 - mri: 1.2.0 - picocolors: 1.1.1 - picomatch: 4.0.2 - prettier: 3.5.3 - tinyexec: 0.3.2 - tslib: 2.8.1 - prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -5726,8 +5603,6 @@ snapshots: semver@6.3.1: {} - semver@7.7.2: {} - server-only@0.0.1: {} set-cookie-parser@2.7.1: {} @@ -5747,8 +5622,6 @@ snapshots: sockette@2.0.6: {} - source-map-generator@2.0.1: {} - source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5830,8 +5703,6 @@ snapshots: es5-ext: 0.10.64 next-tick: 1.1.0 - tinyexec@0.3.2: {} - tinyglobby@0.2.13: dependencies: fdir: 6.4.4(picomatch@4.0.2) diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index ceda0aecae..8e9572a265 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { Box, Typography, Paper, Stack, Fade } from "@mui/material"; +import { Box, Typography, Paper, Stack } from "@mui/material"; import { useLockFn } from "ahooks"; import { closeAllConnections } from "@/services/api"; import { patchClashMode } from "@/services/cmds"; diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 29999f31c8..763c3fd416 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -26,7 +26,7 @@ import { useClashInfo } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { createAuthSockette } from "@/utils/websocket"; import parseTraffic from "@/utils/parse-traffic"; -import { getConnections, isDebugEnabled, gc } from "@/services/api"; +import { isDebugEnabled, gc } from "@/services/api"; import { ReactNode } from "react"; import { useAppData } from "@/providers/app-data-provider"; diff --git a/src/components/home/ip-info-card.tsx b/src/components/home/ip-info-card.tsx index f831ffe823..4c5231376c 100644 --- a/src/components/home/ip-info-card.tsx +++ b/src/components/home/ip-info-card.tsx @@ -1,12 +1,5 @@ import { useTranslation } from "react-i18next"; -import { - Box, - Typography, - Button, - Skeleton, - IconButton, - useTheme, -} from "@mui/material"; +import { Box, Typography, Button, Skeleton, IconButton } from "@mui/material"; import { LocationOnOutlined, RefreshOutlined, diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx index 8d89642200..54308d863a 100644 --- a/src/components/layout/layout-traffic.tsx +++ b/src/components/layout/layout-traffic.tsx @@ -11,7 +11,7 @@ import { TrafficGraph, type TrafficRef } from "./traffic-graph"; import { useVisibility } from "@/hooks/use-visibility"; import parseTraffic from "@/utils/parse-traffic"; import useSWRSubscription from "swr/subscription"; -import { createSockette, createAuthSockette } from "@/utils/websocket"; +import { createAuthSockette } from "@/utils/websocket"; import { useTranslation } from "react-i18next"; import { isDebugEnabled, gc } from "@/services/api"; diff --git a/src/components/profile/proxies-editor-viewer.tsx b/src/components/profile/proxies-editor-viewer.tsx index 34835fb7f2..624d83f9e8 100644 --- a/src/components/profile/proxies-editor-viewer.tsx +++ b/src/components/profile/proxies-editor-viewer.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; import { useTranslation } from "react-i18next"; diff --git a/src/components/profile/rules-editor-viewer.tsx b/src/components/profile/rules-editor-viewer.tsx index 854062d36d..85000813e0 100644 --- a/src/components/profile/rules-editor-viewer.tsx +++ b/src/components/profile/rules-editor-viewer.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useLockFn } from "ahooks"; import yaml from "js-yaml"; import { useTranslation } from "react-i18next"; diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index 4bb9aadd6e..90bcc457c4 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useCallback } from "react"; +import { useEffect, useMemo } from "react"; import { useVerge } from "@/hooks/use-verge"; import { filterSort } from "./use-filter-sort"; import { useWindowWidth } from "./use-window-width"; diff --git a/src/components/setting/mods/controller-viewer.tsx b/src/components/setting/mods/controller-viewer.tsx index 5734d2b78a..7c0b64811d 100644 --- a/src/components/setting/mods/controller-viewer.tsx +++ b/src/components/setting/mods/controller-viewer.tsx @@ -15,7 +15,7 @@ import { Tooltip, } from "@mui/material"; import { useLockFn } from "ahooks"; -import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; +import { forwardRef, useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; export const ControllerViewer = forwardRef((props, ref) => { diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index f892bc8ebf..87079784eb 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -1,5 +1,5 @@ import useSWR, { mutate } from "swr"; -import { useRef, useEffect, useState } from "react"; +import { useRef } from "react"; import { useTranslation } from "react-i18next"; import { SettingsRounded, @@ -19,7 +19,6 @@ import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { getSystemProxy, getAutotemProxy, - installService, uninstallService, restartCore, stopCore, diff --git a/src/components/shared/ProxyControlSwitches.tsx b/src/components/shared/ProxyControlSwitches.tsx index 18a2b1375d..dcf4a651c2 100644 --- a/src/components/shared/ProxyControlSwitches.tsx +++ b/src/components/shared/ProxyControlSwitches.tsx @@ -24,11 +24,7 @@ import { getSystemProxy, getAutotemProxy, getRunningMode, - installService, - restartCore, - isServiceAvailable, } from "@/services/cmds"; -import { useLockFn } from "ahooks"; import { closeAllConnections } from "@/services/api"; import { showNotice } from "@/services/noticeService"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; diff --git a/src/hooks/use-log-data.ts b/src/hooks/use-log-data.ts index 8ceebb3cf0..cecf7168a7 100644 --- a/src/hooks/use-log-data.ts +++ b/src/hooks/use-log-data.ts @@ -1,10 +1,4 @@ -import { useEffect } from "react"; -import { useEnableLog } from "../services/states"; -import { createSockette, createAuthSockette } from "../utils/websocket"; -import { useClashInfo } from "./use-clash"; -import dayjs from "dayjs"; import { create } from "zustand"; -import { useVisibility } from "./use-visibility"; import { useGlobalLogData, clearGlobalLogs, diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index 2d763f5a38..50657fc2d8 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -21,7 +21,6 @@ import { useCustomTheme } from "@/components/layout/use-custom-theme"; import getSystem from "@/utils/get-system"; import "dayjs/locale/ru"; import "dayjs/locale/zh-cn"; -import { getPortableFlag } from "@/services/cmds"; import React from "react"; import { useListen } from "@/hooks/use-listen"; import { listen } from "@tauri-apps/api/event"; diff --git a/src/pages/connections.tsx b/src/pages/connections.tsx index b0da5d125d..4c8b6d4c11 100644 --- a/src/pages/connections.tsx +++ b/src/pages/connections.tsx @@ -19,10 +19,7 @@ import { ConnectionDetailRef, } from "@/components/connection/connection-detail"; import parseTraffic from "@/utils/parse-traffic"; -import { - BaseSearchBox, - type SearchState, -} from "@/components/base/base-search-box"; +import { BaseSearchBox } from "@/components/base/base-search-box"; import { BaseStyledSelect } from "@/components/base/base-styled-select"; import { useTheme } from "@mui/material/styles"; import { useVisibility } from "@/hooks/use-visibility"; diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 7e910f1251..1d9fbaa864 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -37,11 +37,7 @@ import { BasePage } from "@/components/base"; import { ClashInfoCard } from "@/components/home/clash-info-card"; import { SystemInfoCard } from "@/components/home/system-info-card"; import { useLockFn } from "ahooks"; -import { - entry_lightweight_mode, - openWebUrl, - patchVergeConfig, -} from "@/services/cmds"; +import { entry_lightweight_mode, openWebUrl } from "@/services/cmds"; import { TestCard } from "@/components/home/test-card"; import { IpInfoCard } from "@/components/home/ip-info-card"; diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index 7d75e401bb..b9d74ac115 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -45,7 +45,7 @@ import { ProfileMore } from "@/components/profile/profile-more"; import { ProfileItem } from "@/components/profile/profile-item"; import { useProfiles } from "@/hooks/use-profiles"; import { ConfigViewer } from "@/components/setting/mods/config-viewer"; -import { add, throttle } from "lodash-es"; +import { throttle } from "lodash-es"; import { BaseStyledTextField } from "@/components/base/base-styled-text-field"; import { readTextFile } from "@tauri-apps/plugin-fs"; import { readText } from "@tauri-apps/plugin-clipboard-manager"; diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 0fee8a817b..337c74de53 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -1,12 +1,6 @@ -import React, { - createContext, - useContext, - useEffect, - useMemo, - useState, -} from "react"; +import React, { createContext, useContext, useEffect, useMemo } from "react"; import { useVerge } from "@/hooks/use-verge"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; import useSWRSubscription from "swr/subscription"; import { getProxies, diff --git a/src/services/api.ts b/src/services/api.ts index cebaec84d4..f108ebb306 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,7 +1,6 @@ import axios, { AxiosInstance } from "axios"; import { getClashInfo } from "./cmds"; import { invoke } from "@tauri-apps/api/core"; -import { useLockFn } from "ahooks"; let instancePromise: Promise = null!; diff --git a/src/services/global-log-service.ts b/src/services/global-log-service.ts index b8d8731c77..8e94832521 100644 --- a/src/services/global-log-service.ts +++ b/src/services/global-log-service.ts @@ -1,8 +1,7 @@ // 全局日志服务,使应用在任何页面都能收集日志 import { create } from "zustand"; -import { createSockette, createAuthSockette } from "@/utils/websocket"; +import { createAuthSockette } from "@/utils/websocket"; import dayjs from "dayjs"; -import { useState, useEffect } from "react"; // 最大日志数量 const MAX_LOG_NUM = 1000; diff --git a/src/utils/uri-parser.ts b/src/utils/uri-parser.ts index eb8b630420..7aba40a5a5 100644 --- a/src/utils/uri-parser.ts +++ b/src/utils/uri-parser.ts @@ -730,7 +730,7 @@ function URI_Trojan(line: string): IProxyTrojanConfig { function URI_Hysteria2(line: string): IProxyHysteria2Config { line = line.split(/(hysteria2|hy2):\/\//)[2]; - // eslint-disable-next-line no-unused-vars + let [__, password, server, ___, port, ____, addons = "", name] = /^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line) || []; let portNum = parseInt(`${port}`, 10); From d5a174c71b0030337230d3ddd63c8999aa7374b0 Mon Sep 17 00:00:00 2001 From: wonfen Date: Mon, 23 Jun 2025 12:59:24 +0800 Subject: [PATCH 13/15] perf: improve proxy status indicator and toggle responsiveness --- src/components/home/proxy-tun-card.tsx | 10 ++- src/components/setting/setting-system.tsx | 82 ++++--------------- .../shared/ProxyControlSwitches.tsx | 45 ++++------ src/hooks/use-system-proxy-state.ts | 72 ++++++++++++++++ src/providers/app-data-provider.tsx | 5 +- 5 files changed, 110 insertions(+), 104 deletions(-) create mode 100644 src/hooks/use-system-proxy-state.ts diff --git a/src/components/home/proxy-tun-card.tsx b/src/components/home/proxy-tun-card.tsx index 5803c32652..6dcba565af 100644 --- a/src/components/home/proxy-tun-card.tsx +++ b/src/components/home/proxy-tun-card.tsx @@ -19,6 +19,7 @@ import { } from "@mui/icons-material"; import { useVerge } from "@/hooks/use-verge"; import { useSystemState } from "@/hooks/use-system-state"; +import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; import { showNotice } from "@/services/noticeService"; import { getRunningMode } from "@/services/cmds"; import { mutate } from "swr"; @@ -145,8 +146,9 @@ export const ProxyTunCard: FC = () => { const { verge } = useVerge(); const { isAdminMode } = useSystemState(); + const { indicator: systemProxyIndicator } = useSystemProxyState(); - const { enable_system_proxy, enable_tun_mode } = verge ?? {}; + const { enable_tun_mode } = verge ?? {}; const updateLocalStatus = async () => { try { @@ -180,7 +182,7 @@ export const ProxyTunCard: FC = () => { const tabDescription = useMemo(() => { if (activeTab === "system") { return { - text: enable_system_proxy + text: systemProxyIndicator ? t("System Proxy Enabled") : t("System Proxy Disabled"), tooltip: t("System Proxy Info"), @@ -195,7 +197,7 @@ export const ProxyTunCard: FC = () => { tooltip: t("TUN Mode Intercept Info"), }; } - }, [activeTab, enable_system_proxy, enable_tun_mode, isTunAvailable, t]); + }, [activeTab, systemProxyIndicator, enable_tun_mode, isTunAvailable, t]); return ( @@ -214,7 +216,7 @@ export const ProxyTunCard: FC = () => { onClick={() => handleTabChange("system")} icon={ComputerRounded} label={t("System Proxy")} - hasIndicator={enable_system_proxy} + hasIndicator={systemProxyIndicator} /> { const { verge, mutateVerge, patchVerge } = useVerge(); const { installServiceAndRestartCore } = useServiceInstaller(); - - const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy); - const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy); + const { + actualState: systemProxyActualState, + indicator: systemProxyIndicator, + toggleSystemProxy, + } = useSystemProxyState(); const { isAdminMode, isServiceMode, mutateRunningMode } = useSystemState(); @@ -51,26 +48,14 @@ const SettingSystem = ({ onError }: Props) => { const sysproxyRef = useRef(null); const tunRef = useRef(null); - const { - enable_tun_mode, - enable_auto_launch, - enable_silent_start, - enable_system_proxy, - proxy_auto_config, - enable_hover_jump_navigator, - } = verge ?? {}; + const { enable_tun_mode, enable_auto_launch, enable_silent_start } = + verge ?? {}; const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial) => { mutateVerge({ ...verge, ...patch }, false); }; - const updateProxyStatus = async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - await mutate("getSystemProxy"); - await mutate("getAutotemProxy"); - }; - // 抽象服务操作逻辑 const handleServiceOperation = useLockFn( async ({ @@ -194,13 +179,7 @@ const SettingSystem = ({ onError }: Props) => { icon={SettingsRounded} onClick={() => sysproxyRef.current?.open()} /> - {proxy_auto_config ? ( - autoproxy?.enable ? ( - - ) : ( - - ) - ) : sysproxy?.enable ? ( + {systemProxyIndicator ? ( ) : ( @@ -209,44 +188,13 @@ const SettingSystem = ({ onError }: Props) => { } > { - if (autoproxy?.enable === false && sysproxy?.enable === false) { - onChangeData({ enable_system_proxy: !enable_system_proxy }); - } else { - onChangeData({ enable_system_proxy: e }); - } - }} - onGuard={async (e) => { - if (autoproxy?.enable === false && sysproxy?.enable === false) { - await patchVerge({ enable_system_proxy: !enable_system_proxy }); - await updateProxyStatus(); - return; - } - if (!e && verge?.auto_close_connection) { - closeAllConnections(); - } - await patchVerge({ enable_system_proxy: e }); - await updateProxyStatus(); - }} + onGuard={(e) => toggleSystemProxy(e)} > - + diff --git a/src/components/shared/ProxyControlSwitches.tsx b/src/components/shared/ProxyControlSwitches.tsx index dcf4a651c2..f96d5cbeb5 100644 --- a/src/components/shared/ProxyControlSwitches.tsx +++ b/src/components/shared/ProxyControlSwitches.tsx @@ -1,6 +1,6 @@ import { useRef } from "react"; import { useTranslation } from "react-i18next"; -import useSWR, { mutate } from "swr"; +import useSWR from "swr"; import { SettingsRounded, PlayCircleOutlineRounded, @@ -20,12 +20,8 @@ import { GuardState } from "@/components/setting/mods/guard-state"; import { SysproxyViewer } from "@/components/setting/mods/sysproxy-viewer"; import { TunViewer } from "@/components/setting/mods/tun-viewer"; import { useVerge } from "@/hooks/use-verge"; -import { - getSystemProxy, - getAutotemProxy, - getRunningMode, -} from "@/services/cmds"; -import { closeAllConnections } from "@/services/api"; +import { useSystemProxyState } from "@/hooks/use-system-proxy-state"; +import { getRunningMode } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; import { useServiceInstaller } from "@/hooks/useServiceInstaller"; @@ -44,12 +40,13 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { const theme = useTheme(); const { installServiceAndRestartCore } = useServiceInstaller(); - const { data: sysproxy } = useSWR("getSystemProxy", getSystemProxy); - const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy); - const { data: runningMode, mutate: mutateRunningMode } = useSWR( - "getRunningMode", - getRunningMode, - ); + const { + actualState: systemProxyActualState, + indicator: systemProxyIndicator, + toggleSystemProxy, + } = useSystemProxyState(); + + const { data: runningMode } = useSWR("getRunningMode", getRunningMode); // 是否以sidecar模式运行 const isSidecarMode = runningMode === "Sidecar"; @@ -57,8 +54,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { const sysproxyRef = useRef(null); const tunRef = useRef(null); - const { enable_tun_mode, enable_system_proxy, proxy_auto_config } = - verge ?? {}; + const { enable_tun_mode, enable_system_proxy } = verge ?? {}; // 确定当前显示哪个开关 const isSystemProxyMode = label === t("System Proxy") || !label; @@ -69,12 +65,6 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { mutateVerge({ ...verge, ...patch }, false); }; - const updateProxyStatus = async () => { - await new Promise((resolve) => setTimeout(resolve, 100)); - await mutate("getSystemProxy"); - await mutate("getAutotemProxy"); - }; - // 安装系统服务 const onInstallService = installServiceAndRestartCore; @@ -109,7 +99,7 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { }} > - {enable_system_proxy ? ( + {systemProxyIndicator ? ( @@ -151,18 +141,11 @@ const ProxyControlSwitches = ({ label, onError }: ProxySwitchProps) => { onChangeData({ enable_system_proxy: e })} - onGuard={async (e) => { - if (!e && verge?.auto_close_connection) { - closeAllConnections(); - } - await patchVerge({ enable_system_proxy: e }); - await updateProxyStatus(); - }} + onGuard={(e) => toggleSystemProxy(e)} > diff --git a/src/hooks/use-system-proxy-state.ts b/src/hooks/use-system-proxy-state.ts new file mode 100644 index 0000000000..2e4621ba18 --- /dev/null +++ b/src/hooks/use-system-proxy-state.ts @@ -0,0 +1,72 @@ +import useSWR, { mutate } from "swr"; +import { useVerge } from "@/hooks/use-verge"; +import { getAutotemProxy } from "@/services/cmds"; +import { useAppData } from "@/providers/app-data-provider"; +import { closeAllConnections } from "@/services/api"; + +// 系统代理状态检测统一逻辑 +export const useSystemProxyState = () => { + const { verge, mutateVerge, patchVerge } = useVerge(); + const { sysproxy } = useAppData(); + const { data: autoproxy } = useSWR("getAutotemProxy", getAutotemProxy, { + revalidateOnFocus: true, + revalidateOnReconnect: true, + }); + + const { enable_system_proxy, proxy_auto_config } = verge ?? {}; + + const getSystemProxyActualState = () => { + const userEnabled = enable_system_proxy ?? false; + + if (userEnabled) { + return true; + } + + return autoproxy?.enable === false && sysproxy?.enable === false + ? false + : userEnabled; + }; + + const getSystemProxyIndicator = () => { + if (proxy_auto_config) { + return autoproxy?.enable ?? false; + } else { + return sysproxy?.enable ?? false; + } + }; + + const updateProxyStatus = async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); + await mutate("getSystemProxy"); + await mutate("getAutotemProxy"); + }; + + const toggleSystemProxy = (enabled: boolean) => { + mutateVerge({ ...verge, enable_system_proxy: enabled }, false); + + setTimeout(async () => { + try { + if (!enabled && verge?.auto_close_connection) { + closeAllConnections(); + } + await patchVerge({ enable_system_proxy: enabled }); + + updateProxyStatus(); + } catch (error) { + mutateVerge({ ...verge, enable_system_proxy: !enabled }, false); + } + }, 0); + + return Promise.resolve(); + }; + + return { + actualState: getSystemProxyActualState(), + indicator: getSystemProxyIndicator(), + configState: enable_system_proxy ?? false, + sysproxy, + autoproxy, + proxy_auto_config, + toggleSystemProxy, + }; +}; diff --git a/src/providers/app-data-provider.tsx b/src/providers/app-data-provider.tsx index 337c74de53..9e7ab3afd8 100644 --- a/src/providers/app-data-provider.tsx +++ b/src/providers/app-data-provider.tsx @@ -68,7 +68,7 @@ export const AppDataProvider = ({ getProxies, { refreshInterval: 5000, - revalidateOnFocus: false, + revalidateOnFocus: true, suspense: false, errorRetryCount: 3, }, @@ -243,7 +243,8 @@ export const AppDataProvider = ({ "getSystemProxy", getSystemProxy, { - revalidateOnFocus: false, + revalidateOnFocus: true, + revalidateOnReconnect: true, suspense: false, errorRetryCount: 3, }, From 6d519dac1e329cec77939de13c1fba1792146f04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B8=8C=E4=BA=9A=E7=9A=84=E8=A5=BF=E7=BA=A2=E6=9F=BF?= Date: Mon, 23 Jun 2025 23:13:31 +0800 Subject: [PATCH 14/15] fix: auto light-weight mode doesn't take effect in silent-start mode (#3875) * fix: auto light-weight mode does not take effect when silent-start mode is enabled * refactor: streamline window state retrieval and hiding logic * fix: add checks for remote name and existence before format check in pre-push hook * fix: simplify remote checks in pre-push hook to enhance clarity and maintainability --------- Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com> --- .husky/pre-push | 8 ++--- UPDATELOG.md | 2 ++ src-tauri/src/module/lightweight.rs | 21 +++++++----- src-tauri/src/utils/resolve.rs | 1 + src-tauri/src/utils/window_manager.rs | 49 ++++++++++++++++----------- 5 files changed, 48 insertions(+), 33 deletions(-) diff --git a/.husky/pre-push b/.husky/pre-push index 0c95cbfdac..e7b0a02e9c 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -8,11 +8,9 @@ if git diff --cached --name-only | grep -q '^src-tauri/'; then fi fi -remote_name="$1" -remote_url=$(git remote get-url "$remote_name") - -if [[ "$remote_url" =~ github\.com[:/]+clash-verge-rev/clash-verge-rev(\.git)?$ ]]; then - echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev ($remote_url)" +# 检查所有 remote url 是否有目标仓库 +if git remote -v | grep -Eq 'github\\.com[:/]+clash-verge-rev/clash-verge-rev(\\.git)?'; then + echo "[pre-push] Detected push to clash-verge-rev/clash-verge-rev" echo "[pre-push] Running pnpm format:check..." pnpm format:check diff --git a/UPDATELOG.md b/UPDATELOG.md index 5472ddc2d9..76430d0636 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -5,6 +5,8 @@ - 修复系统代理端口不同步问题 - 修复自定义 `css` 背景图无法生效问题 - 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题 +- 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题 +- 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步 ### ✨ 新增功能 diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index f93cc170c2..0b8553e530 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -36,20 +36,20 @@ where pub fn run_once_auto_lightweight() { LightWeightState::default().run_once_time(|| { - let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(true); + let is_silent_start = Config::verge().data().enable_silent_start.unwrap_or(false); let enable_auto = Config::verge() .data() .enable_auto_light_weight_mode - .unwrap_or(true); + .unwrap_or(false); if enable_auto && is_silent_start { logging!( info, Type::Lightweight, true, - "正常创建窗口和添加定时器监听器" + "在静默启动的情况下,创建窗口再添加自动进入轻量模式窗口监听器" ); set_lightweight_mode(false); - disable_auto_light_weight_mode(); + enable_auto_light_weight_mode(); // 触发托盘更新 if let Err(e) = Tray::global().update_part() { @@ -65,8 +65,13 @@ pub fn auto_lightweight_mode_init() { let is_silent_start = { Config::verge().data().enable_silent_start }.unwrap_or(false); let enable_auto = { Config::verge().data().enable_auto_light_weight_mode }.unwrap_or(false); - if enable_auto && is_silent_start { - logging!(info, Type::Lightweight, true, "自动轻量模式静默启动"); + if enable_auto && !is_silent_start { + logging!( + info, + Type::Lightweight, + true, + "非静默启动直接挂载自动进入轻量模式监听器!" + ); set_lightweight_mode(true); enable_auto_light_weight_mode(); @@ -84,7 +89,7 @@ pub fn is_in_lightweight_mode() -> bool { } // 设置轻量模式状态 -fn set_lightweight_mode(value: bool) { +pub fn set_lightweight_mode(value: bool) { with_lightweight_status(|state| { state.set_lightweight_mode(value); }); @@ -126,7 +131,6 @@ pub fn entry_lightweight_mode() { } #[cfg(target_os = "macos")] AppHandleManager::global().set_activation_policy_accessory(); - logging!(info, Type::Lightweight, true, "轻量模式已开启"); } set_lightweight_mode(true); let _ = cancel_light_weight_timer(); @@ -163,7 +167,6 @@ pub fn exit_lightweight_mode() { } set_lightweight_mode(false); - logging!(info, Type::Lightweight, true, "正在退出轻量模式"); // macOS激活策略 #[cfg(target_os = "macos")] diff --git a/src-tauri/src/utils/resolve.rs b/src-tauri/src/utils/resolve.rs index dada58ec65..06701846f3 100644 --- a/src-tauri/src/utils/resolve.rs +++ b/src-tauri/src/utils/resolve.rs @@ -295,6 +295,7 @@ pub fn create_window(is_show: bool) -> bool { if !is_show { logging!(info, Type::Window, true, "静默模式启动时不创建窗口"); + lightweight::set_lightweight_mode(true); handle::Handle::notify_startup_completed(); return false; } diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index c06cb5190f..f627e213d4 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -86,20 +86,27 @@ pub struct WindowManager; impl WindowManager { pub fn get_main_window_state() -> WindowState { - if let Some(window) = Self::get_main_window() { - if window.is_minimized().unwrap_or(false) { - WindowState::Minimized - } else if window.is_visible().unwrap_or(false) { - if window.is_focused().unwrap_or(false) { + match Self::get_main_window() { + Some(window) => { + let is_minimized = window.is_minimized().unwrap_or(false); + let is_visible = window.is_visible().unwrap_or(false); + let is_focused = window.is_focused().unwrap_or(false); + + if is_minimized { + return WindowState::Minimized; + } + + if !is_visible { + return WindowState::Hidden; + } + + if is_focused { WindowState::VisibleFocused } else { WindowState::VisibleUnfocused } - } else { - WindowState::Hidden } - } else { - WindowState::NotExist + None => WindowState::NotExist, } } @@ -316,17 +323,21 @@ impl WindowManager { pub fn hide_main_window() -> WindowOperationResult { logging!(info, Type::Window, true, "开始隐藏主窗口"); - if let Some(window) = Self::get_main_window() { - if window.hide().is_ok() { - logging!(info, Type::Window, true, "窗口已隐藏"); - WindowOperationResult::Hidden - } else { - logging!(warn, Type::Window, true, "隐藏窗口失败"); - WindowOperationResult::Failed + match Self::get_main_window() { + Some(window) => match window.hide() { + Ok(_) => { + logging!(info, Type::Window, true, "窗口已隐藏"); + WindowOperationResult::Hidden + } + Err(e) => { + logging!(warn, Type::Window, true, "隐藏窗口失败: {}", e); + WindowOperationResult::Failed + } + }, + None => { + logging!(info, Type::Window, true, "窗口不存在,无需隐藏"); + WindowOperationResult::NoAction } - } else { - logging!(info, Type::Window, true, "窗口不存在,无需隐藏"); - WindowOperationResult::NoAction } } From 0a8d6e5147a05bdb1dd73391dfee8a67db22a29e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:30:33 +0800 Subject: [PATCH 15/15] fix: add validation for profile URL format during import --- UPDATELOG.md | 1 + src/locales/en.json | 3 ++- src/locales/ru.json | 3 ++- src/locales/zh.json | 3 ++- src/pages/profiles.tsx | 8 +++++--- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/UPDATELOG.md b/UPDATELOG.md index 76430d0636..fab7ad3620 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -7,6 +7,7 @@ - 修复在轻量模式下快速点击托盘图标带来的竞争态卡死问题 - 修复同时开启静默启动与自动进入轻量模式后,自动进入轻量模式失效的问题 - 修复静默启动时托盘工具栏轻量模式开启与关闭状态的同步 +- 修复导入订阅时非 http 协议链接被错误尝试导入 ### ✨ 新增功能 diff --git a/src/locales/en.json b/src/locales/en.json index eb3f8c6e9d..740911dd53 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -623,5 +623,6 @@ "Originals Only": "Originals Only", "No (IP Banned By Disney+)": "No (IP Banned By Disney+)", "Unsupported Country/Region": "Unsupported Country/Region", - "Failed (Network Connection)": "Failed (Network Connection)" + "Failed (Network Connection)": "Failed (Network Connection)", + "Invalid Profile URL": "Invalid profile URL. Please enter a URL starting with http:// or https://" } diff --git a/src/locales/ru.json b/src/locales/ru.json index f4aa2350ca..0bdeab4871 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -581,5 +581,6 @@ "Originals Only": "Только Originals", "No (IP Banned By Disney+)": "Нет (IP забанен Disney+)", "Unsupported Country/Region": "Страна/регион не поддерживается", - "Failed (Network Connection)": "Ошибка подключения" + "Failed (Network Connection)": "Ошибка подключения", + "Invalid Profile URL": "Недопустимая ссылка на профиль, введите адрес, начинающийся с http:// или https://" } diff --git a/src/locales/zh.json b/src/locales/zh.json index 4689bfe634..aa13fc3b68 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -623,5 +623,6 @@ "Originals Only": "仅限原创", "No (IP Banned By Disney+)": "不支持(IP被Disney+禁止)", "Unsupported Country/Region": "不支持的国家/地区", - "Failed (Network Connection)": "测试失败(网络连接问题)" + "Failed (Network Connection)": "测试失败(网络连接问题)", + "Invalid Profile URL": "无效的订阅链接,请输入以 http:// 或 https:// 开头的地址" } diff --git a/src/pages/profiles.tsx b/src/pages/profiles.tsx index b9d74ac115..71bfd73b27 100644 --- a/src/pages/profiles.tsx +++ b/src/pages/profiles.tsx @@ -227,8 +227,12 @@ const ProfilePage = () => { const onImport = async () => { if (!url) return; + // 校验url是否为http/https + if (!/^https?:\/\//i.test(url)) { + showNotice("error", t("Invalid Profile URL")); + return; + } setLoading(true); - try { // 尝试正常导入 await importProfile(url); @@ -240,14 +244,12 @@ const ProfilePage = () => { // 首次导入失败,尝试使用自身代理 const errmsg = err.message || err.toString(); showNotice("info", t("Import failed, retrying with Clash proxy...")); - try { // 使用自身代理尝试导入 await importProfile(url, { with_proxy: false, self_proxy: true, }); - // 回退导入成功 showNotice("success", t("Profile Imported with Clash proxy")); setUrl("");