From 7fc238c27b52a46ad6a03a88eb130488f2e3e93c Mon Sep 17 00:00:00 2001 From: oomeow Date: Wed, 8 Oct 2025 12:32:40 +0800 Subject: [PATCH] refactor: invock mihomo api by use tauri-plugin-mihomo (#4926) * feat: add tauri-plugin-mihomo * refactor: invock mihomo api by use tauri-plugin-mihomo * chore: todo * chore: update * chore: update * chore: update * chore: update * fix: incorrect delay status and update pretty config * chore: update * chore: remove cache * chore: update * chore: update * fix: app freezed when change group proxy * chore: update * chore: update * chore: add rustfmt.toml to tauri-plugin-mihomo * chore: happy clippy * refactor: connect mihomo websocket * chore: update * chore: update * fix: parse bigint to number * chore: update * Revert "fix: parse bigint to number" This reverts commit 74c006522e23aa52cf8979a8fb47d2b1ae0bb043. * chore: use number instead of bigint * chore: cleanup * fix: rule data not refresh when switch profile * chore: update * chore: cleanup * chore: update * fix: traffic graph data display * feat: add ipc connection pool * chore: update * chore: clippy * fix: incorrect delay status * fix: typo * fix: empty proxies tray menu * chore: clippy * chore: import tauri-plugin-mihomo by using git repo * chore: cleanup * fix: mihomo api * fix: incorrect delay status * chore: update tauri-plugin-mihomo dep chore: update --- .prettierignore | 2 + .prettierrc | 2 +- package.json | 3 +- pnpm-lock.yaml | 11 + src-tauri/Cargo.lock | 141 +++++-- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/desktop.json | 3 +- src-tauri/src/cache/mod.rs | 111 ----- src-tauri/src/cmd/clash.rs | 350 +--------------- src-tauri/src/cmd/profile.rs | 10 +- src-tauri/src/cmd/proxy.rs | 107 +---- src-tauri/src/cmd/system.rs | 4 +- src-tauri/src/core/core.rs | 31 +- src-tauri/src/core/handle.rs | 45 ++- src-tauri/src/core/hotkey.rs | 24 +- src-tauri/src/core/logger.rs | 37 ++ src-tauri/src/core/mod.rs | 1 + src-tauri/src/core/service.rs | 35 +- src-tauri/src/core/sysopt.rs | 9 +- src-tauri/src/core/timer.rs | 21 + src-tauri/src/core/tray/mod.rs | 131 +++--- src-tauri/src/feat/clash.rs | 97 +++-- src-tauri/src/feat/config.rs | 8 +- src-tauri/src/feat/profile.rs | 18 +- src-tauri/src/feat/proxy.rs | 14 +- src-tauri/src/feat/window.rs | 25 +- src-tauri/src/ipc/general.rs | 376 ----------------- src-tauri/src/ipc/logs.rs | 330 --------------- src-tauri/src/ipc/memory.rs | 119 ------ src-tauri/src/ipc/mod.rs | 15 - src-tauri/src/ipc/monitor.rs | 120 ------ src-tauri/src/ipc/traffic.rs | 153 ------- src-tauri/src/lib.rs | 87 ++-- src-tauri/src/module/lightweight.rs | 10 +- src-tauri/src/module/sysinfo.rs | 12 +- src-tauri/src/utils/dirs.rs | 61 +-- src-tauri/src/utils/format.rs | 1 + src-tauri/src/utils/init.rs | 10 +- src-tauri/src/utils/logging.rs | 4 +- src-tauri/src/utils/resolve/dns.rs | 16 +- src-tauri/src/utils/resolve/mod.rs | 9 +- src-tauri/src/utils/resolve/window.rs | 14 +- src-tauri/src/utils/window_manager.rs | 9 +- src-tauri/tauri.conf.json | 7 +- .../connection/connection-detail.tsx | 4 +- src/components/connection/connection-item.tsx | 4 +- src/components/home/clash-info-card.tsx | 2 +- src/components/home/clash-mode-card.tsx | 3 +- src/components/home/current-proxy-card.tsx | 6 +- .../home/enhanced-canvas-traffic-graph.tsx | 8 +- .../home/enhanced-traffic-stats.tsx | 113 ++---- src/components/layout/layout-traffic.tsx | 81 ++-- src/components/proxy/provider-button.tsx | 317 +++++++-------- src/components/proxy/proxy-chain.tsx | 15 +- src/components/proxy/proxy-groups.tsx | 27 +- src/components/proxy/proxy-item-mini.tsx | 8 +- src/components/rule/provider-button.tsx | 219 +++++----- .../setting/mods/clash-core-viewer.tsx | 11 +- .../setting/mods/sysproxy-viewer.tsx | 21 +- src/components/setting/setting-clash.tsx | 11 +- src/components/test/test-item.tsx | 6 +- src/hooks/use-clash.ts | 14 +- src/hooks/use-connection-data.ts | 108 +++++ src/hooks/use-log-data-new.ts | 151 +++++++ src/hooks/use-log-data.ts | 4 +- src/hooks/use-memory-data.ts | 84 ++++ src/hooks/use-profiles.ts | 13 +- src/hooks/use-proxy-selection.ts | 24 +- src/hooks/use-system-proxy-state.ts | 3 +- src/hooks/use-traffic-data.ts | 84 ++++ src/hooks/use-traffic-monitor.ts | 224 ++++++----- src/main.tsx | 15 +- src/pages/_layout.tsx | 75 ++-- src/pages/connections.tsx | 33 +- src/pages/logs.tsx | 54 ++- src/pages/profiles.tsx | 2 +- src/pages/proxies.tsx | 5 +- src/pages/rules.tsx | 2 +- src/providers/app-data-context.ts | 30 +- src/providers/app-data-provider.tsx | 379 +++++++++--------- src/services/cmds.ts | 301 ++------------ src/services/delay.ts | 9 +- src/services/global-log-service.ts | 9 +- src/services/ipc-log-service.ts | 30 +- src/services/states.ts | 21 +- 85 files changed, 1780 insertions(+), 3344 deletions(-) delete mode 100644 src-tauri/src/cache/mod.rs create mode 100644 src-tauri/src/core/logger.rs delete mode 100644 src-tauri/src/ipc/general.rs delete mode 100644 src-tauri/src/ipc/logs.rs delete mode 100644 src-tauri/src/ipc/memory.rs delete mode 100644 src-tauri/src/ipc/mod.rs delete mode 100644 src-tauri/src/ipc/monitor.rs delete mode 100644 src-tauri/src/ipc/traffic.rs create mode 100644 src/hooks/use-connection-data.ts create mode 100644 src/hooks/use-log-data-new.ts create mode 100644 src/hooks/use-memory-data.ts create mode 100644 src/hooks/use-traffic-data.ts diff --git a/.prettierignore b/.prettierignore index 6b1f26e7c..0a3b69148 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,3 +6,5 @@ pnpm-lock.yaml src-tauri/target/ src-tauri/gen/ + +target diff --git a/.prettierrc b/.prettierrc index a96ed6584..909174425 100644 --- a/.prettierrc +++ b/.prettierrc @@ -11,6 +11,6 @@ "arrowParens": "always", "proseWrap": "preserve", "htmlWhitespaceSensitivity": "css", - "endOfLine": "lf", + "endOfLine": "auto", "embeddedLanguageFormatting": "auto" } diff --git a/package.json b/package.json index a4159513f..bc15e2cb3 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "react-virtuoso": "^4.14.1", "swr": "^2.3.6", "types-pac": "^1.0.3", - "zustand": "^5.0.8" + "zustand": "^5.0.8", + "tauri-plugin-mihomo-api": "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo" }, "devDependencies": { "@actions/github": "^6.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 42ae30f26..da01e942d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,9 @@ importers: swr: specifier: ^2.3.6 version: 2.3.6(react@19.2.0) + tauri-plugin-mihomo-api: + specifier: git+https://github.com/clash-verge-rev/tauri-plugin-mihomo + version: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/9553c69b0063a3be04e39b5539cc5d6f0e9529c1 types-pac: specifier: ^1.0.3 version: 1.0.3 @@ -3856,6 +3859,10 @@ packages: resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} engines: {node: '>=18'} + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/9553c69b0063a3be04e39b5539cc5d6f0e9529c1: + resolution: {tarball: https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/9553c69b0063a3be04e39b5539cc5d6f0e9529c1} + version: 0.1.0 + terser@5.44.0: resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==} engines: {node: '>=10'} @@ -8412,6 +8419,10 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 + tauri-plugin-mihomo-api@https://codeload.github.com/clash-verge-rev/tauri-plugin-mihomo/tar.gz/9553c69b0063a3be04e39b5539cc5d6f0e9529c1: + dependencies: + '@tauri-apps/api': 2.8.0 + terser@5.44.0: dependencies: '@jridgewell/source-map': 0.3.6 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f0bc4fe5c..73885d218 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1147,6 +1147,7 @@ dependencies = [ "tauri-plugin-fs", "tauri-plugin-global-shortcut", "tauri-plugin-http", + "tauri-plugin-mihomo", "tauri-plugin-notification", "tauri-plugin-process", "tauri-plugin-shell", @@ -1654,6 +1655,12 @@ dependencies = [ "parking_lot_core 0.9.12", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "data-url" version = "0.3.2" @@ -2857,12 +2864,6 @@ dependencies = [ "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" - [[package]] name = "hdrhistogram" version = "7.5.4" @@ -3063,7 +3064,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -3189,7 +3190,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.0", + "windows-core 0.61.2", ] [[package]] @@ -3474,7 +3475,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.15.5", "serde", "serde_core", ] @@ -7305,6 +7306,31 @@ dependencies = [ "urlpattern", ] +[[package]] +name = "tauri-plugin-mihomo" +version = "0.1.0" +source = "git+https://github.com/clash-verge-rev/tauri-plugin-mihomo#0b89eaeb4d592cfeb44fd33ad07b1ef40b6f0671" +dependencies = [ + "base64 0.22.1", + "futures-util", + "http 1.3.1", + "httparse", + "log", + "pin-project", + "rand 0.9.2", + "reqwest", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.16", + "tokio", + "tokio-tungstenite", + "ts-rs", + "urlencoding", + "windows-sys 0.61.0", +] + [[package]] name = "tauri-plugin-notification" version = "2.3.1" @@ -7539,6 +7565,15 @@ dependencies = [ "utf-8", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminfo" version = "0.7.5" @@ -7823,6 +7858,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite", +] + [[package]] name = "tokio-util" version = "0.7.16" @@ -8217,6 +8264,45 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ts-rs" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef1b7a6d914a34127ed8e1fa927eb7088903787bcded4fa3eef8f85ee1568be" +dependencies = [ + "thiserror 2.0.16", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ed7b4c18cc150a6a0a1e9ea1ecfa688791220781af6e119f9599a8502a0a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "termcolor", +] + +[[package]] +name = "tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" +dependencies = [ + "bytes", + "data-encoding", + "http 1.3.1", + "httparse", + "log", + "rand 0.9.2", + "sha1", + "thiserror 2.0.16", + "utf-8", +] + [[package]] name = "typeid" version = "1.0.3" @@ -8339,6 +8425,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -8929,19 +9021,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-core" -version = "0.62.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" -dependencies = [ - "windows-implement 0.60.0", - "windows-interface 0.59.1", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", -] - [[package]] name = "windows-future" version = "0.2.1" @@ -9048,15 +9127,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" -dependencies = [ - "windows-link 0.2.0", -] - [[package]] name = "windows-strings" version = "0.1.0" @@ -9076,15 +9146,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" -dependencies = [ - "windows-link 0.2.0", -] - [[package]] name = "windows-sys" version = "0.45.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fb014170a..0d57e7a44 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -86,6 +86,7 @@ cfg-if = "1.0.3" nu-ansi-term = { version = "0.50.1", optional = true } console-subscriber = { version = "0.4.1", optional = true } tauri-plugin-devtools = { version = "2.0.1" } +tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" } [target.'cfg(windows)'.dependencies] diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json index 7a09c952f..180d5cefa 100755 --- a/src-tauri/capabilities/desktop.json +++ b/src-tauri/capabilities/desktop.json @@ -24,6 +24,7 @@ { "identifier": "http:default", "allow": [{ "url": "https://*/*" }, { "url": "http://*/*" }] - } + }, + "mihomo:default" ] } diff --git a/src-tauri/src/cache/mod.rs b/src-tauri/src/cache/mod.rs deleted file mode 100644 index cea404c14..000000000 --- a/src-tauri/src/cache/mod.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::singleton; -use anyhow::Result; -use dashmap::DashMap; -use serde_json::Value; -use std::sync::Arc; -use std::time::{Duration, Instant}; -use tokio::sync::OnceCell; - -pub const SHORT_TERM_TTL: Duration = Duration::from_millis(4_250); - -pub struct CacheEntry { - pub value: Arc, - pub expires_at: Instant, -} - -pub struct Cache { - pub map: DashMap>>>>, -} - -impl Cache { - fn new() -> Self { - Cache { - map: DashMap::new(), - } - } - - pub fn make_key(prefix: &str, id: &str) -> String { - format!("{prefix}:{id}") - } - - pub async fn get_or_fetch(&self, key: String, ttl: Duration, fetch_fn: F) -> Arc - where - F: Fn() -> Fut + Send + Sync + 'static, - Fut: std::future::Future + Send + 'static, - T: Send + Sync + 'static, - { - loop { - let now = Instant::now(); - let key_cloned = key.clone(); - - // Get or create the cell - let cell = self - .map - .entry(key_cloned.clone()) - .or_insert_with(|| Arc::new(OnceCell::new())) - .clone(); - - // Check if we have a valid cached entry - if let Some(entry) = cell.get() { - if entry.expires_at > now { - return Arc::clone(&entry.value); - } - // Entry is expired, remove it - self.map - .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); - continue; // Retry with fresh cell - } - - // Try to set a new value - let value = fetch_fn().await; - let entry = Box::new(CacheEntry { - value: Arc::new(value), - expires_at: Instant::now() + ttl, - }); - - match cell.set(entry) { - Ok(_) => { - // Successfully set the value, it must exist now - if let Some(set_entry) = cell.get() { - return Arc::clone(&set_entry.value); - } - } - Err(_) => { - if let Some(existing_entry) = cell.get() { - if existing_entry.expires_at > Instant::now() { - return Arc::clone(&existing_entry.value); - } - self.map - .remove_if(&key_cloned, |_, v| Arc::ptr_eq(v, &cell)); - } - } - } - } - } - - // pub fn clean_key(&self, key: &str) { - // self.map.remove(key); - // } - - // TODO - pub fn clean_default_keys(&self) { - // logging!(info, Type::Cache, "Cleaning proxies keys"); - // let proxies_key = Self::make_key("proxies", "default"); - // self.map.remove(&proxies_key); - - // logging!(info, Type::Cache, "Cleaning providers keys"); - // let providers_key = Self::make_key("providers", "default"); - // self.map.remove(&providers_key); - - // !The frontend goes crash if we clean the clash_config cache - // logging!(info, Type::Cache, "Cleaning clash config keys"); - // let clash_config_key = Self::make_key("clash_config", "default"); - // self.map.remove(&clash_config_key); - } -} - -pub type CacheService = Cache>; -pub type CacheProxy = Cache; - -singleton!(Cache, PROXY_INSTANCE); -singleton!(Cache>, SERVICE_INSTANCE); diff --git a/src-tauri/src/cmd/clash.rs b/src-tauri/src/cmd/clash.rs index 0081f9171..9fdb4c447 100644 --- a/src-tauri/src/cmd/clash.rs +++ b/src-tauri/src/cmd/clash.rs @@ -1,21 +1,15 @@ +use std::collections::VecDeque; + use super::CmdResult; use crate::{ - cache::CacheProxy, config::Config, - core::{CoreManager, handle}, -}; -use crate::{ - config::*, - feat, - ipc::{self, IpcManager}, - logging, - utils::logging::Type, - wrap_err, + core::{self, CoreManager, RunningMode, handle, logger}, }; +use crate::{config::*, feat, logging, utils::logging::Type, wrap_err}; use serde_yaml_ng::Mapping; -use std::time::Duration; +// use std::time::Duration; -const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60); +// const CONFIG_REFRESH_INTERVAL: Duration = Duration::from_secs(60); /// 复制Clash环境变量 #[tauri::command] @@ -112,20 +106,6 @@ pub async fn restart_core() -> CmdResult { result } -/// 获取代理延迟 -#[tauri::command] -pub async fn clash_api_get_proxy_delay( - name: String, - url: Option, - timeout: i32, -) -> CmdResult { - wrap_err!( - IpcManager::global() - .test_proxy_delay(&name, url, timeout) - .await - ) -} - /// 测试URL延迟 #[tauri::command] pub async fn test_delay(url: String) -> CmdResult { @@ -307,317 +287,13 @@ pub async fn validate_dns_config() -> CmdResult<(bool, String)> { } } -/// 获取Clash版本信息 #[tauri::command] -pub async fn get_clash_version() -> CmdResult { - wrap_err!(IpcManager::global().get_version().await) -} - -/// 获取Clash配置 -#[tauri::command] -pub async fn get_clash_config() -> CmdResult { - let manager = IpcManager::global(); - let cache = CacheProxy::global(); - let key = CacheProxy::make_key("clash_config", "default"); - let value = cache - .get_or_fetch(key, CONFIG_REFRESH_INTERVAL, || async { - manager.get_config().await.unwrap_or_else(|e| { - logging!(error, Type::Cmd, "Failed to fetch clash config: {e}"); - serde_json::Value::Object(serde_json::Map::new()) - }) - }) - .await; - Ok((*value).clone()) -} - -/// 强制刷新Clash配置缓存 -#[tauri::command] -pub async fn force_refresh_clash_config() -> CmdResult { - let cache = CacheProxy::global(); - let key = CacheProxy::make_key("clash_config", "default"); - cache.map.remove(&key); - get_clash_config().await -} - -/// 更新地理数据 -#[tauri::command] -pub async fn update_geo_data() -> CmdResult { - wrap_err!(IpcManager::global().update_geo_data().await) -} - -/// 升级Clash核心 -#[tauri::command] -pub async fn upgrade_clash_core() -> CmdResult { - wrap_err!(IpcManager::global().upgrade_core().await) -} - -/// 获取规则 -#[tauri::command] -pub async fn get_clash_rules() -> CmdResult { - wrap_err!(IpcManager::global().get_rules().await) -} - -/// 更新代理选择 -#[tauri::command] -pub async fn update_proxy_choice(group: String, proxy: String) -> CmdResult { - wrap_err!(IpcManager::global().update_proxy(&group, &proxy).await) -} - -/// 获取代理提供者 -#[tauri::command] -pub async fn get_proxy_providers() -> CmdResult { - wrap_err!(IpcManager::global().get_providers_proxies().await) -} - -/// 获取规则提供者 -#[tauri::command] -pub async fn get_rule_providers() -> CmdResult { - wrap_err!(IpcManager::global().get_rule_providers().await) -} - -/// 代理提供者健康检查 -#[tauri::command] -pub async fn proxy_provider_health_check(name: String) -> CmdResult { - wrap_err!( - IpcManager::global() - .proxy_provider_health_check(&name) - .await - ) -} - -/// 更新代理提供者 -#[tauri::command] -pub async fn update_proxy_provider(name: String) -> CmdResult { - wrap_err!(IpcManager::global().update_proxy_provider(&name).await) -} - -/// 更新规则提供者 -#[tauri::command] -pub async fn update_rule_provider(name: String) -> CmdResult { - wrap_err!(IpcManager::global().update_rule_provider(&name).await) -} - -/// 获取连接 -#[tauri::command] -pub async fn get_clash_connections() -> CmdResult { - wrap_err!(IpcManager::global().get_connections().await) -} - -/// 删除连接 -#[tauri::command] -pub async fn delete_clash_connection(id: String) -> CmdResult { - wrap_err!(IpcManager::global().delete_connection(&id).await) -} - -/// 关闭所有连接 -#[tauri::command] -pub async fn close_all_clash_connections() -> CmdResult { - wrap_err!(IpcManager::global().close_all_connections().await) -} - -/// 获取流量数据 (使用新的IPC流式监控) -#[tauri::command] -pub async fn get_traffic_data() -> CmdResult { - let traffic = crate::ipc::get_current_traffic().await; - let result = serde_json::json!({ - "up": traffic.total_up, - "down": traffic.total_down, - "up_rate": traffic.up_rate, - "down_rate": traffic.down_rate, - "last_updated": traffic.last_updated.elapsed().as_secs() - }); - Ok(result) -} - -/// 获取内存数据 (使用新的IPC流式监控) -#[tauri::command] -pub async fn get_memory_data() -> CmdResult { - let memory = crate::ipc::get_current_memory().await; - let usage_percent = if memory.oslimit > 0 { - (memory.inuse as f64 / memory.oslimit as f64) * 100.0 - } else { - 0.0 +pub async fn get_clash_logs() -> CmdResult> { + let logs = match core::CoreManager::global().get_running_mode() { + // TODO: 服务模式下日志获取接口 + RunningMode::Service => VecDeque::new(), + RunningMode::Sidecar => logger::Logger::global().get_logs().clone(), + _ => VecDeque::new(), }; - let result = serde_json::json!({ - "inuse": memory.inuse, - "oslimit": memory.oslimit, - "usage_percent": usage_percent, - "last_updated": memory.last_updated.elapsed().as_secs() - }); - Ok(result) -} - -/// 启动流量监控服务 (IPC流式监控自动启动,此函数为兼容性保留) -#[tauri::command] -pub async fn start_traffic_service() -> CmdResult { - logging!(trace, Type::Ipc, "启动流量监控服务 (IPC流式监控)"); - // 新的IPC监控在首次访问时自动启动 - // 触发一次访问以确保监控器已初始化 - let _ = crate::ipc::get_current_traffic().await; - let _ = crate::ipc::get_current_memory().await; - logging!(info, Type::Ipc, "IPC流式监控已激活"); - Ok(()) -} - -/// 停止流量监控服务 (IPC流式监控无需显式停止,此函数为兼容性保留) -#[tauri::command] -pub async fn stop_traffic_service() -> CmdResult { - logging!(trace, Type::Ipc, "停止流量监控服务请求 (IPC流式监控)"); - // 新的IPC监控是持久的,无需显式停止 - logging!(info, Type::Ipc, "IPC流式监控继续运行"); - Ok(()) -} - -/// 获取格式化的流量数据 (包含单位,便于前端显示) -#[tauri::command] -pub async fn get_formatted_traffic_data() -> CmdResult { - logging!(trace, Type::Ipc, "获取格式化流量数据"); - let (up_rate, down_rate, total_up, total_down, is_fresh) = - crate::ipc::get_formatted_traffic().await; - let result = serde_json::json!({ - "up_rate_formatted": up_rate, - "down_rate_formatted": down_rate, - "total_up_formatted": total_up, - "total_down_formatted": total_down, - "is_fresh": is_fresh - }); - logging!( - debug, - Type::Ipc, - "格式化流量数据: ↑{up_rate}/s ↓{down_rate}/s (总计: ↑{total_up} ↓{total_down})" - ); - Ok(result) -} - -/// 获取格式化的内存数据 (包含单位,便于前端显示) -#[tauri::command] -pub async fn get_formatted_memory_data() -> CmdResult { - logging!(info, Type::Ipc, "获取格式化内存数据"); - let (inuse, oslimit, usage_percent, is_fresh) = crate::ipc::get_formatted_memory().await; - let result = serde_json::json!({ - "inuse_formatted": inuse, - "oslimit_formatted": oslimit, - "usage_percent": usage_percent, - "is_fresh": is_fresh - }); - logging!( - debug, - Type::Ipc, - "格式化内存数据: {inuse} / {oslimit} ({usage_percent:.1}%)" - ); - Ok(result) -} - -/// 获取系统监控概览 (流量+内存,便于前端一次性获取所有状态) -#[tauri::command] -pub async fn get_system_monitor_overview() -> CmdResult { - logging!(debug, Type::Ipc, "获取系统监控概览"); - - // 并发获取流量和内存数据 - let (traffic, memory) = tokio::join!( - crate::ipc::get_current_traffic(), - crate::ipc::get_current_memory() - ); - - let (traffic_formatted, memory_formatted) = tokio::join!( - crate::ipc::get_formatted_traffic(), - crate::ipc::get_formatted_memory() - ); - - let traffic_is_fresh = traffic.last_updated.elapsed().as_secs() < 5; - let memory_is_fresh = memory.last_updated.elapsed().as_secs() < 10; - - let result = serde_json::json!({ - "traffic": { - "raw": { - "up": traffic.total_up, - "down": traffic.total_down, - "up_rate": traffic.up_rate, - "down_rate": traffic.down_rate - }, - "formatted": { - "up_rate": traffic_formatted.0, - "down_rate": traffic_formatted.1, - "total_up": traffic_formatted.2, - "total_down": traffic_formatted.3 - }, - "is_fresh": traffic_is_fresh - }, - "memory": { - "raw": { - "inuse": memory.inuse, - "oslimit": memory.oslimit, - "usage_percent": if memory.oslimit > 0 { - (memory.inuse as f64 / memory.oslimit as f64) * 100.0 - } else { - 0.0 - } - }, - "formatted": { - "inuse": memory_formatted.0, - "oslimit": memory_formatted.1, - "usage_percent": memory_formatted.2 - }, - "is_fresh": memory_is_fresh - }, - "overall_status": if traffic_is_fresh && memory_is_fresh { "healthy" } else { "stale" } - }); - - Ok(result) -} - -/// 获取代理组延迟 -#[tauri::command] -pub async fn get_group_proxy_delays( - group_name: String, - url: Option, - timeout: Option, -) -> CmdResult { - wrap_err!( - IpcManager::global() - .get_group_proxy_delays(&group_name, url, timeout.unwrap_or(10000)) - .await - ) -} - -/// 检查调试是否启用 -#[tauri::command] -pub async fn is_clash_debug_enabled() -> CmdResult { - match IpcManager::global().is_debug_enabled().await { - Ok(enabled) => Ok(enabled), - Err(_) => Ok(false), - } -} - -/// 垃圾回收 -#[tauri::command] -pub async fn clash_gc() -> CmdResult { - wrap_err!(IpcManager::global().gc().await) -} - -/// 获取日志 (使用新的流式实现) -#[tauri::command] -pub async fn get_clash_logs() -> CmdResult { - Ok(ipc::get_logs_json().await) -} - -/// 启动日志监控 -#[tauri::command] -pub async fn start_logs_monitoring(level: Option) -> CmdResult { - ipc::start_logs_monitoring(level).await; - Ok(()) -} - -/// 停止日志监控 -#[tauri::command] -pub async fn stop_logs_monitoring() -> CmdResult { - ipc::stop_logs_monitoring().await; - Ok(()) -} - -/// 清除日志 -#[tauri::command] -pub async fn clear_logs() -> CmdResult { - ipc::clear_logs().await; - Ok(()) + Ok(logs) } diff --git a/src-tauri/src/cmd/profile.rs b/src-tauri/src/cmd/profile.rs index c1a098e70..d8e25c661 100644 --- a/src-tauri/src/cmd/profile.rs +++ b/src-tauri/src/cmd/profile.rs @@ -503,11 +503,11 @@ pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult { handle::Handle::refresh_clash(); // 强制刷新代理缓存,确保profile切换后立即获取最新节点数据 - crate::process::AsyncHandler::spawn(|| async move { - if let Err(e) = super::proxy::force_refresh_proxies().await { - log::warn!(target: "app", "强制刷新代理缓存失败: {e}"); - } - }); + // crate::process::AsyncHandler::spawn(|| async move { + // if let Err(e) = super::proxy::force_refresh_proxies().await { + // log::warn!(target: "app", "强制刷新代理缓存失败: {e}"); + // } + // }); if let Err(e) = Tray::global().update_tooltip().await { log::warn!(target: "app", "异步更新托盘提示失败: {e}"); diff --git a/src-tauri/src/cmd/proxy.rs b/src-tauri/src/cmd/proxy.rs index 387da3ad7..a5b34cf46 100644 --- a/src-tauri/src/cmd/proxy.rs +++ b/src-tauri/src/cmd/proxy.rs @@ -1,59 +1,7 @@ -use tauri::Emitter; - use super::CmdResult; -use crate::{ - cache::CacheProxy, - core::{handle::Handle, tray::Tray}, - ipc::IpcManager, - logging, - utils::logging::Type, -}; -use std::time::Duration; - -const PROXIES_REFRESH_INTERVAL: Duration = Duration::from_secs(60); -const PROVIDERS_REFRESH_INTERVAL: Duration = Duration::from_secs(60); - -#[tauri::command] -pub async fn get_proxies() -> CmdResult { - let cache = CacheProxy::global(); - let key = CacheProxy::make_key("proxies", "default"); - let value = cache - .get_or_fetch(key, PROXIES_REFRESH_INTERVAL, || async { - let manager = IpcManager::global(); - manager.get_proxies().await.unwrap_or_else(|e| { - logging!(error, Type::Cmd, "Failed to fetch proxies: {e}"); - serde_json::Value::Object(serde_json::Map::new()) - }) - }) - .await; - Ok((*value).clone()) -} - -/// 强制刷新代理缓存用于profile切换 -#[tauri::command] -pub async fn force_refresh_proxies() -> CmdResult { - let cache = CacheProxy::global(); - let key = CacheProxy::make_key("proxies", "default"); - cache.map.remove(&key); - get_proxies().await -} - -#[tauri::command] -pub async fn get_providers_proxies() -> CmdResult { - let cache = CacheProxy::global(); - let key = CacheProxy::make_key("providers", "default"); - let value = cache - .get_or_fetch(key, PROVIDERS_REFRESH_INTERVAL, || async { - let manager = IpcManager::global(); - manager.get_providers_proxies().await.unwrap_or_else(|e| { - logging!(error, Type::Cmd, "Failed to fetch provider proxies: {e}"); - serde_json::Value::Object(serde_json::Map::new()) - }) - }) - .await; - Ok((*value).clone()) -} +use crate::{logging, utils::logging::Type}; +// TODO: 前端通过 emit 发送更新事件, tray 监听更新事件 /// 同步托盘和GUI的代理选择状态 #[tauri::command] pub async fn sync_tray_proxy_selection() -> CmdResult<()> { @@ -70,54 +18,3 @@ pub async fn sync_tray_proxy_selection() -> CmdResult<()> { } } } - -/// 更新代理选择并同步托盘和GUI状态 -#[tauri::command] -pub async fn update_proxy_and_sync(group: String, proxy: String) -> CmdResult<()> { - match IpcManager::global().update_proxy(&group, &proxy).await { - Ok(_) => { - // println!("Proxy updated successfully: {} -> {}", group,proxy); - logging!( - info, - Type::Cmd, - "Proxy updated successfully: {} -> {}", - group, - proxy - ); - - let cache = CacheProxy::global(); - let key = CacheProxy::make_key("proxies", "default"); - cache.map.remove(&key); - - if let Err(e) = Tray::global().update_menu().await { - logging!(error, Type::Cmd, "Failed to sync tray menu: {}", e); - } - - if let Some(app_handle) = Handle::global().app_handle() { - let _ = app_handle.emit("verge://force-refresh-proxies", ()); - let _ = app_handle.emit("verge://refresh-proxy-config", ()); - } - - logging!( - info, - Type::Cmd, - "Proxy and sync completed successfully: {} -> {}", - group, - proxy - ); - Ok(()) - } - Err(e) => { - println!("1111111111111111"); - logging!( - error, - Type::Cmd, - "Failed to update proxy: {} -> {}, error: {}", - group, - proxy, - e - ); - Err(e.to_string()) - } - } -} diff --git a/src-tauri/src/cmd/system.rs b/src-tauri/src/cmd/system.rs index c3d2ab412..2ef6cf723 100644 --- a/src-tauri/src/cmd/system.rs +++ b/src-tauri/src/cmd/system.rs @@ -28,9 +28,7 @@ pub async fn export_diagnostic_info() -> CmdResult<()> { let sysinfo = PlatformSpecification::new_sync(); let info = format!("{sysinfo:?}"); - let app_handle = handle::Handle::global() - .app_handle() - .ok_or("Failed to get app handle")?; + let app_handle = handle::Handle::app_handle(); let cliboard = app_handle.clipboard(); if cliboard.write_text(info).is_err() { logging!(error, Type::System, "Failed to write to clipboard"); diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index ff1e1cc0d..4ae10993b 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -1,11 +1,11 @@ use crate::AsyncHandler; +use crate::core::logger::Logger; use crate::{ config::*, core::{ handle, service::{self, SERVICE_MANAGER, ServiceStatus}, }, - ipc::IpcManager, logging, logging_error, singleton_lazy, utils::{ dirs, @@ -25,6 +25,10 @@ use std::{ }; use tauri_plugin_shell::{ShellExt, process::CommandChild}; +// TODO: +// - 重构,提升模式切换速度 +// - 内核启动添加启动 IPC 启动参数, `-ext-ctl-unix` / `-ext-ctl-pipe`, 运行时配置需要删除相关配置项 + #[derive(Debug)] pub struct CoreManager { running: Arc>, @@ -250,11 +254,7 @@ impl CoreManager { let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); logging!(info, Type::Config, true, "使用内核: {}", clash_core); - let app_handle = handle::Handle::global().app_handle().ok_or_else(|| { - let msg = "Failed to get app handle"; - logging!(error, Type::Core, true, "{}", msg); - anyhow::anyhow!(msg) - })?; + let app_handle = handle::Handle::app_handle(); let app_dir = dirs::app_home_dir()?; let app_dir_str = dirs::path_to_str(&app_dir)?; logging!(info, Type::Config, true, "验证目录: {}", app_dir_str); @@ -414,7 +414,11 @@ impl CoreManager { logging_error!(Type::Core, true, "{}", msg); msg }); - match IpcManager::global().put_configs_force(run_path_str?).await { + match handle::Handle::mihomo() + .await + .reload_config(true, run_path_str?) + .await + { Ok(_) => { Config::runtime().await.apply(); logging!(info, Type::Core, true, "Configuration updated successfully"); @@ -733,9 +737,7 @@ impl CoreManager { logging!(info, Type::Core, true, "Running core by sidecar"); let config_file = &Config::generate_file(ConfigType::Run).await?; - let app_handle = handle::Handle::global() - .app_handle() - .ok_or(anyhow::anyhow!("failed to get app handle"))?; + let app_handle = handle::Handle::app_handle(); let clash_core = Config::verge().await.latest_ref().get_valid_clash_core(); let config_dir = dirs::app_home_dir()?; @@ -774,12 +776,16 @@ impl CoreManager { while let Some(event) = rx.recv().await { match event { tauri_plugin_shell::process::CommandEvent::Stdout(line) => { - if let Err(e) = writeln!(log_file, "{}", String::from_utf8_lossy(&line)) { + let line = String::from_utf8_lossy(&line); + Logger::global().append_log(line.to_string()); + if let Err(e) = writeln!(log_file, "{}", line) { eprintln!("[Sidecar] write stdout failed: {e}"); } } tauri_plugin_shell::process::CommandEvent::Stderr(line) => { - let _ = writeln!(log_file, "[stderr] {}", String::from_utf8_lossy(&line)); + let line = String::from_utf8_lossy(&line); + Logger::global().append_log(line.to_string()); + let _ = writeln!(log_file, "[stderr] {}", line); } tauri_plugin_shell::process::CommandEvent::Terminated(term) => { let _ = writeln!(log_file, "[terminated] {:?}", term); @@ -900,6 +906,7 @@ impl CoreManager { /// 停止核心运行 pub async fn stop_core(&self) -> Result<()> { + Logger::global().clear_logs(); match self.get_running_mode() { RunningMode::Service => self.stop_core_by_service().await, RunningMode::Sidecar => self.stop_core_by_sidecar(), diff --git a/src-tauri/src/core/handle.rs b/src-tauri/src/core/handle.rs index 6ac0b4f7f..ad79b65bf 100644 --- a/src-tauri/src/core/handle.rs +++ b/src-tauri/src/core/handle.rs @@ -1,4 +1,4 @@ -use crate::singleton; +use crate::{APP_HANDLE, singleton}; use parking_lot::RwLock; use std::{ sync::{ @@ -10,6 +10,8 @@ use std::{ time::{Duration, Instant}, }; use tauri::{AppHandle, Emitter, Manager, WebviewWindow}; +use tauri_plugin_mihomo::{Mihomo, MihomoExt}; +use tokio::sync::{RwLockReadGuard, RwLockWriteGuard}; use crate::{logging, utils::logging::Type}; @@ -107,7 +109,7 @@ impl NotificationSystem { continue; } - if let Some(window) = handle.get_window() { + if let Some(window) = Handle::get_window() { *system.last_emit_time.write() = Instant::now(); let (event_name_str, payload_result) = match event { @@ -249,7 +251,6 @@ impl NotificationSystem { #[derive(Debug, Clone)] pub struct Handle { - pub app_handle: Arc>>, pub is_exiting: Arc>, startup_errors: Arc>>, startup_completed: Arc>, @@ -259,7 +260,6 @@ pub struct Handle { impl Default for Handle { fn default() -> Self { Self { - app_handle: Arc::new(RwLock::new(None)), is_exiting: Arc::new(RwLock::new(false)), startup_errors: Arc::new(RwLock::new(Vec::new())), startup_completed: Arc::new(RwLock::new(false)), @@ -276,18 +276,13 @@ impl Handle { Self::default() } - pub fn init(&self, app_handle: AppHandle) { + pub fn init(&self) { // 如果正在退出,不要重新初始化 if self.is_exiting() { log::debug!("Handle::init called while exiting, skipping initialization"); return; } - { - let mut handle = self.app_handle.write(); - *handle = Some(app_handle); - } - let mut system_opt = self.notification_system.write(); if let Some(system) = system_opt.as_mut() { // 只在未运行时启动 @@ -300,12 +295,22 @@ impl Handle { } /// 获取 AppHandle - pub fn app_handle(&self) -> Option { - self.app_handle.read().clone() + #[allow(clippy::expect_used)] + pub fn app_handle() -> &'static AppHandle { + APP_HANDLE.get().expect("failed to get global app handle") } - pub fn get_window(&self) -> Option { - let app_handle = self.app_handle()?; + pub async fn mihomo() -> RwLockReadGuard<'static, Mihomo> { + Self::app_handle().mihomo().read().await + } + + #[allow(unused)] + pub async fn mihomo_mut() -> RwLockWriteGuard<'static, Mihomo> { + Self::app_handle().mihomo().write().await + } + + pub fn get_window() -> Option { + let app_handle = Self::app_handle(); let window: Option = app_handle.get_webview_window("main"); if window.is_none() { log::debug!(target:"app", "main window not found"); @@ -520,14 +525,10 @@ impl Handle { #[cfg(target_os = "macos")] impl Handle { pub fn set_activation_policy(&self, policy: tauri::ActivationPolicy) -> Result<(), String> { - let app_handle = self.app_handle(); - if let Some(app_handle) = app_handle.as_ref() { - app_handle - .set_activation_policy(policy) - .map_err(|e| e.to_string()) - } else { - Err("AppHandle not initialized".to_string()) - } + let app_handle = Self::app_handle(); + app_handle + .set_activation_policy(policy) + .map_err(|e| e.to_string()) } pub fn set_activation_policy_regular(&self) { diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 67258242d..6802ac9e5 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -200,9 +200,7 @@ impl Hotkey { hotkey: &str, function: HotkeyFunction, ) -> Result<()> { - let app_handle = handle::Handle::global() - .app_handle() - .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?; + let app_handle = handle::Handle::app_handle(); let manager = app_handle.global_shortcut(); logging!( @@ -375,9 +373,7 @@ impl Hotkey { } pub fn reset(&self) -> Result<()> { - let app_handle = handle::Handle::global() - .app_handle() - .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?; + let app_handle = handle::Handle::app_handle(); let manager = app_handle.global_shortcut(); manager.unregister_all()?; Ok(()) @@ -390,9 +386,7 @@ impl Hotkey { } pub fn unregister(&self, hotkey: &str) -> Result<()> { - let app_handle = handle::Handle::global() - .app_handle() - .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for hotkey registration"))?; + let app_handle = handle::Handle::app_handle(); let manager = app_handle.global_shortcut(); manager.unregister(hotkey)?; logging!(debug, Type::Hotkey, "Unregister hotkey {}", hotkey); @@ -468,17 +462,7 @@ impl Hotkey { impl Drop for Hotkey { fn drop(&mut self) { - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - logging!( - error, - Type::Hotkey, - "Failed to get app handle during hotkey cleanup" - ); - return; - } - }; + let app_handle = handle::Handle::app_handle(); if let Err(e) = app_handle.global_shortcut().unregister_all() { logging!( error, diff --git a/src-tauri/src/core/logger.rs b/src-tauri/src/core/logger.rs new file mode 100644 index 000000000..7eb30479c --- /dev/null +++ b/src-tauri/src/core/logger.rs @@ -0,0 +1,37 @@ +use std::{collections::VecDeque, sync::Arc}; + +use once_cell::sync::OnceCell; +use parking_lot::{RwLock, RwLockReadGuard}; + +const LOGS_QUEUE_LEN: usize = 100; + +pub struct Logger { + logs: Arc>>, +} + +impl Logger { + pub fn global() -> &'static Logger { + static LOGGER: OnceCell = OnceCell::new(); + + LOGGER.get_or_init(|| Logger { + logs: Arc::new(RwLock::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))), + }) + } + + pub fn get_logs(&self) -> RwLockReadGuard<'_, VecDeque> { + self.logs.read() + } + + pub fn append_log(&self, text: String) { + let mut logs = self.logs.write(); + if logs.len() > LOGS_QUEUE_LEN { + logs.pop_front(); + } + logs.push_back(text); + } + + pub fn clear_logs(&self) { + let mut logs = self.logs.write(); + logs.clear(); + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 044abaf9c..5972458b9 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -5,6 +5,7 @@ mod core; pub mod event_driven_proxy; pub mod handle; pub mod hotkey; +pub mod logger; pub mod service; pub mod service_ipc; pub mod sysopt; diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 3312f6c06..2f6062fc6 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -1,5 +1,4 @@ use crate::{ - cache::{CacheService, SHORT_TERM_TTL}, config::Config, core::service_ipc::{IpcCommand, send_ipc_request}, logging, logging_error, @@ -333,28 +332,24 @@ pub async fn force_reinstall_service() -> Result<()> { /// 检查服务版本 - 使用IPC通信 async fn check_service_version() -> Result { - let cache = CacheService::global(); - let key = CacheService::make_key("service", "version"); - let version_arc = cache - .get_or_fetch(key, SHORT_TERM_TTL, || async { - logging!(info, Type::Service, true, "开始检查服务版本 (IPC)"); - let payload = serde_json::json!({}); - let response = send_ipc_request(IpcCommand::GetVersion, payload).await?; + let version_arc: Result = { + logging!(info, Type::Service, true, "开始检查服务版本 (IPC)"); + let payload = serde_json::json!({}); + let response = send_ipc_request(IpcCommand::GetVersion, payload).await?; - let data = response - .data - .ok_or_else(|| anyhow::anyhow!("服务版本响应中没有数据"))?; - - if let Some(nested_data) = data.get("data") - && let Some(version) = nested_data.get("version").and_then(|v| v.as_str()) - { - // logging!(info, Type::Service, true, "获取到服务版本: {}", version); - return Ok(version.to_string()); - } + let data = response + .data + .ok_or_else(|| anyhow::anyhow!("服务版本响应中没有数据"))?; + if let Some(nested_data) = data.get("data") + && let Some(version) = nested_data.get("version").and_then(|v| v.as_str()) + { + // logging!(info, Type::Service, true, "获取到服务版本: {}", version); + Ok(version.to_string()) + } else { Ok("unknown".to_string()) - }) - .await; + } + }; match version_arc.as_ref() { Ok(v) => Ok(v.clone()), diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 4aa518995..c25a8c27f 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -262,10 +262,7 @@ impl Sysopt { /// 尝试使用原来的自启动方法 fn try_original_autostart_method(&self, is_enable: bool) { - let Some(app_handle) = Handle::global().app_handle() else { - log::error!(target: "app", "App handle not available for autostart"); - return; - }; + let app_handle = Handle::app_handle(); let autostart_manager = app_handle.autolaunch(); if is_enable { @@ -292,9 +289,7 @@ impl Sysopt { } // 回退到原来的方法 - let app_handle = Handle::global() - .app_handle() - .ok_or_else(|| anyhow::anyhow!("App handle not available"))?; + let app_handle = Handle::app_handle(); let autostart_manager = app_handle.autolaunch(); match autostart_manager.is_enabled() { diff --git a/src-tauri/src/core/timer.rs b/src-tauri/src/core/timer.rs index f3f01c6b3..92a757d46 100644 --- a/src-tauri/src/core/timer.rs +++ b/src-tauri/src/core/timer.rs @@ -139,6 +139,27 @@ impl Timer { Ok(()) } + /// 每 3 秒更新系统托盘菜单,总共执行 3 次 + pub fn add_update_tray_menu_task(&self) -> Result<()> { + let tid = self.timer_count.fetch_add(1, Ordering::SeqCst); + let delay_timer = self.delay_timer.write(); + let task = TaskBuilder::default() + .set_task_id(tid) + .set_maximum_parallel_runnable_num(1) + .set_frequency_count_down_by_seconds(3, 3) + .spawn_async_routine(|| async move { + logging!(info, Type::Timer, "Updating tray menu"); + crate::core::tray::Tray::global() + .update_tray_display() + .await + }) + .context("failed to create update tray menu timer task")?; + delay_timer + .add_task(task) + .context("failed to add update tray menu timer task")?; + Ok(()) + } + /// Refresh timer tasks with better error handling pub async fn refresh(&self) -> Result<()> { // Generate diff outside of lock to minimize lock contention diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index 97dfb2cc9..7fb43dcde 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -3,16 +3,13 @@ use tauri::Emitter; use tauri::tray::TrayIconBuilder; #[cfg(target_os = "macos")] pub mod speed_rate; -use crate::ipc::Rate; use crate::module::lightweight; use crate::process::AsyncHandler; use crate::utils::window_manager::WindowManager; use crate::{ Type, cmd, config::Config, - feat, - ipc::IpcManager, - logging, + feat, logging, module::lightweight::is_in_lightweight_mode, singleton_lazy, utils::{dirs::find_target_icons, i18n::t}, @@ -34,6 +31,8 @@ use tauri::{ tray::{MouseButton, MouseButtonState, TrayIconEvent}, }; +// TODO: 是否需要将可变菜单抽离存储起来,后续直接更新对应菜单实例,无需重新创建菜单(待考虑) + #[derive(Clone)] struct TrayState {} @@ -54,7 +53,7 @@ fn should_handle_tray_click() -> bool { *last_click = now; true } else { - log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms", + log::debug!(target: "app", "托盘点击被防抖机制忽略,距离上次点击 {:?}ms", now.duration_since(*last_click).as_millis()); false } @@ -189,28 +188,25 @@ singleton_lazy!(Tray, TRAY, Tray::default); impl Tray { pub async fn init(&self) -> Result<()> { - let app_handle = handle::Handle::global() - .app_handle() - .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray initialization"))?; + let app_handle = handle::Handle::app_handle(); - match self.create_tray_from_handle(&app_handle).await { + match self.create_tray_from_handle(app_handle).await { Ok(_) => { log::info!(target: "app", "System tray created successfully"); - Ok(()) } Err(e) => { - log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e); // Don't return error, let application continue running without tray - Ok(()) + log::warn!(target: "app", "System tray creation failed: {}, Application will continue running without tray icon", e); } } + // TODO: 初始化时,暂时使用此方法更新系统托盘菜单,有效避免代理节点菜单空白 + crate::core::timer::Timer::global().add_update_tray_menu_task()?; + Ok(()) } /// 更新托盘点击行为 pub async fn update_click_behavior(&self) -> Result<()> { - let app_handle = handle::Handle::global() - .app_handle() - .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?; + let app_handle = handle::Handle::app_handle(); let tray_event = { Config::verge().await.latest_ref().tray_event.clone() }; let tray_event: String = tray_event.unwrap_or("main_window".into()); let tray = app_handle @@ -250,18 +246,12 @@ impl Tray { return Ok(()); } - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::warn!(target: "app", "更新托盘菜单失败: app_handle不存在"); - return Ok(()); - } - }; + let app_handle = handle::Handle::app_handle(); // 设置更新状态 self.menu_updating.store(true, Ordering::Release); - let result = self.update_menu_internal(&app_handle).await; + let result = self.update_menu_internal(app_handle).await; { let mut last_update = self.last_menu_update.lock(); @@ -318,14 +308,8 @@ impl Tray { /// 更新托盘图标 #[cfg(target_os = "macos")] - pub async fn update_icon(&self, _rate: Option) -> Result<()> { - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::warn!(target: "app", "更新托盘图标失败: app_handle不存在"); - return Ok(()); - } - }; + pub async fn update_icon(&self) -> Result<()> { + let app_handle = handle::Handle::app_handle(); let tray = match app_handle.tray_by_id("main") { Some(tray) => tray, @@ -355,14 +339,8 @@ impl Tray { } #[cfg(not(target_os = "macos"))] - pub async fn update_icon(&self, _rate: Option) -> Result<()> { - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::warn!(target: "app", "更新托盘图标失败: app_handle不存在"); - return Ok(()); - } - }; + pub async fn update_icon(&self) -> Result<()> { + let app_handle = handle::Handle::app_handle(); let tray = match app_handle.tray_by_id("main") { Some(tray) => tray, @@ -389,9 +367,7 @@ impl Tray { /// 更新托盘显示状态的函数 pub async fn update_tray_display(&self) -> Result<()> { - let app_handle = handle::Handle::global() - .app_handle() - .ok_or_else(|| anyhow::anyhow!("Failed to get app handle for tray update"))?; + let app_handle = handle::Handle::app_handle(); let _tray = app_handle .tray_by_id("main") .ok_or_else(|| anyhow::anyhow!("Failed to get main tray"))?; @@ -404,13 +380,7 @@ impl Tray { /// 更新托盘提示 pub async fn update_tooltip(&self) -> Result<()> { - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::warn!(target: "app", "更新托盘提示失败: app_handle不存在"); - return Ok(()); - } - }; + let app_handle = handle::Handle::app_handle(); let verge = Config::verge().await.latest_ref().clone(); let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false); @@ -464,7 +434,7 @@ impl Tray { // self.update_menu().await?; // 更新轻量模式显示状态 self.update_tray_display().await?; - self.update_icon(None).await?; + self.update_icon().await?; self.update_tooltip().await?; Ok(()) } @@ -550,7 +520,7 @@ impl Tray { // 确保所有状态更新完成 self.update_tray_display().await?; // self.update_menu().await?; - self.update_icon(None).await?; + self.update_icon().await?; self.update_tooltip().await?; Ok(()) @@ -578,14 +548,7 @@ async fn create_tray_menu( .unwrap_or_default() }; - let proxy_nodes_data = cmd::get_proxies().await.unwrap_or_else(|e| { - logging!( - error, - Type::Cmd, - "Failed to fetch proxies for tray menu: {e}" - ); - serde_json::Value::Object(serde_json::Map::new()) - }); + let proxy_nodes_data = handle::Handle::mihomo().await.get_proxies().await; let version = env!("CARGO_PKG_VERSION"); @@ -638,46 +601,43 @@ async fn create_tray_menu( let mut submenus = Vec::new(); let mut group_name_submenus_hash = HashMap::new(); - if let Some(proxies) = proxy_nodes_data.get("proxies").and_then(|v| v.as_object()) { - for (group_name, group_data) in proxies.iter() { + // TODO: 应用启动时,内核还未启动完全,无法获取代理节点信息 + if let Ok(proxy_nodes_data) = proxy_nodes_data { + for (group_name, group_data) in proxy_nodes_data.proxies.iter() { // Filter groups based on mode let should_show = match mode { "global" => group_name == "GLOBAL", _ => group_name != "GLOBAL", } && // Check if the group is hidden - !group_data.get("hidden").and_then(|v| v.as_bool()).unwrap_or(false); + !group_data.hidden.unwrap_or_default(); if !should_show { continue; } - let Some(all_proxies) = group_data.get("all").and_then(|v| v.as_array()) else { + let Some(all_proxies) = group_data.all.as_ref() else { continue; }; - let now_proxy = group_data.get("now").and_then(|v| v.as_str()).unwrap_or(""); + let now_proxy = group_data.now.as_deref().unwrap_or_default(); // Create proxy items let group_items: Vec> = all_proxies .iter() - .filter_map(|proxy_name| proxy_name.as_str()) .filter_map(|proxy_str| { - let is_selected = proxy_str == now_proxy; + let is_selected = *proxy_str == now_proxy; let item_id = format!("proxy_{}_{}", group_name, proxy_str); // Get delay for display - let delay_text = proxies + let delay_text = proxy_nodes_data + .proxies .get(proxy_str) - .and_then(|p| p.get("history")) - .and_then(|h| h.as_array()) - .and_then(|h| h.last()) - .and_then(|r| r.get("delay")) - .and_then(|d| d.as_i64()) - .map(|delay| match delay { - -1 => "-ms".to_string(), + .and_then(|h| h.history.last()) + .map(|h| match h.delay { + 0 => "-ms".to_string(), delay if delay >= 10000 => "-ms".to_string(), - _ => format!("{}ms", delay), + _ => format!("{}ms", h.delay), }) .unwrap_or_else(|| "-ms".to_string()); @@ -1066,29 +1026,30 @@ fn on_menu_event(_: &AppHandle, event: MenuEvent) { let group_name = parts[1]; let proxy_name = parts[2]; - match cmd::proxy::update_proxy_and_sync( - group_name.to_string(), - proxy_name.to_string(), - ) - .await + match handle::Handle::mihomo() + .await + .select_node_for_group(group_name, proxy_name) + .await { Ok(_) => { log::info!(target: "app", "切换代理成功: {} -> {}", group_name, proxy_name); + let _ = handle::Handle::app_handle() + .emit("verge://refresh-proxy-config", ()); } Err(e) => { log::error!(target: "app", "切换代理失败: {} -> {}, 错误: {:?}", group_name, proxy_name, e); // Fallback to IPC update - if (IpcManager::global() - .update_proxy(group_name, proxy_name) + if (handle::Handle::mihomo() + .await + .select_node_for_group(group_name, proxy_name) .await) .is_ok() { log::info!(target: "app", "代理切换回退成功: {} -> {}", group_name, proxy_name); - if let Some(app_handle) = handle::Handle::global().app_handle() { - let _ = app_handle.emit("verge://force-refresh-proxies", ()); - } + let app_handle = handle::Handle::app_handle(); + let _ = app_handle.emit("verge://force-refresh-proxies", ()); } } } diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 2cd9584dc..11c6e12cc 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -1,14 +1,11 @@ use crate::{ config::Config, core::{CoreManager, handle, tray}, - ipc::IpcManager, logging_error, process::AsyncHandler, utils::{logging::Type, resolve}, }; use serde_yaml_ng::{Mapping, Value}; -use std::env; -use std::process::{Command, exit}; /// Restart the Clash core pub async fn restart_clash_core() { @@ -35,55 +32,57 @@ pub async fn restart_app() { return; } - handle::Handle::notice_message("restart_app::info", "Restarting application..."); + let app_handle = handle::Handle::app_handle(); + app_handle.restart(); + // TODO: PR Ref: https://github.com/clash-verge-rev/clash-verge-rev/pull/4960 + // handle::Handle::notice_message("restart_app::info", "Restarting application..."); - // Use the manual restart method consistently to ensure reliability across platforms - // This addresses the issue where app_handle.restart() doesn't work properly on Windows - let current_exe = match env::current_exe() { - Ok(path) => path, - Err(_) => { - // If we can't get the current executable path, try to use the fallback method - if let Some(app_handle) = handle::Handle::global().app_handle() { - app_handle.restart(); - } - exit(1); // If we reach here, either app_handle was None or restart() failed to restart - } - }; + // // Use the manual restart method consistently to ensure reliability across platforms + // // This addresses the issue where app_handle.restart() doesn't work properly on Windows + // let current_exe = match env::current_exe() { + // Ok(path) => path, + // Err(_) => { + // // If we can't get the current executable path, try to use the fallback method + // if let Some(app_handle) = handle::Handle::global().app_handle() { + // app_handle.restart(); + // } + // exit(1); // If we reach here, either app_handle was None or restart() failed to restart + // } + // }; - let mut cmd = Command::new(current_exe); - cmd.args(env::args().skip(1)); + // let mut cmd = Command::new(current_exe); + // cmd.args(env::args().skip(1)); - match cmd.spawn() { - Ok(child) => { - log::info!(target: "app", "New application instance started with PID: {}", child.id()); - // Successfully started new process, now exit current process - if let Some(app_handle) = handle::Handle::global().app_handle() { - app_handle.exit(0); - } else { - exit(0); - } - } - Err(e) => { - log::error!(target: "app", "Failed to start new application instance: {}", e); - // If manual spawn fails, try the original restart method as a last resort - if let Some(app_handle) = handle::Handle::global().app_handle() { - app_handle.restart(); - } else { - exit(1); - } - } - } + // match cmd.spawn() { + // Ok(child) => { + // log::info!(target: "app", "New application instance started with PID: {}", child.id()); + // // Successfully started new process, now exit current process + // if let Some(app_handle) = handle::Handle::global().app_handle() { + // app_handle.exit(0); + // } else { + // exit(0); + // } + // } + // Err(e) => { + // log::error!(target: "app", "Failed to start new application instance: {}", e); + // // If manual spawn fails, try the original restart method as a last resort + // if let Some(app_handle) = handle::Handle::global().app_handle() { + // app_handle.restart(); + // } else { + // exit(1); + // } + // } + // } } fn after_change_clash_mode() { AsyncHandler::spawn(move || async { - match IpcManager::global().get_connections().await { + let mihomo = handle::Handle::mihomo().await; + match mihomo.get_connections().await { Ok(connections) => { - if let Some(connections_array) = connections["connections"].as_array() { + if let Some(connections_array) = connections.connections { for connection in connections_array { - if let Some(id) = connection["id"].as_str() { - let _ = IpcManager::global().delete_connection(id).await; - } + let _ = mihomo.close_connection(&connection.id).await; } } } @@ -103,7 +102,11 @@ pub async fn change_clash_mode(mode: String) { "mode": mode }); log::debug!(target: "app", "change clash mode to {mode}"); - match IpcManager::global().patch_configs(json_value).await { + match handle::Handle::mihomo() + .await + .patch_base_config(&json_value) + .await + { Ok(_) => { // 更新订阅 Config::clash().await.data_mut().patch_config(mapping); @@ -113,11 +116,7 @@ pub async fn change_clash_mode(mode: String) { if clash_data.save_config().await.is_ok() { handle::Handle::refresh_clash(); logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await); - logging_error!( - Type::Tray, - true, - tray::Tray::global().update_icon(None).await - ); + logging_error!(Type::Tray, true, tray::Tray::global().update_icon().await); } let is_auto_close_connection = Config::verge() diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 15f0325b9..c62a51b11 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -23,11 +23,7 @@ pub async fn patch_clash(patch: Mapping) -> Result<()> { } else { if patch.get("mode").is_some() { logging_error!(Type::Tray, true, tray::Tray::global().update_menu().await); - logging_error!( - Type::Tray, - true, - tray::Tray::global().update_icon(None).await - ); + logging_error!(Type::Tray, true, tray::Tray::global().update_icon().await); } Config::runtime().await.draft_mut().patch_config(patch); CoreManager::global().update_config().await?; @@ -211,7 +207,7 @@ pub async fn patch_verge(patch: IVerge, not_save_file: bool) -> Result<()> { tray::Tray::global().update_menu().await?; } if (update_flags & (UpdateFlags::SystrayIcon as i32)) != 0 { - tray::Tray::global().update_icon(None).await?; + tray::Tray::global().update_icon().await?; } if (update_flags & (UpdateFlags::SystrayTooltip as i32)) != 0 { tray::Tray::global().update_tooltip().await?; diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index 2bf2249cb..2eb92274b 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -143,15 +143,15 @@ pub async fn update_profile( Ok(_) => { logging!(info, Type::Config, true, "[订阅更新] 更新成功"); handle::Handle::refresh_clash(); - if let Err(err) = cmd::proxy::force_refresh_proxies().await { - logging!( - error, - Type::Config, - true, - "[订阅更新] 代理组刷新失败: {}", - err - ); - } + // if let Err(err) = cmd::proxy::force_refresh_proxies().await { + // logging!( + // error, + // Type::Config, + // true, + // "[订阅更新] 代理组刷新失败: {}", + // err + // ); + // } } Err(err) => { logging!(error, Type::Config, true, "[订阅更新] 更新失败: {}", err); diff --git a/src-tauri/src/feat/proxy.rs b/src-tauri/src/feat/proxy.rs index 5934193b3..7d520a897 100644 --- a/src-tauri/src/feat/proxy.rs +++ b/src-tauri/src/feat/proxy.rs @@ -1,9 +1,6 @@ use crate::{ config::{Config, IVerge}, core::handle, - ipc::IpcManager, - logging, - utils::logging::Type, }; use std::env; use tauri_plugin_clipboard_manager::ClipboardExt; @@ -26,7 +23,7 @@ pub async fn toggle_system_proxy() { // 如果当前系统代理即将关闭,且自动关闭连接设置为true,则关闭所有连接 if enable && auto_close_connection - && let Err(err) = IpcManager::global().close_all_connections().await + && let Err(err) = handle::Handle::mihomo().await.close_all_connections().await { log::error!(target: "app", "Failed to close all connections: {err}"); } @@ -78,14 +75,7 @@ pub async fn copy_clash_env() { .unwrap_or_else(|| "127.0.0.1".to_string()), }; - let Some(app_handle) = handle::Handle::global().app_handle() else { - logging!( - error, - Type::System, - "Failed to get app handle for proxy operation" - ); - return; - }; + let app_handle = handle::Handle::app_handle(); let port = { Config::verge() .await diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index b8c5fb0a7..81261aecb 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -2,7 +2,6 @@ use crate::utils::window_manager::WindowManager; use crate::{ config::Config, core::{CoreManager, handle, sysopt}, - ipc::IpcManager, logging, module::lightweight, utils::logging::Type, @@ -23,17 +22,12 @@ async fn open_or_close_dashboard_internal() { pub async fn quit() { logging!(debug, Type::System, true, "启动退出流程"); - let Some(app_handle) = handle::Handle::global().app_handle() else { - logging!( - error, - Type::System, - "Failed to get app handle for quit operation" - ); - return; - }; + // 获取应用句柄并设置退出标志 + let app_handle = handle::Handle::app_handle(); handle::Handle::global().set_is_exiting(); - if let Some(window) = handle::Handle::global().get_window() { + // 优先关闭窗口,提供立即反馈 + if let Some(window) = handle::Handle::get_window() { let _ = window.hide(); log::info!(target: "app", "窗口已隐藏"); } @@ -69,7 +63,14 @@ async fn clean_async() -> bool { #[cfg(not(target_os = "windows"))] let tun_timeout = Duration::from_secs(2); - match timeout(tun_timeout, IpcManager::global().patch_configs(disable_tun)).await { + match timeout( + tun_timeout, + handle::Handle::mihomo() + .await + .patch_base_config(&disable_tun), + ) + .await + { Ok(Ok(_)) => { log::info!(target: "app", "TUN模式已禁用"); tokio::time::sleep(Duration::from_millis(300)).await; @@ -314,7 +315,7 @@ pub async fn hide() { add_light_weight_timer().await; } - if let Some(window) = handle::Handle::global().get_window() + if let Some(window) = handle::Handle::get_window() && window.is_visible().unwrap_or(false) { let _ = window.hide(); diff --git a/src-tauri/src/ipc/general.rs b/src-tauri/src/ipc/general.rs deleted file mode 100644 index 9e90ae905..000000000 --- a/src-tauri/src/ipc/general.rs +++ /dev/null @@ -1,376 +0,0 @@ -use std::time::Duration; - -use kode_bridge::{ - ClientConfig, IpcHttpClient, LegacyResponse, - errors::{AnyError, AnyResult}, -}; -use percent_encoding::{AsciiSet, CONTROLS, utf8_percent_encode}; - -use crate::{ - logging, singleton_with_logging, - utils::{dirs::ipc_path, logging::Type}, -}; - -// 定义用于URL路径的编码集合,只编码真正必要的字符 -const URL_PATH_ENCODE_SET: &AsciiSet = &CONTROLS - .add(b' ') // 空格 - .add(b'/') // 斜杠 - .add(b'?') // 问号 - .add(b'#') // 井号 - .add(b'&') // 和号 - .add(b'%'); // 百分号 - -// Helper function to create AnyError from string -fn create_error(msg: impl Into) -> AnyError { - Box::new(std::io::Error::other(msg.into())) -} - -pub struct IpcManager { - client: IpcHttpClient, -} - -impl IpcManager { - pub fn new() -> Self { - logging!(info, Type::Ipc, true, "Creating new IpcManager instance"); - let ipc_path_buf = ipc_path().unwrap_or_else(|e| { - logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e); - std::path::PathBuf::from("/tmp/clash-verge-ipc") // fallback path - }); - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - let config = ClientConfig { - default_timeout: Duration::from_secs(5), - enable_pooling: false, - max_retries: 4, - retry_delay: Duration::from_millis(125), - max_concurrent_requests: 16, - max_requests_per_second: Some(64.0), - ..Default::default() - }; - #[allow(clippy::unwrap_used)] - let client = IpcHttpClient::with_config(ipc_path, config).unwrap(); - Self { client } - } -} - -impl IpcManager { - pub async fn request( - &self, - method: &str, - path: &str, - body: Option<&serde_json::Value>, - ) -> AnyResult { - self.client.request(method, path, body).await - } -} - -impl IpcManager { - pub async fn send_request( - &self, - method: &str, - path: &str, - body: Option<&serde_json::Value>, - ) -> AnyResult { - let response = IpcManager::global().request(method, path, body).await?; - match method { - "GET" => Ok(response.json()?), - "PATCH" => { - if response.status == 204 { - Ok(serde_json::json!({"code": 204})) - } else { - Ok(response.json()?) - } - } - "PUT" | "DELETE" => { - if response.status == 204 { - Ok(serde_json::json!({"code": 204})) - } else { - match response.json() { - Ok(json) => Ok(json), - Err(_) => Ok(serde_json::json!({ - "code": response.status, - "message": response.body, - "error": "failed to parse response as JSON" - })), - } - } - } - _ => match response.json() { - Ok(json) => Ok(json), - Err(_) => Ok(serde_json::json!({ - "code": response.status, - "message": response.body, - "error": "failed to parse response as JSON" - })), - }, - } - } - - // 基础代理信息获取 - pub async fn get_proxies(&self) -> AnyResult { - let url = "/proxies"; - self.send_request("GET", url, None).await - } - - // 代理提供者信息获取 - pub async fn get_providers_proxies(&self) -> AnyResult { - let url = "/providers/proxies"; - self.send_request("GET", url, None).await - } - - // 连接管理 - pub async fn get_connections(&self) -> AnyResult { - let url = "/connections"; - self.send_request("GET", url, None).await - } - - pub async fn delete_connection(&self, id: &str) -> AnyResult<()> { - let encoded_id = utf8_percent_encode(id, URL_PATH_ENCODE_SET).to_string(); - let url = format!("/connections/{encoded_id}"); - let response = self.send_request("DELETE", &url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"].as_str().unwrap_or("unknown error"), - )) - } - } - - pub async fn close_all_connections(&self) -> AnyResult<()> { - let url = "/connections"; - let response = self.send_request("DELETE", url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_owned(), - )) - } - } -} - -impl IpcManager { - #[allow(dead_code)] - pub async fn is_mihomo_running(&self) -> AnyResult<()> { - let url = "/version"; - let _response = self.send_request("GET", url, None).await?; - Ok(()) - } - - pub async fn put_configs_force(&self, clash_config_path: &str) -> AnyResult<()> { - let url = "/configs?force=true"; - let payload = serde_json::json!({ - "path": clash_config_path, - }); - let _response = self.send_request("PUT", url, Some(&payload)).await?; - Ok(()) - } - - pub async fn patch_configs(&self, config: serde_json::Value) -> AnyResult<()> { - let url = "/configs"; - let response = self.send_request("PATCH", url, Some(&config)).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_owned(), - )) - } - } - - pub async fn test_proxy_delay( - &self, - name: &str, - test_url: Option, - timeout: i32, - ) -> AnyResult { - let test_url = - test_url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string()); - - let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string(); - // 测速URL不再编码,直接传递 - let url = format!("/proxies/{encoded_name}/delay?url={test_url}&timeout={timeout}"); - - self.send_request("GET", &url, None).await - } - - // 版本和配置相关 - pub async fn get_version(&self) -> AnyResult { - let url = "/version"; - self.send_request("GET", url, None).await - } - - pub async fn get_config(&self) -> AnyResult { - let url = "/configs"; - self.send_request("GET", url, None).await - } - - pub async fn update_geo_data(&self) -> AnyResult<()> { - let url = "/configs/geo"; - let response = self.send_request("POST", url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string(), - )) - } - } - - pub async fn upgrade_core(&self) -> AnyResult<()> { - let url = "/upgrade"; - let response = self.send_request("POST", url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string(), - )) - } - } - - // 规则相关 - pub async fn get_rules(&self) -> AnyResult { - let url = "/rules"; - self.send_request("GET", url, None).await - } - - pub async fn get_rule_providers(&self) -> AnyResult { - let url = "/providers/rules"; - self.send_request("GET", url, None).await - } - - pub async fn update_rule_provider(&self, name: &str) -> AnyResult<()> { - let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string(); - let url = format!("/providers/rules/{encoded_name}"); - let response = self.send_request("PUT", &url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string(), - )) - } - } - - // 代理相关 - pub async fn update_proxy(&self, group: &str, proxy: &str) -> AnyResult<()> { - // 使用 percent-encoding 进行正确的 URL 编码 - let encoded_group = utf8_percent_encode(group, URL_PATH_ENCODE_SET).to_string(); - let url = format!("/proxies/{encoded_group}"); - let payload = serde_json::json!({ - "name": proxy - }); - - // println!("group: {}, proxy: {}", group, proxy); - match self.send_request("PUT", &url, Some(&payload)).await { - Ok(_) => { - // println!("updateProxy response: {:?}", response); - Ok(()) - } - Err(e) => { - // println!("updateProxy encountered error: {}", e); - logging!( - error, - crate::utils::logging::Type::Ipc, - true, - "IPC: updateProxy encountered error: {} (ignored, always returning true)", - e - ); - Ok(()) - } - } - } - - pub async fn proxy_provider_health_check(&self, name: &str) -> AnyResult<()> { - let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string(); - let url = format!("/providers/proxies/{encoded_name}/healthcheck"); - let response = self.send_request("GET", &url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string(), - )) - } - } - - pub async fn update_proxy_provider(&self, name: &str) -> AnyResult<()> { - let encoded_name = utf8_percent_encode(name, URL_PATH_ENCODE_SET).to_string(); - let url = format!("/providers/proxies/{encoded_name}"); - let response = self.send_request("PUT", &url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string(), - )) - } - } - - // 延迟测试相关 - pub async fn get_group_proxy_delays( - &self, - group_name: &str, - url: Option, - timeout: i32, - ) -> AnyResult { - let test_url = url.unwrap_or_else(|| "https://cp.cloudflare.com/generate_204".to_string()); - - let encoded_group_name = utf8_percent_encode(group_name, URL_PATH_ENCODE_SET).to_string(); - // 测速URL不再编码,直接传递 - let url = format!("/group/{encoded_group_name}/delay?url={test_url}&timeout={timeout}"); - - self.send_request("GET", &url, None).await - } - - // 调试相关 - pub async fn is_debug_enabled(&self) -> AnyResult { - let url = "/debug/pprof"; - match self.send_request("GET", url, None).await { - Ok(_) => Ok(true), - Err(_) => Ok(false), - } - } - - pub async fn gc(&self) -> AnyResult<()> { - let url = "/debug/gc"; - let response = self.send_request("PUT", url, None).await?; - if response["code"] == 204 { - Ok(()) - } else { - Err(create_error( - response["message"] - .as_str() - .unwrap_or("unknown error") - .to_string(), - )) - } - } - - // 日志相关功能已迁移到 logs.rs 模块,使用流式处理 -} - -// Use singleton macro with logging -singleton_with_logging!(IpcManager, INSTANCE, "IpcManager"); diff --git a/src-tauri/src/ipc/logs.rs b/src-tauri/src/ipc/logs.rs deleted file mode 100644 index f0fd0ae5f..000000000 --- a/src-tauri/src/ipc/logs.rs +++ /dev/null @@ -1,330 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{collections::VecDeque, sync::Arc, time::Instant}; -use tauri::async_runtime::JoinHandle; -use tokio::{sync::RwLock, time::Duration}; - -use crate::{ - ipc::monitor::MonitorData, - logging, - process::AsyncHandler, - singleton_with_logging, - utils::{dirs::ipc_path, logging::Type}, -}; - -const MAX_LOGS: usize = 1000; // Maximum number of logs to keep in memory - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct LogData { - #[serde(rename = "type")] - pub log_type: String, - pub payload: String, -} - -#[derive(Debug, Clone)] -pub struct LogItem { - pub log_type: String, - pub payload: String, - pub time: String, -} - -impl LogItem { - fn new(log_type: String, payload: String) -> Self { - use std::time::{SystemTime, UNIX_EPOCH}; - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_else(|_| std::time::Duration::from_secs(0)) - .as_secs(); - - // Simple time formatting (HH:MM:SS) - let hours = (now / 3600) % 24; - let minutes = (now / 60) % 60; - let seconds = now % 60; - let time_str = format!("{hours:02}:{minutes:02}:{seconds:02}"); - - Self { - log_type, - payload, - time: time_str, - } - } -} - -#[derive(Debug, Clone)] -pub struct CurrentLogs { - pub logs: VecDeque, - // pub level: String, - pub last_updated: Instant, -} - -impl Default for CurrentLogs { - fn default() -> Self { - Self { - logs: VecDeque::with_capacity(MAX_LOGS), - // level: "info".to_string(), - last_updated: Instant::now(), - } - } -} - -impl MonitorData for CurrentLogs { - fn mark_fresh(&mut self) { - self.last_updated = Instant::now(); - } - - fn is_fresh_within(&self, duration: Duration) -> bool { - self.last_updated.elapsed() < duration - } -} - -// Logs monitor with streaming support -pub struct LogsMonitor { - current: Arc>, - task_handle: Arc>>>, - current_monitoring_level: Arc>>, -} - -// Use singleton_with_logging macro -singleton_with_logging!(LogsMonitor, INSTANCE, "LogsMonitor"); - -impl LogsMonitor { - fn new() -> Self { - let current = Arc::new(RwLock::new(CurrentLogs::default())); - - Self { - current, - task_handle: Arc::new(RwLock::new(None)), - current_monitoring_level: Arc::new(RwLock::new(None)), - } - } - - pub async fn start_monitoring(&self, level: Option) { - let filter_level = level.clone().unwrap_or_else(|| "info".to_string()); - - // Check if we're already monitoring the same level - // let level_changed = { - // let current_level = self.current_monitoring_level.read().await; - // if let Some(existing_level) = current_level.as_ref() { - // if existing_level == &filter_level { - // logging!( - // info, - // Type::Ipc, - // true, - // "LogsMonitor: Already monitoring level '{}', skipping duplicate request", - // filter_level - // ); - // return; - // } - // true // Level changed - // } else { - // true // First time or was stopped - // } - // }; - - // Stop existing monitoring task if level changed or first time - { - let mut handle = self.task_handle.write().await; - if let Some(task) = handle.take() { - task.abort(); - logging!( - info, - Type::Ipc, - true, - "LogsMonitor: Stopped previous monitoring task (level changed)" - ); - } - } - - // We want to keep the logs cache even if the level changes, - // so we don't clear it here. The cache will be cleared only when the level changes - // and a new task is started. This allows us to keep logs from previous levels - // even if the level changes during monitoring. - // Clear logs cache when level changes to ensure fresh data - // if level_changed { - // let mut current = self.current.write().await; - // current.logs.clear(); - // current.level = filter_level.clone(); - // current.mark_fresh(); - // logging!( - // info, - // Type::Ipc, - // true, - // "LogsMonitor: Cleared logs cache due to level change to '{}'", - // filter_level - // ); - // } - - // Update current monitoring level - { - let mut current_level = self.current_monitoring_level.write().await; - *current_level = Some(filter_level.clone()); - } - - let monitor_current = Arc::clone(&self.current); - - let task = AsyncHandler::spawn(move || async move { - loop { - // Get fresh IPC path and client for each connection attempt - let (_ipc_path_buf, client) = match Self::create_ipc_client() { - Ok((path, client)) => (path, client), - Err(e) => { - logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e); - tokio::time::sleep(Duration::from_secs(2)).await; - continue; - } - }; - - let url = if filter_level == "all" { - "/logs".to_string() - } else { - format!("/logs?level={filter_level}") - }; - - logging!( - info, - Type::Ipc, - true, - "LogsMonitor: Starting stream for {}", - url - ); - - let _ = client - .get(&url) - .timeout(Duration::from_secs(30)) - .process_lines(|line| { - Self::process_log_line(line, Arc::clone(&monitor_current)) - }) - .await; - - // Wait before retrying - tokio::time::sleep(Duration::from_secs(2)).await; - } - }); - - // Store the task handle - { - let mut handle = self.task_handle.write().await; - *handle = Some(task); - } - - logging!( - info, - Type::Ipc, - true, - "LogsMonitor: Started new monitoring task for level: {:?}", - level - ); - } - - pub async fn stop_monitoring(&self) { - // Stop monitoring task but keep logs - { - let mut handle = self.task_handle.write().await; - if let Some(task) = handle.take() { - task.abort(); - logging!( - info, - Type::Ipc, - true, - "LogsMonitor: Stopped monitoring task" - ); - } - } - - // Reset monitoring level - { - let mut monitoring_level = self.current_monitoring_level.write().await; - *monitoring_level = None; - } - } - - fn create_ipc_client() -> Result< - (std::path::PathBuf, kode_bridge::IpcStreamClient), - Box, - > { - use kode_bridge::IpcStreamClient; - - let ipc_path_buf = ipc_path()?; - let ipc_path = ipc_path_buf.to_str().ok_or("Invalid IPC path")?; - let client = IpcStreamClient::new(ipc_path)?; - Ok((ipc_path_buf, client)) - } - - fn process_log_line( - line: &str, - current: Arc>, - ) -> Result<(), Box> { - if let Ok(log_data) = serde_json::from_str::(line.trim()) { - // Server-side filtering via query parameters handles the level filtering - // We only need to accept all logs since filtering is done at the endpoint level - let log_item = LogItem::new(log_data.log_type, log_data.payload); - - AsyncHandler::spawn(move || async move { - let mut logs = current.write().await; - - // Add new log - logs.logs.push_back(log_item); - - // Keep only the last 1000 logs - if logs.logs.len() > 1000 { - logs.logs.pop_front(); - } - - logs.mark_fresh(); - }); - } - Ok(()) - } - - pub async fn current(&self) -> CurrentLogs { - self.current.read().await.clone() - } - - pub async fn clear_logs(&self) { - let mut current = self.current.write().await; - current.logs.clear(); - current.mark_fresh(); - logging!( - info, - Type::Ipc, - true, - "LogsMonitor: Cleared frontend logs (monitoring continues)" - ); - } - - pub async fn get_logs_as_json(&self) -> serde_json::Value { - let current = self.current().await; - - // Simply return all cached logs since filtering is handled by start_monitoring - // and the cache is cleared when level changes - let logs: Vec = current - .logs - .iter() - .map(|log| { - serde_json::json!({ - "type": log.log_type, - "payload": log.payload, - "time": log.time - }) - }) - .collect(); - - serde_json::Value::Array(logs) - } -} - -pub async fn start_logs_monitoring(level: Option) { - LogsMonitor::global().start_monitoring(level).await; -} - -pub async fn stop_logs_monitoring() { - LogsMonitor::global().stop_monitoring().await; -} - -pub async fn clear_logs() { - LogsMonitor::global().clear_logs().await; -} - -pub async fn get_logs_json() -> serde_json::Value { - LogsMonitor::global().get_logs_as_json().await -} diff --git a/src-tauri/src/ipc/memory.rs b/src-tauri/src/ipc/memory.rs deleted file mode 100644 index ed0516893..000000000 --- a/src-tauri/src/ipc/memory.rs +++ /dev/null @@ -1,119 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{sync::Arc, time::Instant}; -use tokio::{sync::RwLock, time::Duration}; - -use crate::{ - ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser}, - process::AsyncHandler, - singleton_lazy_with_logging, - utils::format::fmt_bytes, -}; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct MemoryData { - pub inuse: u64, - pub oslimit: u64, -} - -#[derive(Debug, Clone)] -pub struct CurrentMemory { - pub inuse: u64, - pub oslimit: u64, - pub last_updated: Instant, -} - -impl Default for CurrentMemory { - fn default() -> Self { - Self { - inuse: 0, - oslimit: 0, - last_updated: Instant::now(), - } - } -} - -impl MonitorData for CurrentMemory { - fn mark_fresh(&mut self) { - self.last_updated = Instant::now(); - } - - fn is_fresh_within(&self, duration: Duration) -> bool { - self.last_updated.elapsed() < duration - } -} - -impl StreamingParser for CurrentMemory { - fn parse_and_update( - line: &str, - current: Arc>, - ) -> Result<(), Box> { - if let Ok(memory) = serde_json::from_str::(line.trim()) { - AsyncHandler::spawn(move || async move { - let mut current_guard = current.write().await; - current_guard.inuse = memory.inuse; - current_guard.oslimit = memory.oslimit; - current_guard.mark_fresh(); - }); - } - Ok(()) - } -} - -// Minimal memory monitor using the new architecture -pub struct MemoryMonitor { - monitor: IpcStreamMonitor, -} - -impl Default for MemoryMonitor { - fn default() -> Self { - MemoryMonitor { - monitor: IpcStreamMonitor::new( - "/memory".to_string(), - Duration::from_secs(10), - Duration::from_secs(2), - Duration::from_secs(10), - ), - } - } -} - -// Use simplified singleton_lazy_with_logging macro -singleton_lazy_with_logging!( - MemoryMonitor, - INSTANCE, - "MemoryMonitor", - MemoryMonitor::default -); - -impl MemoryMonitor { - pub async fn current(&self) -> CurrentMemory { - self.monitor.current().await - } - - pub async fn is_fresh(&self) -> bool { - self.monitor.is_fresh().await - } -} - -pub async fn get_current_memory() -> CurrentMemory { - MemoryMonitor::global().current().await -} - -pub async fn get_formatted_memory() -> (String, String, f64, bool) { - let monitor = MemoryMonitor::global(); - let memory = monitor.current().await; - let is_fresh = monitor.is_fresh().await; - - let usage_percent = if memory.oslimit > 0 { - (memory.inuse as f64 / memory.oslimit as f64) * 100.0 - } else { - 0.0 - }; - - ( - fmt_bytes(memory.inuse), - fmt_bytes(memory.oslimit), - usage_percent, - is_fresh, - ) -} diff --git a/src-tauri/src/ipc/mod.rs b/src-tauri/src/ipc/mod.rs deleted file mode 100644 index 8b2f42f87..000000000 --- a/src-tauri/src/ipc/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod general; -pub mod logs; -pub mod memory; -pub mod monitor; -pub mod traffic; - -pub use general::IpcManager; -pub use logs::{clear_logs, get_logs_json, start_logs_monitoring, stop_logs_monitoring}; -pub use memory::{get_current_memory, get_formatted_memory}; -pub use traffic::{get_current_traffic, get_formatted_traffic}; - -pub struct Rate { - // pub up: usize, - // pub down: usize, -} diff --git a/src-tauri/src/ipc/monitor.rs b/src-tauri/src/ipc/monitor.rs deleted file mode 100644 index f12f36335..000000000 --- a/src-tauri/src/ipc/monitor.rs +++ /dev/null @@ -1,120 +0,0 @@ -use kode_bridge::IpcStreamClient; -use std::sync::Arc; -use tokio::{sync::RwLock, time::Duration}; - -use crate::{ - logging, - process::AsyncHandler, - utils::{dirs::ipc_path, logging::Type}, -}; - -/// Generic base structure for IPC monitoring data with freshness tracking -pub trait MonitorData: Clone + Send + Sync + 'static { - /// Update the last_updated timestamp to now - fn mark_fresh(&mut self); - - /// Check if data is fresh based on the given duration - fn is_fresh_within(&self, duration: Duration) -> bool; -} - -/// Trait for parsing streaming data and updating monitor state -pub trait StreamingParser: MonitorData { - /// Parse a line of streaming data and update the current state - fn parse_and_update( - line: &str, - current: Arc>, - ) -> Result<(), Box>; -} - -/// Generic IPC stream monitor that handles the common streaming pattern -pub struct IpcStreamMonitor -where - T: MonitorData + StreamingParser + Default, -{ - current: Arc>, - #[allow(dead_code)] - endpoint: String, - #[allow(dead_code)] - timeout: Duration, - #[allow(dead_code)] - retry_interval: Duration, - freshness_duration: Duration, -} - -impl IpcStreamMonitor -where - T: MonitorData + StreamingParser + Default, -{ - pub fn new( - endpoint: String, - timeout: Duration, - retry_interval: Duration, - freshness_duration: Duration, - ) -> Self { - let current = Arc::new(RwLock::new(T::default())); - let monitor_current = Arc::clone(¤t); - let endpoint_clone = endpoint.clone(); - - // Start the monitoring task - AsyncHandler::spawn(move || async move { - Self::streaming_task(monitor_current, endpoint_clone, timeout, retry_interval).await; - }); - - Self { - current, - endpoint, - timeout, - retry_interval, - freshness_duration, - } - } - - pub async fn current(&self) -> T { - self.current.read().await.clone() - } - - pub async fn is_fresh(&self) -> bool { - self.current - .read() - .await - .is_fresh_within(self.freshness_duration) - } - - /// The core streaming task that can be specialized per monitor type - async fn streaming_task( - current: Arc>, - endpoint: String, - timeout: Duration, - retry_interval: Duration, - ) { - loop { - let ipc_path_buf = match ipc_path() { - Ok(path) => path, - Err(e) => { - logging!(error, Type::Ipc, true, "Failed to get IPC path: {}", e); - tokio::time::sleep(retry_interval).await; - continue; - } - }; - - let ipc_path = ipc_path_buf.to_str().unwrap_or_default(); - - let client = match IpcStreamClient::new(ipc_path) { - Ok(client) => client, - Err(e) => { - logging!(error, Type::Ipc, true, "Failed to create IPC client: {}", e); - tokio::time::sleep(retry_interval).await; - continue; - } - }; - - let _ = client - .get(&endpoint) - .timeout(timeout) - .process_lines(|line| T::parse_and_update(line, Arc::clone(¤t))) - .await; - - tokio::time::sleep(retry_interval).await; - } - } -} diff --git a/src-tauri/src/ipc/traffic.rs b/src-tauri/src/ipc/traffic.rs deleted file mode 100644 index ac30820ef..000000000 --- a/src-tauri/src/ipc/traffic.rs +++ /dev/null @@ -1,153 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::{sync::Arc, time::Instant}; -use tokio::{sync::RwLock, time::Duration}; - -use crate::{ - ipc::monitor::{IpcStreamMonitor, MonitorData, StreamingParser}, - process::AsyncHandler, - singleton_lazy_with_logging, - utils::format::fmt_bytes, -}; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct TrafficData { - pub up: u64, - pub down: u64, -} - -#[derive(Debug, Clone)] -pub struct CurrentTraffic { - pub up_rate: u64, - pub down_rate: u64, - pub total_up: u64, - pub total_down: u64, - pub last_updated: Instant, -} - -impl Default for CurrentTraffic { - fn default() -> Self { - Self { - up_rate: 0, - down_rate: 0, - total_up: 0, - total_down: 0, - last_updated: Instant::now(), - } - } -} - -impl MonitorData for CurrentTraffic { - fn mark_fresh(&mut self) { - self.last_updated = Instant::now(); - } - - fn is_fresh_within(&self, duration: Duration) -> bool { - self.last_updated.elapsed() < duration - } -} - -// Traffic monitoring state for calculating rates -#[derive(Debug, Clone, Default)] -pub struct TrafficMonitorState { - pub current: CurrentTraffic, - pub last_traffic: Option, -} - -impl MonitorData for TrafficMonitorState { - fn mark_fresh(&mut self) { - self.current.mark_fresh(); - } - - fn is_fresh_within(&self, duration: Duration) -> bool { - self.current.is_fresh_within(duration) - } -} - -impl StreamingParser for TrafficMonitorState { - fn parse_and_update( - line: &str, - current: Arc>, - ) -> Result<(), Box> { - if let Ok(traffic) = serde_json::from_str::(line.trim()) { - AsyncHandler::spawn(move || async move { - let mut state_guard = current.write().await; - - let (up_rate, down_rate) = state_guard - .last_traffic - .as_ref() - .map(|l| { - ( - traffic.up.saturating_sub(l.up), - traffic.down.saturating_sub(l.down), - ) - }) - .unwrap_or((0, 0)); - - state_guard.current = CurrentTraffic { - up_rate, - down_rate, - total_up: traffic.up, - total_down: traffic.down, - last_updated: Instant::now(), - }; - - state_guard.last_traffic = Some(traffic); - }); - } - Ok(()) - } -} - -// Minimal traffic monitor using the new architecture -pub struct TrafficMonitor { - monitor: IpcStreamMonitor, -} - -impl Default for TrafficMonitor { - fn default() -> Self { - TrafficMonitor { - monitor: IpcStreamMonitor::new( - "/traffic".to_string(), - Duration::from_secs(10), - Duration::from_secs(1), - Duration::from_secs(5), - ), - } - } -} - -// Use simplified singleton_lazy_with_logging macro -singleton_lazy_with_logging!( - TrafficMonitor, - INSTANCE, - "TrafficMonitor", - TrafficMonitor::default -); - -impl TrafficMonitor { - pub async fn current(&self) -> CurrentTraffic { - self.monitor.current().await.current - } - - pub async fn is_fresh(&self) -> bool { - self.monitor.is_fresh().await - } -} - -pub async fn get_current_traffic() -> CurrentTraffic { - TrafficMonitor::global().current().await -} - -pub async fn get_formatted_traffic() -> (String, String, String, String, bool) { - let monitor = TrafficMonitor::global(); - let traffic = monitor.current().await; - let is_fresh = monitor.is_fresh().await; - - ( - fmt_bytes(traffic.up_rate), - fmt_bytes(traffic.down_rate), - fmt_bytes(traffic.total_up), - fmt_bytes(traffic.total_down), - is_fresh, - ) -} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ba4237902..54769bc4c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,34 +1,32 @@ #![allow(non_snake_case)] #![recursion_limit = "512"] -mod cache; mod cmd; pub mod config; mod core; mod enhance; mod feat; -mod ipc; mod module; mod process; mod utils; #[cfg(target_os = "macos")] use crate::utils::window_manager::WindowManager; use crate::{ - core::handle, - core::hotkey, + core::{handle, hotkey}, process::AsyncHandler, utils::{resolve, server}, }; use config::Config; -use tauri::AppHandle; -#[cfg(target_os = "macos")] -use tauri::Manager; +use once_cell::sync::OnceCell; +use tauri::{AppHandle, Manager}; #[cfg(target_os = "macos")] use tauri_plugin_autostart::MacosLauncher; use tauri_plugin_deep_link::DeepLinkExt; use tokio::time::{Duration, timeout}; use utils::logging::Type; +pub static APP_HANDLE: OnceCell = OnceCell::new(); + /// Application initialization helper functions mod app_init { use super::*; @@ -41,7 +39,7 @@ mod app_init { Ok(result) => { if result.is_err() { logging!(info, Type::Setup, true, "检测到已有应用实例运行"); - if let Some(app_handle) = handle::Handle::global().app_handle() { + if let Some(app_handle) = APP_HANDLE.get() { app_handle.exit(0); } else { std::process::exit(0); @@ -75,7 +73,13 @@ mod app_init { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_deep_link::init()) - .plugin(tauri_plugin_http::init()); + .plugin(tauri_plugin_http::init()) + .plugin( + tauri_plugin_mihomo::Builder::new() + .protocol(tauri_plugin_mihomo::models::Protocol::LocalSocket) + .socket_path(crate::config::IClashTemp::guard_external_controller_ipc()) + .build(), + ); // Devtools plugin only in debug mode with feature tauri-dev // to avoid duplicated registering of logger since the devtools plugin also registers a logger @@ -184,46 +188,13 @@ mod app_init { cmd::update_proxy_chain_config_in_runtime, cmd::invoke_uwp_tool, cmd::copy_clash_env, - cmd::get_proxies, - cmd::force_refresh_proxies, - cmd::get_providers_proxies, cmd::sync_tray_proxy_selection, - cmd::update_proxy_and_sync, cmd::save_dns_config, cmd::apply_dns_config, cmd::check_dns_config_exists, cmd::get_dns_config_content, cmd::validate_dns_config, - cmd::get_clash_version, - cmd::get_clash_config, - cmd::force_refresh_clash_config, - cmd::update_geo_data, - cmd::upgrade_clash_core, - cmd::get_clash_rules, - cmd::update_proxy_choice, - cmd::get_proxy_providers, - cmd::get_rule_providers, - cmd::proxy_provider_health_check, - cmd::update_proxy_provider, - cmd::update_rule_provider, - cmd::get_clash_connections, - cmd::delete_clash_connection, - cmd::close_all_clash_connections, - cmd::get_group_proxy_delays, - cmd::is_clash_debug_enabled, - cmd::clash_gc, - // Logging and monitoring cmd::get_clash_logs, - cmd::start_logs_monitoring, - cmd::stop_logs_monitoring, - cmd::clear_logs, - cmd::get_traffic_data, - cmd::get_memory_data, - cmd::get_formatted_traffic_data, - cmd::get_formatted_memory_data, - cmd::get_system_monitor_overview, - cmd::start_traffic_service, - cmd::stop_traffic_service, // Verge configuration cmd::get_verge_config, cmd::patch_verge_config, @@ -251,8 +222,6 @@ mod app_init { // Script validation cmd::script_validate_notice, cmd::validate_script_file, - // Clash API - cmd::clash_api_get_proxy_delay, // Backup and WebDAV cmd::create_webdav_backup, cmd::save_webdav_config, @@ -321,6 +290,11 @@ pub fn run() { .setup(|app| { logging!(info, Type::Setup, true, "开始应用初始化..."); + #[allow(clippy::expect_used)] + APP_HANDLE + .set(app.app_handle().clone()) + .expect("failed to set global app handle"); + // Setup autostart plugin if let Err(e) = app_init::setup_autostart(app) { logging!(error, Type::Setup, true, "Failed to setup autostart: {}", e); @@ -348,11 +322,9 @@ pub fn run() { ); } - let app_handle = app.handle().clone(); - logging!(info, Type::Setup, true, "执行主要设置操作..."); - resolve::resolve_setup_handle(app_handle); + resolve::resolve_setup_handle(); resolve::resolve_setup_async(); resolve::resolve_setup_sync(); @@ -368,7 +340,7 @@ pub fn run() { use super::*; /// Handle application ready/resumed events - pub fn handle_ready_resumed(app_handle: &AppHandle) { + pub fn handle_ready_resumed(_app_handle: &AppHandle) { // 双重检查:确保不在退出状态 if handle::Handle::global().is_exiting() { logging!( @@ -381,11 +353,11 @@ pub fn run() { } logging!(info, Type::System, true, "应用就绪或恢复"); - handle::Handle::global().init(app_handle.clone()); + handle::Handle::global().init(); #[cfg(target_os = "macos")] { - if let Some(window) = app_handle.get_webview_window("main") { + if let Some(window) = _app_handle.get_webview_window("main") { logging!(info, Type::Window, true, "设置macOS窗口标题"); let _ = window.set_title("Clash Verge"); } @@ -394,7 +366,7 @@ pub fn run() { /// Handle application reopen events (macOS) #[cfg(target_os = "macos")] - pub async fn handle_reopen(app_handle: &AppHandle, has_visible_windows: bool) { + pub async fn handle_reopen(has_visible_windows: bool) { logging!( info, Type::System, @@ -403,7 +375,7 @@ pub fn run() { has_visible_windows ); - handle::Handle::global().init(app_handle.clone()); + handle::Handle::global().init(); if !has_visible_windows { // 当没有可见窗口时,设置为 regular 模式并显示主窗口 @@ -436,7 +408,7 @@ pub fn run() { log::info!(target: "app", "closing window..."); if let tauri::WindowEvent::CloseRequested { api, .. } = api { api.prevent_close(); - if let Some(window) = core::handle::Handle::global().get_window() { + if let Some(window) = core::handle::Handle::get_window() { let _ = window.hide(); } else { logging!(warn, Type::Window, true, "尝试隐藏窗口但窗口不存在"); @@ -583,12 +555,17 @@ pub fn run() { logging!(debug, Type::System, true, "忽略 Reopen 事件,应用正在退出"); return; } - let app_handle = app_handle.clone(); AsyncHandler::spawn(move || async move { - event_handlers::handle_reopen(&app_handle, has_visible_windows).await; + event_handlers::handle_reopen(has_visible_windows).await; }); } tauri::RunEvent::ExitRequested { api, code, .. } => { + tauri::async_runtime::block_on(async { + let _ = handle::Handle::mihomo() + .await + .clear_all_ws_connections() + .await; + }); // 如果已经在退出流程中,不要阻止退出 if core::handle::Handle::global().is_exiting() { logging!( diff --git a/src-tauri/src/module/lightweight.rs b/src-tauri/src/module/lightweight.rs index ba42963b5..703dfc776 100644 --- a/src-tauri/src/module/lightweight.rs +++ b/src-tauri/src/module/lightweight.rs @@ -1,5 +1,4 @@ use crate::{ - cache::CacheProxy, config::Config, core::{handle, timer::Timer, tray::Tray}, log_err, logging, @@ -176,7 +175,6 @@ pub async fn entry_lightweight_mode() -> bool { // 回到 In set_state(LightweightState::In); - CacheProxy::global().clean_default_keys(); true } @@ -219,7 +217,7 @@ pub async fn add_light_weight_timer() { } fn setup_window_close_listener() { - if let Some(window) = handle::Handle::global().get_window() { + if let Some(window) = handle::Handle::get_window() { let handler = window.listen("tauri://close-requested", move |_event| { std::mem::drop(AsyncHandler::spawn(|| async { if let Err(e) = setup_light_weight_timer().await { @@ -239,7 +237,7 @@ fn setup_window_close_listener() { } fn cancel_window_close_listener() { - if let Some(window) = handle::Handle::global().get_window() { + if let Some(window) = handle::Handle::get_window() { let handler = WINDOW_CLOSE_HANDLER.swap(0, Ordering::AcqRel); if handler != 0 { window.unlisten(handler); @@ -249,7 +247,7 @@ fn cancel_window_close_listener() { } fn setup_webview_focus_listener() { - if let Some(window) = handle::Handle::global().get_window() { + if let Some(window) = handle::Handle::get_window() { let handler = window.listen("tauri://focus", move |_event| { log_err!(cancel_light_weight_timer()); logging!( @@ -264,7 +262,7 @@ fn setup_webview_focus_listener() { } fn cancel_webview_focus_listener() { - if let Some(window) = handle::Handle::global().get_window() { + if let Some(window) = handle::Handle::get_window() { let handler = WEBVIEW_FOCUS_HANDLER.swap(0, Ordering::AcqRel); if handler != 0 { window.unlisten(handler); diff --git a/src-tauri/src/module/sysinfo.rs b/src-tauri/src/module/sysinfo.rs index fc348f49a..fb2b25791 100644 --- a/src-tauri/src/module/sysinfo.rs +++ b/src-tauri/src/module/sysinfo.rs @@ -38,17 +38,7 @@ impl PlatformSpecification { let system_kernel_version = System::kernel_version().unwrap_or("Null".into()); let system_arch = System::cpu_arch(); - let Some(handler) = handle::Handle::global().app_handle() else { - return Self { - system_name, - system_version, - system_kernel_version, - system_arch, - verge_version: "unknown".into(), - running_mode: "NotRunning".to_string(), - is_admin: false, - }; - }; + let handler = handle::Handle::app_handle(); let verge_version = handler.package_info().version.to_string(); // 使用默认值避免在同步上下文中执行异步操作 diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index 9b6c681ed..714e644c3 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -51,53 +51,7 @@ pub fn app_home_dir() -> Result { } // 避免在Handle未初始化时崩溃 - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::warn!(target: "app", "app_handle not initialized, using default path"); - // 使用可执行文件目录作为备用 - let exe_path = tauri::utils::platform::current_exe()?; - let exe_dir = exe_path - .parent() - .ok_or(anyhow::anyhow!("failed to get executable directory"))?; - - // 使用系统临时目录 + 应用ID - #[cfg(target_os = "windows")] - { - if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") { - let path = PathBuf::from(local_app_data).join(APP_ID); - return Ok(path); - } - } - - #[cfg(target_os = "macos")] - { - if let Some(home) = std::env::var_os("HOME") { - let path = PathBuf::from(home) - .join("Library") - .join("Application Support") - .join(APP_ID); - return Ok(path); - } - } - - #[cfg(target_os = "linux")] - { - if let Some(home) = std::env::var_os("HOME") { - let path = PathBuf::from(home) - .join(".local") - .join("share") - .join(APP_ID); - return Ok(path); - } - } - - // 如果无法获取系统目录,则回退到可执行文件目录 - let fallback_dir = PathBuf::from(exe_dir).join(".config").join(APP_ID); - log::warn!(target: "app", "Using fallback data directory: {fallback_dir:?}"); - return Ok(fallback_dir); - } - }; + let app_handle = handle::Handle::app_handle(); match app_handle.path().data_dir() { Ok(dir) => Ok(dir.join(APP_ID)), @@ -111,18 +65,7 @@ pub fn app_home_dir() -> Result { /// get the resources dir pub fn app_resources_dir() -> Result { // 避免在Handle未初始化时崩溃 - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::warn!(target: "app", "app_handle not initialized in app_resources_dir, using fallback"); - // 使用可执行文件目录作为备用 - let exe_dir = tauri::utils::platform::current_exe()? - .parent() - .ok_or(anyhow::anyhow!("failed to get executable directory"))? - .to_path_buf(); - return Ok(exe_dir.join("resources")); - } - }; + let app_handle = handle::Handle::app_handle(); match app_handle.path().resource_dir() { Ok(dir) => Ok(dir.join("resources")), diff --git a/src-tauri/src/utils/format.rs b/src-tauri/src/utils/format.rs index 270206c3b..86437a496 100644 --- a/src-tauri/src/utils/format.rs +++ b/src-tauri/src/utils/format.rs @@ -1,4 +1,5 @@ /// Format bytes into human readable string (B, KB, MB, GB) +#[allow(unused)] pub fn fmt_bytes(bytes: u64) -> String { const UNITS: &[&str] = &["B", "KB", "MB", "GB"]; let (mut val, mut unit) = (bytes as f64, 0); diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index 325fbbb7b..09b356e0f 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -494,15 +494,7 @@ pub fn init_scheme() -> Result<()> { } pub async fn startup_script() -> Result<()> { - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - return Err(anyhow::anyhow!( - "app_handle not available for startup script execution" - )); - } - }; - + let app_handle = handle::Handle::app_handle(); let script_path = { let verge = Config::verge().await; let verge = verge.latest_ref(); diff --git a/src-tauri/src/utils/logging.rs b/src-tauri/src/utils/logging.rs index 1390ca0e7..7f95b9b4c 100644 --- a/src-tauri/src/utils/logging.rs +++ b/src-tauri/src/utils/logging.rs @@ -28,7 +28,7 @@ pub enum Type { Lightweight, Network, ProxyMode, - Ipc, + // Ipc, // Cache, ClashVergeRev, } @@ -51,7 +51,7 @@ impl fmt::Display for Type { Type::Lightweight => write!(f, "[Lightweight]"), Type::Network => write!(f, "[Network]"), Type::ProxyMode => write!(f, "[ProxMode]"), - Type::Ipc => write!(f, "[IPC]"), + // Type::Ipc => write!(f, "[IPC]"), // Type::Cache => write!(f, "[Cache]"), Type::ClashVergeRev => write!(f, "[ClashVergeRev]"), } diff --git a/src-tauri/src/utils/resolve/dns.rs b/src-tauri/src/utils/resolve/dns.rs index e3618178f..4b710bdc7 100644 --- a/src-tauri/src/utils/resolve/dns.rs +++ b/src-tauri/src/utils/resolve/dns.rs @@ -2,13 +2,7 @@ pub async fn set_public_dns(dns_server: String) { use crate::{core::handle, utils::dirs}; use tauri_plugin_shell::ShellExt; - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::error!(target: "app", "app_handle not available for DNS configuration"); - return; - } - }; + let app_handle = handle::Handle::app_handle(); log::info!(target: "app", "try to set system dns"); let resource_dir = match dirs::app_resources_dir() { @@ -50,13 +44,7 @@ pub async fn set_public_dns(dns_server: String) { pub async fn restore_public_dns() { use crate::{core::handle, utils::dirs}; use tauri_plugin_shell::ShellExt; - let app_handle = match handle::Handle::global().app_handle() { - Some(handle) => handle, - None => { - log::error!(target: "app", "app_handle not available for DNS restoration"); - return; - } - }; + let app_handle = handle::Handle::app_handle(); log::info!(target: "app", "try to unset system dns"); let resource_dir = match dirs::app_resources_dir() { Ok(dir) => dir, diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index bdb7fa5ea..5cc3c8472 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use tauri::AppHandle; use crate::{ config::Config, @@ -18,8 +17,8 @@ pub mod ui; pub mod window; pub mod window_script; -pub fn resolve_setup_handle(app_handle: AppHandle) { - init_handle(app_handle); +pub fn resolve_setup_handle() { + init_handle(); } pub fn resolve_setup_sync() { @@ -121,9 +120,9 @@ pub async fn resolve_reset_async() -> Result<(), anyhow::Error> { Ok(()) } -pub fn init_handle(app_handle: AppHandle) { +pub fn init_handle() { logging!(info, Type::Setup, true, "Initializing app handle..."); - handle::Handle::global().init(app_handle); + handle::Handle::global().init(); } pub(super) fn init_scheme() { diff --git a/src-tauri/src/utils/resolve/window.rs b/src-tauri/src/utils/resolve/window.rs index e99e3307f..31036f43e 100644 --- a/src-tauri/src/utils/resolve/window.rs +++ b/src-tauri/src/utils/resolve/window.rs @@ -2,7 +2,7 @@ use tauri::WebviewWindow; use crate::{ core::handle, - logging, logging_error, + logging_error, utils::{ logging::Type, resolve::window_script::{INITIAL_LOADING_OVERLAY, WINDOW_INITIAL_SCRIPT}, @@ -18,18 +18,10 @@ const MINIMAL_HEIGHT: f64 = 520.0; /// 构建新的 WebView 窗口 pub fn build_new_window() -> Result { - let app_handle = handle::Handle::global().app_handle().ok_or_else(|| { - logging!( - error, - Type::Window, - true, - "无法获取app_handle,窗口创建失败" - ); - "无法获取app_handle".to_string() - })?; + let app_handle = handle::Handle::app_handle(); match tauri::WebviewWindowBuilder::new( - &app_handle, + app_handle, "main", /* the unique window label */ tauri::WebviewUrl::App("index.html".into()), ) diff --git a/src-tauri/src/utils/window_manager.rs b/src-tauri/src/utils/window_manager.rs index d982a8e25..7ce5ce36e 100644 --- a/src-tauri/src/utils/window_manager.rs +++ b/src-tauri/src/utils/window_manager.rs @@ -67,7 +67,7 @@ fn should_handle_window_operation() -> bool { let now = Instant::now(); let elapsed = now.duration_since(*last_operation); - log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", + log::debug!(target: "app", "[防抖] 检查窗口操作间隔: {}ms (需要>={}ms)", elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); if elapsed >= Duration::from_millis(WINDOW_OPERATION_DEBOUNCE_MS) { @@ -76,7 +76,7 @@ fn should_handle_window_operation() -> bool { log::info!(target: "app", "[防抖] 窗口操作被允许执行"); true } else { - log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", + log::warn!(target: "app", "[防抖] 窗口操作被防抖机制忽略,距离上次操作 {}ms < {}ms", elapsed.as_millis(), WINDOW_OPERATION_DEBOUNCE_MS); false } @@ -117,9 +117,8 @@ impl WindowManager { /// 获取主窗口实例 pub fn get_main_window() -> Option> { - handle::Handle::global() - .app_handle() - .and_then(|app| app.get_webview_window("main")) + let app_handle = handle::Handle::app_handle(); + app_handle.get_webview_window("main") } /// 智能显示主窗口 diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 747e9909a..8e2c72cef 100755 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -49,8 +49,11 @@ "security": { "capabilities": ["desktop-capability", "migrated"], "assetProtocol": { - "scope": ["$APPDATA/**", "$RESOURCE/../**", "**"], - "enable": true + "enable": true, + "scope": { + "allow": ["**"], + "requireLiteralLeadingDot": false + } }, "csp": null } diff --git a/src/components/connection/connection-detail.tsx b/src/components/connection/connection-detail.tsx index dd129f5c3..b2ab3f8f9 100644 --- a/src/components/connection/connection-detail.tsx +++ b/src/components/connection/connection-detail.tsx @@ -4,8 +4,8 @@ import dayjs from "dayjs"; import { t } from "i18next"; import { useImperativeHandle, useState, type Ref } from "react"; -import { deleteConnection } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; +import { closeConnections } from "tauri-plugin-mihomo-api"; export interface ConnectionDetailRef { open: (detail: IConnectionsItem) => void; @@ -97,7 +97,7 @@ const InnerConnectionDetail = ({ data, onClose }: InnerProps) => { { label: t("Type"), value: `${metadata.type}(${metadata.network})` }, ]; - const onDelete = useLockFn(async () => deleteConnection(data.id)); + const onDelete = useLockFn(async () => closeConnections(data.id)); return ( diff --git a/src/components/connection/connection-item.tsx b/src/components/connection/connection-item.tsx index 77d2c7e94..370f4fa4d 100644 --- a/src/components/connection/connection-item.tsx +++ b/src/components/connection/connection-item.tsx @@ -10,8 +10,8 @@ import { import { useLockFn } from "ahooks"; import dayjs from "dayjs"; -import { deleteConnection } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; +import { closeConnections } from "tauri-plugin-mihomo-api"; const Tag = styled("span")(({ theme }) => ({ fontSize: "10px", @@ -34,7 +34,7 @@ export const ConnectionItem = (props: Props) => { const { id, metadata, chains, start, curUpload, curDownload } = value; - const onDelete = useLockFn(async () => deleteConnection(id)); + const onDelete = useLockFn(async () => closeConnections(id)); const showTraffic = curUpload! >= 100 || curDownload! >= 100; return ( diff --git a/src/components/home/clash-info-card.tsx b/src/components/home/clash-info-card.tsx index a3ca1f75d..9b6ff4b71 100644 --- a/src/components/home/clash-info-card.tsx +++ b/src/components/home/clash-info-card.tsx @@ -53,7 +53,7 @@ export const ClashInfoCard = () => { {t("Mixed Port")} - {clashConfig["mixed-port"] || "-"} + {clashConfig.mixedPort || "-"} diff --git a/src/components/home/clash-mode-card.tsx b/src/components/home/clash-mode-card.tsx index bceaadb4f..9458da050 100644 --- a/src/components/home/clash-mode-card.tsx +++ b/src/components/home/clash-mode-card.tsx @@ -7,10 +7,11 @@ import { Box, Paper, Stack, Typography } from "@mui/material"; import { useLockFn } from "ahooks"; import { useMemo } from "react"; import { useTranslation } from "react-i18next"; +import { closeAllConnections } from "tauri-plugin-mihomo-api"; import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-context"; -import { closeAllConnections, patchClashMode } from "@/services/cmds"; +import { patchClashMode } from "@/services/cmds"; export const ClashModeCard = () => { const { t } = useTranslation(); diff --git a/src/components/home/current-proxy-card.tsx b/src/components/home/current-proxy-card.tsx index 1e8d140fa..5bed1a104 100644 --- a/src/components/home/current-proxy-card.tsx +++ b/src/components/home/current-proxy-card.tsx @@ -36,8 +36,8 @@ import { EnhancedCard } from "@/components/home/enhanced-card"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-context"; -import { getGroupProxyDelays, providerHealthCheck } from "@/services/cmds"; import delayManager from "@/services/delay"; +import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; // 本地存储的键名 const STORAGE_KEY_GROUP = "clash-verge-selected-proxy-group"; @@ -466,7 +466,7 @@ export const CurrentProxyCard = () => { if (providers.size > 0) { console.log(`[CurrentProxyCard] 开始测试提供者节点`); await Promise.allSettled( - [...providers].map((p) => providerHealthCheck(p)), + [...providers].map((p) => healthcheckProxyProvider(p)), ); } @@ -478,7 +478,7 @@ export const CurrentProxyCard = () => { try { await Promise.race([ delayManager.checkListDelay(proxyNames, groupName, timeout), - getGroupProxyDelays(groupName, url, timeout), + delayGroup(groupName, url, timeout), ]); console.log(`[CurrentProxyCard] 延迟测试完成,组: ${groupName}`); } catch (error) { diff --git a/src/components/home/enhanced-canvas-traffic-graph.tsx b/src/components/home/enhanced-canvas-traffic-graph.tsx index 47a9c9249..7f78cf66f 100644 --- a/src/components/home/enhanced-canvas-traffic-graph.tsx +++ b/src/components/home/enhanced-canvas-traffic-graph.tsx @@ -92,7 +92,7 @@ export const EnhancedCanvasTrafficGraph = memo( const { t } = useTranslation(); // 使用增强版全局流量数据管理 - const { dataPoints, getDataForTimeRange, isDataFresh, samplerStats } = + const { dataPoints, getDataForTimeRange, samplerStats } = useTrafficGraphDataEnhanced(); // 基础状态 @@ -865,6 +865,7 @@ export const EnhancedCanvasTrafficGraph = memo( }} onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave} + onClick={toggleStyle} /> {/* 控制层覆盖 */} @@ -962,8 +963,8 @@ export const EnhancedCanvasTrafficGraph = memo( lineHeight: 1.2, }} > - Points: {displayData.length} | Fresh: {isDataFresh ? "✓" : "✗"} | - Compressed: {samplerStats.compressedBufferSize} + Points: {displayData.length} | Compressed:{" "} + {samplerStats.compressedBufferSize} {/* 悬浮提示框 */} @@ -988,6 +989,7 @@ export const EnhancedCanvasTrafficGraph = memo( boxShadow: "0 4px 12px rgba(0,0,0,0.15)", backdropFilter: "none", opacity: 1, + whiteSpace: "nowrap", }} > diff --git a/src/components/home/enhanced-traffic-stats.tsx b/src/components/home/enhanced-traffic-stats.tsx index 9bf97200a..e731ac72c 100644 --- a/src/components/home/enhanced-traffic-stats.tsx +++ b/src/components/home/enhanced-traffic-stats.tsx @@ -7,7 +7,6 @@ import { MemoryRounded, } from "@mui/icons-material"; import { - Box, Grid, PaletteColor, Paper, @@ -15,16 +14,16 @@ import { alpha, useTheme, } from "@mui/material"; -import { ReactNode, memo, useCallback, useMemo, useRef } from "react"; +import { useRef, memo, useMemo } from "react"; +import { ReactNode } from "react"; import { useTranslation } from "react-i18next"; -import useSWR from "swr"; import { TrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; -import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor"; +import { useConnectionData } from "@/hooks/use-connection-data"; +import { useMemoryData } from "@/hooks/use-memory-data"; +import { useTrafficData } from "@/hooks/use-traffic-data"; import { useVerge } from "@/hooks/use-verge"; import { useVisibility } from "@/hooks/use-visibility"; -import { useAppData } from "@/providers/app-data-context"; -import { gc, isDebugEnabled } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; import { @@ -148,51 +147,33 @@ export const EnhancedTrafficStats = () => { const trafficRef = useRef(null); const pageVisible = useVisibility(); - // 使用AppDataProvider - const { connections } = useAppData(); + const { + response: { data: traffic }, + } = useTrafficData(); - // 使用增强版的统一流量数据Hook - const { traffic, memory, isLoading, isDataFresh, hasValidData } = - useTrafficDataEnhanced(); + const { + response: { data: memory }, + } = useMemoryData(); + + const { + response: { data: connections }, + } = useConnectionData(); // 是否显示流量图表 const trafficGraph = verge?.traffic_graph ?? true; - // 检查是否支持调试 - // TODO: merge this hook with layout-traffic.tsx - const { data: isDebug } = useSWR( - `clash-verge-rev-internal://isDebugEnabled`, - () => isDebugEnabled(), - { - // default value before is fetched - fallbackData: false, - }, - ); - // Canvas组件现在直接从全局Hook获取数据,无需手动添加数据点 - // 执行垃圾回收 - const handleGarbageCollection = useCallback(async () => { - if (isDebug) { - try { - await gc(); - console.log("[Debug] 垃圾回收已执行"); - } catch (err) { - console.error("[Debug] 垃圾回收失败:", err); - } - } - }, [isDebug]); - // 使用useMemo计算解析后的流量数据 const parsedData = useMemo(() => { - const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0); - const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0); - const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0); + const [up, upUnit] = parseTraffic(traffic?.up || 0); + const [down, downUnit] = parseTraffic(traffic?.down || 0); + const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0); const [uploadTotal, uploadTotalUnit] = parseTraffic( - connections.uploadTotal, + connections?.uploadTotal, ); const [downloadTotal, downloadTotalUnit] = parseTraffic( - connections.downloadTotal, + connections?.downloadTotal, ); return { @@ -206,7 +187,7 @@ export const EnhancedTrafficStats = () => { uploadTotalUnit, downloadTotal, downloadTotalUnit, - connectionsCount: connections.count, + connectionsCount: connections?.connections.length, }; }, [traffic, memory, connections]); @@ -228,33 +209,10 @@ export const EnhancedTrafficStats = () => { >
- {isDebug && ( -
- DEBUG: {trafficRef.current ? "图表已初始化" : "图表未初始化"} -
- 状态: {isDataFresh ? "active" : "inactive"} -
- 数据新鲜度: {traffic?.is_fresh ? "Fresh" : "Stale"} -
- {new Date().toISOString().slice(11, 19)} -
- )}
); - }, [trafficGraph, pageVisible, theme.palette.divider, isDebug]); + }, [trafficGraph, pageVisible, theme.palette.divider]); // 使用useMemo计算统计卡片配置 const statCards = useMemo( @@ -300,10 +258,10 @@ export const EnhancedTrafficStats = () => { value: parsedData.inuse, unit: parsedData.inuseUnit, color: "error" as const, - onClick: isDebug ? handleGarbageCollection : undefined, + onClick: undefined, }, ], - [t, parsedData, isDebug, handleGarbageCollection], + [t, parsedData], ); return ( @@ -320,28 +278,11 @@ export const EnhancedTrafficStats = () => { )} {/* 统计卡片区域 */} - {statCards.map((card, index) => ( - - + {statCards.map((card, _index) => ( + + ))} - - {/* 数据状态指示器(调试用)*/} - {isDebug && ( - - - 数据状态: {isDataFresh ? "新鲜" : "过期"} | 有效数据:{" "} - {hasValidData ? "是" : "否"} | 加载中: {isLoading ? "是" : "否"} - - - )} ); diff --git a/src/components/layout/layout-traffic.tsx b/src/components/layout/layout-traffic.tsx index 71f662f2f..fd0526923 100644 --- a/src/components/layout/layout-traffic.tsx +++ b/src/components/layout/layout-traffic.tsx @@ -6,34 +6,19 @@ import { import { Box, Typography } from "@mui/material"; import { useEffect, useRef } from "react"; import { useTranslation } from "react-i18next"; -import useSWR from "swr"; import { LightweightTrafficErrorBoundary } from "@/components/common/traffic-error-boundary"; -import { useClashInfo } from "@/hooks/use-clash"; -import { useTrafficDataEnhanced } from "@/hooks/use-traffic-monitor"; import { useVerge } from "@/hooks/use-verge"; import { useVisibility } from "@/hooks/use-visibility"; -import { isDebugEnabled, gc, startTrafficService } from "@/services/cmds"; import parseTraffic from "@/utils/parse-traffic"; import { TrafficGraph, type TrafficRef } from "./traffic-graph"; +import { useTrafficData } from "@/hooks/use-traffic-data"; +import { useMemoryData } from "@/hooks/use-memory-data"; // setup the traffic export const LayoutTraffic = () => { - const { data: isDebug } = useSWR( - "clash-verge-rev-internal://isDebugEnabled", - () => isDebugEnabled(), - { - // default value before is fetched - fallbackData: false, - }, - ); - - if (isDebug) { - console.debug("[Traffic][LayoutTraffic] 组件正在渲染"); - } const { t } = useTranslation(); - const { clashInfo } = useClashInfo(); const { verge } = useVerge(); // whether hide traffic graph @@ -42,31 +27,19 @@ export const LayoutTraffic = () => { const trafficRef = useRef(null); const pageVisible = useVisibility(); - // 使用增强版的统一流量数据Hook - const { traffic, memory } = useTrafficDataEnhanced(); - - // 启动流量服务 - useEffect(() => { - console.log( - "[Traffic][LayoutTraffic] useEffect 触发,clashInfo:", - clashInfo, - "pageVisible:", - pageVisible, - ); - - // 简化条件,只要组件挂载就尝试启动服务 - console.log("[Traffic][LayoutTraffic] 开始启动流量服务"); - startTrafficService().catch((error) => { - console.error("[Traffic][LayoutTraffic] 启动流量服务失败:", error); - }); - }, []); // 移除依赖,只在组件挂载时启动一次 + const { + response: { data: traffic }, + } = useTrafficData(); + const { + response: { data: memory }, + } = useMemoryData(); // 监听数据变化,为图表添加数据点 useEffect(() => { - if (traffic?.raw && trafficRef.current) { + if (trafficRef.current) { trafficRef.current.appendData({ - up: traffic.raw.up_rate || 0, - down: traffic.raw.down_rate || 0, + up: traffic?.up || 0, + down: traffic?.down || 0, }); } }, [traffic]); @@ -75,9 +48,9 @@ export const LayoutTraffic = () => { const displayMemory = verge?.enable_memory_usage ?? true; // 使用parseTraffic统一处理转换,保持与首页一致的显示格式 - const [up, upUnit] = parseTraffic(traffic?.raw?.up_rate || 0); - const [down, downUnit] = parseTraffic(traffic?.raw?.down_rate || 0); - const [inuse, inuseUnit] = parseTraffic(memory?.raw?.inuse || 0); + const [up, upUnit] = parseTraffic(traffic?.up || 0); + const [down, downUnit] = parseTraffic(traffic?.down || 0); + const [inuse, inuseUnit] = parseTraffic(memory?.inuse || 0); const boxStyle: any = { display: "flex", @@ -114,18 +87,16 @@ export const LayoutTraffic = () => { 0 ? "secondary" : "disabled" - } + color={(traffic?.up || 0) > 0 ? "secondary" : "disabled"} /> {up} @@ -134,18 +105,16 @@ export const LayoutTraffic = () => { 0 ? "primary" : "disabled" - } + color={(traffic?.down || 0) > 0 ? "primary" : "disabled"} /> {down} @@ -155,15 +124,15 @@ export const LayoutTraffic = () => { {displayMemory && ( { - isDebug && (await gc()); + // isDebug && (await gc()); }} > diff --git a/src/components/proxy/provider-button.tsx b/src/components/proxy/provider-button.tsx index 522a62cde..e22b856e0 100644 --- a/src/components/proxy/provider-button.tsx +++ b/src/components/proxy/provider-button.tsx @@ -20,26 +20,12 @@ import { useLockFn } from "ahooks"; import dayjs from "dayjs"; import { useState } from "react"; import { useTranslation } from "react-i18next"; +import { updateProxyProvider } from "tauri-plugin-mihomo-api"; import { useAppData } from "@/providers/app-data-context"; -import { proxyProviderUpdate } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; import parseTraffic from "@/utils/parse-traffic"; -// 定义代理提供者类型 -interface ProxyProviderItem { - name?: string; - proxies: any[]; - updatedAt: number; - vehicleType: string; - subscriptionInfo?: { - Upload: number; - Download: number; - Total: number; - Expire: number; - }; -} - // 样式化组件 - 类型框 const TypeBox = styled(Box)<{ component?: React.ElementType }>(({ theme }) => ({ display: "inline-block", @@ -74,7 +60,7 @@ export const ProviderButton = () => { // 设置更新状态 setUpdating((prev) => ({ ...prev, [name]: true })); - await proxyProviderUpdate(name); + await updateProxyProvider(name); // 刷新数据 await refreshProxy(); @@ -115,7 +101,7 @@ export const ProviderButton = () => { // 改为串行逐个更新所有provider for (const name of allProviders) { try { - await proxyProviderUpdate(name); + await updateProxyProvider(name); // 每个更新完成后更新状态 setUpdating((prev) => ({ ...prev, [name]: false })); } catch (err) { @@ -177,161 +163,164 @@ export const ProviderButton = () => { - {Object.entries(proxyProviders || {}).map(([key, item]) => { - const provider = item as ProxyProviderItem; - const time = dayjs(provider.updatedAt); - const isUpdating = updating[key]; + {Object.entries(proxyProviders || {}) + .sort() + .map(([key, item]) => { + const provider = item; + const time = dayjs(provider.updatedAt); + const isUpdating = updating[key]; - // 订阅信息 - const sub = provider.subscriptionInfo; - const hasSubInfo = !!sub; - const upload = sub?.Upload || 0; - const download = sub?.Download || 0; - const total = sub?.Total || 0; - const expire = sub?.Expire || 0; + // 订阅信息 + const sub = provider.subscriptionInfo; + const hasSubInfo = !!sub; + const upload = sub?.Upload || 0; + const download = sub?.Download || 0; + const total = sub?.Total || 0; + const expire = sub?.Expire || 0; - // 流量使用进度 - const progress = - total > 0 - ? Math.min( - Math.round(((download + upload) * 100) / total) + 1, - 100, - ) - : 0; + // 流量使用进度 + const progress = + total > 0 + ? Math.min( + Math.round(((download + upload) * 100) / total) + 1, + 100, + ) + : 0; - return ( - { - const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; - const hoverColor = - mode === "light" - ? alpha(primary.main, 0.1) - : alpha(primary.main, 0.2); + return ( + { + const bgcolor = + mode === "light" ? "#ffffff" : "#24252f"; + const hoverColor = + mode === "light" + ? alpha(primary.main, 0.1) + : alpha(primary.main, 0.2); - return { - backgroundColor: bgcolor, - "&:hover": { - backgroundColor: hoverColor, - }, - }; - }, - ]} - > - - - {key} - - {provider.proxies.length} - - - {provider.vehicleType} - - - - - {t("Update At")}: - {time.fromNow()} - - - } - secondary={ - <> - {/* 订阅信息 */} - {hasSubInfo && ( - <> - - - {parseTraffic(upload + download)} /{" "} - {parseTraffic(total)} - - - {parseExpire(expire)} - - - - {/* 进度条 */} - 0 ? 1 : 0, - }} - /> - - )} - - } - /> - - - { - updateProvider(key); - }} - disabled={isUpdating} + + + {key} + + {provider.proxies.length} + + + {provider.vehicleType} + + + + + {t("Update At")}: + {time.fromNow()} + + + } + secondary={ + <> + {/* 订阅信息 */} + {hasSubInfo && ( + <> + + + {parseTraffic(upload + download)} /{" "} + {parseTraffic(total)} + + + {parseExpire(expire)} + + + + {/* 进度条 */} + 0 ? 1 : 0, + }} + /> + + )} + + } + /> + + - - - - - ); - })} + { + updateProvider(key); + }} + disabled={isUpdating} + sx={{ + animation: isUpdating + ? "spin 1s linear infinite" + : "none", + "@keyframes spin": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + }} + title={t("Update Provider") as string} + > + + + + + ); + })} diff --git a/src/components/proxy/proxy-chain.tsx b/src/components/proxy/proxy-chain.tsx index 285a6889c..651fcbc95 100644 --- a/src/components/proxy/proxy-chain.tsx +++ b/src/components/proxy/proxy-chain.tsx @@ -34,14 +34,13 @@ import { import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import useSWR from "swr"; - -import { useAppData } from "@/providers/app-data-context"; import { closeAllConnections, - getProxies, - updateProxyAndSync, - updateProxyChainConfigInRuntime, -} from "@/services/cmds"; + selectNodeForGroup, +} from "tauri-plugin-mihomo-api"; + +import { useAppData } from "@/providers/app-data-context"; +import { calcuProxies, updateProxyChainConfigInRuntime } from "@/services/cmds"; interface ProxyChainItem { id: string; @@ -204,7 +203,7 @@ export const ProxyChain = ({ // 获取当前代理信息以检查连接状态 const { data: currentProxies, mutate: mutateProxies } = useSWR( "getProxies", - getProxies, + calcuProxies, { revalidateOnFocus: true, revalidateIfStale: true, @@ -367,7 +366,7 @@ export const ProxyChain = ({ const targetGroup = mode === "global" ? "GLOBAL" : selectedGroup; - await updateProxyAndSync(targetGroup || "GLOBAL", lastNode.name); + await selectNodeForGroup(targetGroup || "GLOBAL", lastNode.name); localStorage.setItem("proxy-chain-group", targetGroup || "GLOBAL"); localStorage.setItem("proxy-chain-exit-node", lastNode.name); diff --git a/src/components/proxy/proxy-groups.tsx b/src/components/proxy/proxy-groups.tsx index 2622dfb1b..f3fd255c0 100644 --- a/src/components/proxy/proxy-groups.tsx +++ b/src/components/proxy/proxy-groups.tsx @@ -14,14 +14,13 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"; import useSWR from "swr"; +import { delayGroup, healthcheckProxyProvider } from "tauri-plugin-mihomo-api"; import { useProxySelection } from "@/hooks/use-proxy-selection"; import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-context"; import { - getGroupProxyDelays, getRuntimeConfig, - providerHealthCheck, updateProxyChainConfigInRuntime, } from "@/services/cmds"; import delayManager from "@/services/delay"; @@ -153,15 +152,14 @@ export const ProxyGroups = (props: Props) => { // 添加和清理滚动事件监听器 useEffect(() => { - const currentScroller = scrollerRef.current; - if (currentScroller) { - currentScroller.addEventListener("scroll", handleScroll, { - passive: true, - }); - return () => { - currentScroller.removeEventListener("scroll", handleScroll); - }; - } + if (!scrollerRef.current) return; + scrollerRef.current.addEventListener("scroll", handleScroll, { + passive: true, + }); + + return () => { + scrollerRef.current?.removeEventListener("scroll", handleScroll); + }; }, [handleScroll]); // 滚动到顶部 @@ -215,6 +213,7 @@ export const ProxyGroups = (props: Props) => { const currentGroup = getCurrentGroup(); const availableGroups = getAvailableGroups(); + // TODO: 频繁点击切换代理节点,导致应用卡死 const handleChangeProxy = useCallback( (group: IProxyGroupItem, proxy: IProxyItem) => { if (isChainMode) { @@ -273,7 +272,7 @@ export const ProxyGroups = (props: Props) => { if (providers.size) { console.log(`[ProxyGroups] 发现提供者,数量: ${providers.size}`); Promise.allSettled( - [...providers].map((p) => providerHealthCheck(p)), + [...providers].map((p) => healthcheckProxyProvider(p)), ).then(() => { console.log(`[ProxyGroups] 提供者健康检查完成`); onProxies(); @@ -289,7 +288,7 @@ export const ProxyGroups = (props: Props) => { try { await Promise.race([ delayManager.checkListDelay(names, groupName, timeout), - getGroupProxyDelays(groupName, url, timeout).then((result) => { + delayGroup(groupName, url, timeout).then((result) => { console.log( `[ProxyGroups] getGroupProxyDelays返回结果数量:`, Object.keys(result || {}).length, @@ -518,7 +517,7 @@ export const ProxyGroups = (props: Props) => { }, }} > - {availableGroups.map((group: any, index: number) => ( + {availableGroups.map((group: any, _index: number) => ( handleGroupSelect(group.name)} diff --git a/src/components/proxy/proxy-item-mini.tsx b/src/components/proxy/proxy-item-mini.tsx index aa6e9a6bf..8e456f295 100644 --- a/src/components/proxy/proxy-item-mini.tsx +++ b/src/components/proxy/proxy-item-mini.tsx @@ -37,12 +37,12 @@ export const ProxyItemMini = (props: Props) => { return () => { delayManager.removeListener(proxy.name, group.name); }; - }, [proxy.name, group.name]); + }, [isPreset, proxy.name, group.name]); useEffect(() => { if (!proxy) return; setDelay(delayManager.getDelayFix(proxy, group.name)); - }, [proxy]); + }, [proxy, group.name]); const onDelay = useLockFn(async () => { setDelay(-2); @@ -200,7 +200,7 @@ export const ProxyItemMini = (props: Props) => { )} - {delay > 0 && ( + {delay >= 0 && ( // 显示延迟 { {delayManager.formatDelay(delay, timeout)} )} - {delay !== -2 && delay <= 0 && selected && ( + {proxy.type !== "Direct" && delay !== -2 && delay < 0 && selected && ( // 展示已选择的icon (({ theme }) => ({ display: "inline-block", @@ -60,7 +52,7 @@ export const ProviderButton = () => { // 设置更新状态 setUpdating((prev) => ({ ...prev, [name]: true })); - await ruleProviderUpdate(name); + await updateRuleProvider(name); // 刷新数据 await refreshRules(); @@ -101,7 +93,7 @@ export const ProviderButton = () => { // 改为串行逐个更新所有provider for (const name of allProviders) { try { - await ruleProviderUpdate(name); + await updateRuleProvider(name); // 每个更新完成后更新状态 setUpdating((prev) => ({ ...prev, [name]: false })); } catch (err) { @@ -160,112 +152,117 @@ export const ProviderButton = () => { - {Object.entries(ruleProviders || {}).map(([key, item]) => { - const provider = item as RuleProviderItem; - const time = dayjs(provider.updatedAt); - const isUpdating = updating[key]; + {Object.entries(ruleProviders || {}) + .sort() + .map(([key, item]) => { + const provider = item; + const time = dayjs(provider.updatedAt); + const isUpdating = updating[key]; - return ( - { - const bgcolor = mode === "light" ? "#ffffff" : "#24252f"; - const hoverColor = - mode === "light" - ? alpha(primary.main, 0.1) - : alpha(primary.main, 0.2); + return ( + { + const bgcolor = + mode === "light" ? "#ffffff" : "#24252f"; + const hoverColor = + mode === "light" + ? alpha(primary.main, 0.1) + : alpha(primary.main, 0.2); - return { - backgroundColor: bgcolor, - "&:hover": { - backgroundColor: hoverColor, - borderColor: alpha(primary.main, 0.3), - }, - }; - }, - ]} - > - - - {key} - - {provider.ruleCount} - - - - - {t("Update At")}: - {time.fromNow()} - - - } - secondary={ - - - {provider.vehicleType} - - {provider.behavior} - - } - /> - - - updateProvider(key)} - disabled={isUpdating} + + + {key} + + {provider.ruleCount} + + + + + {t("Update At")}: + {time.fromNow()} + + + } + secondary={ + + + {provider.vehicleType} + + + {provider.behavior} + + + } + /> + + - - - - - ); - })} + updateProvider(key)} + disabled={isUpdating} + sx={{ + animation: isUpdating + ? "spin 1s linear infinite" + : "none", + "@keyframes spin": { + "0%": { transform: "rotate(0deg)" }, + "100%": { transform: "rotate(360deg)" }, + }, + }} + title={t("Update Provider") as string} + > + + +
+ + ); + })} diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx index 373b61fe8..be951de6c 100644 --- a/src/components/setting/mods/clash-core-viewer.tsx +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -16,16 +16,11 @@ import type { Ref } from "react"; import { useImperativeHandle, useState } from "react"; import { useTranslation } from "react-i18next"; import { mutate } from "swr"; +import { closeAllConnections, upgradeCore } from "tauri-plugin-mihomo-api"; import { BaseDialog, DialogRef } from "@/components/base"; import { useVerge } from "@/hooks/use-verge"; -import { - changeClashCore, - closeAllConnections, - forceRefreshClashConfig, - restartCore, - upgradeCore, -} from "@/services/cmds"; +import { changeClashCore, restartCore } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; const VALID_CORE = [ @@ -66,8 +61,6 @@ export function ClashCoreViewer({ ref }: { ref?: Ref }) { mutateVerge(); setTimeout(async () => { - // 核心切换后强制刷新配置缓存 - await forceRefreshClashConfig(); mutate("getClashConfig"); mutate("getVersion"); setChangingCore(null); diff --git a/src/components/setting/mods/sysproxy-viewer.tsx b/src/components/setting/mods/sysproxy-viewer.tsx index b8bc792bc..00aa2bce4 100644 --- a/src/components/setting/mods/sysproxy-viewer.tsx +++ b/src/components/setting/mods/sysproxy-viewer.tsx @@ -20,6 +20,7 @@ import { } from "react"; import { useTranslation } from "react-i18next"; import useSWR, { mutate } from "swr"; +import { getBaseConfig } from "tauri-plugin-mihomo-api"; import { BaseDialog, DialogRef, Switch } from "@/components/base"; import { BaseFieldset } from "@/components/base/base-fieldset"; @@ -29,7 +30,6 @@ import { useVerge } from "@/hooks/use-verge"; import { useAppData } from "@/providers/app-data-context"; import { getAutotemProxy, - getClashConfig, getNetworkInterfacesInfo, getSystemHostname, getSystemProxy, @@ -123,26 +123,21 @@ export const SysproxyViewer = forwardRef((props, ref) => { return "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,172.29.0.0/16,localhost,*.local,*.crashlytics.com,"; }; - const { data: clashConfig } = useSWR("getClashConfig", getClashConfig, { + const { data: clashConfig } = useSWR("getClashConfig", getBaseConfig, { revalidateOnFocus: false, revalidateIfStale: true, dedupingInterval: 1000, errorRetryInterval: 5000, }); - const [prevMixedPort, setPrevMixedPort] = useState( - clashConfig?.["mixed-port"], - ); + const [prevMixedPort, setPrevMixedPort] = useState(clashConfig?.mixedPort); useEffect(() => { - if ( - clashConfig?.["mixed-port"] && - clashConfig?.["mixed-port"] !== prevMixedPort - ) { - setPrevMixedPort(clashConfig?.["mixed-port"]); + if (clashConfig?.mixedPort && clashConfig.mixedPort !== prevMixedPort) { + setPrevMixedPort(clashConfig.mixedPort); resetSystemProxy(); } - }, [clashConfig?.["mixed-port"]]); + }, [clashConfig?.mixedPort]); const resetSystemProxy = async () => { try { @@ -180,7 +175,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { if (isPacMode) { const host = value.proxy_host || "127.0.0.1"; - const port = verge?.verge_mixed_port || clashConfig["mixed-port"] || 7897; + const port = verge?.verge_mixed_port || clashConfig.mixedPort || 7897; return `${host}:${port}`; } else { return systemProxyAddress; @@ -332,7 +327,7 @@ export const SysproxyViewer = forwardRef((props, ref) => { if (pacContent) { pacContent = pacContent.replace(/%proxy_host%/g, value.proxy_host); // 将 mixed-port 转换为字符串 - const mixedPortStr = (clashConfig?.["mixed-port"] || "").toString(); + const mixedPortStr = (clashConfig?.mixedPort || "").toString(); pacContent = pacContent.replace(/%mixed-port%/g, mixedPortStr); } diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index 481683fa5..1d2b982fd 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -4,14 +4,15 @@ import { invoke } from "@tauri-apps/api/core"; import { useLockFn } from "ahooks"; import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { updateGeo } from "tauri-plugin-mihomo-api"; import { DialogRef, Switch } from "@/components/base"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; import { useClash } from "@/hooks/use-clash"; import { useVerge } from "@/hooks/use-verge"; import { invoke_uwp_tool } from "@/services/cmds"; -import { updateGeoData } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; +import { useClashLog } from "@/services/states"; import getSystem from "@/utils/get-system"; import { ClashCoreViewer } from "./mods/clash-core-viewer"; @@ -35,6 +36,7 @@ const SettingClash = ({ onError }: Props) => { const { clash, version, mutateClash, patchClash } = useClash(); const { verge, patchVerge } = useVerge(); + const [, setClashLog] = useClashLog(); const { ipv6, @@ -64,7 +66,7 @@ const SettingClash = ({ onError }: Props) => { }; const onUpdateGeo = async () => { try { - await updateGeoData(); + await updateGeo(); showNotice("success", t("GeoData Updated")); } catch (err: any) { showNotice("error", err?.response.data.message || err.toString()); @@ -186,7 +188,10 @@ const SettingClash = ({ onError }: Props) => { onCatch={onError} onFormat={(e: any) => e.target.value} onChange={(e) => onChangeData({ "log-level": e })} - onGuard={(e) => patchClash({ "log-level": e })} + onGuard={(e) => { + setClashLog((pre: any) => ({ ...pre, logLevel: e })); + return patchClash({ "log-level": e }); + }} >