Compare commits

...
15 Commits
Author SHA1 Message Date
AetherWingandSlinetrac e1d914e61d Chore(i18n): Improve Chinese–English typesetting (#6351)
* chore(i18n): add spacing after "TUN"

* docs: Changelog

* chore(i18n): Improve Chinese–English typesetting

* Apply suggestions from code review

* chore: i18n

---------

Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-23 05:17:23 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
6dba62a3b4 chore(deps): lock file maintenance (#6348)
* chore(deps): lock file maintenance

* chore(deps): pnpm update

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-23 01:52:41 +00:00
renovate[bot]renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>Slinetrac
acab77a1b4 chore(deps): lock file maintenance (#6347)
* chore(deps): lock file maintenance

* chore(deps): update Cargo.toml

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Slinetrac <realakayuki@gmail.com>
2026-02-23 01:52:27 +00:00
Slinetrac 1bf445ddcc fix(tun): avoid service IPC startup race on Windows (#6340)
* fix(tun): avoid service IPC startup race on Windows

* docs: Changelog.md

* style: prettier
2026-02-22 13:09:47 +00:00
wonfen 700011688b feat: mask all URLs embedded in an error/log string for safe logging 2026-02-22 07:37:40 +08:00
oomeowandTunglies 0cc9bb2f30 refactor: mihomo ipc (#6312)
* refactor: mihomo ipc

* feat: enable tauri-plugin-mihomo log

* chore: filter tauri ipc modules log

* chore: update tauri-plugin-mihomo dep

* chore: bump tauri-plugin-mihomo version to 0.1.6

---------

Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
2026-02-21 18:36:49 +00:00
renovate[bot]andrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com> 7d29c0c6ee chore(deps): lock file maintenance (#6318)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-21 17:55:26 +00:00
wonfen 321017413d chore: replace node test with cp.cloudflare.com 2026-02-21 14:28:27 +08:00
wonfen 7f045943e2 chore: update changelog 2026-02-21 04:23:03 +08:00
wonfen fa07dfbc9a feat: mask sensitive parts of a subscription URL for safe logging 2026-02-21 04:19:46 +08:00
Tunglies 119aaee546 refactor: optimize profile change notifications to use references 2026-02-20 17:58:04 +08:00
Tunglies 5f573ca2d6 refactor: simplfy backend notify message to frontend (#6323) 2026-02-20 03:18:51 +00:00
Tunglies e5dd127bcc feat: enhance profile update handling with manual trigger 2026-02-20 11:12:18 +08:00
Tunglies 7528c238c4 chore: bump version to 2.4.7 2026-02-20 10:58:32 +08:00
wonfen 10601e873e fix: telegram release link 2026-02-20 05:46:59 +08:00
32 changed files with 1140 additions and 1240 deletions
Generated
+269 -314
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -56,7 +56,7 @@ tauri-plugin-clash-verge-sysinfo = { path = "crates/tauri-plugin-clash-verge-sys
tauri = { version = "2.10.2" }
tauri-plugin-clipboard-manager = "2.3.2"
parking_lot = { version = "0.12.5", features = ["hardware-lock-elision"] }
anyhow = "1.0.101"
anyhow = "1.0.102"
criterion = { version = "0.8.2", features = ["async_tokio"] }
tokio = { version = "1.49.0", features = [
"rt-multi-thread",
@@ -73,7 +73,7 @@ compact_str = { version = "0.9.0", features = ["serde"] }
serde = { version = "1.0.228" }
serde_json = { version = "1.0.149" }
serde_yaml_ng = { version = "0.10.0" }
bitflags = { version = "2.10.0" }
bitflags = { version = "2.11.0" }
# *** For Windows platform only ***
deelevate = "0.2.0"
+5 -29
View File
@@ -1,43 +1,19 @@
## v(2.4.6)
> [!IMPORTANT]
> 历经多轮磨合与修正,这是自 2.0 以来我们最满意的里程碑版本。建议所有用户立即升级。
## v2.4.7
### 🐞 修复问题
- 修复首次启动时代理信息刷新缓慢
- 修复无网络时无限请求 IP 归属查询
- 修复 WebDAV 页面重试逻辑
- 修复 Linux 通过 GUI 安装服务模式权限不符合预期
- 修复 macOS 因网口顺序导致无法正确设置代理
- 修复恢复休眠后无法操作托盘
- 修复首页当前节点图标语义显示不一致
- 修复使用 URL scheme 导入订阅时没有及时重载配置
- 修复规则界面里的行号展示逻辑
- 修复 Windows 托盘打开日志失败
- 修复 KDE 首次启动报错
- 修复 Windows 管理员身份运行时开关 TUN 模式异常
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 升级 Mihomo 内核到最新
- 支持订阅设置自动延时监测间隔
- 新增流量隧道管理界面,支持可视化添加/删除隧道配置
- Masque 协议的 GUI 支持
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 安装服务失败时报告更详细的错误
- 避免脏订阅地址无法 Scheme 导入订阅
- macOS TUN 覆盖 DNS 时使用 114.114.114.114
- 连通性测试替换为更快的 http://1.0.0.1
- 连接、规则、日志等页面的过滤搜索组件新增了清空输入框按钮
- 链式代理增加明显的入口出口与数据流向标识
- 优化 IP 信息卡
- 美化代理组图标样式
- 移除 Linux resources 文件夹下多余的服务二进制文件
- 优化订阅错误通知,仅在手动触发时
- 隐藏日志中的订阅信息
- 优化部分界面文案文本
</details>
@@ -8,10 +8,12 @@ rust-version = "1.91"
tauri = { workspace = true }
tauri-plugin-clipboard-manager = { workspace = true }
parking_lot = { workspace = true }
sysinfo = { version = "0.38.0", features = ["network", "system"] }
# sysinfo 0.38.2 conflicts with dark-light
# see https://github.com/GuillaumeGomez/sysinfo/issues/1623
sysinfo = { version = "0.38", features = ["network", "system"] }
[target.'cfg(not(windows))'.dependencies]
libc = "0.2.180"
libc = "0.2.182"
[target.'cfg(windows)'.dependencies]
deelevate = { workspace = true }
+44
View File
@@ -1,3 +1,47 @@
## v(2.4.6)
> [!IMPORTANT]
> 历经多轮磨合与修正,这是自 2.0 以来我们最满意的里程碑版本。建议所有用户立即升级。
### 🐞 修复问题
- 修复首次启动时代理信息刷新缓慢
- 修复无网络时无限请求 IP 归属查询
- 修复 WebDAV 页面重试逻辑
- 修复 Linux 通过 GUI 安装服务模式权限不符合预期
- 修复 macOS 因网口顺序导致无法正确设置代理
- 修复恢复休眠后无法操作托盘
- 修复首页当前节点图标语义显示不一致
- 修复使用 URL scheme 导入订阅时没有及时重载配置
- 修复规则界面里的行号展示逻辑
- 修复 Windows 托盘打开日志失败
- 修复 KDE 首次启动报错
<details>
<summary><strong> ✨ 新增功能 </strong></summary>
- 升级 Mihomo 内核到最新
- 支持订阅设置自动延时监测间隔
- 新增流量隧道管理界面,支持可视化添加/删除隧道配置
- Masque 协议的 GUI 支持
</details>
<details>
<summary><strong> 🚀 优化改进 </strong></summary>
- 安装服务失败时报告更详细的错误
- 避免脏订阅地址无法 Scheme 导入订阅
- macOS TUN 覆盖 DNS 时使用 114.114.114.114
- 连通性测试替换为更快的 http://1.0.0.1
- 连接、规则、日志等页面的过滤搜索组件新增了清空输入框按钮
- 链式代理增加明显的入口出口与数据流向标识
- 优化 IP 信息卡
- 美化代理组图标样式
- 移除 Linux resources 文件夹下多余的服务二进制文件
</details>
## v2.4.5
- **Mihomo(Meta) 内核升级至 v1.19.19**
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "clash-verge",
"version": "2.4.6",
"version": "2.4.7",
"license": "GPL-3.0-only",
"scripts": {
"prepare": "husky || true",
@@ -41,9 +41,9 @@
"@emotion/styled": "^11.14.1",
"@juggle/resize-observer": "^3.4.0",
"@monaco-editor/react": "^4.7.0",
"@mui/icons-material": "^7.3.7",
"@mui/icons-material": "^7.3.8",
"@mui/lab": "7.0.0-beta.17",
"@mui/material": "^7.3.7",
"@mui/material": "^7.3.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.18",
"@tauri-apps/api": "2.10.1",
@@ -59,17 +59,17 @@
"dayjs": "1.11.19",
"foxact": "^0.2.52",
"foxts": "^5.2.1",
"i18next": "^25.8.4",
"i18next": "^25.8.13",
"ipaddr.js": "^2.3.0",
"js-yaml": "^4.1.1",
"lodash-es": "^4.17.23",
"monaco-editor": "^0.55.1",
"monaco-yaml": "^5.4.0",
"monaco-yaml": "^5.4.1",
"nanoid": "^5.1.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-error-boundary": "6.1.1",
"react-hook-form": "^7.71.1",
"react-hook-form": "^7.71.2",
"react-i18next": "16.5.4",
"react-markdown": "10.1.0",
"react-router": "^7.13.0",
@@ -81,12 +81,12 @@
},
"devDependencies": {
"@actions/github": "^9.0.0",
"@eslint-react/eslint-plugin": "^2.12.2",
"@eslint-react/eslint-plugin": "^2.13.0",
"@eslint/js": "^10.0.1",
"@tauri-apps/cli": "2.10.0",
"@types/js-yaml": "^4.0.9",
"@types/lodash-es": "^4.17.12",
"@types/node": "^24.10.12",
"@types/node": "^24.10.13",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-legacy": "^7.2.1",
@@ -95,7 +95,7 @@
"cli-color": "^2.0.4",
"commander": "^14.0.3",
"cross-env": "^10.1.0",
"eslint": "^10.0.0",
"eslint": "^10.0.1",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import-x": "^4.16.1",
@@ -103,8 +103,8 @@
"eslint-plugin-react-compiler": "19.1.0-rc.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.0",
"eslint-plugin-unused-imports": "^4.3.0",
"glob": "^13.0.1",
"eslint-plugin-unused-imports": "^4.4.1",
"glob": "^13.0.6",
"globals": "^17.3.0",
"https-proxy-agent": "^7.0.6",
"husky": "^9.1.7",
@@ -113,10 +113,10 @@
"node-fetch": "^3.3.2",
"prettier": "^3.8.1",
"sass": "^1.97.3",
"tar": "^7.5.7",
"tar": "^7.5.9",
"terser": "^5.46.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.54.0",
"typescript-eslint": "^8.56.0",
"vite": "^7.3.1",
"vite-plugin-svgr": "^4.5.0"
},
+581 -592
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -90,7 +90,8 @@ async function sendTelegramNotification() {
const releaseTitle = isAutobuild ? "滚动更新版发布" : "正式发布";
const encodedVersion = encodeURIComponent(version);
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/autobuild">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
const releaseTag = isAutobuild ? "autobuild" : `v${version}`;
const content = `<b>🎉 <a href="https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/${releaseTag}">Clash Verge Rev v${version}</a> ${releaseTitle}</b>\n\n${formattedContent}`;
// 发送到 Telegram
try {
+5 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "clash-verge"
version = "2.4.6"
version = "2.4.7"
description = "clash verge"
authors = ["zzzgydi", "Tunglies", "wonfen", "MystiPanda"]
license = "GPL-3.0-only"
@@ -81,12 +81,12 @@ tauri-plugin-fs = "2.4.5"
tauri-plugin-process = "2.3.1"
tauri-plugin-deep-link = "2.4.7"
tauri-plugin-window-state = "2.4.1"
zip = "8.0.0"
zip = "8.1.0"
reqwest_dav = "0.3.2"
aes-gcm = { version = "0.10.3", features = ["std"] }
base64 = "0.22.1"
getrandom = "0.4.1"
futures = "0.3.31"
futures = "0.3.32"
gethostname = "1.1.0"
scopeguard = "1.2.0"
tauri-plugin-notification = "2.3.3"
@@ -95,13 +95,13 @@ backoff = { version = "0.4.0", features = ["tokio"] }
tauri-plugin-http = "2.5.7"
console-subscriber = { version = "0.5.0", optional = true }
tauri-plugin-devtools = { version = "2.0.1" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo" }
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo"}
clash_verge_logger = { git = "https://github.com/clash-verge-rev/clash-verge-logger" }
async-trait = "0.1.89"
clash_verge_service_ipc = { version = "2.1.3", features = [
"client",
], git = "https://github.com/clash-verge-rev/clash-verge-service-ipc" }
arc-swap = "1.8.1"
arc-swap = "1.8.2"
rust_iso3166 = "0.1.14"
# Use the git repo until the next release after v2.0.0.
dark-light = { git = "https://github.com/rust-dark-light/dark-light" }
+9 -10
View File
@@ -65,7 +65,7 @@ pub async fn enhance_profiles() -> CmdResult {
/// 导入配置文件
#[tauri::command]
pub async fn import_profile(url: std::string::String, option: Option<PrfOption>) -> CmdResult {
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", url);
logging!(info, Type::Cmd, "[导入订阅] 开始导入: {}", help::mask_url(&url));
// 直接依赖 PrfItem::from_url 自身的超时/重试逻辑,不再使用 tokio::time::timeout 包裹
let item = &mut match PrfItem::from_url(&url, None, None, option.as_ref()).await {
@@ -96,19 +96,18 @@ pub async fn import_profile(url: std::string::String, option: Option<PrfOption>)
if let Some(uid) = &item.uid {
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid.clone());
handle::Handle::notify_profile_changed(uid);
}
// 异步保存配置文件并发送全局通知
let uid_clone = item.uid.clone();
if let Some(uid) = uid_clone {
if let Some(uid) = &item.uid {
// 延迟发送,确保文件已完全写入
tokio::time::sleep(Duration::from_millis(100)).await;
logging!(info, Type::Cmd, "[导入订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid);
}
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", url);
logging!(info, Type::Cmd, "[导入订阅] 导入完成: {}", help::mask_url(&url));
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
Ok(())
}
@@ -137,7 +136,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
match profiles_append_item_with_filedata_safe(&item, file_data).await {
Ok(_) => {
// 发送配置变更通知
if let Some(uid) = item.uid.clone() {
if let Some(uid) = &item.uid {
logging!(info, Type::Cmd, "[创建订阅] 发送配置变更通知: {}", uid);
handle::Handle::notify_profile_changed(uid);
}
@@ -158,7 +157,7 @@ pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResu
/// 更新配置文件
#[tauri::command]
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
match feat::update_profile(&index, option.as_ref(), true, true).await {
match feat::update_profile(&index, option.as_ref(), true, true, true).await {
Ok(_) => {
let _: () = Config::profiles().await.apply();
Ok(())
@@ -184,7 +183,7 @@ pub async fn delete_profile(index: String) -> CmdResult {
handle::Handle::refresh_clash();
// 发送配置变更通知
logging!(info, Type::Cmd, "[删除订阅] 发送配置变更通知: {}", index);
handle::Handle::notify_profile_changed(index);
handle::Handle::notify_profile_changed(&index);
AutoBackupManager::trigger_backup(AutoBackupTrigger::ProfileChange);
}
Err(e) => {
@@ -315,7 +314,7 @@ async fn handle_success(current_value: Option<&String>) -> CmdResult<bool> {
&& WindowManager::get_main_window().is_some()
{
logging!(info, Type::Cmd, "向前端发送配置变更事件: {}", current);
handle::Handle::notify_profile_changed(current.to_owned());
handle::Handle::notify_profile_changed(current);
}
Ok(true)
@@ -434,7 +433,7 @@ pub async fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
logging!(error, Type::Timer, "刷新定时器失败: {}", e);
} else {
// 刷新成功后发送自定义事件,不触发配置重载
crate::core::handle::Handle::notify_timer_updated(index);
crate::core::handle::Handle::notify_timer_updated(&index);
}
});
}
-1
View File
@@ -23,7 +23,6 @@ pub mod timing {
use super::Duration;
pub const CONFIG_UPDATE_DEBOUNCE: Duration = Duration::from_millis(300);
pub const EVENT_EMIT_DELAY: Duration = Duration::from_millis(20);
pub const STARTUP_ERROR_DELAY: Duration = Duration::from_secs(2);
#[cfg(target_os = "windows")]
+19 -74
View File
@@ -1,10 +1,6 @@
use crate::{APP_HANDLE, singleton, utils::window_manager::WindowManager};
use parking_lot::RwLock;
use crate::{APP_HANDLE, singleton};
use smartstring::alias::String;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::AppHandle;
use tauri_plugin_mihomo::{Mihomo, MihomoExt as _};
use tokio::sync::RwLockReadGuard;
@@ -14,14 +10,12 @@ use super::notification::{FrontendEvent, NotificationSystem};
#[derive(Debug)]
pub struct Handle {
is_exiting: AtomicBool,
pub(crate) notification_system: Arc<RwLock<Option<NotificationSystem>>>,
}
impl Default for Handle {
fn default() -> Self {
Self {
is_exiting: AtomicBool::new(false),
notification_system: Arc::new(RwLock::new(Some(NotificationSystem::new()))),
}
}
}
@@ -33,19 +27,6 @@ impl Handle {
Self::default()
}
pub fn init(&self) {
if self.is_exiting() {
return;
}
let mut system_opt = self.notification_system.write();
if let Some(system) = system_opt.as_mut()
&& !system.is_running()
{
system.start();
}
}
pub fn app_handle() -> &'static AppHandle {
#[allow(clippy::expect_used)]
APP_HANDLE.get().expect("App handle not initialized")
@@ -56,61 +37,33 @@ impl Handle {
}
pub fn refresh_clash() {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::RefreshClash);
}
Self::send_event(FrontendEvent::RefreshClash);
}
pub fn refresh_verge() {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(FrontendEvent::RefreshVerge);
}
Self::send_event(FrontendEvent::RefreshVerge);
}
pub fn notify_profile_changed(profile_id: String) {
pub fn notify_profile_changed(profile_id: &String) {
Self::send_event(FrontendEvent::ProfileChanged {
current_profile_id: profile_id,
});
}
pub fn notify_timer_updated(profile_index: String) {
pub fn notify_timer_updated(profile_index: &String) {
Self::send_event(FrontendEvent::TimerUpdated { profile_index });
}
pub fn notify_profile_update_started(uid: String) {
pub fn notify_profile_update_started(uid: &String) {
Self::send_event(FrontendEvent::ProfileUpdateStarted { uid });
}
pub fn notify_profile_update_completed(uid: String) {
pub fn notify_profile_update_completed(uid: &String) {
Self::send_event(FrontendEvent::ProfileUpdateCompleted { uid });
}
// TODO 利用 &str 等缩短 Clone
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
let handle = Self::global();
if handle.is_exiting() {
return;
}
// We only send notice when main window exists
if WindowManager::get_main_window().is_none() {
return;
}
let status_str = status.into();
pub fn notice_message<S: AsRef<str>, M: Into<String>>(status: S, msg: M) {
let status_str = status.as_ref();
let msg_str = msg.into();
Self::send_event(FrontendEvent::NoticeMessage {
@@ -119,29 +72,21 @@ impl Handle {
});
}
pub fn set_is_exiting(&self) {
self.is_exiting.store(true, Ordering::Release);
}
pub fn is_exiting(&self) -> bool {
self.is_exiting.load(Ordering::Acquire)
}
fn send_event(event: FrontendEvent) {
let handle = Self::global();
if handle.is_exiting() {
return;
}
let system_opt = handle.notification_system.read();
if let Some(system) = system_opt.as_ref() {
system.send_event(event);
}
}
pub fn set_is_exiting(&self) {
self.is_exiting.store(true, Ordering::Release);
let mut system_opt = self.notification_system.write();
if let Some(system) = system_opt.as_mut() {
system.shutdown();
}
}
pub fn is_exiting(&self) -> bool {
self.is_exiting.load(Ordering::Acquire)
NotificationSystem::send_event(event);
}
}
+3 -4
View File
@@ -68,8 +68,7 @@ impl Logger {
self.log_max_size.store(log_max_size, Ordering::SeqCst);
self.log_max_count.store(log_max_count, Ordering::SeqCst);
#[cfg(not(feature = "tokio-trace"))]
#[cfg(not(feature = "tauri-dev"))]
#[cfg(not(any(feature = "tauri-dev", feature = "tokio-trace")))]
{
let log_spec = Self::generate_log_spec(log_level);
let log_dir = dirs::app_logs_dir()?;
@@ -89,9 +88,9 @@ impl Logger {
let mut filter_modules = vec!["wry", "tokio_tungstenite", "tungstenite"];
#[cfg(not(feature = "tracing"))]
filter_modules.push("tauri");
filter_modules.push("tauri::ipc");
#[cfg(feature = "tracing")]
filter_modules.extend(["tauri_plugin_mihomo", "kode_bridge"]);
filter_modules.extend(["kode_bridge"]);
let logger = logger.filter(Box::new(clash_verge_logging::NoModuleFilter(filter_modules)));
let handle = logger.start()?;
+7 -1
View File
@@ -83,7 +83,7 @@ impl CoreManager {
#[cfg(target_os = "windows")]
async fn wait_for_service_if_needed(&self) {
use crate::{config::Config, constants::timing};
use crate::{config::Config, constants::timing, core::service};
use backoff::{Error as BackoffError, ExponentialBackoff};
let needs_service = Config::verge().await.latest_arc().enable_tun_mode.unwrap_or(false);
@@ -108,6 +108,12 @@ impl CoreManager {
return Ok(());
}
// If the service IPC path is not ready yet, treat it as transient and retry.
// Running init/refresh too early can mark service state unavailable and break later config reloads.
if !service::is_service_ipc_path_exists() {
return Err(BackoffError::transient(anyhow::anyhow!("Service IPC not ready")));
}
manager.init().await.map_err(BackoffError::transient)?;
let _ = manager.refresh().await;
+17 -108
View File
@@ -1,100 +1,27 @@
use super::handle::Handle;
use crate::{constants::timing, utils::window_manager::WindowManager};
use crate::utils::window_manager::WindowManager;
use clash_verge_logging::{Type, logging};
use serde_json::json;
use smartstring::alias::String;
use std::{sync::mpsc, thread};
use tauri::{Emitter as _, WebviewWindow};
// TODO 重构或优化,避免 Clone 过多
#[derive(Debug, Clone)]
pub enum FrontendEvent {
#[derive(Debug)]
pub enum FrontendEvent<'a> {
RefreshClash,
RefreshVerge,
NoticeMessage { status: String, message: String },
ProfileChanged { current_profile_id: String },
TimerUpdated { profile_index: String },
ProfileUpdateStarted { uid: String },
ProfileUpdateCompleted { uid: String },
NoticeMessage { status: &'a str, message: String },
ProfileChanged { current_profile_id: &'a String },
TimerUpdated { profile_index: &'a String },
ProfileUpdateStarted { uid: &'a String },
ProfileUpdateCompleted { uid: &'a String },
}
#[derive(Debug)]
pub struct NotificationSystem {
sender: Option<mpsc::Sender<FrontendEvent>>,
#[allow(clippy::type_complexity)]
worker_handle: Option<thread::JoinHandle<()>>,
}
impl Default for NotificationSystem {
fn default() -> Self {
Self::new()
}
}
pub struct NotificationSystem {}
impl NotificationSystem {
pub const fn new() -> Self {
Self {
sender: None,
worker_handle: None,
}
}
pub const fn is_running(&self) -> bool {
self.sender.is_some() && self.worker_handle.is_some()
}
pub fn start(&mut self) {
if self.is_running() {
return;
}
let (tx, rx) = mpsc::channel();
self.sender = Some(tx);
//? Do we have to create a new thread for this?
let result = thread::Builder::new()
.name("frontend-notifier".into())
.spawn(move || Self::worker_loop(rx));
match result {
Ok(handle) => self.worker_handle = Some(handle),
Err(e) => logging!(error, Type::System, "Failed to start notification worker: {}", e),
}
}
fn worker_loop(rx: mpsc::Receiver<FrontendEvent>) {
let handle = Handle::global();
loop {
if handle.is_exiting() {
break;
}
match rx.recv() {
Ok(event) => Self::process_event(handle, event),
Err(e) => {
logging!(error, Type::System, "Notification System will exit, recv error: {}", e);
break;
}
}
}
}
fn process_event(handle: &super::handle::Handle, event: FrontendEvent) {
let binding = handle.notification_system.read();
let system = match binding.as_ref() {
Some(s) => s,
None => return,
};
if let Some(window) = WindowManager::get_main_window() {
system.emit_to_window(&window, event);
drop(binding);
thread::sleep(timing::EVENT_EMIT_DELAY);
}
}
fn emit_to_window(&self, window: &WebviewWindow, event: FrontendEvent) {
let (event_name, payload) = self.serialize_event(event);
let Ok(payload) = payload else {
fn emit_to_window(window: &WebviewWindow, event: FrontendEvent) {
let (event_name, Ok(payload)) = Self::serialize_event(event) else {
return;
};
@@ -103,9 +30,7 @@ impl NotificationSystem {
}
}
fn serialize_event(&self, event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
use serde_json::json;
fn serialize_event(event: FrontendEvent) -> (&'static str, Result<serde_json::Value, serde_json::Error>) {
match event {
FrontendEvent::RefreshClash => ("verge://refresh-clash-config", Ok(json!("yes"))),
FrontendEvent::RefreshVerge => ("verge://refresh-verge-config", Ok(json!("yes"))),
@@ -119,25 +44,9 @@ impl NotificationSystem {
}
}
pub fn send_event(&self, event: FrontendEvent) -> bool {
if !self.is_running() {
return false;
}
if let Some(sender) = &self.sender {
sender.send(event).is_ok()
} else {
false
}
}
pub fn shutdown(&mut self) {
if let Some(sender) = self.sender.take() {
drop(sender);
}
if let Some(handle) = self.worker_handle.take() {
let _ = handle.join();
pub(crate) fn send_event(event: FrontendEvent) {
if let Some(window) = WindowManager::get_main_window() {
Self::emit_to_window(&window, event);
}
}
}
+5 -5
View File
@@ -409,12 +409,12 @@ impl Timer {
}
/// Emit update events for frontend notification
fn emit_update_event(_uid: &str, _is_start: bool) {
fn emit_update_event(uid: &String, is_start: bool) {
{
if _is_start {
super::handle::Handle::notify_profile_update_started(_uid.into());
if is_start {
super::handle::Handle::notify_profile_update_started(uid);
} else {
super::handle::Handle::notify_profile_update_completed(_uid.into());
super::handle::Handle::notify_profile_update_completed(uid);
}
}
}
@@ -430,7 +430,7 @@ impl Timer {
let is_current = Config::profiles().await.latest_arc().current.as_ref() == Some(uid);
logging!(info, Type::Timer, "配置 {} 是否为当前激活配置: {}", uid, is_current);
feat::update_profile(uid, None, is_current, false).await
feat::update_profile(uid, None, is_current, false, false).await
})
.await
{
+20 -8
View File
@@ -2,6 +2,7 @@ use crate::{
cmd,
config::{Config, PrfItem, PrfOption, profiles::profiles_draft_update_item_safe},
core::{CoreManager, handle, tray},
utils::help::{mask_err, mask_url},
};
use anyhow::{Result, bail};
use clash_verge_logging::{Type, logging, logging_error};
@@ -83,9 +84,11 @@ async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result
Type::Config,
"[订阅更新] {} 是远程订阅,URL: {}",
uid,
item.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
mask_url(
item.url
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?
)
);
Ok(Some((
item.url.clone().ok_or_else(|| anyhow::anyhow!("Profile URL is None"))?,
@@ -99,6 +102,7 @@ async fn perform_profile_update(
url: &String,
opt: Option<&PrfOption>,
option: Option<&PrfOption>,
is_mannual_trigger: bool,
) -> Result<bool> {
logging!(info, Type::Config, "[订阅更新] 开始下载新的订阅内容");
let mut merged_opt = PrfOption::merge(opt, option);
@@ -125,7 +129,8 @@ async fn perform_profile_update(
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err},尝试使用Clash代理更新"
"Warning: [订阅更新] 正常更新失败: {},尝试使用Clash代理更新",
mask_err(&err.to_string())
);
last_err = err;
}
@@ -146,7 +151,8 @@ async fn perform_profile_update(
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err},尝试使用Clash代理更新"
"Warning: [订阅更新] Clash代理更新失败: {},尝试使用系统代理更新",
mask_err(&err.to_string())
);
last_err = err;
}
@@ -167,13 +173,16 @@ async fn perform_profile_update(
logging!(
warn,
Type::Config,
"Warning: [订阅更新] 正常更新失败: {err},尝试使用系统代理更新"
"Warning: [订阅更新] 系统代理更新失败: {},所有重试均已失败",
mask_err(&err.to_string())
);
last_err = err;
}
}
handle::Handle::notice_message("update_failed_even_with_clash", format!("{profile_name} - {last_err}"));
if is_mannual_trigger {
handle::Handle::notice_message("update_failed_even_with_clash", format!("{profile_name} - {last_err}"));
}
Ok(is_current)
}
@@ -182,12 +191,15 @@ pub async fn update_profile(
option: Option<&PrfOption>,
auto_refresh: bool,
ignore_auto_update: bool,
is_mannual_trigger: bool,
) -> Result<()> {
logging!(info, Type::Config, "[订阅更新] 开始更新订阅 {}", uid);
let url_opt = should_update_profile(uid, ignore_auto_update).await?;
let should_refresh = match url_opt {
Some((url, opt)) => perform_profile_update(uid, &url, opt.as_ref(), option).await? && auto_refresh,
Some((url, opt)) => {
perform_profile_update(uid, &url, opt.as_ref(), option, is_mannual_trigger).await? && auto_refresh
}
None => auto_refresh,
};
-14
View File
@@ -23,7 +23,6 @@ use tauri::{AppHandle, Manager as _};
#[cfg(target_os = "macos")]
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_deep_link::DeepLinkExt as _;
use tauri_plugin_mihomo::RejectPolicy;
pub static APP_HANDLE: OnceCell<AppHandle> = OnceCell::new();
/// Application initialization helper functions
@@ -58,15 +57,6 @@ mod app_init {
tauri_plugin_mihomo::Builder::new()
.protocol(tauri_plugin_mihomo::models::Protocol::LocalSocket)
.socket_path(crate::config::IClashTemp::guard_external_controller_ipc())
.pool_config(
tauri_plugin_mihomo::IpcPoolConfigBuilder::new()
.min_connections(3)
.max_connections(32)
.idle_timeout(std::time::Duration::from_secs(60))
.health_check_interval(std::time::Duration::from_secs(60))
.reject_policy(RejectPolicy::Wait)
.build(),
)
.build(),
);
@@ -250,7 +240,6 @@ pub fn run() {
logging!(error, Type::Setup, "Failed to setup window state: {}", e);
}
resolve::resolve_setup_handle();
resolve::resolve_setup_async();
resolve::resolve_setup_sync();
resolve::init_signal();
@@ -282,7 +271,6 @@ pub fn run() {
}
logging!(info, Type::System, "应用就绪");
handle::Handle::global().init();
#[cfg(target_os = "macos")]
if let Some(window) = _app_handle.get_webview_window("main") {
@@ -292,8 +280,6 @@ pub fn run() {
#[cfg(target_os = "macos")]
pub async fn handle_reopen(has_visible_windows: bool) {
handle::Handle::global().init();
if lightweight::is_in_lightweight_mode() {
lightweight::exit_lightweight_mode().await;
return;
+86
View File
@@ -98,6 +98,92 @@ pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
})
}
/// Mask sensitive parts of a subscription URL for safe logging.
/// Examples:
/// - `https://example.com/api/v1/clash?token=abc123` → `https://example.com/api/v1/clash?token=***`
/// - `https://example.com/abc123def456ghi789/clash` → `https://example.com/***/clash`
pub fn mask_url(url: &str) -> String {
// Split off query string
let (path_part, query_part) = match url.find('?') {
Some(pos) => (&url[..pos], Some(&url[pos + 1..])),
None => (url, None),
};
// Extract scheme+host prefix (everything up to the first '/' after "://")
let host_end = path_part
.find("://")
.and_then(|scheme_end| {
path_part[scheme_end + 3..]
.find('/')
.map(|slash| scheme_end + 3 + slash)
})
.unwrap_or(path_part.len());
let scheme_and_host = &path_part[..host_end];
let path = &path_part[host_end..]; // starts with '/' or empty
let mut result = scheme_and_host.to_owned();
// Mask path segments that look like tokens (longer than 16 chars)
if !path.is_empty() {
let masked: Vec<&str> = path
.split('/')
.map(|seg| if seg.len() > 16 { "***" } else { seg })
.collect();
result.push_str(&masked.join("/"));
}
// Keep query param keys, mask values
if let Some(query) = query_part {
result.push('?');
let masked_query: Vec<String> = query
.split('&')
.map(|param| match param.find('=') {
Some(eq) => format!("{}=***", &param[..eq]),
None => param.to_owned(),
})
.collect();
result.push_str(&masked_query.join("&"));
}
result
}
/// Mask all URLs embedded in an error/log string for safe logging.
///
/// Scans the string for `http://` or `https://` and replaces each URL
/// (terminated by whitespace or `)`, `]`, `"`, `'`) with its masked form.
/// Text between URLs is copied verbatim.
pub fn mask_err(err: &str) -> String {
let mut result = String::with_capacity(err.len());
let mut remaining = err;
loop {
let http = remaining.find("http://");
let https = remaining.find("https://");
let start = match (http, https) {
(None, None) => {
result.push_str(remaining);
break;
}
(Some(a), None) | (None, Some(a)) => a,
(Some(a), Some(b)) => a.min(b),
};
result.push_str(&remaining[..start]);
remaining = &remaining[start..];
let url_end = remaining
.find(|c: char| c.is_whitespace() || matches!(c, ')' | ']' | '"' | '\''))
.unwrap_or(remaining.len());
result.push_str(&mask_url(&remaining[..url_end]));
remaining = &remaining[url_end..];
}
result
}
/// get the last part of the url, if not found, return empty string
pub fn get_last_part_and_decode(url: &str) -> Option<String> {
let path = url.split('?').next().unwrap_or(""); // Splits URL and takes the path part
+1 -9
View File
@@ -5,7 +5,7 @@ use anyhow::Result;
use crate::{
config::Config,
core::{
CoreManager, Timer, handle,
CoreManager, Timer,
hotkey::Hotkey,
logger::Logger,
service::{SERVICE_MANAGER, ServiceManager, is_service_ipc_path_exists},
@@ -39,10 +39,6 @@ pub fn init_work_dir_and_logger() -> anyhow::Result<()> {
})
}
pub fn resolve_setup_handle() {
init_handle();
}
pub fn resolve_setup_sync() {
AsyncHandler::spawn(|| async {
AsyncHandler::spawn_blocking(init_scheme);
@@ -95,10 +91,6 @@ pub async fn resolve_reset_async() -> Result<(), anyhow::Error> {
Ok(())
}
pub fn init_handle() {
handle::Handle::global().init();
}
pub(super) fn init_scheme() {
logging_error!(Type::Setup, init::init_scheme());
}
+2 -2
View File
@@ -116,7 +116,7 @@ async fn fetch_profile_item(url: &str, name: Option<&String>) -> Option<PrfItem>
async fn post_import_updates(uid: &String, had_current_profile: bool) {
handle::Handle::refresh_verge();
handle::Handle::notify_profile_changed(uid.clone());
handle::Handle::notify_profile_changed(uid);
tokio::time::sleep(Duration::from_millis(100)).await;
let should_update_core = if uid.is_empty() || had_current_profile {
@@ -125,7 +125,7 @@ async fn post_import_updates(uid: &String, had_current_profile: bool) {
let profiles = Config::profiles().await;
profiles.latest_arc().is_current_profile_index(uid)
};
handle::Handle::notify_profile_changed(uid.clone());
handle::Handle::notify_profile_changed(uid);
if should_update_core {
refresh_core_config().await;
+1 -1
View File
@@ -1,5 +1,5 @@
{
"version": "2.4.6",
"version": "2.4.7",
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"bundle": {
"active": true,
@@ -615,7 +615,7 @@ export const GroupsEditorViewer = (props: Props) => {
/>
<TextField
autoComplete="new-password"
placeholder="http://1.0.0.1"
placeholder="http://cp.cloudflare.com"
size="small"
sx={{ width: "calc(100% - 150px)" }}
{...field}
+1 -1
View File
@@ -66,7 +66,7 @@ export const ProxyHead = ({
const { verge } = useVerge();
const defaultLatencyUrl =
verge?.default_latency_test?.trim() || "http://1.0.0.1";
verge?.default_latency_test?.trim() || "http://cp.cloudflare.com";
useEffect(() => {
delayManager.setUrl(groupName, testUrl?.trim() || url || defaultLatencyUrl);
+1 -1
View File
@@ -383,7 +383,7 @@ export const MiscViewer = forwardRef<DialogRef>((props, ref) => {
spellCheck="false"
sx={{ width: 250, marginLeft: "auto" }}
value={values.defaultLatencyTest}
placeholder="http://1.0.0.1"
placeholder="http://cp.cloudflare.com"
onChange={(e) =>
setValues((v) => ({ ...v, defaultLatencyTest: e.target.value }))
}
+3 -3
View File
@@ -31,13 +31,13 @@
"status": {
"systemProxyEnabled": "系统代理已启用,您的应用将通过代理访问网络",
"systemProxyDisabled": "系统代理已关闭,建议大多数用户打开此选项",
"tunModeServiceRequired": "TUN模式需要服务模式,请先安装服务",
"tunModeServiceRequired": "TUN 模式需要服务模式,请先安装服务",
"tunModeEnabled": "TUN 模式已启用,应用将通过虚拟网卡访问网络",
"tunModeDisabled": "TUN 模式已关闭,适用于特殊应用"
},
"tooltips": {
"systemProxy": "修改操作系统的代理设置,如果开启失败,可手动修改操作系统的代理设置",
"tunMode": "TUN模式可以接管所有应用流量,适用于特殊不遵循系统代理设置的应用"
"tunMode": "TUN 模式可以接管所有应用流量,适用于特殊不遵循系统代理设置的应用"
}
},
"clashInfo": {
@@ -128,7 +128,7 @@
"descriptions": {
"rule": "基于预设规则智能判断流量走向,提供灵活的代理策略",
"global": "所有流量均通过代理服务器,适用于需要全局科学上网的场景",
"direct": "所有流量不经过代理节点,但经过Clash内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景"
"direct": "所有流量不经过代理节点,但经过 Clash 内核转发连接目标服务器,适用于需要通过内核进行分流的特定场景"
}
}
}
+4 -4
View File
@@ -59,18 +59,18 @@
"SRC-PORT": "匹配请求来源端口范围 (SRC-PORT)",
"DST-PORT": "匹配请求目标端口范围 (DST-PORT)",
"IN-PORT": "匹配入站端口 (IN-PORT)",
"DSCP": "DSCP 标记(仅限 TPROXY UDP 入站)(DSCP)",
"DSCP": "匹配 DSCP 标记 (DSCP)",
"PROCESS-NAME": "匹配进程名称 (PROCESS-NAME)",
"PROCESS-PATH": "匹配完整进程路径 (PROCESS-PATH)",
"PROCESS-NAME-REGEX": "正则匹配完整进程名称 (PROCESS-NAME-REGEX)",
"PROCESS-PATH-REGEX": "正则匹配完整进程路径 (PROCESS-PATH-REGEX)",
"NETWORK": "匹配传输协议 (TCP/UDP) (NETWORK)",
"NETWORK": "匹配 TCP/UDP (NETWORK)",
"UID": "匹配 Linux USER ID (UID)",
"IN-TYPE": "匹配入站类型 (IN-TYPE)",
"IN-USER": "匹配入站用户名 (IN-USER)",
"IN-NAME": "匹配入站名称 (IN-NAME)",
"SUB-RULE": "子规则 (SUB-RULE)",
"RULE-SET": "匹配规则集 (RULE-SET)",
"SUB-RULE": "匹配至子规则 (SUB-RULE)",
"RULE-SET": "引用规则集合 (RULE-SET)",
"AND": "逻辑与 (AND)",
"OR": "逻辑或 (OR)",
"NOT": "逻辑非 (NOT)",
+2 -2
View File
@@ -177,10 +177,10 @@
"tooltips": {
"backupInfo": "支持本地或 WebDAV 方式备份配置文件",
"openConfDir": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件",
"liteMode": "关闭GUI界面,仅保留内核运行"
"liteMode": "关闭 GUI 界面,仅保留内核运行"
},
"actions": {
"copyVersion": "复制Verge版本号"
"copyVersion": "复制 Verge 版本号"
},
"notifications": {
"latestVersion": "当前已是最新版本",
+1 -1
View File
@@ -32,7 +32,7 @@
"completed": "检测完成",
"disallowedIsp": "不允许的 ISP",
"originalsOnly": "仅限原创",
"noDisney": "不支持(IP被Disney+禁止)",
"noDisney": "不支持(IP 被 Disney+ 禁止)",
"unsupportedRegion": "不支持的国家/地区",
"failedNetwork": "测试失败(网络连接问题)"
}
+30 -30
View File
@@ -42,39 +42,39 @@
}
},
"ruleTypes": {
"DOMAIN": "配對完整網域 (DOMAIN)",
"DOMAIN-SUFFIX": "配對網域後綴 (DOMAIN-SUFFIX)",
"DOMAIN-KEYWORD": "配對網域關鍵字 (DOMAIN-KEYWORD)",
"DOMAIN-REGEX": "配對網域正規表示式 (DOMAIN-REGEX)",
"GEOSITE": "配對 GeoSite 內的網域 (GEOSITE)",
"GEOIP": "配對 IP 所屬國家代碼 (GEOIP)",
"SRC-GEOIP": "配對來源 IP 所屬國家代碼 (SRC-GEOIP)",
"IP-ASN": "配對 IP 所屬 ASN (IP-ASN)",
"SRC-IP-ASN": "配對來源 IP 所屬 ASN (SRC-IP-ASN)",
"IP-CIDR": "配對 IP 位址範圍 (IP-CIDR)",
"IP-CIDR6": "配對 IP 位址範圍 (IP-CIDR6)",
"SRC-IP-CIDR": "配對來源 IP 位址範圍 (SRC-IP-CIDR)",
"IP-SUFFIX": "配對 IP 後綴範圍 (IP-SUFFIX)",
"SRC-IP-SUFFIX": "配對來源 IP 後綴範圍 (SRC-IP-SUFFIX)",
"SRC-PORT": "配對請求來源連接埠範圍 (SRC-PORT)",
"DST-PORT": "配對請求目標連接埠範圍 (DST-PORT)",
"IN-PORT": "配對傳入連接埠 (IN-PORT)",
"DSCP": "DSCP標記(僅限 TPROXY UDP 傳入)(DSCP)",
"PROCESS-NAME": "配對程序名稱 (PROCESS-NAME)",
"PROCESS-PATH": "配對完整程序路徑 (PROCESS-PATH)",
"PROCESS-NAME-REGEX": "正規表示式配對完整程序名稱 (PROCESS-NAME-REGEX)",
"PROCESS-PATH-REGEX": "正規表示式配對完整程序路徑 (PROCESS-PATH-REGEX)",
"NETWORK": "配對傳輸協定 (TCP/UDP) (NETWORK)",
"UID": "配對 Linux 使用者 ID (UID)",
"IN-TYPE": "配對傳入類型 (IN-TYPE)",
"IN-USER": "配對傳入使用者名稱 (IN-USER)",
"IN-NAME": "配對傳入名稱 (IN-NAME)",
"SUB-RULE": "子規則 (SUB-RULE)",
"RULE-SET": "配對規則集 (RULE-SET)",
"DOMAIN": "匹配完整網域 (DOMAIN)",
"DOMAIN-SUFFIX": "匹配網域後綴 (DOMAIN-SUFFIX)",
"DOMAIN-KEYWORD": "匹配網域關鍵字 (DOMAIN-KEYWORD)",
"DOMAIN-REGEX": "匹配網域正規表示式 (DOMAIN-REGEX)",
"GEOSITE": "匹配 GeoSite 內的網域 (GEOSITE)",
"GEOIP": "匹配 IP 所屬國家代碼 (GEOIP)",
"SRC-GEOIP": "匹配來源 IP 所屬國家代碼 (SRC-GEOIP)",
"IP-ASN": "匹配 IP 所屬 ASN (IP-ASN)",
"SRC-IP-ASN": "匹配來源 IP 所屬 ASN (SRC-IP-ASN)",
"IP-CIDR": "匹配 IP 位址範圍 (IP-CIDR)",
"IP-CIDR6": "匹配 IP 位址範圍 (IP-CIDR6)",
"SRC-IP-CIDR": "匹配來源 IP 位址範圍 (SRC-IP-CIDR)",
"IP-SUFFIX": "匹配 IP 後綴範圍 (IP-SUFFIX)",
"SRC-IP-SUFFIX": "匹配來源 IP 後綴範圍 (SRC-IP-SUFFIX)",
"SRC-PORT": "匹配請求來源連接埠範圍 (SRC-PORT)",
"DST-PORT": "匹配請求目標連接埠範圍 (DST-PORT)",
"IN-PORT": "匹配傳入連接埠 (IN-PORT)",
"DSCP": "匹配 DSCP 標記 (DSCP)",
"PROCESS-NAME": "匹配程序名稱 (PROCESS-NAME)",
"PROCESS-PATH": "匹配完整程序路徑 (PROCESS-PATH)",
"PROCESS-NAME-REGEX": "正規表示式匹配完整程序名稱 (PROCESS-NAME-REGEX)",
"PROCESS-PATH-REGEX": "正規表示式匹配完整程序路徑 (PROCESS-PATH-REGEX)",
"NETWORK": "匹配 TCP/UDP (NETWORK)",
"UID": "匹配 Linux USER ID (UID)",
"IN-TYPE": "匹配傳入類型 (IN-TYPE)",
"IN-USER": "匹配傳入使用者名稱 (IN-USER)",
"IN-NAME": "匹配傳入名稱 (IN-NAME)",
"SUB-RULE": "匹配至子規則 (SUB-RULE)",
"RULE-SET": "引用規則集合 (RULE-SET)",
"AND": "邏輯與 (AND)",
"OR": "邏輯或 (OR)",
"NOT": "邏輯非 (NOT)",
"MATCH": "配對所有請求 (MATCH)"
"MATCH": "匹配所有請求 (MATCH)"
},
"title": "編輯規則"
}
+1 -1
View File
@@ -340,7 +340,7 @@ export async function cmdGetProxyDelay(
url?: string,
) {
// 确保URL不为空
const testUrl = url || "http://1.0.0.1";
const testUrl = url || "http://cp.cloudflare.com";
try {
// 不再在前端编码代理名称,由后端统一处理编码
+1 -1
View File
@@ -120,7 +120,7 @@ class DelayManager {
`[DelayManager] 获取测试URL,组: ${group}, URL: ${url || "未设置"}`,
);
// 如果未设置URL,返回默认URL
return url || "http://1.0.0.1";
return url || "http://cp.cloudflare.com";
}
setListener(