From 3bb708233e929c96fd0333937229cc2aed18e07a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:52:24 +0800 Subject: [PATCH 01/51] refactor(utils): drop dead reset helper and tidy init_resources - remove resolve_reset_async, which had no callers - drop the duplicated time.*.com entry from the default fake-ip-filter - init_resources no longer recreates app_home (ensure_directories during init_config already created it) and now propagates res_dir creation errors --- src-tauri/src/utils/init.rs | 7 ++----- src-tauri/src/utils/resolve/mod.rs | 14 -------------- 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/src-tauri/src/utils/init.rs b/src-tauri/src/utils/init.rs index be7f016a1..927fc797f 100644 --- a/src-tauri/src/utils/init.rs +++ b/src-tauri/src/utils/init.rs @@ -354,7 +354,6 @@ pub(super) async fn init_dns_config() -> Result<()> { Value::String("*.arpa".into()), Value::String("time.*.com".into()), Value::String("ntp.*.com".into()), - Value::String("time.*.com".into()), Value::String("+.market.xiaomi.com".into()), Value::String("localhost.ptlogin2.qq.com".into()), Value::String("*.msftncsi.com".into()), @@ -509,11 +508,9 @@ pub async fn init_resources() -> Result<()> { let app_dir = dirs::app_home_dir()?; let res_dir = dirs::app_resources_dir()?; - if !app_dir.exists() { - std::mem::drop(fs::create_dir_all(&app_dir).await); - } + // app_home is created by `ensure_directories` in `init_config`, which always runs first. if !res_dir.exists() { - std::mem::drop(fs::create_dir_all(&res_dir).await); + fs::create_dir_all(&res_dir).await?; } let file_list = ["Country.mmdb", "ASN.mmdb", "geoip.dat", "geosite.dat"]; diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index 7ba959ae3..fa408f2af 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -1,8 +1,6 @@ use crate::core::notify::NoticeStatus; use std::sync::atomic::{AtomicBool, Ordering}; -use anyhow::Result; - use crate::{ config::Config, core::{ @@ -97,18 +95,6 @@ async fn resolve_setup() { resolve_done(); } -pub async fn resolve_reset_async() -> Result<(), anyhow::Error> { - CoreManager::global().stop_core().await?; - - #[cfg(target_os = "macos")] - { - use dns::restore_public_dns; - restore_public_dns().await?; - } - - Ok(()) -} - fn init_scheme() { logging_error!(Type::Setup, init::init_scheme()); } From dbaee8a5e453897b83c82508d33c784c03863498 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:58:27 +0800 Subject: [PATCH 02/51] refactor(profile): remove identical blind retry when switching node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The second select_node_for_group call in switch_proxy_node was a zero-interval retry with identical arguments. Its success path was a degraded copy of the first (missing the Refresh::Proxies announcement ever since the old force-refresh emit was removed), and its distinct "回退/最终失败" log wording misled maintainers. Keep a single call, the primary success aftermath, and one error log. --- src-tauri/src/feat/profile.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src-tauri/src/feat/profile.rs b/src-tauri/src/feat/profile.rs index 56faeb01d..a74e7dd39 100644 --- a/src-tauri/src/feat/profile.rs +++ b/src-tauri/src/feat/profile.rs @@ -47,26 +47,11 @@ pub async fn switch_proxy_node(group_name: &str, proxy_name: &str) { record_switched_node(group_name, proxy_name).await; announce(Refresh::Proxies); let _ = tray::Tray::global().update_menu().await; - return; } Err(err) => { logging!(error, Type::Tray, "切换代理失败: {err:?}"); } } - - match handle::Handle::mihomo() - .select_node_for_group(group_name, proxy_name) - .await - { - Ok(_) => { - logging!(info, Type::Tray, "代理切换回退成功: {} -> {}", group_name, proxy_name); - record_switched_node(group_name, proxy_name).await; - let _ = tray::Tray::global().update_menu().await; - } - Err(err) => { - logging!(error, Type::Tray, "代理切换最终失败: {err:?}"); - } - } } async fn should_update_profile(uid: &String, ignore_auto_update: bool) -> Result)>> { From 4c778a93dbf90fffc51f6f76df424383ff82f117 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:03:19 +0800 Subject: [PATCH 03/51] refactor(enhance): simplify tun mode check and lowercase helpers - Drop the always-true second clause in use_tun's enhanced-mode check: the key is absent only when the unwrap_or default already makes current_mode == "fake-ip" true. - Collapse use_lowercase into a delegation to use_lowercase_owned. --- src-tauri/src/enhance/field.rs | 9 +-------- src-tauri/src/enhance/tun.rs | 2 +- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src-tauri/src/enhance/field.rs b/src-tauri/src/enhance/field.rs index 04e73a786..fe9537f03 100644 --- a/src-tauri/src/enhance/field.rs +++ b/src-tauri/src/enhance/field.rs @@ -25,14 +25,7 @@ fn lowercase_key(key: &str) -> Value { } pub fn use_lowercase(config: &Mapping) -> Mapping { - let mut lowercased = Mapping::new(); - - for (key, value) in config.into_iter() { - if let Some(key_str) = key.as_str() { - lowercased.insert(lowercase_key(key_str), value.clone()); - } - } - lowercased + use_lowercase_owned(config.clone()) } pub fn use_lowercase_owned(config: Mapping) -> Mapping { diff --git a/src-tauri/src/enhance/tun.rs b/src-tauri/src/enhance/tun.rs index f5d1af7ab..e56810135 100644 --- a/src-tauri/src/enhance/tun.rs +++ b/src-tauri/src/enhance/tun.rs @@ -28,7 +28,7 @@ pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping { .and_then(|v| v.as_str()) .unwrap_or("fake-ip"); - if current_mode == "fake-ip" || !dns_val.contains_key(Value::from("enhanced-mode")) { + if current_mode == "fake-ip" { revise!(dns_val, "enable", true); revise!(dns_val, "ipv6", ipv6_val); From a6d680236f3c786029dbbb17f72b2a3fef81047a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:09:39 +0800 Subject: [PATCH 04/51] refactor(core): drop unreachable hotkey dtor and dedupe tray predicate Remove impl Drop for Hotkey: the instance lives in a OnceLock static via the singleton! macro and is never dropped; exit-time unregistration is already handled by Hotkey::reset(). Spell the include_proxy_groups/display-mode predicate identically at both evaluation sites in tray menu building; behavior is unchanged (Option != Some("disable") is equivalent to unwrap_or("default") != "disable"). --- src-tauri/src/core/hotkey.rs | 9 --------- src-tauri/src/core/tray/mod.rs | 3 ++- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/core/hotkey.rs b/src-tauri/src/core/hotkey.rs index 75027d176..d23ae2a57 100755 --- a/src-tauri/src/core/hotkey.rs +++ b/src-tauri/src/core/hotkey.rs @@ -466,12 +466,3 @@ impl Hotkey { (del_list, add_list) } } - -impl Drop for Hotkey { - fn drop(&mut self) { - let app_handle = handle::Handle::app_handle(); - if let Err(e) = app_handle.global_shortcut().unregister_all() { - logging!(error, Type::Hotkey, "Error unregistering all hotkeys: {:?}", e); - } - } -} diff --git a/src-tauri/src/core/tray/mod.rs b/src-tauri/src/core/tray/mod.rs index ae8c63c2c..f7fa4a6d6 100644 --- a/src-tauri/src/core/tray/mod.rs +++ b/src-tauri/src/core/tray/mod.rs @@ -636,7 +636,8 @@ async fn create_tray_menu( .tray_proxy_groups_display_mode .as_deref() .unwrap_or("default"); - let include_proxy_groups = options.include_proxy_groups && tray_proxy_groups_display_mode != "disable"; + let include_proxy_groups = + options.include_proxy_groups && verge_settings.tray_proxy_groups_display_mode.as_deref() != Some("disable"); let proxy_group_order_map = runtime_proxy_groups_order; From 8702779375fa93c2719898eea828b61620ca0742 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:20:02 +0800 Subject: [PATCH 05/51] refactor(config): fold port guards into single helper The five IClashTemp port guards duplicated the same read/parse/default logic with drifting defaults: some hardcoded 7895/7898/7899 while others referenced network::ports constants. Route them all through one guard_port_value helper and use the constants everywhere, keeping each guard's name, key and cfg attributes. --- src-tauri/src/config/clash.rs | 69 +++++++---------------------------- 1 file changed, 13 insertions(+), 56 deletions(-) diff --git a/src-tauri/src/config/clash.rs b/src-tauri/src/config/clash.rs index 106c55d9f..59687278c 100644 --- a/src-tauri/src/config/clash.rs +++ b/src-tauri/src/config/clash.rs @@ -184,84 +184,41 @@ impl IClashTemp { _ => None, }) } - #[cfg(not(target_os = "windows"))] - fn guard_redir_port(config: &Mapping) -> u16 { + fn guard_port_value(config: &Mapping, key: &str, default: u16) -> u16 { let mut port = config - .get("redir-port") + .get(key) .and_then(|value| match value { Value::String(val_str) => val_str.parse().ok(), Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), _ => None, }) - .unwrap_or(7895); + .unwrap_or(default); if port == 0 { - port = 7895; + port = default; } port } + #[cfg(not(target_os = "windows"))] + fn guard_redir_port(config: &Mapping) -> u16 { + Self::guard_port_value(config, "redir-port", network::ports::DEFAULT_REDIR) + } + #[cfg(target_os = "linux")] fn guard_tproxy_port(config: &Mapping) -> u16 { - let mut port = config - .get("tproxy-port") - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(network::ports::DEFAULT_TPROXY); - if port == 0 { - port = network::ports::DEFAULT_TPROXY; - } - port + Self::guard_port_value(config, "tproxy-port", network::ports::DEFAULT_TPROXY) } fn guard_mixed_port(config: &Mapping) -> u16 { - let raw_value = config.get("mixed-port"); - - let mut port = raw_value - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(network::ports::DEFAULT_MIXED); - - if port == 0 { - port = network::ports::DEFAULT_MIXED; - } - - port + Self::guard_port_value(config, "mixed-port", network::ports::DEFAULT_MIXED) } fn guard_socks_port(config: &Mapping) -> u16 { - let mut port = config - .get("socks-port") - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(7898); - if port == 0 { - port = 7898; - } - port + Self::guard_port_value(config, "socks-port", network::ports::DEFAULT_SOCKS) } fn guard_port(config: &Mapping) -> u16 { - let mut port = config - .get("port") - .and_then(|value| match value { - Value::String(val_str) => val_str.parse().ok(), - Value::Number(val_num) => val_num.as_u64().map(|u| u as u16), - _ => None, - }) - .unwrap_or(7899); - if port == 0 { - port = 7899; - } - port + Self::guard_port_value(config, "port", network::ports::DEFAULT_HTTP) } pub(super) fn guard_server_ctrl(config: &Mapping) -> String { From b79c5615a5b94e89f0199e287d75af035910307e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:21:12 +0800 Subject: [PATCH 06/51] refactor(config): dedupe prfitem factories and companion seeding from_rules/from_proxies/from_groups differed only in uid prefix, type string and template, so they now delegate to a shared from_seq_item. The five current_* accessors collapse into a local macro. The identical five-block companion seeding in from_local and from_url moves into ensure_companion_items, preserving seeding order and error propagation. --- src-tauri/src/config/prfitem.rs | 150 ++++++++++++-------------------- 1 file changed, 55 insertions(+), 95 deletions(-) diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index da8728d21..eb9ddb325 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -181,6 +181,36 @@ impl PrfItem { } } + async fn append_companion_item(item: &mut Self) -> Result> { + profiles::profiles_append_item_safe(item).await?; + Ok(item.uid.clone()) + } + + async fn ensure_companion_items( + merge: &mut Option, + script: &mut Option, + rules: &mut Option, + proxies: &mut Option, + groups: &mut Option, + ) -> Result<()> { + if merge.is_none() { + *merge = Self::append_companion_item(&mut Self::from_merge(None)).await?; + } + if script.is_none() { + *script = Self::append_companion_item(&mut Self::from_script(None)).await?; + } + if rules.is_none() { + *rules = Self::append_companion_item(&mut Self::from_rules()).await?; + } + if proxies.is_none() { + *proxies = Self::append_companion_item(&mut Self::from_proxies()).await?; + } + if groups.is_none() { + *groups = Self::append_companion_item(&mut Self::from_groups()).await?; + } + Ok(()) + } + async fn from_local( name: String, desc: String, @@ -197,31 +227,7 @@ impl PrfItem { let mut proxies = opt_ref.and_then(|o| o.proxies.clone()); let mut groups = opt_ref.and_then(|o| o.groups.clone()); - if merge.is_none() { - let merge_item = &mut Self::from_merge(None); - profiles::profiles_append_item_safe(merge_item).await?; - merge = merge_item.uid.clone(); - } - if script.is_none() { - let script_item = &mut Self::from_script(None); - profiles::profiles_append_item_safe(script_item).await?; - script = script_item.uid.clone(); - } - if rules.is_none() { - let rules_item = &mut Self::from_rules(); - profiles::profiles_append_item_safe(rules_item).await?; - rules = rules_item.uid.clone(); - } - if proxies.is_none() { - let proxies_item = &mut Self::from_proxies(); - profiles::profiles_append_item_safe(proxies_item).await?; - proxies = proxies_item.uid.clone(); - } - if groups.is_none() { - let groups_item = &mut Self::from_groups(); - profiles::profiles_append_item_safe(groups_item).await?; - groups = groups_item.uid.clone(); - } + Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?; Ok(Self { uid: Some(uid), itype: Some("local".into()), @@ -375,31 +381,7 @@ impl PrfItem { bail!("profile does not contain `proxies` or `proxy-providers`"); } - if merge.is_none() { - let merge_item = &mut Self::from_merge(None); - profiles::profiles_append_item_safe(merge_item).await?; - merge = merge_item.uid.clone(); - } - if script.is_none() { - let script_item = &mut Self::from_script(None); - profiles::profiles_append_item_safe(script_item).await?; - script = script_item.uid.clone(); - } - if rules.is_none() { - let rules_item = &mut Self::from_rules(); - profiles::profiles_append_item_safe(rules_item).await?; - rules = rules_item.uid.clone(); - } - if proxies.is_none() { - let proxies_item = &mut Self::from_proxies(); - profiles::profiles_append_item_safe(proxies_item).await?; - proxies = proxies_item.uid.clone(); - } - if groups.is_none() { - let groups_item = &mut Self::from_groups(); - profiles::profiles_append_item_safe(groups_item).await?; - groups = groups_item.uid.clone(); - } + Self::ensure_companion_items(&mut merge, &mut script, &mut rules, &mut proxies, &mut groups).await?; Ok(Self { uid: Some(uid), @@ -461,46 +443,30 @@ impl PrfItem { } } - fn from_rules() -> Self { - let uid = help::get_uid("r").into(); + fn from_seq_item(prefix: &str, itype: &str, template: &str) -> Self { + let uid = help::get_uid(prefix).into(); let file = format!("{uid}.yaml").into(); // yaml ext Self { uid: Some(uid), - itype: Some("rules".into()), + itype: Some(itype.into()), file: Some(file), updated: Some(chrono::Local::now().timestamp() as usize), - file_data: Some(tmpl::ITEM_RULES.into()), + file_data: Some(template.into()), ..Default::default() } } + fn from_rules() -> Self { + Self::from_seq_item("r", "rules", tmpl::ITEM_RULES) + } + fn from_proxies() -> Self { - let uid = help::get_uid("p").into(); - let file = format!("{uid}.yaml").into(); // yaml ext - - Self { - uid: Some(uid), - itype: Some("proxies".into()), - file: Some(file), - updated: Some(chrono::Local::now().timestamp() as usize), - file_data: Some(tmpl::ITEM_PROXIES.into()), - ..Default::default() - } + Self::from_seq_item("p", "proxies", tmpl::ITEM_PROXIES) } fn from_groups() -> Self { - let uid = help::get_uid("g").into(); - let file = format!("{uid}.yaml").into(); // yaml ext - - Self { - uid: Some(uid), - itype: Some("groups".into()), - file: Some(file), - updated: Some(chrono::Local::now().timestamp() as usize), - file_data: Some(tmpl::ITEM_GROUPS.into()), - ..Default::default() - } + Self::from_seq_item("g", "groups", tmpl::ITEM_GROUPS) } pub(crate) async fn read_file(&self) -> Result { @@ -516,26 +482,20 @@ impl PrfItem { } } +macro_rules! current_field { + ($name:ident, $field:ident) => { + pub(crate) fn $name(&self) -> Option<&String> { + self.option.as_ref().and_then(|o| o.$field.as_ref()) + } + }; +} + impl PrfItem { - pub(crate) fn current_merge(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.merge.as_ref()) - } - - pub(crate) fn current_script(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.script.as_ref()) - } - - pub(crate) fn current_rules(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.rules.as_ref()) - } - - pub(crate) fn current_proxies(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.proxies.as_ref()) - } - - pub(crate) fn current_groups(&self) -> Option<&String> { - self.option.as_ref().and_then(|o| o.groups.as_ref()) - } + current_field!(current_merge, merge); + current_field!(current_script, script); + current_field!(current_rules, rules); + current_field!(current_proxies, proxies); + current_field!(current_groups, groups); } fn allow_auto_update_enabled(option: Option<&PrfOption>) -> bool { From bfb50ac0d74e9ae2254a069092197e3669910499 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:24:28 +0800 Subject: [PATCH 07/51] refactor(config): share patch macro and profiles helpers IVerge::patch_config, IProfiles::patch_item and PrfOption::merge each hand-rolled the same copy-Some-overwrite pattern; they now share one patch_opt_field macro (the or-else merge form is field-for-field equivalent for Option fields). The four profiles *_safe wrappers that inlined Config::profiles().with_data_modify boilerplate delegate to a with_profiles_edit helper, and record/forget_selected_node share the current-item lookup prelude via current_item_mut. --- src-tauri/src/config/mod.rs | 9 ++ src-tauri/src/config/prfitem.rs | 27 +++--- src-tauri/src/config/profiles.rs | 102 ++++++++------------ src-tauri/src/config/verge.rs | 158 +++++++++++++++---------------- 4 files changed, 138 insertions(+), 158 deletions(-) diff --git a/src-tauri/src/config/mod.rs b/src-tauri/src/config/mod.rs index add685f62..1be472f88 100644 --- a/src-tauri/src/config/mod.rs +++ b/src-tauri/src/config/mod.rs @@ -14,6 +14,15 @@ mod verge; pub(crate) use self::config::Config; pub use self::{clash::*, encrypt::*, mixed_port::*, prfitem::*, profiles::*, verge::*}; +macro_rules! patch_opt_field { + ($lv:expr, $rv:expr, $key:tt) => { + if ($rv.$key).is_some() { + $lv.$key = $rv.$key.clone(); + } + }; +} +pub(crate) use patch_opt_field; + pub const DEFAULT_PAC: &str = r#"function FindProxyForURL(url, host) { return "PROXY 127.0.0.1:%mixed-port%; SOCKS5 127.0.0.1:%mixed-port%; DIRECT;"; } diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index eb9ddb325..23278dcfc 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -1,3 +1,5 @@ +use super::patch_opt_field; + use crate::{ config::profiles, utils::{ @@ -131,19 +133,18 @@ impl PrfOption { match (one, other) { (Some(a_ref), Some(b_ref)) => { let mut result = a_ref.clone(); - result.user_agent = b_ref.user_agent.clone().or(result.user_agent); - result.with_proxy = b_ref.with_proxy.or(result.with_proxy); - result.self_proxy = b_ref.self_proxy.or(result.self_proxy); - result.danger_accept_invalid_certs = - b_ref.danger_accept_invalid_certs.or(result.danger_accept_invalid_certs); - result.allow_auto_update = b_ref.allow_auto_update.or(result.allow_auto_update); - result.update_interval = b_ref.update_interval.or(result.update_interval); - result.merge = b_ref.merge.clone().or(result.merge); - result.script = b_ref.script.clone().or(result.script); - result.rules = b_ref.rules.clone().or(result.rules); - result.proxies = b_ref.proxies.clone().or(result.proxies); - result.groups = b_ref.groups.clone().or(result.groups); - result.timeout_seconds = b_ref.timeout_seconds.or(result.timeout_seconds); + patch_opt_field!(result, b_ref, user_agent); + patch_opt_field!(result, b_ref, with_proxy); + patch_opt_field!(result, b_ref, self_proxy); + patch_opt_field!(result, b_ref, danger_accept_invalid_certs); + patch_opt_field!(result, b_ref, allow_auto_update); + patch_opt_field!(result, b_ref, update_interval); + patch_opt_field!(result, b_ref, merge); + patch_opt_field!(result, b_ref, script); + patch_opt_field!(result, b_ref, rules); + patch_opt_field!(result, b_ref, proxies); + patch_opt_field!(result, b_ref, groups); + patch_opt_field!(result, b_ref, timeout_seconds); Some(result) } (Some(a_ref), None) => Some(a_ref.clone()), diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 4b149b716..23826cdc1 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,5 +1,5 @@ use super::{ - PrfOption, + PrfOption, patch_opt_field, prfitem::{PrfItem, PrfSelected, normalize_profile_home_url}, }; use crate::core::notify::{Refresh, announce}; @@ -86,14 +86,6 @@ impl ProfileDeletePlan { } } -macro_rules! patch { - ($lv: expr, $rv: expr, $key: tt) => { - if ($rv.$key).is_some() { - $lv.$key = $rv.$key.to_owned(); - } - }; -} - impl IProfiles { fn take_item_file_by_uid(items: &mut Vec, target_uid: Option<&str>) -> Option { let index = items.iter().position(|item| item.uid.as_deref() == target_uid)?; @@ -244,15 +236,15 @@ impl IProfiles { for each in items.iter_mut() { if each.uid.as_deref() == Some(uid) { - patch!(each, item, itype); - patch!(each, item, name); - patch!(each, item, desc); - patch!(each, item, file); - patch!(each, item, url); - patch!(each, item, selected); - patch!(each, item, extra); - patch!(each, item, updated); - patch!(each, item, option); + patch_opt_field!(each, item, itype); + patch_opt_field!(each, item, name); + patch_opt_field!(each, item, desc); + patch_opt_field!(each, item, file); + patch_opt_field!(each, item, url); + patch_opt_field!(each, item, selected); + patch_opt_field!(each, item, extra); + patch_opt_field!(each, item, updated); + patch_opt_field!(each, item, option); self.items = Some(items); return self.save_file().await; @@ -420,6 +412,21 @@ pub(crate) async fn profiles_append_item_with_filedata_safe(item: &PrfItem, file profiles_append_item_safe(item).await } +/// Applies `edit` to the committed profiles; commits only when the edit succeeds. +async fn with_profiles_edit(edit: F) -> Result<()> +where + F: FnOnce(IProfiles) -> Fut + Send, + Fut: std::future::Future> + Send, +{ + Config::profiles() + .await + .with_data_modify(|profiles| async move { + let profiles = edit(profiles).await?; + Ok((profiles, ())) + }) + .await +} + pub(crate) async fn profiles_append_item_safe(item: &mut PrfItem) -> Result<()> { let profiles = Config::profiles().await; profiles_append_item_to_safe(&profiles, item).await @@ -435,43 +442,19 @@ pub(super) async fn profiles_append_item_to_safe(profiles: &Draft, it } pub(crate) async fn profiles_patch_item_safe(index: &str, item: &PrfItem) -> Result<()> { - Config::profiles() - .await - .with_data_modify(|mut profiles| async move { - profiles.patch_item(index, item).await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|mut profiles| async move { profiles.patch_item(index, item).await.map(|_| profiles) }).await } pub(crate) async fn profiles_reorder_safe(active_id: &str, over_id: &str) -> Result<()> { - Config::profiles() - .await - .with_data_modify(|mut profiles| async move { - profiles.reorder(active_id, over_id).await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|mut profiles| async move { profiles.reorder(active_id, over_id).await.map(|_| profiles) }).await } pub(crate) async fn profiles_save_file_safe() -> Result<()> { - Config::profiles() - .await - .with_data_modify(|profiles| async move { - profiles.save_file().await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|profiles| async move { profiles.save_file().await.map(|_| profiles) }).await } pub(crate) async fn profiles_update_item_safe(index: &str, item: &mut PrfItem) -> Result<()> { - Config::profiles() - .await - .with_data_modify(|mut profiles| async move { - profiles.update_item(index, item).await?; - Ok((profiles, ())) - }) - .await + with_profiles_edit(|mut profiles| async move { profiles.update_item(index, item).await.map(|_| profiles) }).await } #[derive(Debug, PartialEq, Eq)] @@ -713,19 +696,21 @@ where Ok(()) } +fn current_item_mut(profiles: &mut IProfiles) -> Option<&mut PrfItem> { + let current = profiles.current.clone()?; + profiles + .items + .as_mut()? + .iter_mut() + .find(|item| item.uid.as_ref() == Some(¤t)) +} + /// Records a backend-made selection so the next core start restores it. pub(crate) async fn record_selected_node(group_name: &str, node: &str) -> Result<()> { let group_name = String::from(group_name); let node = String::from(node); persist_selected_update(true, move |mut profiles| async move { - let Some(current) = profiles.current.clone() else { - return Ok((profiles, false)); - }; - let Some(item) = profiles - .items - .as_mut() - .and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t))) - else { + let Some(item) = current_item_mut(&mut profiles) else { return Ok((profiles, false)); }; @@ -760,14 +745,7 @@ fn remove_selected_node(selected: &mut Vec, group_name: &str) -> bo pub(crate) async fn forget_selected_node(group_name: &str) -> Result<()> { persist_selected_update(true, move |mut profiles| async move { - let Some(current) = profiles.current.clone() else { - return Ok((profiles, false)); - }; - let Some(item) = profiles - .items - .as_mut() - .and_then(|items| items.iter_mut().find(|item| item.uid.as_ref() == Some(¤t))) - else { + let Some(item) = current_item_mut(&mut profiles) else { return Ok((profiles, false)); }; diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index 45b0e18ec..ab7962ee8 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -1,6 +1,6 @@ use crate::config::Config; use crate::{ - config::{DEFAULT_PAC, deserialize_encrypted, serialize_encrypted}, + config::{DEFAULT_PAC, deserialize_encrypted, patch_opt_field, serialize_encrypted}, constants::network, utils::{dirs, help}, }; @@ -432,98 +432,90 @@ impl IVerge { #[allow(clippy::cognitive_complexity)] pub fn patch_config(&mut self, patch: &Self) { - macro_rules! patch { - ($key: tt) => { - if patch.$key.is_some() { - self.$key = patch.$key.clone(); - } - }; - } + patch_opt_field!(self, patch, app_log_level); + patch_opt_field!(self, patch, app_log_max_size); + patch_opt_field!(self, patch, app_log_max_count); - patch!(app_log_level); - patch!(app_log_max_size); - patch!(app_log_max_count); - - patch!(language); - patch!(theme_mode); - patch!(tray_event); - patch!(env_type); - patch!(start_page); - patch!(startup_script); - patch!(traffic_graph); - patch!(enable_memory_usage); - patch!(enable_group_icon); - patch!(pause_render_traffic_stats_on_blur); + patch_opt_field!(self, patch, language); + patch_opt_field!(self, patch, theme_mode); + patch_opt_field!(self, patch, tray_event); + patch_opt_field!(self, patch, env_type); + patch_opt_field!(self, patch, start_page); + patch_opt_field!(self, patch, startup_script); + patch_opt_field!(self, patch, traffic_graph); + patch_opt_field!(self, patch, enable_memory_usage); + patch_opt_field!(self, patch, enable_group_icon); + patch_opt_field!(self, patch, pause_render_traffic_stats_on_blur); #[cfg(target_os = "macos")] - patch!(tray_icon); - patch!(menu_icon); - patch!(menu_order); - patch!(proxy_group_tools_position); - patch!(notice_position); - patch!(collapse_navbar); - patch!(common_tray_icon); - patch!(sysproxy_tray_icon); - patch!(tun_tray_icon); + patch_opt_field!(self, patch, tray_icon); + patch_opt_field!(self, patch, menu_icon); + patch_opt_field!(self, patch, menu_order); + patch_opt_field!(self, patch, proxy_group_tools_position); + patch_opt_field!(self, patch, notice_position); + patch_opt_field!(self, patch, collapse_navbar); + patch_opt_field!(self, patch, common_tray_icon); + patch_opt_field!(self, patch, sysproxy_tray_icon); + patch_opt_field!(self, patch, tun_tray_icon); - patch!(enable_tun_mode); - patch!(enable_auto_launch); - patch!(enable_silent_start); - patch!(enable_hover_jump_navigator); - patch!(hover_jump_navigator_delay); + patch_opt_field!(self, patch, enable_tun_mode); + patch_opt_field!(self, patch, enable_auto_launch); + patch_opt_field!(self, patch, enable_silent_start); + patch_opt_field!(self, patch, enable_hover_jump_navigator); + patch_opt_field!(self, patch, hover_jump_navigator_delay); #[cfg(not(target_os = "windows"))] - patch!(verge_redir_port); + patch_opt_field!(self, patch, verge_redir_port); #[cfg(not(target_os = "windows"))] - patch!(verge_redir_enabled); + patch_opt_field!(self, patch, verge_redir_enabled); #[cfg(target_os = "linux")] - patch!(verge_tproxy_port); + patch_opt_field!(self, patch, verge_tproxy_port); #[cfg(target_os = "linux")] - patch!(verge_tproxy_enabled); - patch!(verge_mixed_port); - patch!(verge_socks_port); - patch!(verge_socks_enabled); - patch!(verge_port); - patch!(verge_http_enabled); - patch!(enable_system_proxy); - patch!(enable_proxy_guard); - patch!(enable_bypass_check); - patch!(use_default_bypass); - patch!(system_proxy_bypass); - patch!(proxy_guard_duration); - patch!(proxy_auto_config); - patch!(pac_file_content); - patch!(proxy_host); - patch!(theme_setting); - patch!(web_ui_list); - patch!(clash_core); - patch!(hotkeys); - patch!(enable_global_hotkey); + patch_opt_field!(self, patch, verge_tproxy_enabled); + patch_opt_field!(self, patch, verge_mixed_port); + patch_opt_field!(self, patch, verge_socks_port); + patch_opt_field!(self, patch, verge_socks_enabled); + patch_opt_field!(self, patch, verge_port); + patch_opt_field!(self, patch, verge_http_enabled); + patch_opt_field!(self, patch, enable_system_proxy); + patch_opt_field!(self, patch, enable_proxy_guard); + patch_opt_field!(self, patch, enable_bypass_check); + patch_opt_field!(self, patch, use_default_bypass); + patch_opt_field!(self, patch, system_proxy_bypass); + patch_opt_field!(self, patch, proxy_guard_duration); + patch_opt_field!(self, patch, proxy_auto_config); + patch_opt_field!(self, patch, pac_file_content); + patch_opt_field!(self, patch, proxy_host); + patch_opt_field!(self, patch, theme_setting); + patch_opt_field!(self, patch, web_ui_list); + patch_opt_field!(self, patch, clash_core); + patch_opt_field!(self, patch, hotkeys); + patch_opt_field!(self, patch, enable_global_hotkey); - patch!(auto_close_connection); - patch!(auto_check_update); - patch!(default_latency_test); - patch!(default_latency_timeout); - patch!(enable_auto_delay_detection); - patch!(auto_delay_detection_interval_minutes); - patch!(enable_builtin_enhanced); - patch!(proxy_layout_column); - patch!(test_list); - patch!(auto_log_clean); - patch!(enable_auto_backup_schedule); - patch!(auto_backup_interval_hours); - patch!(auto_backup_on_change); + patch_opt_field!(self, patch, auto_close_connection); + patch_opt_field!(self, patch, auto_check_update); + patch_opt_field!(self, patch, default_latency_test); + patch_opt_field!(self, patch, default_latency_timeout); + patch_opt_field!(self, patch, enable_auto_delay_detection); + patch_opt_field!(self, patch, auto_delay_detection_interval_minutes); + patch_opt_field!(self, patch, enable_builtin_enhanced); + patch_opt_field!(self, patch, proxy_layout_column); + patch_opt_field!(self, patch, test_list); + patch_opt_field!(self, patch, auto_log_clean); + patch_opt_field!(self, patch, enable_auto_backup_schedule); + patch_opt_field!(self, patch, auto_backup_interval_hours); + patch_opt_field!(self, patch, auto_backup_on_change); - patch!(webdav_url); - patch!(webdav_username); - patch!(webdav_password); + patch_opt_field!(self, patch, webdav_url); + patch_opt_field!(self, patch, webdav_username); + patch_opt_field!(self, patch, webdav_password); #[cfg(target_os = "macos")] - patch!(enable_tray_speed); - patch!(tray_proxy_groups_display_mode); - patch!(tray_inline_outbound_modes); - patch!(enable_auto_light_weight_mode); - patch!(auto_light_weight_minutes); - patch!(enable_dns_settings); - patch!(home_cards); - patch!(enable_external_controller); + patch_opt_field!(self, patch, enable_tray_speed); + patch_opt_field!(self, patch, tray_proxy_groups_display_mode); + patch_opt_field!(self, patch, tray_inline_outbound_modes); + patch_opt_field!(self, patch, enable_auto_light_weight_mode); + patch_opt_field!(self, patch, auto_light_weight_minutes); + patch_opt_field!(self, patch, enable_dns_settings); + patch_opt_field!(self, patch, home_cards); + patch_opt_field!(self, patch, enable_external_controller); } pub fn get_log_level(&self) -> LevelFilter { From 9063181468a93c3c9d0219ae99e04b139f6a4fd1 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:38:50 +0800 Subject: [PATCH 08/51] refactor(enhance): merge chain processing and drop unused builtin js payload - collapse the identical process_global_items/process_profile_items into one parameterized process_override_items (entry uid sources unchanged) - fold the identical rules/proxies/groups arms of ChainItem::from_async into a single arm with an inner dispatch - stop embedding builtin JS payloads never executed by any path: the enhance loop dispatches builtin items by uid to native implementations, so ChainItem::builtin now carries empty script data (uids unchanged) - source ProfileItems fallback literals from Default instead of a second hand-written copy in collect_profile_items --- src-tauri/src/enhance/builtin/meta_guard.js | 8 -- src-tauri/src/enhance/builtin/meta_hy_alpn.js | 12 -- src-tauri/src/enhance/chain.rs | 48 +++----- src-tauri/src/enhance/mod.rs | 114 +++++------------- 4 files changed, 46 insertions(+), 136 deletions(-) delete mode 100644 src-tauri/src/enhance/builtin/meta_guard.js delete mode 100644 src-tauri/src/enhance/builtin/meta_hy_alpn.js diff --git a/src-tauri/src/enhance/builtin/meta_guard.js b/src-tauri/src/enhance/builtin/meta_guard.js deleted file mode 100644 index d427974c1..000000000 --- a/src-tauri/src/enhance/builtin/meta_guard.js +++ /dev/null @@ -1,8 +0,0 @@ -// This function is exported for use by the Clash core -// eslint-disable-next-line unused-imports/no-unused-vars -function main(config, _name) { - if (config.mode === 'script') { - config.mode = 'rule' - } - return config -} diff --git a/src-tauri/src/enhance/builtin/meta_hy_alpn.js b/src-tauri/src/enhance/builtin/meta_hy_alpn.js deleted file mode 100644 index d7e57dd2a..000000000 --- a/src-tauri/src/enhance/builtin/meta_hy_alpn.js +++ /dev/null @@ -1,12 +0,0 @@ -// This function is exported for use by the Clash core -// eslint-disable-next-line unused-imports/no-unused-vars -function main(config, _name) { - if (Array.isArray(config.proxies)) { - config.proxies.forEach((p, i) => { - if (p.type === 'hysteria' && typeof p.alpn === 'string') { - config.proxies[i].alpn = [p.alpn] - } - }) - } - return config -} diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index 797b02cd7..ac2fe27ee 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -53,26 +53,14 @@ impl AsyncChainItemFrom for Option { uid, data: ChainType::Merge(help::read_mapping(&path).await.ok()?), }), - "rules" => { + "rules" | "proxies" | "groups" => { let seq_map = help::read_yaml::(&path).await.ok()?; - Some(ChainItem { - uid, - data: ChainType::Rules(seq_map), - }) - } - "proxies" => { - let seq_map = help::read_yaml::(&path).await.ok()?; - Some(ChainItem { - uid, - data: ChainType::Proxies(seq_map), - }) - } - "groups" => { - let seq_map = help::read_yaml::(&path).await.ok()?; - Some(ChainItem { - uid, - data: ChainType::Groups(seq_map), - }) + let data = match itype { + "rules" => ChainType::Rules(seq_map), + "proxies" => ChainType::Proxies(seq_map), + _ => ChainType::Groups(seq_map), + }; + Some(ChainItem { uid, data }) } _ => None, } @@ -81,23 +69,15 @@ impl AsyncChainItemFrom for Option { impl ChainItem { /// 内建支持一些脚本 pub fn builtin() -> Vec<(ChainSupport, Self)> { - // meta 的一些处理 - let meta_guard = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); - - // meta 1.13.2 alpn string 转 数组 - let hy_alpn = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); - - // meta 的一些处理 - let meta_guard_alpha = Self::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js")); - - // meta 1.13.2 alpn string 转 数组 - let hy_alpn_alpha = Self::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js")); + // data intentionally empty: the enhance loop dispatches on uid to native implementations + let hy_alpn = Self::to_script("verge_hy_alpn", ""); + let meta_guard = Self::to_script("verge_meta_guard", ""); vec![ - (ChainSupport::ClashMeta, hy_alpn), - (ChainSupport::ClashMeta, meta_guard), - (ChainSupport::ClashMetaAlpha, hy_alpn_alpha), - (ChainSupport::ClashMetaAlpha, meta_guard_alpha), + (ChainSupport::ClashMeta, hy_alpn.clone()), + (ChainSupport::ClashMeta, meta_guard.clone()), + (ChainSupport::ClashMetaAlpha, hy_alpn), + (ChainSupport::ClashMetaAlpha, meta_guard), ] } diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index ba0ba5708..045a4f545 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -199,35 +199,25 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result { let name = current_item.name.clone().unwrap_or_default(); + let ProfileItems { + merge_item: default_merge, + script_item: default_script, + rules_item: default_rules, + proxies_item: default_proxies, + groups_item: default_groups, + global_merge: default_global_merge, + global_script: default_global_script, + .. + } = ProfileItems::default(); + let (merge_item, script_item, rules_item, proxies_item, groups_item, global_merge, global_script) = tokio::join!( - chain_item_or_default(profiles.get_item(&merge_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Merge(Mapping::new()), - },), - chain_item_or_default(profiles.get_item(&script_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - },), - chain_item_or_default(profiles.get_item(&rules_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Rules(SeqMap::default()), - },), - chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Proxies(SeqMap::default()), - },), - chain_item_or_default(profiles.get_item(&groups_uid).ok(), || ChainItem { - uid: "".into(), - data: ChainType::Groups(SeqMap::default()), - },), - chain_item_or_default(profiles.get_item("Merge").ok(), || ChainItem { - uid: "Merge".into(), - data: ChainType::Merge(Mapping::new()), - },), - chain_item_or_default(profiles.get_item("Script").ok(), || ChainItem { - uid: "Script".into(), - data: ChainType::Script(tmpl::ITEM_SCRIPT.into()), - },), + chain_item_or_default(profiles.get_item(&merge_uid).ok(), || default_merge,), + chain_item_or_default(profiles.get_item(&script_uid).ok(), || default_script,), + chain_item_or_default(profiles.get_item(&rules_uid).ok(), || default_rules,), + chain_item_or_default(profiles.get_item(&proxies_uid).ok(), || default_proxies,), + chain_item_or_default(profiles.get_item(&groups_uid).ok(), || default_groups,), + chain_item_or_default(profiles.get_item("Merge").ok(), || default_global_merge,), + chain_item_or_default(profiles.get_item("Script").ok(), || default_global_script,), ); Ok(ProfileItems { @@ -243,16 +233,16 @@ async fn collect_profile_items(profiles: &IProfiles) -> Result { }) } -async fn process_global_items( +async fn process_override_items( mut config: Mapping, mut exists_keys: Vec, mut result_map: HashMap, - global_merge: ChainItem, - global_script: ChainItem, + merge_item: ChainItem, + script_item: ChainItem, profile_name: &String, authoritative: &AuthoritativeFields, ) -> (Mapping, Vec, HashMap) { - if let ChainType::Merge(merge) = global_merge.data { + if let ChainType::Merge(merge) = merge_item.data { exists_keys.extend(use_keys(&merge)); let before = authoritative.current(&config); config = use_merge(&merge, config); @@ -262,11 +252,11 @@ async fn process_global_items( .map(discarded_note) .collect(); if !notes.is_empty() { - result_map.entry(global_merge.uid).or_default().extend(notes); + result_map.entry(merge_item.uid).or_default().extend(notes); } } - if let ChainType::Script(script) = global_script.data { + if let ChainType::Script(script) = script_item.data { let before = authoritative.current(&config); let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await; exists_keys.extend(changed_keys); @@ -277,7 +267,7 @@ async fn process_global_items( .into_iter() .map(discarded_note), ); - result_map.insert(global_script.uid, logs); + result_map.insert(script_item.uid, logs); } (config, exists_keys, result_map) @@ -544,46 +534,6 @@ fn ensure_lan_bind_address(mut config: Mapping) -> Mapping { config } -async fn process_profile_items( - mut config: Mapping, - mut exists_keys: Vec, - mut result_map: HashMap, - merge_item: ChainItem, - script_item: ChainItem, - profile_name: &String, - authoritative: &AuthoritativeFields, -) -> (Mapping, Vec, HashMap) { - if let ChainType::Merge(merge) = merge_item.data { - exists_keys.extend(use_keys(&merge)); - let before = authoritative.current(&config); - config = use_merge(&merge, config); - let notes: ResultLog = authoritative - .overridden(&before, &authoritative.current(&config)) - .into_iter() - .map(discarded_note) - .collect(); - if !notes.is_empty() { - result_map.entry(merge_item.uid).or_default().extend(notes); - } - } - - if let ChainType::Script(script) = script_item.data { - let before = authoritative.current(&config); - let (res_config, changed_keys, mut logs) = use_script(script, config, profile_name.clone()).await; - exists_keys.extend(changed_keys); - config = res_config; - logs.extend( - authoritative - .overridden(&before, &authoritative.current(&config)) - .into_iter() - .map(discarded_note), - ); - result_map.insert(script_item.uid, logs); - } - - (config, exists_keys, result_map) -} - /// Merges the app's clash config over the profile; switches are parameters to keep it pure. fn merge_default_config( mut config: Mapping, @@ -935,7 +885,7 @@ pub async fn enhance( let authoritative = AuthoritativeFields::capture(&config, &gui_tun_keys, dns_settings); - let (config, exists_keys, result_map) = process_global_items( + let (config, exists_keys, result_map) = process_override_items( config, exists_keys, result_map, @@ -946,7 +896,7 @@ pub async fn enhance( ) .await; - let (config, exists_keys, result_map) = process_profile_items( + let (config, exists_keys, result_map) = process_override_items( config, exists_keys, result_map, @@ -1428,7 +1378,7 @@ mod authoritative_field_tests { mod tests { use super::{ AuthoritativeFields, ChainItem, ChainType, Mapping, cleanup_proxy_groups, ensure_lan_bind_address, - process_global_items, process_profile_items, use_keys, + process_override_items, use_keys, }; use std::collections::HashMap; @@ -1476,7 +1426,7 @@ mod tests { data: ChainType::Merge(merge.clone()), }; let profile_name = "test-profile".into(); - let (config, keys, logs) = process_global_items( + let (config, keys, logs) = process_override_items( config, vec![], HashMap::new(), @@ -1486,7 +1436,7 @@ mod tests { &authoritative, ) .await; - let (config, _, logs) = process_profile_items( + let (config, _, logs) = process_override_items( config, keys, logs, @@ -1564,7 +1514,7 @@ mod tests { let profile_name = "test-profile".into(); let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new()); - let (config, exists_keys, result_map) = process_global_items( + let (config, exists_keys, result_map) = process_override_items( config, exists_keys, HashMap::new(), @@ -1574,7 +1524,7 @@ mod tests { &authoritative, ) .await; - let (config, exists_keys, _) = process_profile_items( + let (config, exists_keys, _) = process_override_items( config, exists_keys, result_map, From ad0035edb9ba0f5b0cce330a2f546d6dee8a3be9 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:50:22 +0800 Subject: [PATCH 09/51] refactor(net): reuse webpki tls config and fallback Extract build_webpki_rooted_tls_config so the delay-test TLS connector and the reqwest clients share one static-webpki-roots builder; ALPN stays caller-specific (only reqwest offers h2). Collapse the duplicated platform-verifier -> static-webpki-roots retry in get/get_bytes into request_with_static_webpki_fallback, gated by the caller's certificate-verification setting. --- src-tauri/src/feat/clash.rs | 11 +---- src-tauri/src/utils/network.rs | 82 +++++++++++++++++----------------- 2 files changed, 43 insertions(+), 50 deletions(-) diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 0f90dffc4..beddd2dec 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -14,15 +14,8 @@ use smartstring::alias::String; use std::sync::{Arc, LazyLock}; #[allow(clippy::expect_used)] -static TLS_CONFIG: LazyLock> = LazyLock::new(|| { - let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) - .with_safe_default_protocol_versions() - .expect("Failed to set TLS versions") - .with_root_certificates(root_store) - .with_no_client_auth(); - Arc::new(config) -}); +static TLS_CONFIG: LazyLock> = + LazyLock::new(|| Arc::new(utils::network::build_webpki_rooted_tls_config().expect("Failed to set TLS versions"))); pub async fn restart_clash_core() { match CoreManager::global().restart_core().await { diff --git a/src-tauri/src/utils/network.rs b/src-tauri/src/utils/network.rs index d325fecbd..89d2ddc6f 100644 --- a/src-tauri/src/utils/network.rs +++ b/src-tauri/src/utils/network.rs @@ -48,6 +48,17 @@ enum TlsRootMode { StaticWebpkiRoots, } +/// ALPN stays unset: raw TLS connectors offer none, while reqwest needs the h2 offer. +pub(crate) fn build_webpki_rooted_tls_config() -> Result { + let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); + let config = rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) + .with_safe_default_protocol_versions()? + .with_root_certificates(root_store) + .with_no_client_auth(); + + Ok(config) +} + pub struct NetworkManager; impl NetworkManager { @@ -102,13 +113,7 @@ impl NetworkManager { } fn build_static_webpki_tls_config() -> Result { - let root_store = rustls::RootCertStore::from_iter(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let mut config = - rustls::ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider())) - .with_safe_default_protocol_versions()? - .with_root_certificates(root_store) - .with_no_client_auth(); - + let mut config = build_webpki_rooted_tls_config()?; config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; Ok(config) @@ -155,6 +160,23 @@ impl NetworkManager { detail.contains("protocolversion") || detail.contains("protocol version") } + /// `allow_fallback` stays false when the caller already disabled certificate verification. + async fn request_with_static_webpki_fallback(allow_fallback: bool, mut request: F) -> Result + where + F: FnMut(TlsRootMode) -> Fut, + Fut: std::future::Future>, + { + match request(TlsRootMode::PlatformVerifier).await { + Ok(value) => Ok(value), + Err(err) if allow_fallback && Self::should_retry_with_static_webpki_roots(&err) => { + request(TlsRootMode::StaticWebpkiRoots).await.map_err(|fallback_err| { + fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") + }) + } + Err(err) => Err(err), + } + } + async fn get_with_tls_mode( &self, url: &str, @@ -264,34 +286,21 @@ impl NetworkManager { user_agent: Option, accept_invalid_certs: bool, ) -> Result { - let platform_result = self - .get_with_tls_mode( - url, - proxy_type, - timeout_secs, - user_agent.clone(), - accept_invalid_certs, - TlsRootMode::PlatformVerifier, - ) - .await; - - match platform_result { - Ok(response) => Ok(response), - Err(err) if !accept_invalid_certs && Self::should_retry_with_static_webpki_roots(&err) => self - .get_with_tls_mode( + Self::request_with_static_webpki_fallback(!accept_invalid_certs, |tls_root_mode| { + let user_agent = user_agent.clone(); + async move { + self.get_with_tls_mode( url, proxy_type, timeout_secs, user_agent, accept_invalid_certs, - TlsRootMode::StaticWebpkiRoots, + tls_root_mode, ) .await - .map_err(|fallback_err| { - fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") - }), - Err(err) => Err(err), - } + } + }) + .await } async fn get_bytes_with_tls_mode( @@ -340,19 +349,10 @@ impl NetworkManager { timeout_secs: Option, max_bytes: usize, ) -> Result> { - let platform_result = self - .get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::PlatformVerifier) - .await; - - match platform_result { - Ok(body) => Ok(body), - Err(err) if Self::should_retry_with_static_webpki_roots(&err) => self - .get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, TlsRootMode::StaticWebpkiRoots) + Self::request_with_static_webpki_fallback(true, |tls_root_mode| async move { + self.get_bytes_with_tls_mode(url, proxy_type, timeout_secs, max_bytes, tls_root_mode) .await - .map_err(|fallback_err| { - fallback_err.context("static webpki roots fallback failed after platform TLS verifier failed") - }), - Err(err) => Err(err), - } + }) + .await } } From 808f27abb4323619180e972d79fea101ba57b1e5 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:13:19 +0800 Subject: [PATCH 10/51] refactor(app): extract common exit cleanup restart_app and quit duplicate the whole exit preparation: set the exiting flag, persist configs, run interactive cleanup, and on a failed core stop clear the flag, restore public DNS under the lifecycle lock on macOS, and send the stop-failed notice. Move that flow into run_exit_cleanup_flow next to clean_async, taking the notice variant as a parameter and returning the CleanupResult so each caller keeps its own tail: restart aborts before shutdown, quit always shuts down and exits with the cleanup code. --- src-tauri/src/feat/clash.rs | 33 ++------------------------------- src-tauri/src/feat/window.rs | 15 ++++++++++----- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index beddd2dec..9b1d84b3c 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -3,7 +3,7 @@ use crate::core::notify::{Refresh, announce}; use crate::{ config::{Config, MixedPort}, core::{CoreManager, handle, tray}, - feat::clean_async, + feat::run_exit_cleanup_flow, process::AsyncHandler, utils, }; @@ -31,38 +31,9 @@ pub async fn restart_clash_core() { pub async fn restart_app() { logging!(debug, Type::System, "启动重启应用流程"); - handle::Handle::global().set_is_exiting(); - - Config::apply_all_and_save_file().await; - - let cleanup_result = clean_async().await; - - logging!( - info, - Type::System, - "资源清理完成,退出代码: {}", - if cleanup_result.all_success { 0 } else { 1 } - ); + let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppRestartCoreStopFailed).await; if !cleanup_result.core_stopped { - handle::Handle::global().clear_is_exiting(); - // A failed stop may still have marked the core stopped, and exit cleanup skipped the DNS restore. - // A core that kept running keeps its DNS, whatever an unapplied config draft says. - #[cfg(target_os = "macos")] - { - let manager = CoreManager::global(); - let _lifecycle = manager.lifecycle_lock.lock().await; - if matches!( - *manager.get_running_mode(), - crate::core::manager::RunningMode::NotRunning - ) { - crate::utils::resolve::dns::sync_public_dns().await; - } - } - handle::Handle::notice( - NoticeStatus::AppRestartCoreStopFailed, - cleanup_result.stop_error.unwrap_or_default(), - ); return; } diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index e618701ed..ebb6cb397 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -130,9 +130,7 @@ pub async fn open_or_close_dashboard() { logging!(info, Type::Window, "Window toggle result: {result:?}"); } -pub async fn quit() { - logging!(debug, Type::System, "启动退出流程"); - // 设置退出标志 +pub(crate) async fn run_exit_cleanup_flow(stop_failed_notice: NoticeStatus) -> CleanupResult { handle::Handle::global().set_is_exiting(); Config::apply_all_and_save_file().await; @@ -162,11 +160,18 @@ pub async fn quit() { } } handle::Handle::notice( - NoticeStatus::AppQuitCoreStopFailed, - cleanup_result.stop_error.unwrap_or_default(), + stop_failed_notice, + cleanup_result.stop_error.clone().unwrap_or_default(), ); } + cleanup_result +} + +pub async fn quit() { + logging!(debug, Type::System, "启动退出流程"); + let cleanup_result = run_exit_cleanup_flow(NoticeStatus::AppQuitCoreStopFailed).await; + utils::server::shutdown_embedded_server(); let app_handle = handle::Handle::app_handle(); app_handle.exit(if cleanup_result.all_success { 0 } else { 1 }); From c82719af45eee15b4079a770d33de7b8bc6d8f75 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:30:17 +0800 Subject: [PATCH 11/51] refactor(backup): share restore finalize path restore_webdav_backup and restore_local_backup duplicated the credential extraction (backup creation strips the WebDAV credentials, so both re-inject the live ones), the archive extraction and the finalize tail. Move that tail into a private restore_backup_archive helper so the two entries keep only their download/locate logic; the config-write lock scope and the off-runtime extraction land in one place. The webdav entry keeps its downloaded-file cleanup. --- src-tauri/src/feat/backup.rs | 79 ++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 43 deletions(-) diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 7c246d36a..449fba642 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -43,14 +43,14 @@ async fn finalize_restored_verge_config( let restored_clash = IClashTemp::new().await; let clash_draft = Config::clash().await; clash_draft.edit_draft(|d| { - *d = restored_clash.clone(); + *d = restored_clash; }); clash_draft.apply(); let restored_profiles = IProfiles::new().await; let profiles_draft = Config::profiles().await; profiles_draft.edit_draft(|d| { - *d = restored_profiles.clone(); + *d = restored_profiles; }); profiles_draft.apply(); @@ -62,20 +62,20 @@ async fn finalize_restored_verge_config( *d = restored.clone(); }); verge_draft.apply(); + } - // Turn it off here; a failing core side effect below would otherwise skip this step. - if !restored.enable_system_proxy.unwrap_or_default() { - let result = async { - let _lifecycle = CoreManager::global().lifecycle_lock.lock().await; - proxy_control::apply().await?; - proxy_control::refresh_guard().await - } - .await; - if let Err(err) = result { - logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}"); - if SystemProxyStateUnknown::is_in(&err) { - return Err(err); - } + // Turn it off here; a failing core side effect below would otherwise skip this step. + if !restored.enable_system_proxy.unwrap_or_default() { + let result = async { + let _lifecycle = CoreManager::global().lifecycle_lock.lock().await; + proxy_control::apply().await?; + proxy_control::refresh_guard().await + } + .await; + if let Err(err) = result { + logging!(error, Type::Backup, "Failed to turn the system proxy off: {err:#}"); + if SystemProxyStateUnknown::is_in(&err) { + return Err(err); } } } @@ -134,14 +134,27 @@ pub async fn delete_webdav_backup(filename: String) -> Result<()> { }) } +/// Backup creation strips the WebDAV credentials, so the live ones are kept. +async fn restore_backup_archive(path: PathBuf) -> Result<()> { + let (webdav_url, webdav_username, webdav_password) = { + let verge = Config::verge().await; + let verge = verge.latest_arc(); + ( + verge.webdav_url.clone(), + verge.webdav_username.clone(), + verge.webdav_password.clone(), + ) + }; + + let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&path)).await??; + let mut zip = zip::ZipArchive::new(file)?; + let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; + zip.extract(app_home_dir()?)?; + finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await +} + #[tracing::instrument(skip_all, level = "info", fields(filename = %filename))] pub async fn restore_webdav_backup(filename: String) -> Result<()> { - let verge = Config::verge().await; - let verge_data = verge.latest_arc(); - let webdav_url = verge_data.webdav_url.clone(); - let webdav_username = verge_data.webdav_username.clone(); - let webdav_password = verge_data.webdav_password.clone(); - let backup_storage_path = app_home_dir() .map_err(|e| anyhow::anyhow!("Failed to get app home dir: {e}"))? .join(filename.as_str()); @@ -158,12 +171,7 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { err })?; - let value = backup_storage_path.clone(); - let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&value)).await??; - let mut zip = zip::ZipArchive::new(file)?; - let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; - zip.extract(app_home_dir()?)?; - let res = finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await; + let res = restore_backup_archive(backup_storage_path.clone()).await; let _ = backup_storage_path.remove_if_exists().await; res } @@ -332,22 +340,7 @@ pub async fn restore_local_backup(filename: String) -> Result<()> { return Err(anyhow!("Backup file not found: {}", filename)); } - let (webdav_url, webdav_username, webdav_password) = { - let verge = Config::verge().await; - let verge = verge.latest_arc(); - ( - verge.webdav_url.clone(), - verge.webdav_username.clone(), - verge.webdav_password.clone(), - ) - }; - - let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&target_path)).await??; - let mut zip = zip::ZipArchive::new(file)?; - let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; - zip.extract(app_home_dir()?)?; - finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await?; - Ok(()) + restore_backup_archive(target_path).await } #[tracing::instrument(skip_all, level = "info", fields(filename = %filename, destination = %destination))] From 0474c9cd0c7ccef0ad7d36123c5de9c4d4266c89 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:53:06 +0800 Subject: [PATCH 12/51] refactor(sysopt): drop redundant lock and busy-wait The proxy target pair sat behind an Arc with no readers and an Arc that was never cloned; both writers already serialize on update_lock, so store the pair inside that mutex and delete the RwLock. refresh_guard waited for the guard to become ready with a yield_now spin at full CPU while holding guard_operation_lock; a 10 ms sleep keeps the same wait condition and lock scope without the spin. --- src-tauri/src/core/sysopt.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 9b3d158dc..6fccb0e3e 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -241,20 +241,19 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState } pub(crate) struct Sysopt { - update_lock: TokioMutex<()>, + // Owns the proxy target pair; every reader and writer goes through this lock. + update_lock: TokioMutex<(Sysproxy, Autoproxy)>, guard_operation_lock: TokioMutex<()>, reset_sysproxy: AtomicBool, - inner_proxy: Arc>, guard: Arc>, } impl Default for Sysopt { fn default() -> Self { Self { - update_lock: TokioMutex::new(()), + update_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())), guard_operation_lock: TokioMutex::new(()), reset_sysproxy: AtomicBool::new(false), - inner_proxy: Arc::new(RwLock::new((Sysproxy::default(), Autoproxy::default()))), guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), } } @@ -361,7 +360,7 @@ impl Sysopt { } } while self.access_guard().read().get_state().is_pendding() { - tokio::task::yield_now().await; + tokio::time::sleep(Duration::from_millis(10)).await; } // `start()` false may also mean a guard was already running. !self.access_guard().read().get_state().is_stopped() @@ -413,14 +412,17 @@ impl Sysopt { } /// init the sysproxy + #[allow( + clippy::significant_drop_tightening, + reason = "the update lock must stay held until the OS write finishes; wait_idle relies on it" + )] pub(super) async fn update_sysproxy(&self) -> Result<()> { - let _lock = self.update_lock.lock().await; + let mut inner_proxy = self.update_lock.lock().await; let verge = Config::verge().await.latest_arc(); // Configured, not live: this runs while the Core is being started or restarted, and // asking a Core that is not up yet would only fall back here anyway. let port = MixedPort::desired().await; let pac_port = server::embedded_server_port()?; - // 先 await, 避免持有锁导致的 Send 问题 let bypass = get_bypass().await; let (sys_enable, pac_enable, proxy_host, proxy_guard) = ( @@ -431,7 +433,7 @@ impl Sysopt { ); let (sys, auto, guard_type) = { - let (sys, auto) = &mut *self.inner_proxy.write(); + let (sys, auto) = &mut *inner_proxy; sys.host = proxy_host.into(); sys.port = port; sys.bypass = bypass.into(); @@ -528,6 +530,10 @@ impl Sysopt { } /// reset the sysproxy + #[allow( + clippy::significant_drop_tightening, + reason = "the update lock must stay held until the disable finishes; wait_idle relies on it" + )] pub(super) async fn reset_sysproxy(&self) -> Result<()> { if self .reset_sysproxy @@ -539,13 +545,13 @@ impl Sysopt { defer! { self.reset_sysproxy.store(false, Ordering::SeqCst); } - let _lock = self.update_lock.lock().await; + let mut inner_proxy = self.update_lock.lock().await; let _guard_operation = self.guard_operation_lock.lock().await; let drained = self.stop_proxy_guard_locked().await; // 直接关闭所有代理 let (sys, auto) = { - let (sys, auto) = &mut *self.inner_proxy.write(); + let (sys, auto) = &mut *inner_proxy; sys.enable = false; auto.enable = false; (sys.clone(), auto.clone()) From acde0398209321f3104bb3ef0f6abffd040f166d Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:54:25 +0800 Subject: [PATCH 13/51] refactor(sysopt): share macos bypass formatting The macOS default bypass list and its three-way composition (empty -> default, default + custom, custom only) were maintained twice: in sysopt's format_bypass and inline in proxy_control's service_bypass. Keep the literal once in constants and compose both paths through format_bypass; service_bypass keeps its NUL check and UTF-8 truncation. --- src-tauri/src/constants.rs | 6 ++++++ src-tauri/src/core/proxy_control.rs | 12 ++---------- src-tauri/src/core/sysopt.rs | 5 ++--- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index da67ce13e..20b461f31 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -14,6 +14,12 @@ pub mod network { } } +pub mod proxy { + /// Single source of the macOS default bypass list; both proxy backends compose from it. + pub const MACOS_DEFAULT_BYPASS: &str = + "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; +} + pub mod timing { use super::Duration; diff --git a/src-tauri/src/core/proxy_control.rs b/src-tauri/src/core/proxy_control.rs index e9f482247..19bc09456 100644 --- a/src-tauri/src/core/proxy_control.rs +++ b/src-tauri/src/core/proxy_control.rs @@ -6,7 +6,7 @@ use crate::{ notification::{self, FailedOperation}, runstate::RUN_STATE, service, - sysopt::Sysopt, + sysopt::{Sysopt, format_bypass}, }, process::AsyncHandler, utils::{ @@ -204,8 +204,6 @@ const fn refusal_classification(service_usable: Option) -> SysproxyFailure } const LOOPBACK_HOST: &str = "127.0.0.1"; -const MACOS_DEFAULT_BYPASS: &str = - "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; const MAX_SERVICE_BYPASS_LEN: usize = 8192; static SERVICE_PROXY_OPERATIONS: ServiceProxyOperations = ServiceProxyOperations::new(); @@ -327,13 +325,7 @@ fn service_bypass(verge: &IVerge) -> Result { let custom = verge.system_proxy_bypass.as_deref().unwrap_or(""); ensure!(!custom.contains('\0'), "system proxy bypass contains NUL"); - let mut bypass = if custom.is_empty() { - MACOS_DEFAULT_BYPASS.to_owned() - } else if verge.use_default_bypass.unwrap_or(true) { - format!("{MACOS_DEFAULT_BYPASS},{custom}") - } else { - custom.to_owned() - }; + let mut bypass: String = format_bypass(verge.use_default_bypass.unwrap_or(true), custom).into(); truncate_utf8(&mut bypass, MAX_SERVICE_BYPASS_LEN); Ok(bypass) } diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index 6fccb0e3e..c503ab86c 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -268,10 +268,9 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172 #[cfg(any(target_os = "linux", target_os = "macos"))] static BYPASS_SEPARATOR: &str = ","; #[cfg(target_os = "macos")] -static DEFAULT_BYPASS: &str = - "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; +static DEFAULT_BYPASS: &str = crate::constants::proxy::MACOS_DEFAULT_BYPASS; -fn format_bypass(use_default: bool, custom_bypass: &str) -> String { +pub(super) fn format_bypass(use_default: bool, custom_bypass: &str) -> String { if custom_bypass.is_empty() { DEFAULT_BYPASS.into() } else if use_default { From 7e9fa61f52b9912cf9b7c25f49ee4cb84a09a969 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:09:49 +0800 Subject: [PATCH 14/51] refactor(core): dedupe window listeners, elevated runners, backup settings - Collapse the close/focus window listener pairs in lightweight.rs onto a shared WindowListener with per-listener event and log label. - Extract the windows runas-or-direct execution into service::run_maybe_elevated, shared by win_uwp and the service uninstaller, and factor the common uninstaller path resolution and status check shared across platforms. - Make the auto-backup watch channel the single settings source, replacing the duplicate Arc copy with a stored receiver. --- src-tauri/src/core/auto_backup.rs | 20 +++---- src-tauri/src/core/lightweight.rs | 95 +++++++++++++++++-------------- src-tauri/src/core/service.rs | 83 ++++++++++++++------------- src-tauri/src/core/win_uwp.rs | 13 +---- 4 files changed, 105 insertions(+), 106 deletions(-) diff --git a/src-tauri/src/core/auto_backup.rs b/src-tauri/src/core/auto_backup.rs index c475cb711..a3048bab6 100644 --- a/src-tauri/src/core/auto_backup.rs +++ b/src-tauri/src/core/auto_backup.rs @@ -7,11 +7,10 @@ use crate::{ use anyhow::Result; use chrono::Local; use clash_verge_logging::{Type, logging}; -use parking_lot::RwLock; use std::{ path::PathBuf, sync::{ - Arc, OnceLock, + OnceLock, atomic::{AtomicBool, AtomicI64, Ordering}, }, time::{Duration, UNIX_EPOCH}, @@ -82,8 +81,9 @@ impl Default for AutoBackupSettings { } pub struct AutoBackupManager { - settings: Arc>, settings_tx: watch::Sender, + // The watch channel is the single settings store; no duplicate copies. + settings_rx: watch::Receiver, runner_started: AtomicBool, exec_lock: Mutex<()>, last_backup: AtomicI64, @@ -93,10 +93,10 @@ impl AutoBackupManager { pub fn global() -> &'static Self { static INSTANCE: OnceLock = OnceLock::new(); INSTANCE.get_or_init(|| { - let (tx, _rx) = watch::channel(AutoBackupSettings::default()); + let (settings_tx, settings_rx) = watch::channel(AutoBackupSettings::default()); Self { - settings: Arc::new(RwLock::new(AutoBackupSettings::default())), - settings_tx: tx, + settings_tx, + settings_rx, runner_started: AtomicBool::new(false), exec_lock: Mutex::new(()), last_backup: AtomicI64::new(0), @@ -106,9 +106,6 @@ impl AutoBackupManager { pub async fn init(&self) -> Result<()> { let settings = Self::load_settings().await; - { - *self.settings.write() = settings; - } let _ = self.settings_tx.send(settings); self.maybe_start_runner(settings); Ok(()) @@ -116,9 +113,6 @@ impl AutoBackupManager { pub async fn refresh_settings(&self) -> Result<()> { let settings = Self::load_settings().await; - { - *self.settings.write() = settings; - } let _ = self.settings_tx.send(settings); self.maybe_start_runner(settings); Ok(()) @@ -193,7 +187,7 @@ impl AutoBackupManager { } async fn execute_trigger(&self, trigger: AutoBackupTrigger) -> Result<()> { - let snapshot = *self.settings.read(); + let snapshot = *self.settings_rx.borrow(); if trigger.is_schedule() && !snapshot.schedule_enabled { return Ok(()); diff --git a/src-tauri/src/core/lightweight.rs b/src-tauri/src/core/lightweight.rs index 9b4cb0041..fd1e4d363 100644 --- a/src-tauri/src/core/lightweight.rs +++ b/src-tauri/src/core/lightweight.rs @@ -40,8 +40,47 @@ impl LightweightState { static LIGHTWEIGHT_STATE: AtomicU8 = AtomicU8::new(LightweightState::Normal as u8); -static WINDOW_CLOSE_HANDLER_ID: AtomicU32 = AtomicU32::new(0); -static WEBVIEW_FOCUS_HANDLER_ID: AtomicU32 = AtomicU32::new(0); +struct WindowListener { + handler_id: AtomicU32, + event: &'static str, + label: &'static str, +} + +impl WindowListener { + fn setup(&self, on_event: impl Fn() + Send + 'static) { + if let Some(window) = WindowManager::get_main_window() { + let previous_handler_id = self.handler_id.swap(0, Ordering::AcqRel); + if previous_handler_id != 0 { + window.unlisten(previous_handler_id); + logging!(debug, Type::Lightweight, "覆盖旧的{}监听", self.label); + } + let handler_id = window.listen(self.event, move |_event| on_event()); + self.handler_id.store(handler_id, Ordering::Release); + } + } + + fn cancel(&self) { + let id = self.handler_id.swap(0, Ordering::AcqRel); + if id != 0 { + if let Some(window) = WindowManager::get_main_window() { + window.unlisten(id); + } + logging!(debug, Type::Lightweight, "取消了{}监听", self.label); + } + } +} + +static WINDOW_CLOSE_LISTENER: WindowListener = WindowListener { + handler_id: AtomicU32::new(0), + event: "tauri://close-requested", + label: "窗口关闭", +}; + +static WEBVIEW_FOCUS_LISTENER: WindowListener = WindowListener { + handler_id: AtomicU32::new(0), + event: "tauri://focus", + label: "窗口焦点", +}; static CANCEL_TX: Mutex>> = Mutex::new(None); @@ -154,55 +193,27 @@ pub async fn add_light_weight_timer() { } fn setup_window_close_listener() { - if let Some(window) = WindowManager::get_main_window() { - let previous_handler_id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); - if previous_handler_id != 0 { - window.unlisten(previous_handler_id); - logging!(debug, Type::Lightweight, "覆盖旧的窗口关闭监听"); - } - let handler_id = window.listen("tauri://close-requested", move |_event| { - std::mem::drop(AsyncHandler::spawn(|| async { - setup_light_weight_timer().await; - })); - logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时"); - }); - WINDOW_CLOSE_HANDLER_ID.store(handler_id, Ordering::Release); - } + WINDOW_CLOSE_LISTENER.setup(|| { + std::mem::drop(AsyncHandler::spawn(|| async { + setup_light_weight_timer().await; + })); + logging!(info, Type::Lightweight, "监听到关闭请求,开始轻量模式计时"); + }); } fn cancel_window_close_listener() { - let id = WINDOW_CLOSE_HANDLER_ID.swap(0, Ordering::AcqRel); - if id != 0 { - if let Some(window) = WindowManager::get_main_window() { - window.unlisten(id); - } - logging!(debug, Type::Lightweight, "取消了窗口关闭监听"); - } + WINDOW_CLOSE_LISTENER.cancel(); } fn setup_webview_focus_listener() { - if let Some(window) = WindowManager::get_main_window() { - let previous_handler_id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); - if previous_handler_id != 0 { - window.unlisten(previous_handler_id); - logging!(debug, Type::Lightweight, "覆盖旧的窗口焦点监听"); - } - let handler_id = window.listen("tauri://focus", move |_event| { - cancel_light_weight_timer(); - logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时"); - }); - WEBVIEW_FOCUS_HANDLER_ID.store(handler_id, Ordering::Release); - } + WEBVIEW_FOCUS_LISTENER.setup(|| { + cancel_light_weight_timer(); + logging!(debug, Type::Lightweight, "监听到窗口获得焦点,取消轻量模式计时"); + }); } fn cancel_webview_focus_listener() { - let id = WEBVIEW_FOCUS_HANDLER_ID.swap(0, Ordering::AcqRel); - if id != 0 { - if let Some(window) = WindowManager::get_main_window() { - window.unlisten(id); - } - logging!(debug, Type::Lightweight, "取消了窗口焦点监听"); - } + WEBVIEW_FOCUS_LISTENER.cancel(); } async fn setup_light_weight_timer() { diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 7d9242087..5733a24aa 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -593,50 +593,71 @@ fn packaged_service_tool_path(file_name: &str, packaged_path: impl FnOnce() -> R } #[cfg(target_os = "windows")] -fn uninstall_service() -> Result<()> { - logging!(info, Type::Service, "uninstall service"); - +pub(super) fn run_maybe_elevated(program: &Path, hide_window: bool) -> Result { use deelevate::{PrivilegeLevel, Token}; use runas::Command as RunasCommand; use std::os::windows::process::CommandExt as _; - let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall.exe", || { - Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe")) - })?; - - if !uninstall_path.exists() { - bail!(format!("uninstaller not found: {uninstall_path:?}")); - } - let token = Token::with_current_process()?; let level = token.privilege_level()?; let status = match level { - PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?, - _ => StdCommand::new(uninstall_path).creation_flags(0x08000000).status()?, + PrivilegeLevel::NotPrivileged => { + let mut command = RunasCommand::new(program); + if hide_window { + command.show(false); + } + command.status()? + } + _ => { + let mut command = StdCommand::new(program); + if hide_window { + // 0x08000000 is CREATE_NO_WINDOW, the direct counterpart of runas's show(false). + command.creation_flags(0x08000000); + } + command.status()? + } }; + Ok(status) +} +fn uninstaller_tool_path(file_name: &str, packaged_path: impl FnOnce() -> Result) -> Result { + let uninstall_path = packaged_service_tool_path(file_name, packaged_path)?; + if !uninstall_path.exists() { + bail!(format!("uninstaller not found: {uninstall_path:?}")); + } + Ok(uninstall_path) +} + +fn ensure_uninstall_success(status: &std::process::ExitStatus) -> Result<()> { if !status.success() { bail!( "failed to uninstall service with status {}", status.code().unwrap_or(-1) ); } - Ok(()) } +#[cfg(target_os = "windows")] +fn uninstall_service() -> Result<()> { + logging!(info, Type::Service, "uninstall service"); + + let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall.exe", || { + Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall.exe")) + })?; + + let status = run_maybe_elevated(&uninstall_path, true)?; + ensure_uninstall_success(&status) +} + #[cfg(target_os = "linux")] fn uninstall_service() -> Result<()> { logging!(info, Type::Service, "uninstall service"); - let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || { + let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || { Ok(tauri::utils::platform::current_exe()?.with_file_name("clash-verge-service-uninstall")) })?; - if !uninstall_path.exists() { - bail!(format!("uninstaller not found: {uninstall_path:?}")); - } - let elevator = crate::utils::help::linux_elevator(); let status = if linux_running_as_root() { StdCommand::new(&uninstall_path).status()? @@ -666,14 +687,7 @@ fn uninstall_service() -> Result<()> { status.code().unwrap_or(-1) ); - if !status.success() { - bail!( - "failed to uninstall service with status {}", - status.code().unwrap_or(-1) - ); - } - - Ok(()) + ensure_uninstall_success(&status) } #[cfg(target_os = "linux")] @@ -688,14 +702,10 @@ fn linux_running_as_root() -> bool { fn uninstall_service() -> Result<()> { logging!(info, Type::Service, "uninstall service"); - let uninstall_path = packaged_service_tool_path("clash-verge-service-uninstall", || { + let uninstall_path = uninstaller_tool_path("clash-verge-service-uninstall", || { Ok(dirs::service_path()?.with_file_name("clash-verge-service-uninstall")) })?; - if !uninstall_path.exists() { - bail!(format!("uninstaller not found: {uninstall_path:?}")); - } - let uninstall_path = macos_service_tool_path(&uninstall_path)?; let uninstall_shell: String = uninstall_path.to_string_lossy().into_owned(); @@ -711,14 +721,7 @@ fn uninstall_service() -> Result<()> { let status = StdCommand::new("osascript").args(vec!["-e", &command]).status()?; - if !status.success() { - bail!( - "failed to uninstall service with status {}", - status.code().unwrap_or(-1) - ); - } - - Ok(()) + ensure_uninstall_success(&status) } fn install_service() -> Result<()> { diff --git a/src-tauri/src/core/win_uwp.rs b/src-tauri/src/core/win_uwp.rs index 3db9a1a44..47a186a0a 100644 --- a/src-tauri/src/core/win_uwp.rs +++ b/src-tauri/src/core/win_uwp.rs @@ -1,10 +1,7 @@ #![cfg(target_os = "windows")] -use crate::utils::dirs; +use crate::{core::service, utils::dirs}; use anyhow::{Result, bail}; -use deelevate::{PrivilegeLevel, Token}; -use runas::Command as RunasCommand; -use std::process::Command as StdCommand; pub fn invoke_uwptools() -> Result<()> { let resource_dir = dirs::app_resources_dir()?; @@ -14,13 +11,7 @@ pub fn invoke_uwptools() -> Result<()> { bail!("enableLoopback exe not found"); } - let token = Token::with_current_process()?; - let level = token.privilege_level()?; - - match level { - PrivilegeLevel::NotPrivileged => RunasCommand::new(tool_path).status()?, - _ => StdCommand::new(tool_path).status()?, - }; + service::run_maybe_elevated(&tool_path, false)?; Ok(()) } From e5b691b47a383e7753231066b7975f39dc1845b4 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:26:18 +0800 Subject: [PATCH 15/51] refactor(enhance): fold lowercase helpers into single owned function use_lowercase_owned disappears; the surviving use_lowercase takes the owned Mapping and borrowed callers clone at the call site. --- src-tauri/src/enhance/field.rs | 6 +----- src-tauri/src/enhance/merge.rs | 2 +- src-tauri/src/enhance/mod.rs | 4 ++-- src-tauri/src/enhance/script.rs | 8 ++++---- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src-tauri/src/enhance/field.rs b/src-tauri/src/enhance/field.rs index fe9537f03..1b1567fed 100644 --- a/src-tauri/src/enhance/field.rs +++ b/src-tauri/src/enhance/field.rs @@ -24,11 +24,7 @@ fn lowercase_key(key: &str) -> Value { Value::from(key.as_str()) } -pub fn use_lowercase(config: &Mapping) -> Mapping { - use_lowercase_owned(config.clone()) -} - -pub fn use_lowercase_owned(config: Mapping) -> Mapping { +pub fn use_lowercase(config: Mapping) -> Mapping { let mut lowercased = Mapping::new(); for (key, value) in config { diff --git a/src-tauri/src/enhance/merge.rs b/src-tauri/src/enhance/merge.rs index c1c00fc05..f2ba0c56a 100644 --- a/src-tauri/src/enhance/merge.rs +++ b/src-tauri/src/enhance/merge.rs @@ -17,7 +17,7 @@ fn deep_merge(a: &mut Value, b: Value) { } pub fn use_merge(merge: &Mapping, mut config: Mapping) -> Mapping { - for (key, value) in use_lowercase(merge) { + for (key, value) in use_lowercase(merge.clone()) { match (config.get_mut(&key), value) { (Some(Value::Mapping(existing)), Value::Mapping(overlay)) if key.as_str() == Some("dns") => { existing.extend(overlay); diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 045a4f545..e4d44d6a2 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -8,7 +8,7 @@ mod tun; use self::{ chain::{AsyncChainItemFrom as _, ChainItem, ChainType}, - field::{use_keys, use_lowercase_owned, use_sort}, + field::{use_keys, use_lowercase, use_sort}, merge::use_merge, script::use_script, seq::{SeqMap, use_seq}, @@ -614,7 +614,7 @@ fn apply_builtin_scripts(mut config: Mapping, clash_core: Option, enable .collect(); if !items.is_empty() { // The JS path saw a lowercased view; lowercase once, only when a builtin runs. - config = use_lowercase_owned(config); + config = use_lowercase(config); for item in items { logging!(debug, Type::Core, "run builtin script {}", item.uid); config = match item.uid.as_str() { diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs index 2aaee1121..33ea4f804 100644 --- a/src-tauri/src/enhance/script.rs +++ b/src-tauri/src/enhance/script.rs @@ -1,6 +1,6 @@ use crate::{process::AsyncHandler, utils::tmpl}; -use super::field::{use_lowercase, use_lowercase_owned}; +use super::field::use_lowercase; use anyhow::{Error, Result}; use boa_engine::{Context, JsString, JsValue, Source, js_string, native_function::NativeFunction, property::Attribute}; use clash_verge_logging::{Type, logging}; @@ -22,10 +22,10 @@ type EvalOutcome = (Result, Vec<(String, String)>) pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult { // The template is identity-plus-lowercasing; skip the Boa round-trip. if script == tmpl::ITEM_SCRIPT { - return (use_lowercase_owned(config), vec![], vec![]); + return (use_lowercase(config), vec![], vec![]); } - let json = match serde_json::to_string(&use_lowercase(&config)) { + let json = match serde_json::to_string(&use_lowercase(config.clone())) { Ok(json) if json.len() <= MAX_JSON_SIZE => json, Ok(_) => { let message = "Configuration size exceeds maximum allowed size"; @@ -173,7 +173,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome { } match parse_json_safely(&result) { - Ok(config) => (Ok(use_lowercase_owned(config)), outputs.lock().to_vec()), + Ok(config) => (Ok(use_lowercase(config)), outputs.lock().to_vec()), Err(err) => { logging!( error, From 93f70f4c3689f6800e969a2c1de69b9d842e4416 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:26:18 +0800 Subject: [PATCH 16/51] refactor(enhance): rename ChainSupport variants to Mihomo names Clash is a legacy name; the variants match the verge-mihomo and verge-mihomo-alpha cores they gate. --- src-tauri/src/constants.rs | 1 + src-tauri/src/enhance/chain.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index 20b461f31..b241ff220 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -16,6 +16,7 @@ pub mod network { pub mod proxy { /// Single source of the macOS default bypass list; both proxy backends compose from it. + #[cfg(target_os = "macos")] pub const MACOS_DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,"; } diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index ac2fe27ee..6c627075d 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -24,8 +24,8 @@ pub enum ChainType { #[derive(Debug, Clone)] pub enum ChainSupport { - ClashMeta, - ClashMetaAlpha, + Mihomo, + MihomoAlpha, } // Helper trait to allow async conversion @@ -74,10 +74,10 @@ impl ChainItem { let meta_guard = Self::to_script("verge_meta_guard", ""); vec![ - (ChainSupport::ClashMeta, hy_alpn.clone()), - (ChainSupport::ClashMeta, meta_guard.clone()), - (ChainSupport::ClashMetaAlpha, hy_alpn), - (ChainSupport::ClashMetaAlpha, meta_guard), + (ChainSupport::Mihomo, hy_alpn.clone()), + (ChainSupport::Mihomo, meta_guard.clone()), + (ChainSupport::MihomoAlpha, hy_alpn), + (ChainSupport::MihomoAlpha, meta_guard), ] } @@ -94,7 +94,7 @@ impl ChainSupport { match core { Some(core) => matches!( (self, core.as_str()), - (Self::ClashMeta, "verge-mihomo") | (Self::ClashMetaAlpha, "verge-mihomo-alpha") + (Self::Mihomo, "verge-mihomo") | (Self::MihomoAlpha, "verge-mihomo-alpha") ), None => true, } From 1a5094caf07763bb70e4fd878a662adb1d13a5fd Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:08:06 +0800 Subject: [PATCH 17/51] refactor(enhance): lowercase takes a reference and chain support is copy use_lowercase borrows the mapping and clones only the values; call sites no longer clone whole configs. ChainSupport is a fieldless enum and now Copy; builtin() registers each support pair without clones. --- src-tauri/src/enhance/chain.rs | 13 +++++-------- src-tauri/src/enhance/field.rs | 6 +++--- src-tauri/src/enhance/merge.rs | 2 +- src-tauri/src/enhance/mod.rs | 2 +- src-tauri/src/enhance/script.rs | 6 +++--- 5 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index 6c627075d..38c2b780b 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -22,7 +22,7 @@ pub enum ChainType { Groups(SeqMap), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum ChainSupport { Mihomo, MihomoAlpha, @@ -70,14 +70,11 @@ impl ChainItem { /// 内建支持一些脚本 pub fn builtin() -> Vec<(ChainSupport, Self)> { // data intentionally empty: the enhance loop dispatches on uid to native implementations - let hy_alpn = Self::to_script("verge_hy_alpn", ""); - let meta_guard = Self::to_script("verge_meta_guard", ""); - vec![ - (ChainSupport::Mihomo, hy_alpn.clone()), - (ChainSupport::Mihomo, meta_guard.clone()), - (ChainSupport::MihomoAlpha, hy_alpn), - (ChainSupport::MihomoAlpha, meta_guard), + (ChainSupport::Mihomo, Self::to_script("verge_hy_alpn", "")), + (ChainSupport::Mihomo, Self::to_script("verge_meta_guard", "")), + (ChainSupport::MihomoAlpha, Self::to_script("verge_hy_alpn", "")), + (ChainSupport::MihomoAlpha, Self::to_script("verge_meta_guard", "")), ] } diff --git a/src-tauri/src/enhance/field.rs b/src-tauri/src/enhance/field.rs index 1b1567fed..038980f55 100644 --- a/src-tauri/src/enhance/field.rs +++ b/src-tauri/src/enhance/field.rs @@ -24,12 +24,12 @@ fn lowercase_key(key: &str) -> Value { Value::from(key.as_str()) } -pub fn use_lowercase(config: Mapping) -> Mapping { +pub fn use_lowercase(config: &Mapping) -> Mapping { let mut lowercased = Mapping::new(); - for (key, value) in config { + for (key, value) in config.iter() { if let Some(key_str) = key.as_str() { - lowercased.insert(lowercase_key(key_str), value); + lowercased.insert(lowercase_key(key_str), value.clone()); } } lowercased diff --git a/src-tauri/src/enhance/merge.rs b/src-tauri/src/enhance/merge.rs index f2ba0c56a..c1c00fc05 100644 --- a/src-tauri/src/enhance/merge.rs +++ b/src-tauri/src/enhance/merge.rs @@ -17,7 +17,7 @@ fn deep_merge(a: &mut Value, b: Value) { } pub fn use_merge(merge: &Mapping, mut config: Mapping) -> Mapping { - for (key, value) in use_lowercase(merge.clone()) { + for (key, value) in use_lowercase(merge) { match (config.get_mut(&key), value) { (Some(Value::Mapping(existing)), Value::Mapping(overlay)) if key.as_str() == Some("dns") => { existing.extend(overlay); diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index e4d44d6a2..09f9e3635 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -614,7 +614,7 @@ fn apply_builtin_scripts(mut config: Mapping, clash_core: Option, enable .collect(); if !items.is_empty() { // The JS path saw a lowercased view; lowercase once, only when a builtin runs. - config = use_lowercase(config); + config = use_lowercase(&config); for item in items { logging!(debug, Type::Core, "run builtin script {}", item.uid); config = match item.uid.as_str() { diff --git a/src-tauri/src/enhance/script.rs b/src-tauri/src/enhance/script.rs index 33ea4f804..b69ee7494 100644 --- a/src-tauri/src/enhance/script.rs +++ b/src-tauri/src/enhance/script.rs @@ -22,10 +22,10 @@ type EvalOutcome = (Result, Vec<(String, String)>) pub(super) async fn use_script(script: String, config: Mapping, name: String) -> ScriptResult { // The template is identity-plus-lowercasing; skip the Boa round-trip. if script == tmpl::ITEM_SCRIPT { - return (use_lowercase(config), vec![], vec![]); + return (use_lowercase(&config), vec![], vec![]); } - let json = match serde_json::to_string(&use_lowercase(config.clone())) { + let json = match serde_json::to_string(&use_lowercase(&config)) { Ok(json) if json.len() <= MAX_JSON_SIZE => json, Ok(_) => { let message = "Configuration size exceeds maximum allowed size"; @@ -173,7 +173,7 @@ fn eval_script(script: &str, config_json: &str, name: &String) -> EvalOutcome { } match parse_json_safely(&result) { - Ok(config) => (Ok(use_lowercase(config)), outputs.lock().to_vec()), + Ok(config) => (Ok(use_lowercase(&config)), outputs.lock().to_vec()), Err(err) => { logging!( error, From a5d0b8dacbf82bdcbb592159821f6897827f346e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:08:07 +0800 Subject: [PATCH 18/51] refactor(config): name prfitem constructor for what it builds new_seq_item states that a fresh item is built from a template; the from_ prefix wrongly implied an existing source. append_companion_item returns the uid directly: append_item bails on a missing uid, so the Option was always Some on the Ok path. --- src-tauri/src/config/prfitem.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index 23278dcfc..eafb29cf9 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -182,9 +182,11 @@ impl PrfItem { } } - async fn append_companion_item(item: &mut Self) -> Result> { + async fn append_companion_item(item: &mut Self) -> Result { profiles::profiles_append_item_safe(item).await?; - Ok(item.uid.clone()) + item.uid + .clone() + .ok_or_else(|| anyhow::anyhow!("appended companion item has no uid")) } async fn ensure_companion_items( @@ -195,19 +197,19 @@ impl PrfItem { groups: &mut Option, ) -> Result<()> { if merge.is_none() { - *merge = Self::append_companion_item(&mut Self::from_merge(None)).await?; + *merge = Some(Self::append_companion_item(&mut Self::from_merge(None)).await?); } if script.is_none() { - *script = Self::append_companion_item(&mut Self::from_script(None)).await?; + *script = Some(Self::append_companion_item(&mut Self::from_script(None)).await?); } if rules.is_none() { - *rules = Self::append_companion_item(&mut Self::from_rules()).await?; + *rules = Some(Self::append_companion_item(&mut Self::from_rules()).await?); } if proxies.is_none() { - *proxies = Self::append_companion_item(&mut Self::from_proxies()).await?; + *proxies = Some(Self::append_companion_item(&mut Self::from_proxies()).await?); } if groups.is_none() { - *groups = Self::append_companion_item(&mut Self::from_groups()).await?; + *groups = Some(Self::append_companion_item(&mut Self::from_groups()).await?); } Ok(()) } @@ -444,7 +446,7 @@ impl PrfItem { } } - fn from_seq_item(prefix: &str, itype: &str, template: &str) -> Self { + fn new_seq_item(prefix: &str, itype: &str, template: &str) -> Self { let uid = help::get_uid(prefix).into(); let file = format!("{uid}.yaml").into(); // yaml ext @@ -459,15 +461,15 @@ impl PrfItem { } fn from_rules() -> Self { - Self::from_seq_item("r", "rules", tmpl::ITEM_RULES) + Self::new_seq_item("r", "rules", tmpl::ITEM_RULES) } fn from_proxies() -> Self { - Self::from_seq_item("p", "proxies", tmpl::ITEM_PROXIES) + Self::new_seq_item("p", "proxies", tmpl::ITEM_PROXIES) } fn from_groups() -> Self { - Self::from_seq_item("g", "groups", tmpl::ITEM_GROUPS) + Self::new_seq_item("g", "groups", tmpl::ITEM_GROUPS) } pub(crate) async fn read_file(&self) -> Result { From 1313692b3d6e0e0c2301aadce8d5c5c0e8cc585c Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 00:08:07 +0800 Subject: [PATCH 19/51] refactor(sysopt): name the proxy state lock for its full role The lock now owns the (Sysproxy, Autoproxy) state, so proxy_state_lock says what it guards; the guard locals follow. constants::proxy held a single sysproxy constant and is renamed sysproxy. --- src-tauri/src/constants.rs | 2 +- src-tauri/src/core/sysopt.rs | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/constants.rs b/src-tauri/src/constants.rs index b241ff220..3f85ced31 100644 --- a/src-tauri/src/constants.rs +++ b/src-tauri/src/constants.rs @@ -14,7 +14,7 @@ pub mod network { } } -pub mod proxy { +pub mod sysproxy { /// Single source of the macOS default bypass list; both proxy backends compose from it. #[cfg(target_os = "macos")] pub const MACOS_DEFAULT_BYPASS: &str = diff --git a/src-tauri/src/core/sysopt.rs b/src-tauri/src/core/sysopt.rs index c503ab86c..c32b4ac15 100644 --- a/src-tauri/src/core/sysopt.rs +++ b/src-tauri/src/core/sysopt.rs @@ -242,7 +242,7 @@ async fn current_os_proxy_state(sys: Sysproxy, auto: Autoproxy) -> OsProxyState pub(crate) struct Sysopt { // Owns the proxy target pair; every reader and writer goes through this lock. - update_lock: TokioMutex<(Sysproxy, Autoproxy)>, + proxy_state_lock: TokioMutex<(Sysproxy, Autoproxy)>, guard_operation_lock: TokioMutex<()>, reset_sysproxy: AtomicBool, guard: Arc>, @@ -251,7 +251,7 @@ pub(crate) struct Sysopt { impl Default for Sysopt { fn default() -> Self { Self { - update_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())), + proxy_state_lock: TokioMutex::new((Sysproxy::default(), Autoproxy::default())), guard_operation_lock: TokioMutex::new(()), reset_sysproxy: AtomicBool::new(false), guard: Arc::new(RwLock::new(GuardMonitor::new(GuardType::None, Duration::from_secs(30)))), @@ -268,7 +268,7 @@ static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172 #[cfg(any(target_os = "linux", target_os = "macos"))] static BYPASS_SEPARATOR: &str = ","; #[cfg(target_os = "macos")] -static DEFAULT_BYPASS: &str = crate::constants::proxy::MACOS_DEFAULT_BYPASS; +static DEFAULT_BYPASS: &str = crate::constants::sysproxy::MACOS_DEFAULT_BYPASS; pub(super) fn format_bypass(use_default: bool, custom_bypass: &str) -> String { if custom_bypass.is_empty() { @@ -407,16 +407,16 @@ impl Sysopt { /// configuration instead of a partially-applied one (e.g. SOCKS already /// disabled but HTTP still enabled mid-transition). pub(crate) async fn wait_idle(&self) { - let _ = self.update_lock.lock().await; + let _ = self.proxy_state_lock.lock().await; } /// init the sysproxy #[allow( clippy::significant_drop_tightening, - reason = "the update lock must stay held until the OS write finishes; wait_idle relies on it" + reason = "the proxy state lock must stay held until the OS write finishes; wait_idle relies on it" )] pub(super) async fn update_sysproxy(&self) -> Result<()> { - let mut inner_proxy = self.update_lock.lock().await; + let mut proxy_state = self.proxy_state_lock.lock().await; let verge = Config::verge().await.latest_arc(); // Configured, not live: this runs while the Core is being started or restarted, and // asking a Core that is not up yet would only fall back here anyway. @@ -432,7 +432,7 @@ impl Sysopt { ); let (sys, auto, guard_type) = { - let (sys, auto) = &mut *inner_proxy; + let (sys, auto) = &mut *proxy_state; sys.host = proxy_host.into(); sys.port = port; sys.bypass = bypass.into(); @@ -531,7 +531,7 @@ impl Sysopt { /// reset the sysproxy #[allow( clippy::significant_drop_tightening, - reason = "the update lock must stay held until the disable finishes; wait_idle relies on it" + reason = "the proxy state lock must stay held until the disable finishes; wait_idle relies on it" )] pub(super) async fn reset_sysproxy(&self) -> Result<()> { if self @@ -544,13 +544,13 @@ impl Sysopt { defer! { self.reset_sysproxy.store(false, Ordering::SeqCst); } - let mut inner_proxy = self.update_lock.lock().await; + let mut proxy_state = self.proxy_state_lock.lock().await; let _guard_operation = self.guard_operation_lock.lock().await; let drained = self.stop_proxy_guard_locked().await; // 直接关闭所有代理 let (sys, auto) = { - let (sys, auto) = &mut *inner_proxy; + let (sys, auto) = &mut *proxy_state; sys.enable = false; auto.enable = false; (sys.clone(), auto.clone()) From ab0e1b957aeb2ac8b48f15cacd507904119df3fa Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:31:20 +0800 Subject: [PATCH 20/51] refactor: replace remaining clash meta naming with mihomo Doc comment, generated bindings and the prebuild sidecar helpers stop referring to the legacy Clash Meta name; installer legacy-process cleanup keeps the old executable names on purpose. --- scripts/prebuild.mjs | 13 ++++++------- src-tauri/src/config/verge.rs | 2 +- src/services/bindings.ts | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/scripts/prebuild.mjs b/scripts/prebuild.mjs index 22dd96032..0230668ab 100644 --- a/scripts/prebuild.mjs +++ b/scripts/prebuild.mjs @@ -266,13 +266,13 @@ async function getLatestReleaseVersion() { } if (!META_MAP[`${platform}-${arch}`]) { - throw new Error(`clash meta unsupported platform "${platform}-${arch}"`) + throw new Error(`mihomo unsupported platform "${platform}-${arch}"`) } if (!META_ALPHA_MAP[`${platform}-${arch}`]) { - throw new Error(`clash meta alpha unsupported platform "${platform}-${arch}"`) + throw new Error(`mihomo alpha unsupported platform "${platform}-${arch}"`) } -function clashMetaAlpha() { +function mihomoAlpha() { const name = META_ALPHA_MAP[`${platform}-${arch}`] const isWin = platform === 'win32' const urlExt = isWin ? 'zip' : 'gz' @@ -285,7 +285,7 @@ function clashMetaAlpha() { } } -function clashMeta() { +function mihomo() { const name = META_MAP[`${platform}-${arch}`] const isWin = platform === 'win32' const urlExt = isWin ? 'zip' : 'gz' @@ -733,13 +733,12 @@ const tasks = [ { name: 'verge-mihomo-alpha', func: () => - getLatestAlphaVersion().then(() => resolveSidecar(clashMetaAlpha())), + getLatestAlphaVersion().then(() => resolveSidecar(mihomoAlpha())), retry: 5, }, { name: 'verge-mihomo', - func: () => - getLatestReleaseVersion().then(() => resolveSidecar(clashMeta())), + func: () => getLatestReleaseVersion().then(() => resolveSidecar(mihomo())), retry: 5, }, // After both sidecar tasks: it hashes what they downloaded. diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index ab7962ee8..d3b58dcf0 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -39,7 +39,7 @@ pub struct IVerge { pub traffic_graph: Option, - /// show memory info (only for Clash Meta) + /// show memory info (only for Mihomo) pub enable_memory_usage: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/src/services/bindings.ts b/src/services/bindings.ts index 4a1623bdc..073da075a 100644 --- a/src/services/bindings.ts +++ b/src/services/bindings.ts @@ -365,7 +365,7 @@ export type IVerge_Deserialize = { start_page?: string | null startup_script?: string | null traffic_graph?: boolean | null - /** show memory info (only for Clash Meta) */ + /** show memory info (only for Mihomo) */ enable_memory_usage?: boolean | null enable_group_icon?: boolean | null pause_render_traffic_stats_on_blur?: boolean | null @@ -455,7 +455,7 @@ export type IVerge_Serialize = { start_page: string | null startup_script: string | null traffic_graph: boolean | null - /** show memory info (only for Clash Meta) */ + /** show memory info (only for Mihomo) */ enable_memory_usage: boolean | null enable_group_icon?: boolean | null pause_render_traffic_stats_on_blur: boolean | null From 238d4de328879e4806c6eff802859b96bc727a43 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:04:31 +0800 Subject: [PATCH 21/51] refactor(enhance): drop builtin core filtering that never discriminates Both valid cores matched a different ChainSupport variant, yet each variant registered the same uid pair, so the filtered builtin list was identical for stable and alpha. Invalid cores are coerced at startup (validate_and_fix_config / get_valid_clash_core) and the None arm of is_support was unreachable. builtin() now returns the items directly and the clash_core threading through ConfigValues goes away; re-add core discrimination when a builtin actually differs per core. --- src-tauri/src/enhance/chain.rs | 26 +++---------------------- src-tauri/src/enhance/mod.rs | 35 ++++++++++++---------------------- 2 files changed, 15 insertions(+), 46 deletions(-) diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index 38c2b780b..27750d4d7 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -22,12 +22,6 @@ pub enum ChainType { Groups(SeqMap), } -#[derive(Debug, Clone, Copy)] -pub enum ChainSupport { - Mihomo, - MihomoAlpha, -} - // Helper trait to allow async conversion pub trait AsyncChainItemFrom { async fn from_async(item: &PrfItem) -> Option; @@ -68,13 +62,11 @@ impl AsyncChainItemFrom for Option { } impl ChainItem { /// 内建支持一些脚本 - pub fn builtin() -> Vec<(ChainSupport, Self)> { + pub fn builtin() -> Vec { // data intentionally empty: the enhance loop dispatches on uid to native implementations vec![ - (ChainSupport::Mihomo, Self::to_script("verge_hy_alpn", "")), - (ChainSupport::Mihomo, Self::to_script("verge_meta_guard", "")), - (ChainSupport::MihomoAlpha, Self::to_script("verge_hy_alpn", "")), - (ChainSupport::MihomoAlpha, Self::to_script("verge_meta_guard", "")), + Self::to_script("verge_hy_alpn", ""), + Self::to_script("verge_meta_guard", ""), ] } @@ -85,15 +77,3 @@ impl ChainItem { } } } - -impl ChainSupport { - pub fn is_support(&self, core: Option<&String>) -> bool { - match core { - Some(core) => matches!( - (self, core.as_str()), - (Self::Mihomo, "verge-mihomo") | (Self::MihomoAlpha, "verge-mihomo-alpha") - ), - None => true, - } - } -} diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 09f9e3635..38385d752 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -34,7 +34,6 @@ type ResultLog = Vec<(String, String)>; #[derive(Debug)] struct ConfigValues { clash_config: Mapping, - clash_core: Option, enable_tun: bool, enable_builtin: bool, socks_enabled: bool, @@ -128,8 +127,7 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues { let dns_settings = verge_arc.dns_settings_for(profile_uid); let dns_override_confirmation = dns_settings.confirmation; - let (clash_core, enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( - Some(verge_arc.get_valid_clash_core()), + let (enable_tun, enable_builtin, socks_enabled, http_enabled, enable_dns_settings) = ( enable_tun_mode.unwrap_or(false), enable_builtin_enhanced.unwrap_or(true), verge_socks_enabled.unwrap_or(false), @@ -148,7 +146,6 @@ async fn get_config_values(profile_uid: &str) -> ConfigValues { ConfigValues { clash_config, - clash_core, enable_tun, enable_builtin, socks_enabled, @@ -605,24 +602,17 @@ fn merge_default_config( config } -fn apply_builtin_scripts(mut config: Mapping, clash_core: Option, enable_builtin: bool) -> Mapping { +fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping { if enable_builtin { - let items: Vec<_> = ChainItem::builtin() - .into_iter() - .filter(|(s, _)| s.is_support(clash_core.as_ref())) - .map(|(_, c)| c) - .collect(); - if !items.is_empty() { - // The JS path saw a lowercased view; lowercase once, only when a builtin runs. - config = use_lowercase(&config); - for item in items { - logging!(debug, Type::Core, "run builtin script {}", item.uid); - config = match item.uid.as_str() { - "verge_hy_alpn" => builtin_hy_alpn(config), - "verge_meta_guard" => builtin_meta_guard(config), - _ => config, - }; - } + // The JS path saw a lowercased view; lowercase once, only when a builtin runs. + config = use_lowercase(&config); + for item in ChainItem::builtin() { + logging!(debug, Type::Core, "run builtin script {}", item.uid); + config = match item.uid.as_str() { + "verge_hy_alpn" => builtin_hy_alpn(config), + "verge_meta_guard" => builtin_meta_guard(config), + _ => config, + }; } } @@ -829,7 +819,6 @@ pub async fn enhance( let cfg_vals = get_config_values(profile_uid).await; let ConfigValues { clash_config, - clash_core, enable_tun, enable_builtin, socks_enabled, @@ -879,7 +868,7 @@ pub async fn enhance( tproxy_enabled, ); - let config = apply_builtin_scripts(config, clash_core, enable_builtin); + let config = apply_builtin_scripts(config, enable_builtin); let config = use_tun(config, enable_tun); let (config, dns_settings) = apply_dns_settings(config, enable_dns_settings).await; From f489c7afa66eae8cb7dd524d8f8c88e3b1f6b6ad Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:31:06 +0800 Subject: [PATCH 22/51] refactor(enhance): drop unreachable mode script guard merge_default_config overwrites the profile's mode with the verge-owned clash config, whose mode is always present (template seeds rule and patch semantics never clear it) and restricted to rule/global/direct; change_clash_mode is rejected by the core before persisting anything else. mode:script can therefore only arrive through a hand-crafted patch_clash call, and for that path an explicit invalid-mode validation error is more honest than silently rewriting to rule. mihomo rejects mode:script outright (invalid mode), and user merge/script chains run after the builtins, so the guard never covered them anyway. --- src-tauri/src/enhance/chain.rs | 5 +---- src-tauri/src/enhance/mod.rs | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index 27750d4d7..ce85e0e1f 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -64,10 +64,7 @@ impl ChainItem { /// 内建支持一些脚本 pub fn builtin() -> Vec { // data intentionally empty: the enhance loop dispatches on uid to native implementations - vec![ - Self::to_script("verge_hy_alpn", ""), - Self::to_script("verge_meta_guard", ""), - ] + vec![Self::to_script("verge_hy_alpn", "")] } pub fn to_script, D: Into>(uid: U, data: D) -> Self { diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index 38385d752..d153e747c 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -610,7 +610,6 @@ fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping { logging!(debug, Type::Core, "run builtin script {}", item.uid); config = match item.uid.as_str() { "verge_hy_alpn" => builtin_hy_alpn(config), - "verge_meta_guard" => builtin_meta_guard(config), _ => config, }; } @@ -633,14 +632,6 @@ fn builtin_hy_alpn(mut config: Mapping) -> Mapping { config } -fn builtin_meta_guard(mut config: Mapping) -> Mapping { - if config.get("mode").and_then(Value::as_str) == Some("script") { - config.insert(Value::from("mode"), Value::from("rule")); - } - - config -} - fn cleanup_proxy_groups(mut config: Mapping) -> Mapping { // built-in proxies docs: https://wiki.metacubex.one/config/proxies/built-in const BUILTIN_POLICIES: &[&str] = &["DIRECT", "REJECT", "REJECT-DROP", "PASS", "PASS-RULE"]; From acef5759355e5938b572db9ee099e575268a0755 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:38:31 +0800 Subject: [PATCH 23/51] refactor(enhance): call the hysteria alpn builtin directly builtin() existed to feed the uid-dispatch loop; with a single builtin the vec and match are ceremony. apply_builtin_scripts calls builtin_hy_alpn directly and to_script goes away; tests construct ChainItem literals the way the merge items already did. --- src-tauri/src/enhance/chain.rs | 14 ---------- src-tauri/src/enhance/mod.rs | 47 +++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src-tauri/src/enhance/chain.rs b/src-tauri/src/enhance/chain.rs index ce85e0e1f..df25c712f 100644 --- a/src-tauri/src/enhance/chain.rs +++ b/src-tauri/src/enhance/chain.rs @@ -60,17 +60,3 @@ impl AsyncChainItemFrom for Option { } } } -impl ChainItem { - /// 内建支持一些脚本 - pub fn builtin() -> Vec { - // data intentionally empty: the enhance loop dispatches on uid to native implementations - vec![Self::to_script("verge_hy_alpn", "")] - } - - pub fn to_script, D: Into>(uid: U, data: D) -> Self { - Self { - uid: uid.into(), - data: ChainType::Script(data.into()), - } - } -} diff --git a/src-tauri/src/enhance/mod.rs b/src-tauri/src/enhance/mod.rs index d153e747c..6fab9c2be 100644 --- a/src-tauri/src/enhance/mod.rs +++ b/src-tauri/src/enhance/mod.rs @@ -604,15 +604,10 @@ fn merge_default_config( fn apply_builtin_scripts(mut config: Mapping, enable_builtin: bool) -> Mapping { if enable_builtin { - // The JS path saw a lowercased view; lowercase once, only when a builtin runs. + // The JS path saw a lowercased view; lowercase once, only when the builtin runs. config = use_lowercase(&config); - for item in ChainItem::builtin() { - logging!(debug, Type::Core, "run builtin script {}", item.uid); - config = match item.uid.as_str() { - "verge_hy_alpn" => builtin_hy_alpn(config), - _ => config, - }; - } + logging!(debug, Type::Core, "run builtin script verge_hy_alpn"); + config = builtin_hy_alpn(config); } config @@ -1411,7 +1406,10 @@ mod tests { vec![], HashMap::new(), merge_item("global-merge"), - ChainItem::to_script("global-script", script), + ChainItem { + uid: "global-script".into(), + data: ChainType::Script(script.into()), + }, &profile_name, &authoritative, ) @@ -1421,7 +1419,10 @@ mod tests { keys, logs, merge_item("profile-merge"), - ChainItem::to_script("profile-script", script), + ChainItem { + uid: "profile-script".into(), + data: ChainType::Script(script.into()), + }, &profile_name, &authoritative, ) @@ -1466,16 +1467,19 @@ mod tests { nested: {winner: global-merge}, dns: {enable: false}, tun: {enable: false}}", )), }; - let global_script = ChainItem::to_script( - "Script", - r#"function main(config) { + let global_script = ChainItem { + uid: "Script".into(), + data: ChainType::Script( + r#"function main(config) { config["global-script-wins"] = "global-script"; config["profile-merge-wins"] = "global-script"; config["profile-script-wins"] = "global-script"; config.nested.winner = "global-script"; return config; - }"#, - ); + }"# + .into(), + ), + }; let profile_merge = ChainItem { uid: "profile-merge".into(), data: ChainType::Merge(mapping( @@ -1483,14 +1487,17 @@ mod tests { nested: {winner: profile-merge}}", )), }; - let profile_script = ChainItem::to_script( - "profile-script", - r#"function main(config) { + let profile_script = ChainItem { + uid: "profile-script".into(), + data: ChainType::Script( + r#"function main(config) { config["profile-script-wins"] = "profile-script"; config.nested.winner = "profile-script"; return config; - }"#, - ); + }"# + .into(), + ), + }; let profile_name = "test-profile".into(); let authoritative = AuthoritativeFields::capture(&config, &[], Mapping::new()); From 34607dce551787d72d17cb130681181ebb4133af Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:17:15 +0800 Subject: [PATCH 24/51] fix(backup): serialize restore with config write lock Acquire the configuration write lock at the entry of finalize_restored_verge_config so the clash, profiles and verge draft applies all happen under it, instead of only the verge layer. A concurrent patch (patch_clash / patch_verge via try_lock_config_write) could otherwise stage a draft that the unlocked restore applies overwrite, or interleave its persistence with the restore. Also drop the pointless clones in the clash and profiles edit_draft closures; edit_draft takes FnOnce so the restored values can be moved in. The lock is released before patch_verge, which re-acquires it via try_lock_config_write. --- Changelog.md | 1 + src-tauri/src/feat/backup.rs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Changelog.md b/Changelog.md index a8213f79c..d84d3a7ab 100644 --- a/Changelog.md +++ b/Changelog.md @@ -10,6 +10,7 @@ - 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新 - 修复 代理 在全局模式下无法展开节点组 - 修复 自定义 CSS 无法修改编辑器字体和字号的问题 +- 修复恢复备份的同时修改配置,可能导致配置丢失或错乱的问题 **🐧 Linux** diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 449fba642..392e23d74 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -33,6 +33,9 @@ async fn finalize_restored_verge_config( webdav_username: Option, webdav_password: Option, ) -> Result<()> { + // The lock must cover every restored layer; an unlocked apply races a concurrently staged draft. + let config_write = Config::lock_config_write().await; + // A broken restored `verge.yaml` is a restore failure, not a reason to load defaults. let mut restored = help::read_yaml::(&verge_path()?).await?; restored.webdav_url = webdav_url; @@ -55,14 +58,10 @@ async fn finalize_restored_verge_config( profiles_draft.apply(); let verge_draft = Config::verge().await; - { - // Hold the write lock so a concurrent patch cannot stage a draft the proxy write reads. - let _config_write = Config::lock_config_write().await; - verge_draft.edit_draft(|d| { - *d = restored.clone(); - }); - verge_draft.apply(); - } + verge_draft.edit_draft(|d| { + *d = restored.clone(); + }); + verge_draft.apply(); // Turn it off here; a failing core side effect below would otherwise skip this step. if !restored.enable_system_proxy.unwrap_or_default() { @@ -80,6 +79,9 @@ async fn finalize_restored_verge_config( } } + // `patch_verge` re-acquires the lock through `try_lock_config_write`. + drop(config_write); + // Run configuration side effects without rewriting the already-restored file. if let Err(err) = super::patch_verge(&restored, true).await { logging!(error, Type::Backup, "Failed to apply restored verge config: {err:#}"); From cefc0d8c62037095fe4a937eb58795b50b60e3d8 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:17:38 +0800 Subject: [PATCH 25/51] perf(backup): extract archive off the runtime Run File::open, ZipArchive::new and the extraction of the backup archive inside spawn_blocking. Extracting a multi-file backup blocked a tokio worker for hundreds of milliseconds to seconds while the runtime kept serving other work on the remaining workers. Move the PROFILE_WRITE_LOCK acquisition after the extraction, right before finalize_restored_verge_config: the lock protects the config-data transition (extracted files reloaded into drafts), not the plain filesystem writes, so profile operations no longer queue behind the blocking unzip. --- src-tauri/src/feat/backup.rs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index 392e23d74..8f81ef55c 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -148,10 +148,18 @@ async fn restore_backup_archive(path: PathBuf) -> Result<()> { ) }; - let file = AsyncHandler::spawn_blocking(move || std::fs::File::open(&path)).await??; - let mut zip = zip::ZipArchive::new(file)?; + let home_dir = app_home_dir()?; + AsyncHandler::spawn_blocking(move || -> Result<()> { + let file = std::fs::File::open(&path)?; + let mut zip = zip::ZipArchive::new(file)?; + zip.extract(&home_dir)?; + Ok(()) + }) + .await + .map_err(|join| anyhow!("backup extraction task failed: {join}"))??; + + // Held for the config-data transition only; extraction is plain filesystem work. let _profile_write = crate::config::profiles::PROFILE_WRITE_LOCK.lock().await; - zip.extract(app_home_dir()?)?; finalize_restored_verge_config(webdav_url, webdav_username, webdav_password).await } From c2bb6cc3629cafe3f3778cfe8684bb5bb5f7425a Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Wed, 28 Oct 2026 09:17:54 +0800 Subject: [PATCH 26/51] fix(clash): take config write lock when changing mode change_clash_mode committed the clash draft and saved config.yaml without claiming CONFIG_WRITE_LOCK, so a concurrent patch_clash transaction could have its staged draft committed here or its file-snapshot rollback swallow the mode change. Take lock_config_write (waiting) rather than try_lock_config_write: two of the three callers (hotkey, tray) discard the result, so fail-fast would silently drop the switch, and the lock is only attempted after the mihomo patch already applied, where an Err would roll the frontend back while the core runs the new mode. --- Changelog.md | 1 + src-tauri/src/feat/clash.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Changelog.md b/Changelog.md index d84d3a7ab..59bcb859a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -11,6 +11,7 @@ - 修复 代理 在全局模式下无法展开节点组 - 修复 自定义 CSS 无法修改编辑器字体和字号的问题 - 修复恢复备份的同时修改配置,可能导致配置丢失或错乱的问题 +- 修复切换规则模式与配置变更并发时可能丢失或错乱的问题 **🐧 Linux** diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 9b1d84b3c..853b35481 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -67,11 +67,14 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> { } let clash = Config::clash().await; - clash.edit_draft(|d| d.patch_config(&mapping)); - clash.apply(); - - let clash_data = clash.data_arc(); - if clash_data.save_config().await.is_ok() { + // Serialize with patch transactions: an unlocked commit can flush a staged draft mid-rollback. + let save_result = { + let _config_write = Config::lock_config_write().await; + clash.edit_draft(|d| d.patch_config(&mapping)); + clash.apply(); + clash.data_arc().save_config().await + }; + if save_result.is_ok() { announce(Refresh::Clash); tray::Tray::global().update_menu_and_icon().await; } From 8128227302dea0a8fe116437f96e812e56dfbf06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9D=A4=E6=98=AF=E7=BA=B1=E9=9B=BE=E9=85=B1=E5=93=9F?= =?UTF-8?q?=EF=BD=9E?= <49941141+Dragon1573@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:33:10 +0000 Subject: [PATCH 27/51] chore(deps): update dependency react-i18next to v17.1.9 (#8448) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2595c6d64..0f4633ca4 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "react-dom": "19.3.1", "react-error-boundary": "6.1.6", "react-hook-form": "^7.88.0", - "react-i18next": "17.1.8", + "react-i18next": "17.1.9", "react-markdown": "10.1.0", "react-router": "^8.4.0", "rehype-raw": "^7.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b3e45be0..3c50dab65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,8 +275,8 @@ importers: specifier: ^7.88.0 version: 7.88.0(react@19.3.1) react-i18next: - specifier: 17.1.8 - version: 17.1.8(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3) + specifier: 17.1.9 + version: 17.1.9(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3) react-markdown: specifier: 10.1.0 version: 10.1.0(@types/react@19.3.0)(react@19.3.1)(supports-color@7.2.0) @@ -4443,8 +4443,8 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-i18next@17.1.8: - resolution: {integrity: sha512-vGH+Tbhqbv5LdN9KvC9GEmQ6ATxJAjyxPEK0zSCSRlkeDqCPNxVtEOvDNRAz+51HTxc6QPwhnHaFD7ICu6pD1A==} + react-i18next@17.1.9: + resolution: {integrity: sha512-OgYqIH13XHPlWTaaI795Dz+k2AdKnXcOxNQVnf750T8Ij0TYWp1nYvXRpoJBulPqlyk2OAbn1PM4tFKiIj/byA==} peerDependencies: i18next: '>= 26.2.0' react: '>= 16.8.0' @@ -9529,7 +9529,7 @@ snapshots: dependencies: react: 19.3.1 - react-i18next@17.1.8(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3): + react-i18next@17.1.9(i18next@26.4.2(typescript@6.0.3))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 html-parse-stringify: 4.0.1 From c9a6cf240da13701d3c8f86e1614f7c05b30441b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9D=A4=E6=98=AF=E7=BA=B1=E9=9B=BE=E9=85=B1=E5=93=9F?= =?UTF-8?q?=EF=BD=9E?= <49941141+Dragon1573@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:21:49 +0000 Subject: [PATCH 28/51] chore(deps): update node.js to v26 (#8450) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/devcontainer.json | 2 +- .github/workflows/autobuild.yml | 10 +++++----- .github/workflows/contract-parity.yml | 2 +- .github/workflows/cross_check.yaml | 2 +- .github/workflows/dev.yml | 2 +- .github/workflows/frontend-check.yml | 2 +- .github/workflows/release.yml | 12 ++++++------ .github/workflows/signpath-test.yml | 2 +- .github/workflows/telegram-notify.yml | 2 +- .github/workflows/updater.yml | 4 ++-- .tool-versions | 2 +- 11 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d28cc301e..afde56823 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,7 @@ "features": { "ghcr.io/devcontainers/features/node:2": { - "version": "24.22.1", + "version": "26.13.0", "pnpmVersion": "12.15.0" }, "ghcr.io/devcontainers/features/rust:1": { diff --git a/.github/workflows/autobuild.yml b/.github/workflows/autobuild.yml index 577d7d360..683171e50 100644 --- a/.github/workflows/autobuild.yml +++ b/.github/workflows/autobuild.yml @@ -51,7 +51,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - name: Install dependencies run: pnpm install --frozen-lockfile @@ -185,7 +185,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' cache: 'pnpm' - name: Pnpm Cache @@ -282,7 +282,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' cache: 'pnpm' - name: Pnpm Cache @@ -426,7 +426,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' cache: 'pnpm' - name: Pnpm Cache @@ -524,7 +524,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6.1.0 name: Install pnpm diff --git a/.github/workflows/contract-parity.yml b/.github/workflows/contract-parity.yml index d83f437a9..50e03fdf3 100644 --- a/.github/workflows/contract-parity.yml +++ b/.github/workflows/contract-parity.yml @@ -63,7 +63,7 @@ jobs: - uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' cache: 'pnpm' - run: pnpm install --frozen-lockfile diff --git a/.github/workflows/cross_check.yaml b/.github/workflows/cross_check.yaml index 1366e0c7a..e923222bb 100644 --- a/.github/workflows/cross_check.yaml +++ b/.github/workflows/cross_check.yaml @@ -43,7 +43,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index ec8a7d320..c3256c5d4 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -103,7 +103,7 @@ jobs: if: github.event.inputs[matrix.input] == 'true' uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' cache: 'pnpm' - name: Pnpm Cache diff --git a/.github/workflows/frontend-check.yml b/.github/workflows/frontend-check.yml index d18b3993d..923a7e61c 100644 --- a/.github/workflows/frontend-check.yml +++ b/.github/workflows/frontend-check.yml @@ -36,7 +36,7 @@ jobs: - uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' cache: 'pnpm' - name: Restore pnpm cache diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e075ad35..16c8aaed4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -230,7 +230,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -331,7 +331,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - name: Install pnpm uses: pnpm/action-setup@v6 @@ -466,7 +466,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -547,7 +547,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -573,7 +573,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -638,7 +638,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.github/workflows/signpath-test.yml b/.github/workflows/signpath-test.yml index 2d2055d24..5fbe310ef 100644 --- a/.github/workflows/signpath-test.yml +++ b/.github/workflows/signpath-test.yml @@ -28,7 +28,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - name: Install pnpm uses: pnpm/action-setup@v6 diff --git a/.github/workflows/telegram-notify.yml b/.github/workflows/telegram-notify.yml index ac3e7e82d..c0a484d60 100644 --- a/.github/workflows/telegram-notify.yml +++ b/.github/workflows/telegram-notify.yml @@ -34,7 +34,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.github/workflows/updater.yml b/.github/workflows/updater.yml index f55927bf6..947e0ee9f 100644 --- a/.github/workflows/updater.yml +++ b/.github/workflows/updater.yml @@ -15,7 +15,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm @@ -39,7 +39,7 @@ jobs: - name: Install Node uses: actions/setup-node@v7 with: - node-version: '24.22.1' + node-version: '26.13.0' - uses: pnpm/action-setup@v6 name: Install pnpm diff --git a/.tool-versions b/.tool-versions index f10dc6119..651818210 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -nodejs 24.22.1 +nodejs 26.13.0 pnpm 12.15.0 rust 1.99.1 From 4b87634da99056e208f2e11623c8e7e588667407 Mon Sep 17 00:00:00 2001 From: Uddoo <19704816+Uddoo@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:04:33 +0000 Subject: [PATCH 29/51] fix(macos): restore Big Sur home rendering without Array.at (#8358) * fix(macos): restore Big Sur home rendering without Array.at Include the Array.prototype.at polyfill for the declared Safari 14 target so delay and traffic rendering can read their latest records. Fixes clash-verge-rev/clash-verge-rev#8333 AI assistance: Codex generated the change and ran validation; the implementation model identifier and effort are not exposed. Independent review used GPT-6.1-Sol at medium effort. * build(web): auto-detect modern polyfills instead of a manual list modernPolyfills: true lets @vitejs/plugin-legacy derive the polyfill set from the emitted bundle, dependencies included, so post-Safari-14 API coverage no longer depends on remembering to extend the list (#8333, and the dependency-side gaps behind #1126 and #6582). --------- Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com> --- Changelog.md | 4 ++++ vite.config.mts | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index a8213f79c..943cb6d74 100644 --- a/Changelog.md +++ b/Changelog.md @@ -11,6 +11,10 @@ - 修复 代理 在全局模式下无法展开节点组 - 修复 自定义 CSS 无法修改编辑器字体和字号的问题 +**🍎 macOS** + +- macOS:修复 Bigsur 上首页测速和流量图无法显示的问题 + **🐧 Linux** - Linux:修复 deb/rpm 安装包错误包含多余的 `frontend-contract` 可执行文件,导致下游重新打包失败的问题 diff --git a/vite.config.mts b/vite.config.mts index 6f68493b5..f8c9ee1c0 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -14,7 +14,9 @@ export default defineConfig({ legacy({ modernTargets: ['edge>=109', 'safari>=14'], renderLegacyChunks: false, - modernPolyfills: ['es.object.has-own', 'web.structured-clone'], + // Auto-detected from the emitted bundle, dependencies included, + // so post-Safari-14 APIs never depend on extending a manual list. + modernPolyfills: true, additionalModernPolyfills: [ path.resolve('./src/polyfills/matchMedia.js'), path.resolve('./src/polyfills/WeakRef.js'), From de22706f3bc1bd87764929066757706577a3445b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:16:28 +0000 Subject: [PATCH 30/51] chore(deps): update rust crate zip to v9.0.5 [security] (#8454) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 384c6330f..1e15536ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1175,7 +1175,7 @@ dependencies = [ "windows-service", "windows-sys 0.61.2", "winreg 0.56.0", - "zip 9.0.4", + "zip 9.0.5", ] [[package]] @@ -7805,7 +7805,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -10066,9 +10066,9 @@ dependencies = [ [[package]] name = "zip" -version = "9.0.4" +version = "9.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a954114b47a54df49f83d2722814e86b23d4bb1b54a1b90fb7d92b41619ddc22" +checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" dependencies = [ "aes", "bzip2", From df70146b025e06661c8364351cf8b250267ed985 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 10:37:26 +0000 Subject: [PATCH 31/51] chore(deps): update npm dependencies (#8453) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 +- pnpm-lock.yaml | 105 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index 0f4633ca4..c34f842fd 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "@eslint/js": "^10.0.1", "@tauri-apps/cli": "2.12.3", "@types/lodash-es": "^4.17.12", - "@types/node": "^24.13.6", + "@types/node": "^26.0.0", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", "@types/validator": "^13.15.10", @@ -118,7 +118,7 @@ "eslint-plugin-react-refresh": "^0.5.7", "eslint-plugin-sukka": "^10.2.0", "eslint-plugin-unused-imports": "^4.4.1", - "glob": "^13.0.6", + "glob": "^14.0.0", "globals": "^17.12.0", "https-proxy-agent": "^9.1.0", "husky": "^9.1.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c50dab65..28c774065 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -324,8 +324,8 @@ importers: specifier: ^4.17.12 version: 4.17.12 '@types/node': - specifier: ^24.13.6 - version: 24.13.6 + specifier: ^26.0.0 + version: 26.6.5 '@types/react': specifier: 19.3.0 version: 19.3.0 @@ -337,10 +337,10 @@ importers: version: 13.15.10 '@vitejs/plugin-legacy': specifier: ^8.2.3 - version: 8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) + version: 8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) '@vitejs/plugin-react': specifier: ^6.1.1 - version: 6.1.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) + version: 6.1.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) '@wdio/tauri-service': specifier: 1.5.0 version: 1.5.0(expect-webdriverio@6.1.0)(supports-color@7.2.0)(webdriverio@10.1.0(supports-color@7.2.0)(yauzl@2.10.0)) @@ -387,8 +387,8 @@ importers: specifier: ^4.4.1 version: 4.4.1(@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3))(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0)) glob: - specifier: ^13.0.6 - version: 13.0.6 + specifier: ^14.0.0 + version: 14.0.3 globals: specifier: ^17.12.0 version: 17.12.0 @@ -427,13 +427,13 @@ importers: version: 8.70.0(eslint@10.11.0(jiti@2.7.0)(supports-color@7.2.0))(supports-color@7.2.0)(typescript@6.0.3) vite: specifier: ^8.3.0 - version: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) + version: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) vite-plugin-svgr: specifier: ^5.2.0 - version: 5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) + version: 5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) vitest: specifier: ^5.0.1 - version: 5.0.1(@types/node@24.13.6)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) + version: 5.0.1(@types/node@26.6.5)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) webdriverio: specifier: 10.1.0 version: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0) @@ -1387,6 +1387,9 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} + '@isaacs/nonrecursive-glob-match@1.0.2': + resolution: {integrity: sha512-GJPHIgPPtFl+nLhFHyF0zZoFcOFLsZWbHFFzo+Zr+PWDXJbl9hHxb2w+9SiF2fHmLgKaXIpUzQo3SbBBZ9WObw==} + '@jest/diff-sequences@30.5.0': resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -2321,8 +2324,8 @@ packages: '@types/node@22.20.5': resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==} - '@types/node@24.13.6': - resolution: {integrity: sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==} + '@types/node@26.6.5': + resolution: {integrity: sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==} '@types/parse-json@4.0.2': resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} @@ -3594,6 +3597,10 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + glob@14.0.3: + resolution: {integrity: sha512-A1vjIwC3qpdCso45Iw127gnp7sPhqgY8lUQhDrArIs+GbmfaaVPFuoyy7WyLT9EPZCRZ7j7aLdwUhQmHWesEow==} + engines: {node: 18 || 20 || >=22} + globals@17.12.0: resolution: {integrity: sha512-cezEd/DTyyht9cvSSURyygXPfy04GtWO/5e6ZPvH7fCtjKz9PYOmuawphw1Ctd1f6C+5JypXfGD7ahNMXvevBA==} engines: {node: '>=18'} @@ -4176,6 +4183,10 @@ packages: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} + minimatch@11.0.2: + resolution: {integrity: sha512-pWTgz5CN3tjqyZNSK2tePlSmu9IwM41q8yn8Usdqqwe9TIHh+9dEpLLjF4uApsfPEAZY71qtSiA2B2APEr+1Yw==} + engines: {node: 18 || 20 || >=22} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -4337,6 +4348,10 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} + path-scurry@3.0.0: + resolution: {integrity: sha512-1pTbQqH1wIwMi/eleQ5leFTTCkzFsPcgXNjwOA8YGGW/W4/kHT3dssE7Z6NH+zuqNLOzOB0uaD61yl7cAcVhgg==} + engines: {node: 18 || 20 || >=22} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4865,8 +4880,8 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} undici@6.28.1: resolution: {integrity: sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==} @@ -6303,6 +6318,8 @@ snapshots: dependencies: minipass: 7.1.3 + '@isaacs/nonrecursive-glob-match@1.0.2': {} + '@jest/diff-sequences@30.5.0': {} '@jest/expect-utils@30.5.2': @@ -6313,7 +6330,7 @@ snapshots: '@jest/pattern@30.5.0': dependencies: - '@types/node': 24.13.6 + '@types/node': 26.6.5 jest-regex-util: 30.5.0 '@jest/schemas@30.5.0': @@ -6326,7 +6343,7 @@ snapshots: '@jest/schemas': 30.5.0 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.13.6 + '@types/node': 26.6.5 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -7017,9 +7034,9 @@ snapshots: dependencies: undici-types: 6.21.0 - '@types/node@24.13.6': + '@types/node@26.6.5': dependencies: - undici-types: 7.18.2 + undici-types: 8.9.0 '@types/parse-json@4.0.2': {} @@ -7041,7 +7058,7 @@ snapshots: '@types/split2@4.2.3': dependencies: - '@types/node': 24.13.6 + '@types/node': 26.6.5 '@types/stack-utils@2.0.3': {} @@ -7056,7 +7073,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 24.13.6 + '@types/node': 26.6.5 '@types/yargs-parser@21.0.3': {} @@ -7227,7 +7244,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitejs/plugin-legacy@8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': + '@vitejs/plugin-legacy@8.2.3(supports-color@7.2.0)(terser@5.51.2)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': dependencies: '@babel/core': 7.29.7(supports-color@7.2.0) '@babel/plugin-transform-dynamic-import': 7.29.7(@babel/core@7.29.7(supports-color@7.2.0)) @@ -7242,23 +7259,23 @@ snapshots: regenerator-runtime: 0.14.1 systemjs: 6.15.1 terser: 5.51.2 - vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) + vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@6.1.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': + '@vitejs/plugin-react@6.1.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) + vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) - '@vitest/mocker@5.0.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': + '@vitest/mocker@5.0.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1))': dependencies: '@jridgewell/trace-mapping': 0.3.31 '@vitest/spy': 5.0.1 estree-walker: 3.0.3 magic-string: 1.4.1 optionalDependencies: - vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) + vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) '@vitest/pretty-format@4.1.11': dependencies: @@ -8436,6 +8453,12 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 + glob@14.0.3: + dependencies: + minimatch: 11.0.2 + minipass: 7.1.3 + path-scurry: 3.0.0 + globals@17.12.0: {} gopd@1.2.0: {} @@ -8685,7 +8708,7 @@ snapshots: dependencies: '@jest/expect-utils': 30.5.2 '@jest/types': 30.5.1 - '@types/node': 24.13.6 + '@types/node': 26.6.5 jest-util: 30.5.1 jest-regex-util@30.5.0: {} @@ -8693,7 +8716,7 @@ snapshots: jest-util@30.5.1: dependencies: '@jest/types': 30.5.1 - '@types/node': 24.13.6 + '@types/node': 26.6.5 chalk: 4.1.2 ci-info: 4.4.0 graceful-fs: 4.2.11 @@ -9250,6 +9273,11 @@ snapshots: dependencies: brace-expansion: 5.0.12 + minimatch@11.0.2: + dependencies: + '@isaacs/nonrecursive-glob-match': 1.0.2 + brace-expansion: 5.0.12 + minipass@7.1.3: {} minizlib@3.1.0: @@ -9450,6 +9478,11 @@ snapshots: lru-cache: 11.5.3 minipass: 7.1.3 + path-scurry@3.0.0: + dependencies: + lru-cache: 11.5.3 + minipass: 7.1.3 + path-type@4.0.0: {} pathe@2.0.3: {} @@ -9983,7 +10016,7 @@ snapshots: undici-types@6.21.0: {} - undici-types@7.18.2: {} + undici-types@8.9.0: {} undici@6.28.1: {} @@ -10101,18 +10134,18 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-svgr@5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)): + vite-plugin-svgr@5.2.0(supports-color@7.2.0)(typescript@6.0.3)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)): dependencies: '@rollup/pluginutils': 5.4.0 '@svgr/core': 8.1.0(supports-color@7.2.0)(typescript@6.0.3) '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(supports-color@7.2.0)(typescript@6.0.3))(supports-color@7.2.0) - vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) + vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) transitivePeerDependencies: - rollup - supports-color - typescript - vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1): + vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 @@ -10120,7 +10153,7 @@ snapshots: rolldown: 1.2.9 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 24.13.6 + '@types/node': 26.6.5 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 @@ -10128,10 +10161,10 @@ snapshots: terser: 5.51.2 yaml: 2.9.1 - vitest@5.0.1(@types/node@24.13.6)(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)): + vitest@5.0.1(@types/node@26.6.5)(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)): dependencies: '@types/chai': 5.2.3 - '@vitest/mocker': 5.0.1(vite@8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) + '@vitest/mocker': 5.0.1(vite@8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1)) chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 @@ -10142,10 +10175,10 @@ snapshots: tinybench: 6.1.4 tinyexec: 1.3.0 tinyglobby: 0.2.17 - vite: 8.3.0(@types/node@24.13.6)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) + vite: 8.3.0(@types/node@26.6.5)(esbuild@0.28.2)(jiti@2.7.0)(sass@1.104.1)(terser@5.51.2)(yaml@2.9.1) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.13.6 + '@types/node': 26.6.5 transitivePeerDependencies: - msw From e156ff09aeabc7ffa114ee1201c90c201a2e5084 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:39:08 +0000 Subject: [PATCH 32/51] chore(deps): update rust crate zip to v9.1.0 [security] (#8458) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e15536ee..be776cec2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1175,7 +1175,7 @@ dependencies = [ "windows-service", "windows-sys 0.61.2", "winreg 0.56.0", - "zip 9.0.5", + "zip 9.1.0", ] [[package]] @@ -4051,9 +4051,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "lzma-rust2" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7277c9742229dc25243236d578b6471a798c554512c6f6a04365ec2e87c2a3bb" +checksum = "f1c46f3fcd2e888afdb01a07efc071cb21300e07c68d32c3e8b01b5c8cb1fcaa" dependencies = [ "sha2 0.11.1", ] @@ -8553,9 +8553,9 @@ dependencies = [ [[package]] name = "typed-path" -version = "0.12.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" +checksum = "9ce898823d8cd0f028662d2f349694b1af0a5cc9be83f1d213a00d3147445e1e" [[package]] name = "typeid" @@ -10066,9 +10066,9 @@ dependencies = [ [[package]] name = "zip" -version = "9.0.5" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e04976da629d3b04204ba8105b396a193af8ad2583b2c79bfb3ab8af580a11e" +checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704" dependencies = [ "aes", "bzip2", @@ -10087,6 +10087,7 @@ dependencies = [ "sha1 0.11.0", "time", "typed-path", + "windows-sys 0.61.2", "zeroize", "zopfli", "zstd", From 8bf20d199c90334c25ca6bc6f459e2ff53bc73ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:26:16 +0000 Subject: [PATCH 33/51] chore(deps): update rust crate zip to v9.1.1 [security] (#8460) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be776cec2..96e623773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1175,7 +1175,7 @@ dependencies = [ "windows-service", "windows-sys 0.61.2", "winreg 0.56.0", - "zip 9.1.0", + "zip 9.1.1", ] [[package]] @@ -2034,7 +2034,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2293,7 +2293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2809,7 +2809,7 @@ dependencies = [ "gobject-sys 0.21.5", "libc", "system-deps 7.0.8", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4234,7 +4234,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4401,7 +4401,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4878,7 +4878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.45.0", ] [[package]] @@ -5543,7 +5543,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6060,7 +6060,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6119,7 +6119,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6739,7 +6739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7808,7 +7808,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8474,7 +8474,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8583,7 +8583,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9125,7 +9125,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -9146,7 +9146,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "windows-sys 0.61.2", + "windows-sys 0.60.2", "windows-version", ] @@ -9721,7 +9721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -10066,9 +10066,9 @@ dependencies = [ [[package]] name = "zip" -version = "9.1.0" +version = "9.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886b00003a72c4ccee3f42cfc34908d33b2508a74d827dc2a4244706475c704" +checksum = "518b847fd864440c379748ad684c449f293af68978a485a4ff16f7c901c1f7c4" dependencies = [ "aes", "bzip2", From afd9d4b5a0b0aae0ba68f6e7aae3c22d8d7bc107 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:45:12 +0000 Subject: [PATCH 34/51] chore(deps): update rust crate zip to v9.1.2 [security] (#8461) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 96e623773..55acbe5f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1175,7 +1175,7 @@ dependencies = [ "windows-service", "windows-sys 0.61.2", "winreg 0.56.0", - "zip 9.1.1", + "zip 9.1.2", ] [[package]] @@ -10066,9 +10066,9 @@ dependencies = [ [[package]] name = "zip" -version = "9.1.1" +version = "9.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518b847fd864440c379748ad684c449f293af68978a485a4ff16f7c901c1f7c4" +checksum = "ec2e3161de27091f18a831d9494e9c53378a34833af5c1dda35ad85849da18d6" dependencies = [ "aes", "bzip2", From 44d50eb67d86d80015b0b1664f33ed88880f19ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 Oct 2026 03:52:54 +0000 Subject: [PATCH 35/51] chore(deps): update rust crate zip to v9.1.3 [security] (#8464) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Cargo.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55acbe5f0..fd21f8a21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,6 +33,7 @@ dependencies = [ "cipher", "cpubits", "cpufeatures 0.3.0", + "zeroize", ] [[package]] @@ -1175,7 +1176,7 @@ dependencies = [ "windows-service", "windows-sys 0.61.2", "winreg 0.56.0", - "zip 9.1.2", + "zip 9.1.3", ] [[package]] @@ -10066,9 +10067,9 @@ dependencies = [ [[package]] name = "zip" -version = "9.1.2" +version = "9.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e3161de27091f18a831d9494e9c53378a34833af5c1dda35ad85849da18d6" +checksum = "1156b39d142a34b999c2310a50455e0cb3760bf7efad0122f796d813bee26ccb" dependencies = [ "aes", "bzip2", From bdea31a7cac63dc8578308c0ce652e818ec26e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9D=A4=E6=98=AF=E7=BA=B1=E9=9B=BE=E9=85=B1=E5=93=9F?= =?UTF-8?q?=EF=BD=9E?= <49941141+Dragon1573@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:04:11 +0000 Subject: [PATCH 36/51] chore(deps): update dependency pnpm to v12.16.1 (#8475) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .devcontainer/devcontainer.json | 2 +- .tool-versions | 2 +- package.json | 2 +- pnpm-lock.yaml | 122 ++++++++++++++++---------------- 4 files changed, 64 insertions(+), 64 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index afde56823..0892e0574 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,7 +5,7 @@ "features": { "ghcr.io/devcontainers/features/node:2": { "version": "26.13.0", - "pnpmVersion": "12.15.0" + "pnpmVersion": "12.16.1" }, "ghcr.io/devcontainers/features/rust:1": { "version": "1.99.1", diff --git a/.tool-versions b/.tool-versions index 651818210..eeb93234c 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ nodejs 26.13.0 -pnpm 12.15.0 +pnpm 12.16.1 rust 1.99.1 diff --git a/package.json b/package.json index c34f842fd..cc17f0827 100644 --- a/package.json +++ b/package.json @@ -147,5 +147,5 @@ ] }, "type": "module", - "packageManager": "pnpm@12.15.0" + "packageManager": "pnpm@12.16.1" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 28c774065..dc6473bac 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,153 +7,153 @@ importers: configDependencies: {} packageManagerDependencies: pnpm: - specifier: 12.15.0 - version: 12.15.0 + specifier: 12.16.1 + version: 12.16.1 packages: - '@pnpm/exe.android-arm64@12.15.0': - resolution: {integrity: sha512-eVCqmscY70LGvgRzu3mi8IBWBFGix+a6mNZoWmXxESsBdeiKEF9xQSuaeel10yn+zI7TXy2jlI63vFLzETk+aQ==} + '@pnpm/exe.android-arm64@12.16.1': + resolution: {integrity: sha512-PKf78Ew+i0CBYf0Xc+eMcqQiqyBT+u8XCVg1F5roc98/6wWfdYlWhrUMmEBiKWaJ3JqHxrtt8l1NwZ7qfJt2/Q==} cpu: [arm64] os: [android] - '@pnpm/exe.android-x64@12.15.0': - resolution: {integrity: sha512-Gx/WbkFcdUFUg8g8AShezcS9EiQxfYvdAm2hfKLO8sfJmJvKOCt5fzlgHWDzmGhoclibgW+tUGZ/KF4F4wOTCA==} + '@pnpm/exe.android-x64@12.16.1': + resolution: {integrity: sha512-xdu7gMbB98pdvhwMeA6612Of515FDlRUtfA/1f7nWPZfPLnEdSRBO2u+1N0XBtQ/ennhPH/lrxB1gC6gnR/cdg==} cpu: [x64] os: [android] - '@pnpm/exe.darwin-arm64@12.15.0': - resolution: {integrity: sha512-N0ZykiDWCvqSBJvt71j7cSoBR993FqvYZFtmpCtqvW3pguiI+NAgraLPA5OTwu5mV+/d8hYpVcPktJA3xFDLtA==} + '@pnpm/exe.darwin-arm64@12.16.1': + resolution: {integrity: sha512-pVbmIIbhixiLvADLu1WA3TaW8FZy/D72R1De27WbbFx8/2mxvnsTyFVm4fO9kjpjMY7RzJd48CiQ96p8fO3ebQ==} cpu: [arm64] os: [darwin] - '@pnpm/exe.darwin-x64@12.15.0': - resolution: {integrity: sha512-7Ntr2HlBUxzwWOg7NwDxCbRW/3vXuqKO+hcXm+w3SBi9rrHY53li1wLqWVT7Gjxd+lVEHM1QJZo6WJEGFU8C6A==} + '@pnpm/exe.darwin-x64@12.16.1': + resolution: {integrity: sha512-xZvQBFqcZjO7vFKTK3XsJpqY6lnBL9EWSLCzVUPq3kZ94+D96gwJCa76xxGNK8Cj32f4eeM91Qg+yNRR4sRpDw==} cpu: [x64] os: [darwin] - '@pnpm/exe.freebsd-x64@12.15.0': - resolution: {integrity: sha512-btOiyL7fL7W4uQGH0aqFai2TNC3IoKcZ7gNHBVbEFZt2dWrnrD0SufL3iGs3fLlmb4OtuU35uf4Zpr9V9Jh9mA==} + '@pnpm/exe.freebsd-x64@12.16.1': + resolution: {integrity: sha512-zTtunfi9mBhRXiXEJYfwiJj0tk7MgTSfqDSRh+Az9FZ9THazVIiQiVNS8jpcsftDOqu7LKNffQdjmgwHDThfUg==} cpu: [x64] os: [freebsd] - '@pnpm/exe.linux-arm64-musl@12.15.0': - resolution: {integrity: sha512-pNC38k+O+90FOjbPNSnw0E7apq1PKTmz3fFe1amn+lys/U5kL++uzir8bRiho8ONcyA3d9EU7wdqcY0DKkj+JA==} + '@pnpm/exe.linux-arm64-musl@12.16.1': + resolution: {integrity: sha512-BJ3mOmcmQgcUo92Ee/WfEOt1pRiv3y2MMAkhEiYHd85UiPpHTfFhv+SpCbALmfExzXk3UlaRtMYwHlkQymc4VA==} cpu: [arm64] os: [linux] libc: [musl] - '@pnpm/exe.linux-arm64@12.15.0': - resolution: {integrity: sha512-qML/8aCXslh8td/w4U+HJNqUcZ2+m6EfTbWScIzZRmyPsRFN+xB4LP1U/hymxc1iSJiKtG56HJx2fvUErWFIGw==} + '@pnpm/exe.linux-arm64@12.16.1': + resolution: {integrity: sha512-JaRd5gNxwdNGBBkrZFt8bknrK4YmHVJBW0JMhUQ0fJ0dEYp6C2GT9ExaCqOO8EwYEyqM+jpKgJIK+8WFaJy3ew==} cpu: [arm64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-ppc64@12.15.0': - resolution: {integrity: sha512-tjRDUUl7RCJ6T/6eoNPxOqkwU4Mzi5RtTXmSAxujyz37OTwwsMU7Ps087Zk4gHcUvRSeyF40ZFsew3vtE+nmUA==} + '@pnpm/exe.linux-ppc64@12.16.1': + resolution: {integrity: sha512-FnSV0Jv40S1cMv7vFNWwvu0X567Lec4L4HCRead3CFcD6zQDNbGw81s8N5bcKPahtPD9ApVuhsbmFCx4rw6RMw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-riscv64@12.15.0': - resolution: {integrity: sha512-bF1uBWEfbwaeryz6lIiaJ8AWD082otZfvzGX5l+6LZRpRwUEpJHUsJrORaYP5/cNMv5ZR2p6TvvcJcQzwtDzmw==} + '@pnpm/exe.linux-riscv64@12.16.1': + resolution: {integrity: sha512-C1Sww4FN5IttD4atRRhAKmQtSGWVp04ZAKNxO8pfu2bkRPwOW+7v+2DH1Xs1W+kZYYpO86BYDvUcadefF+AgaQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@pnpm/exe.linux-s390x@12.15.0': - resolution: {integrity: sha512-zKMhIzsexYMToeLmqV5jPsvV/f9vwsJb2kUH2NFYzalbxImeBrlA4kL7GwF8gD4pagV6jYYcJb8RjbZIdjHptg==} + '@pnpm/exe.linux-s390x@12.16.1': + resolution: {integrity: sha512-kn2fautsTcxIMODoovQVqZyfpDRdNgGkoeVqNdrKqbeBBvrEl+fXLkmZiq5xDht1LMr4sYiCNcsMfNMT6JPFmg==} cpu: [s390x] os: [linux] libc: [glibc] - '@pnpm/exe.linux-x64-musl@12.15.0': - resolution: {integrity: sha512-A9J0Jg0Rdx+qQU4qXtwyST5tto9IOu1zi4fDk35nbc6DWahggbcKtFvZPKFwL2qgUXGXOX//hXv6z/sp6emO8A==} + '@pnpm/exe.linux-x64-musl@12.16.1': + resolution: {integrity: sha512-PM6ccHCOe/O9SsTflTJTYWfwogJZa6EmUvpnwZgiPguJBddh20wDQKgxUPXf48rDsPm9vMsIij0uHswwwbXL8g==} cpu: [x64] os: [linux] libc: [musl] - '@pnpm/exe.linux-x64@12.15.0': - resolution: {integrity: sha512-yc+QlZR+9+nr29J4fRzhMoapgxTBDndq/+peyoq1XUufnQFnfbXUMFudDIKzHi545linGqmOmXfn0NoufNgcOg==} + '@pnpm/exe.linux-x64@12.16.1': + resolution: {integrity: sha512-RK//8ZLcXGW8A2k8bQOXZDBN1Qu92zN4dYnG1bzj1zoQ1fkZ6jNzZ1AvXtsfGVma795CJtvoaF5bLXf4l/FIUw==} cpu: [x64] os: [linux] libc: [glibc] - '@pnpm/exe.win32-arm64@12.15.0': - resolution: {integrity: sha512-fPXZxuY1VclkOga9bc53qNCXcegfeoBR8+Ue+AospDwKzuGGSHll0oDxkLfkmpjyRqpe7bH5DyQPYp/xSMaIVA==} + '@pnpm/exe.win32-arm64@12.16.1': + resolution: {integrity: sha512-Ox3kNNVxtGw6pK7SVejmeOUgHhUTZgAM7NQQ6cU7frPw3ix9+j7d2MGbRg3fyEFAaKWq8ZAZLTQGcDyLnAu+qA==} cpu: [arm64] os: [win32] - '@pnpm/exe.win32-x64@12.15.0': - resolution: {integrity: sha512-nrd+Tp1We1DGj4DGG5Bea9lZNh9y1PjlLBdEwdInS4D0h0UIDtM7JaGGGDJhNzIwcHASyihWI7HLV+dtG+fhYg==} + '@pnpm/exe.win32-x64@12.16.1': + resolution: {integrity: sha512-k+FhRJ0ttphFoFIVB1Sk4v76YJAulafmA/Ly57cysesUhhIzVlApeOShUz0jXubKx317TThlEfpQrukldnqORA==} cpu: [x64] os: [win32] - pnpm@12.15.0: - resolution: {integrity: sha512-s5UdbW/0qfE9x89WzSjSuOs328ycaOUuHhGRhz6p+CoBcoerULqtxczElPA2xnwOR+sTYIwvo29HcRth6hKOYQ==} + pnpm@12.16.1: + resolution: {integrity: sha512-CR0d7nknV1Zhsl2rPCrjHK/BxU1rce1qZgDPDByxBDSkR+rhQBXxsj4ua+3u1535SpMuIWTSgyKA3OP7MvZW0Q==} engines: {node: '>=18'} hasBin: true snapshots: - '@pnpm/exe.android-arm64@12.15.0': + '@pnpm/exe.android-arm64@12.16.1': optional: true - '@pnpm/exe.android-x64@12.15.0': + '@pnpm/exe.android-x64@12.16.1': optional: true - '@pnpm/exe.darwin-arm64@12.15.0': + '@pnpm/exe.darwin-arm64@12.16.1': optional: true - '@pnpm/exe.darwin-x64@12.15.0': + '@pnpm/exe.darwin-x64@12.16.1': optional: true - '@pnpm/exe.freebsd-x64@12.15.0': + '@pnpm/exe.freebsd-x64@12.16.1': optional: true - '@pnpm/exe.linux-arm64-musl@12.15.0': + '@pnpm/exe.linux-arm64-musl@12.16.1': optional: true - '@pnpm/exe.linux-arm64@12.15.0': + '@pnpm/exe.linux-arm64@12.16.1': optional: true - '@pnpm/exe.linux-ppc64@12.15.0': + '@pnpm/exe.linux-ppc64@12.16.1': optional: true - '@pnpm/exe.linux-riscv64@12.15.0': + '@pnpm/exe.linux-riscv64@12.16.1': optional: true - '@pnpm/exe.linux-s390x@12.15.0': + '@pnpm/exe.linux-s390x@12.16.1': optional: true - '@pnpm/exe.linux-x64-musl@12.15.0': + '@pnpm/exe.linux-x64-musl@12.16.1': optional: true - '@pnpm/exe.linux-x64@12.15.0': + '@pnpm/exe.linux-x64@12.16.1': optional: true - '@pnpm/exe.win32-arm64@12.15.0': + '@pnpm/exe.win32-arm64@12.16.1': optional: true - '@pnpm/exe.win32-x64@12.15.0': + '@pnpm/exe.win32-x64@12.16.1': optional: true - pnpm@12.15.0: + pnpm@12.16.1: optionalDependencies: - '@pnpm/exe.android-arm64': 12.15.0 - '@pnpm/exe.android-x64': 12.15.0 - '@pnpm/exe.darwin-arm64': 12.15.0 - '@pnpm/exe.darwin-x64': 12.15.0 - '@pnpm/exe.freebsd-x64': 12.15.0 - '@pnpm/exe.linux-arm64': 12.15.0 - '@pnpm/exe.linux-arm64-musl': 12.15.0 - '@pnpm/exe.linux-ppc64': 12.15.0 - '@pnpm/exe.linux-riscv64': 12.15.0 - '@pnpm/exe.linux-s390x': 12.15.0 - '@pnpm/exe.linux-x64': 12.15.0 - '@pnpm/exe.linux-x64-musl': 12.15.0 - '@pnpm/exe.win32-arm64': 12.15.0 - '@pnpm/exe.win32-x64': 12.15.0 + '@pnpm/exe.android-arm64': 12.16.1 + '@pnpm/exe.android-x64': 12.16.1 + '@pnpm/exe.darwin-arm64': 12.16.1 + '@pnpm/exe.darwin-x64': 12.16.1 + '@pnpm/exe.freebsd-x64': 12.16.1 + '@pnpm/exe.linux-arm64': 12.16.1 + '@pnpm/exe.linux-arm64-musl': 12.16.1 + '@pnpm/exe.linux-ppc64': 12.16.1 + '@pnpm/exe.linux-riscv64': 12.16.1 + '@pnpm/exe.linux-s390x': 12.16.1 + '@pnpm/exe.linux-x64': 12.16.1 + '@pnpm/exe.linux-x64-musl': 12.16.1 + '@pnpm/exe.win32-arm64': 12.16.1 + '@pnpm/exe.win32-x64': 12.16.1 --- lockfileVersion: '9.0' From c236f5094e0608b7be82f62350a4ef397dc062ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9D=A4=E6=98=AF=E7=BA=B1=E9=9B=BE=E9=85=B1=E5=93=9F?= =?UTF-8?q?=EF=BD=9E?= <49941141+Dragon1573@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:04:34 +0000 Subject: [PATCH 37/51] deps: upgrade updater to remove vulnerable zip (#8476) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(deps): update updater to patched zip release Upgrade tauri-plugin-updater to 2.14.1 and resolve the lockfile without vulnerable zip 4.6.1. Assisted-by: Copilot SDK Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> * chore(deps): align updater package versions Upgrade the PNPM updater plugin to match the Rust updater crate at 2.14.1. Assisted-by: Copilot SDK Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> --------- Signed-off-by: ❤是纱雾酱哟~ <49941141+Dragon1573@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Cargo.lock | 76 +++++++++++++------------------------------- package.json | 2 +- pnpm-lock.yaml | 10 +++--- src-tauri/Cargo.toml | 4 +-- 4 files changed, 30 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fd21f8a21..e1e4da74e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,15 +136,6 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" -[[package]] -name = "arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] - [[package]] name = "arboard" version = "3.6.1" @@ -1176,7 +1167,7 @@ dependencies = [ "windows-service", "windows-sys 0.61.2", "winreg 0.56.0", - "zip 9.1.3", + "zip", ] [[package]] @@ -1871,17 +1862,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "derive_more" version = "2.1.1" @@ -2035,7 +2015,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2294,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2810,7 +2790,7 @@ dependencies = [ "gobject-sys 0.21.5", "libc", "system-deps 7.0.8", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4235,7 +4215,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4402,7 +4382,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4879,7 +4859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -5544,7 +5524,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6061,7 +6041,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6120,7 +6100,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6740,7 +6720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7576,11 +7556,11 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259989d4fe11b2bf51f7d671642249b1837c4ae2da1430dc713ae38f98ceeec" +checksum = "d957abbcb3f9f6c76d22a3a0f012f30880640d0bccb5634d2b12a9937e4c872b" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "dirs 7.0.0", "flate2", "futures-util", @@ -7605,7 +7585,7 @@ dependencies = [ "tokio", "url", "windows-sys 0.61.2", - "zip 4.6.1", + "zip", ] [[package]] @@ -7806,10 +7786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8475,7 +8455,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8584,7 +8564,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -9126,7 +9106,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -9147,7 +9127,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "windows-sys 0.60.2", + "windows-sys 0.61.2", "windows-version", ] @@ -9722,7 +9702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -10053,18 +10033,6 @@ dependencies = [ "syn 3.0.5", ] -[[package]] -name = "zip" -version = "4.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" -dependencies = [ - "arbitrary", - "crc32fast", - "indexmap 2.14.0", - "memchr", -] - [[package]] name = "zip" version = "9.1.3" diff --git a/package.json b/package.json index cc17f0827..1034facdc 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@tauri-apps/plugin-fs": "2.7.1", "@tauri-apps/plugin-http": "2.8.3", "@tauri-apps/plugin-opener": "2.7.2", - "@tauri-apps/plugin-updater": "2.14.0", + "@tauri-apps/plugin-updater": "2.14.1", "ahooks": "^3.10.0", "cidr-block": "^2.3.0", "compare-versions": "^6.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc6473bac..be8be9f22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -218,8 +218,8 @@ importers: specifier: 2.7.2 version: 2.7.2 '@tauri-apps/plugin-updater': - specifier: 2.14.0 - version: 2.14.0 + specifier: 2.14.1 + version: 2.14.1 ahooks: specifier: ^3.10.0 version: 3.10.0(react-dom@19.3.1(react@19.3.1))(react@19.3.1) @@ -2221,8 +2221,8 @@ packages: '@tauri-apps/plugin-opener@2.7.2': resolution: {integrity: sha512-LyybP1VSzTMQiaFYcsbAse47KpivlyXdLZMQzGLEnkgQacGESnpjAfFoQ6h5sqWgAdZDH9szYjPzHMpv0Sz+TQ==} - '@tauri-apps/plugin-updater@2.14.0': - resolution: {integrity: sha512-gdylJ9tger4hkyOn24QZd6MNOKVo2Eh53iw5th2Qj1vKTR/+u4PIwJWuiis5hohYvobn6iOvlnALfrqIPSzh7Q==} + '@tauri-apps/plugin-updater@2.14.1': + resolution: {integrity: sha512-Hr7nlBm1wT6xbwzjZ/S36YULazu8MNlj5vvOwMl47fXI7HTa5Bjl1PbDEwVHaZTAKzE1VZsUy35UqnbrQxmYQw==} '@tsrs/darwin-arm64@0.20.0-ts7.1.0-beta': resolution: {integrity: sha512-mhHj8QGUmdJDBvJEnMXx5P/jvAKXYU3hy/z3qxrWBo8XHlbLES32PgC0ZpTRbJA960ReTpVqqvCOYaC6EL32/Q==} @@ -6949,7 +6949,7 @@ snapshots: dependencies: '@tauri-apps/api': 2.12.4 - '@tauri-apps/plugin-updater@2.14.0': + '@tauri-apps/plugin-updater@2.14.1': dependencies: '@tauri-apps/api': 2.12.4 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 824c83107..f6822677c 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -79,7 +79,7 @@ tauri-plugin-fs = "2.7.1" tauri-plugin-process = "2.4.0" tauri-plugin-deep-link = "2.6.2" tauri-plugin-window-state = "2.5.0" -zip = "9.0.4" +zip = "9.1.3" flate2 = "1.1.10" reqwest_dav = "0.3.3" aes-gcm = { version = "0.11.1" } @@ -154,7 +154,7 @@ windows-sys.workspace = true [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-autostart = "2.7.0" tauri-plugin-global-shortcut = "2.4.1" -tauri-plugin-updater = "2.14.0" +tauri-plugin-updater = "2.14.1" [dev-dependencies] criterion = { workspace = true } From ef784e459d31fe9d1946568b6fe47ac3a7e692b7 Mon Sep 17 00:00:00 2001 From: heeeex100 <35334194+heeeex100@users.noreply.github.com> Date: Fri, 30 Oct 2026 15:48:33 +0000 Subject: [PATCH 38/51] docs: document complete development setup (#8457) * docs: document complete development setup * docs: follow review feedback on toolchain setup --- CONTRIBUTING.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a1b464a17..005d67bdc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,7 +28,7 @@ Before contributing, you need to set up your development environment. Follow the ### Prerequisites 1. **Install Rust and Node.js** - Our project requires both Rust and Node.js. Follow the official installation instructions [here](https://tauri.app/start/prerequisites/). + Follow the [Tauri prerequisites](https://tauri.app/start/prerequisites/) for your operating system. Use the Node.js version in [`.tool-versions`](.tool-versions); [`rust-toolchain.toml`](rust-toolchain.toml) selects the Rust version and required components when you run Cargo from this repository. ### Windows Users @@ -51,18 +51,21 @@ rustup set default-host x86_64-pc-windows-msvc ### Install Node.js Package Manager -Enable `corepack`: +[Corepack is no longer bundled with Node.js starting with Node.js 25](https://nodejs.org/api/corepack.html). Install it globally if it is not already available, then enable it: ```bash +npm install --global corepack corepack enable ``` +Run pnpm from this repository so Corepack selects the version pinned by `packageManager` in [`package.json`](package.json). + ### Install Project Dependencies Node.js dependencies: ```bash -pnpm install +pnpm install --frozen-lockfile ``` Ubuntu-only system packages: @@ -78,14 +81,24 @@ pnpm run prebuild pnpm run prebuild --force # Re-download and overwrite Mihomo core and service binaries ``` +### Prepare the Development Service Source + +`pnpm dev` builds the development service from a sibling checkout of [`clash-verge-service-ipc`](https://github.com/clash-verge-rev/clash-verge-service-ipc). The binaries downloaded by `pnpm prebuild` do not replace this source checkout. + +Check the `clash_verge_service_ipc` dependency in [`src-tauri/Cargo.toml`](src-tauri/Cargo.toml), then clone the service repository alongside `clash-verge-rev` and check out the matching tag. If the sibling directory already exists, reuse it and check its version rather than overwriting it. Without this checkout, `pnpm dev` fails when it tries to access `../clash-verge-service-ipc/Cargo.toml`. + ### Run the Development Server +Run these commands from the root of `clash-verge-rev`: + ```bash -pnpm dev # Standard -pnpm dev:diff # If an app instance already exists -pnpm dev:tauri # Run Tauri development mode +pnpm dev # Build the development service and start the desktop app +pnpm dev:diff # Alias of pnpm dev +pnpm dev:tauri # Run Tauri directly with the tauri-dev feature ``` +For frontend-only development, use `pnpm web:dev`. It does not start the Rust backend or build the development service. + ### Build the Project Standard build: From 719f63b57fdc97ad229d47211441c6fd2fa84c63 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:30:28 +0800 Subject: [PATCH 39/51] chore: update tao and wry --- Cargo.lock | 51 +++++++++++++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1e4da74e..2cfa5fdd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1334,7 +1334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -2015,7 +2015,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2274,7 +2274,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2790,7 +2790,7 @@ dependencies = [ "gobject-sys 0.21.5", "libc", "system-deps 7.0.8", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -3344,7 +3344,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -4215,7 +4215,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4382,7 +4382,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4859,7 +4859,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5524,7 +5524,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6028,7 +6028,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -6041,7 +6041,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6100,7 +6100,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -6720,7 +6720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7069,9 +7069,9 @@ checksum = "c0e973b34477b7823833469eb0f5a3a60370fef7a453e02d751b59180d0a5a05" [[package]] name = "tao" -version = "0.37.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37f381f4e048e6cdf038b5705f8cf14ad108279d46eb968140a7b291aba9400" +checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548" dependencies = [ "bitflags 2.13.2", "block2", @@ -7786,10 +7786,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -8455,7 +8455,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.20", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8564,7 +8564,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9106,7 +9106,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -9127,7 +9127,7 @@ dependencies = [ "objc2-foundation", "objc2-quartz-core", "raw-window-handle", - "windows-sys 0.61.2", + "windows-sys 0.60.2", "windows-version", ] @@ -9702,7 +9702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d6f32a0ff4a9f6f01231eb2059cc85479330739333e0e58cadf03b6af2cca10" dependencies = [ "cfg-if", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -9743,11 +9743,11 @@ checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "wry" -version = "0.57.0" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a819957a01b3119af85e638a38d242af76dbc87d130dca67bfd0441072e21ff0" +checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9" dependencies = [ - "base64 0.22.1", + "base64 0.23.1", "block2", "cookie", "crossbeam-channel", @@ -9768,7 +9768,6 @@ dependencies = [ "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", - "once_cell", "percent-encoding", "raw-window-handle", "sha2 0.10.9", From 8e9832712a123eaa637d82ad49df9113f97080b6 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:59:14 +0800 Subject: [PATCH 40/51] doc: clash-verge-rev with mihomo 1.19.33 --- Changelog.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Changelog.md b/Changelog.md index 943cb6d74..b1107a976 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,7 @@ ## v2.5.9 +- **Mihomo(Meta) 内核升级至 v1.19.33** +
🐞 修复问题 From 12f842d038c69f842f8c458fe708a099e24c1db0 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:33:26 +0800 Subject: [PATCH 41/51] fix(macos): prevent menu bar flicker when closing the window Keep the application activation policy unchanged when hiding the main window. Lightweight mode still hides the Dock icon. Closes #8319 --- src-tauri/src/feat/window.rs | 1 - src-tauri/src/lib.rs | 3 --- 2 files changed, 4 deletions(-) diff --git a/src-tauri/src/feat/window.rs b/src-tauri/src/feat/window.rs index e618701ed..f3bf5db7b 100644 --- a/src-tauri/src/feat/window.rs +++ b/src-tauri/src/feat/window.rs @@ -316,7 +316,6 @@ pub async fn hide() { { let _ = window.hide(); } - handle::Handle::global().set_activation_policy_accessory(); } #[cfg(test)] diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7e11e5c02..24d3f5203 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -289,9 +289,6 @@ pub fn run() -> std::process::ExitCode { } pub fn handle_window_close(api: &tauri::WindowEvent) { - #[cfg(target_os = "macos")] - handle::Handle::global().set_activation_policy_accessory(); - if core::handle::Handle::global().is_exiting() { return; } From 94942b91055f1c04a42983e8c1067987a7a604b0 Mon Sep 17 00:00:00 2001 From: nrenvo Date: Sun, 1 Nov 2026 07:43:42 +0000 Subject: [PATCH 42/51] fix(backup): publish scheduler settings with send_replace (#8386) `watch::Sender::send` drops the value when there are no receivers at startup, leaving the scheduler on the default disabled settings. `send_replace` stores it for the runner that subscribes right after. --- Changelog.md | 1 + src-tauri/src/core/auto_backup.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Changelog.md b/Changelog.md index b1107a976..bdf056092 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ 🐞 修复问题 - 修复规则扩展包含空列表项时,「编辑规则」界面报错的问题 +- 修复 重启应用后定时自动备份不执行的问题 - 修复托盘测速与代理页行为不一致的问题,移除托盘「测试延迟」入口 - 修复通过 URL 导入订阅时,即使导入的订阅并非当前使用的订阅,也会重新加载当前配置 - 修复 首页 网站测速失败后卡片一直显示测试中 diff --git a/src-tauri/src/core/auto_backup.rs b/src-tauri/src/core/auto_backup.rs index c475cb711..9b0fd9dad 100644 --- a/src-tauri/src/core/auto_backup.rs +++ b/src-tauri/src/core/auto_backup.rs @@ -109,7 +109,7 @@ impl AutoBackupManager { { *self.settings.write() = settings; } - let _ = self.settings_tx.send(settings); + let _ = self.settings_tx.send_replace(settings); self.maybe_start_runner(settings); Ok(()) } @@ -119,7 +119,7 @@ impl AutoBackupManager { { *self.settings.write() = settings; } - let _ = self.settings_tx.send(settings); + let _ = self.settings_tx.send_replace(settings); self.maybe_start_runner(settings); Ok(()) } From 0cae907de659b0856364e172fe7d2bf4ab9cb09e Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:12:40 +0800 Subject: [PATCH 43/51] fix(config): wait for pending configuration writes Serialize Verge patches behind the active configuration writer so toggling the system proxy after TUN does not fail immediately on lock contention. CLose: #6613, #8481, #8357 --- src-tauri/src/feat/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/feat/config.rs b/src-tauri/src/feat/config.rs index 2a1fdbe6c..da19781bd 100644 --- a/src-tauri/src/feat/config.rs +++ b/src-tauri/src/feat/config.rs @@ -27,7 +27,7 @@ pub async fn patch_verge(patch: &IVerge, not_save_file: bool) -> Result<()> { /// Apply a patch without post-update reconciliation. pub(super) async fn apply_verge_patch(patch: &IVerge, not_save_file: bool) -> Result<()> { - let config_write = Config::try_lock_config_write()?; + let config_write = Config::lock_config_write().await; apply_verge_patch_locked(&config_write, patch, not_save_file).await } From 261178b6d89b898f98fd8bd3a6bb8864ae29c405 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:29:51 +0800 Subject: [PATCH 44/51] fix(updater): render release notes without remark-gfm Remove GFM extensions that require unsupported regular expressions on older WebKit. Keep native Markdown links, existing release-note styling, and HTML sanitization. Closes #8079 Closes #8082 --- package.json | 1 - pnpm-lock.yaml | 201 ------------------ src/components/setting/mods/update-viewer.tsx | 3 - 3 files changed, 205 deletions(-) diff --git a/package.json b/package.json index 1034facdc..3c4aa0f70 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "react-router": "^8.4.0", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", - "remark-gfm": "^4.0.1", "swr": "^2.5.1", "tauri-plugin-mihomo-api": "github:clash-verge-rev/tauri-plugin-mihomo#main", "types-pac": "^1.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index be8be9f22..2c8dff462 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,9 +289,6 @@ importers: rehype-sanitize: specifier: ^6.0.0 version: 6.0.0 - remark-gfm: - specifier: ^4.0.1 - version: 4.0.1(supports-color@7.2.0) swr: specifier: ^2.5.1 version: 2.5.1(react@19.3.1) @@ -3241,10 +3238,6 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - eslint-import-context@0.1.9: resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -4018,9 +4011,6 @@ packages: magic-string@1.4.1: resolution: {integrity: sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==} - markdown-table@3.0.4: - resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - marked@14.0.0: resolution: {integrity: sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==} engines: {node: '>= 18'} @@ -4030,30 +4020,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - mdast-util-find-and-replace@3.0.2: - resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} - mdast-util-from-markdown@2.0.3: resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} - mdast-util-gfm-autolink-literal@2.0.1: - resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} - - mdast-util-gfm-footnote@2.1.0: - resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} - - mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} - - mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} - - mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} - - mdast-util-gfm@3.1.0: - resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} - mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -4090,27 +4059,6 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} - micromark-extension-gfm-autolink-literal@2.1.0: - resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} - - micromark-extension-gfm-footnote@2.1.0: - resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} - - micromark-extension-gfm-strikethrough@2.1.0: - resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} - - micromark-extension-gfm-table@2.1.2: - resolution: {integrity: sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==} - - micromark-extension-gfm-tagfilter@2.0.0: - resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} - - micromark-extension-gfm-task-list-item@2.1.0: - resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} - - micromark-extension-gfm@3.0.0: - resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} - micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} @@ -4551,18 +4499,12 @@ packages: rehype-sanitize@6.0.0: resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} - remark-gfm@4.0.1: - resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} - remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} - remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} - resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -8006,8 +7948,6 @@ snapshots: escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - eslint-import-context@0.1.9(unrs-resolver@1.12.2): dependencies: get-tsconfig: 4.14.3 @@ -8898,19 +8838,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 - markdown-table@3.0.4: {} - marked@14.0.0: {} math-intrinsics@1.1.0: {} - mdast-util-find-and-replace@3.0.2: - dependencies: - '@types/mdast': 4.0.4 - escape-string-regexp: 5.0.0 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 @@ -8928,63 +8859,6 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-gfm-autolink-literal@2.0.1: - dependencies: - '@types/mdast': 4.0.4 - ccount: 2.0.1 - devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.2 - micromark-util-character: 2.1.1 - - mdast-util-gfm-footnote@2.1.0(supports-color@7.2.0): - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) - mdast-util-to-markdown: 2.1.2 - micromark-util-normalize-identifier: 2.0.1 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-strikethrough@2.0.0(supports-color@7.2.0): - dependencies: - '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-table@2.0.0(supports-color@7.2.0): - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm-task-list-item@2.0.0(supports-color@7.2.0): - dependencies: - '@types/mdast': 4.0.4 - devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - - mdast-util-gfm@3.1.0(supports-color@7.2.0): - dependencies: - mdast-util-from-markdown: 2.0.3(supports-color@7.2.0) - mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0(supports-color@7.2.0) - mdast-util-gfm-strikethrough: 2.0.0(supports-color@7.2.0) - mdast-util-gfm-table: 2.0.0(supports-color@7.2.0) - mdast-util-gfm-task-list-item: 2.0.0(supports-color@7.2.0) - mdast-util-to-markdown: 2.1.2 - transitivePeerDependencies: - - supports-color - mdast-util-mdx-expression@2.0.1(supports-color@7.2.0): dependencies: '@types/estree-jsx': 1.0.5 @@ -9091,64 +8965,6 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 - micromark-extension-gfm-autolink-literal@2.1.0: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-footnote@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-core-commonmark: 2.0.3 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-normalize-identifier: 2.0.1 - micromark-util-sanitize-uri: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-strikethrough@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-util-chunked: 2.0.1 - micromark-util-classify-character: 2.0.1 - micromark-util-resolve-all: 2.0.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-table@2.1.2: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm-tagfilter@2.0.0: - dependencies: - micromark-util-types: 2.0.2 - - micromark-extension-gfm-task-list-item@2.1.0: - dependencies: - devlop: 1.1.0 - micromark-factory-space: 2.0.1 - micromark-util-character: 2.1.1 - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-extension-gfm@3.0.0: - dependencies: - micromark-extension-gfm-autolink-literal: 2.1.0 - micromark-extension-gfm-footnote: 2.1.0 - micromark-extension-gfm-strikethrough: 2.1.0 - micromark-extension-gfm-table: 2.1.2 - micromark-extension-gfm-tagfilter: 2.0.0 - micromark-extension-gfm-task-list-item: 2.1.0 - micromark-util-combine-extensions: 2.0.1 - micromark-util-types: 2.0.2 - micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -9674,17 +9490,6 @@ snapshots: '@types/hast': 3.0.5 hast-util-sanitize: 5.0.2 - remark-gfm@4.0.1(supports-color@7.2.0): - dependencies: - '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0(supports-color@7.2.0) - micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0(supports-color@7.2.0) - remark-stringify: 11.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - supports-color - remark-parse@11.0.0(supports-color@7.2.0): dependencies: '@types/mdast': 4.0.4 @@ -9702,12 +9507,6 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 - remark-stringify@11.0.0: - dependencies: - '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.2 - unified: 11.0.5 - resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index d179f737f..e29c5ba74 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -72,12 +72,10 @@ const LazyReactMarkdown = lazy(async () => { { default: ReactMarkdown }, { default: rehypeRaw }, { default: rehypeSanitize, defaultSchema }, - { default: remarkGfm }, ] = await Promise.all([ import('react-markdown'), import('rehype-raw'), import('rehype-sanitize'), - import('remark-gfm'), ]) const sanitizeSchema = { @@ -104,7 +102,6 @@ const LazyReactMarkdown = lazy(async () => { default: (props: ReactMarkdownOptions) => ( ), From 633f166e46920d6e6dd9b14a25f40760d5b32adf Mon Sep 17 00:00:00 2001 From: nrenvo Date: Sun, 1 Nov 2026 12:40:42 +0000 Subject: [PATCH 45/51] ci: trigger rust checks when their configuration changes (#8487) --- .github/workflows/lint-clippy.yml | 1 + .github/workflows/rustfmt.yml | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.github/workflows/lint-clippy.yml b/.github/workflows/lint-clippy.yml index 8a623f515..75983ef23 100644 --- a/.github/workflows/lint-clippy.yml +++ b/.github/workflows/lint-clippy.yml @@ -8,6 +8,7 @@ on: - "Cargo.toml" - "Cargo.lock" - "rust-toolchain.toml" + - ".cargo/config.toml" - ".github/workflows/lint-clippy.yml" workflow_dispatch: env: diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml index 1ae6035dd..99df739eb 100644 --- a/.github/workflows/rustfmt.yml +++ b/.github/workflows/rustfmt.yml @@ -9,6 +9,8 @@ on: paths: - "src-tauri/**" - "**/*.rs" + - "rustfmt.toml" + - "rust-toolchain.toml" - ".github/workflows/rustfmt.yml" permissions: From 693f2f7ac74a4764237971723970d348b8ca2783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9D=A4=E6=98=AF=E7=BA=B1=E9=9B=BE=E9=85=B1=E5=93=9F?= =?UTF-8?q?=EF=BD=9E?= <49941141+Dragon1573@users.noreply.github.com> Date: Sun, 1 Nov 2026 13:08:26 +0000 Subject: [PATCH 46/51] chore(deps): update npm dependencies (#8490) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 4 ++-- pnpm-lock.yaml | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 3c4aa0f70..53eed07ba 100644 --- a/package.json +++ b/package.json @@ -122,7 +122,7 @@ "https-proxy-agent": "^9.1.0", "husky": "^9.1.7", "jiti": "^2.7.0", - "knip": "6.43.0", + "knip": "6.44.1", "lint-staged": "^17.5.1", "sass": "^1.104.1", "tar": "^7.5.22", @@ -134,7 +134,7 @@ "vite-plugin-svgr": "^5.2.0", "vitest": "^5.0.1", "webdriverio": "10.1.0", - "ws": "8.22.0" + "ws": "8.22.1" }, "lint-staged": { "*.{ts,tsx,js,mjs}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c8dff462..a38283819 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -399,8 +399,8 @@ importers: specifier: ^2.7.0 version: 2.7.0 knip: - specifier: 6.43.0 - version: 6.43.0 + specifier: 6.44.1 + version: 6.44.1 lint-staged: specifier: ^17.5.1 version: 17.5.1 @@ -435,8 +435,8 @@ importers: specifier: 10.1.0 version: 10.1.0(supports-color@7.2.0)(yauzl@2.10.0) ws: - specifier: 8.22.0 - version: 8.22.0 + specifier: 8.22.1 + version: 8.22.1 packages: @@ -3861,8 +3861,8 @@ packages: keyv@5.6.0: resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} - knip@6.43.0: - resolution: {integrity: sha512-8l3cCOg/EXoILO0qcSoMra49yQM91+lEIR4fu5Ko8rrArGX8xKdKZiGI9psPiY6rhLmfLERcMfFEByF0wli5CA==} + knip@6.44.1: + resolution: {integrity: sha512-Hbd8nvkIsNgFfXgC7Jsb5LHv7jj6VwA3tmfHd53XPX2z9Vc9RnAZqV0BIiP4OrtYqKwCoLseFHQZerb1bNlbSw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -5074,8 +5074,8 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - ws@8.22.0: - resolution: {integrity: sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==} + ws@8.22.1: + resolution: {integrity: sha512-oyL6OPYme/CmiFvNuGzeoxCJ4nOrZE2O1FwM9MWM1AsGri6nLXB2pg+lNA8aLeXMSav0J6eIVS1j6vUPzUtK9g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -8701,7 +8701,7 @@ snapshots: dependencies: '@keyv/serialize': 1.1.1 - knip@6.43.0: + knip@6.44.1: dependencies: formatly: 0.11.4 get-tsconfig: 4.14.3 @@ -10023,7 +10023,7 @@ snapshots: deepmerge-ts: 8.0.2 https-proxy-agent: 9.1.0(supports-color@7.2.0) undici: 8.11.2 - ws: 8.22.0 + ws: 8.22.1 transitivePeerDependencies: - bufferutil - kerberos @@ -10091,7 +10091,7 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 - ws@8.22.0: {} + ws@8.22.1: {} y18n@5.0.8: {} From 07ee0a602a8a1035ab8240aad7d5ba00742499e4 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Sun, 1 Nov 2026 21:21:05 +0800 Subject: [PATCH 47/51] ci(release): unify RC and stable publication through dev --- .github/workflows/prepare-release.yml | 79 +++ .github/workflows/release.yml | 187 ++++--- eslint.config.ts | 8 + scripts-workflow/release.mjs | 761 ++++++++++++++++++++++++++ 4 files changed, 965 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/prepare-release.yml create mode 100644 scripts-workflow/release.mjs diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml new file mode 100644 index 000000000..912b7c821 --- /dev/null +++ b/.github/workflows/prepare-release.yml @@ -0,0 +1,79 @@ +name: Prepare Release + +on: + pull_request_target: + types: [edited, closed] + branches: [dev] + workflow_dispatch: + inputs: + channel: + description: Release channel + required: true + type: choice + options: [rc, stable] + default: rc + release_tag: + description: 'Optional tag: vX.Y.Z-rc[.N] for RC, vX.Y.Z for stable' + required: false + type: string + +permissions: + contents: write + pull-requests: write + +concurrency: + group: prepare-release + cancel-in-progress: false + +jobs: + prepare: + if: >- + ${{ github.event_name == 'workflow_dispatch' || + (github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login == 'github-actions[bot]' && + ((startsWith(github.event.pull_request.head.ref, 'release/v') && + ((github.event.action == 'closed' && github.event.pull_request.merged == true) || + (github.event.changes.body && github.event.pull_request.state == 'open' && + contains(github.event.pull_request.body, '[x] Refresh this PR from latest dev')))) || + (startsWith(github.event.pull_request.head.ref, 'next-dev/v') && + github.event.changes.body && github.event.pull_request.state == 'open' && + (contains(github.event.pull_request.body, '[x] Refresh this PR from latest dev') || + contains(github.event.pull_request.body, '[x] Use the next minor version') != + contains(github.event.changes.body.from, '[x] Use the next minor version'))))) }} + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Require the dev branch + if: github.event_name == 'workflow_dispatch' + env: + SOURCE_REF: ${{ github.ref }} + run: | + if [[ "$SOURCE_REF" != 'refs/heads/dev' ]]; then + echo '::error::Run Prepare Release from the dev branch.' + exit 1 + fi + + - uses: actions/checkout@v7 + with: + ref: refs/heads/dev + fetch-depth: 0 + + - uses: actions/setup-node@v7 + with: + node-version: '26.13.0' + + - uses: pnpm/action-setup@v6 + if: github.event.action != 'closed' + with: + run_install: false + + - run: pnpm install --frozen-lockfile --ignore-scripts + if: github.event.action != 'closed' + + - name: Create or refresh version or publication PR + env: + RELEASE_TAG: ${{ inputs.release_tag }} + RELEASE_CHANNEL: ${{ inputs.channel }} + RELEASE_COMMAND: ${{ github.event.action == 'closed' && 'publish-pr' || startsWith(github.event.pull_request.head.ref, 'next-dev/') && 'next-dev' || 'prepare' }} + run: node scripts-workflow/release.mjs "$RELEASE_COMMAND" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16c8aaed4..335f99087 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,9 @@ name: Release Build on: + pull_request: + types: [edited] + branches: [main] push: # -rc tag 时预览发布, 跳过 telegram 通知、跳过 winget 提交、跳过 latest.json 文件更新 tags: @@ -11,75 +14,59 @@ env: RUST_BACKTRACE: short HUSKY: 0 concurrency: - group: '${{ github.workflow }} - ${{ github.head_ref || github.ref }}' - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + group: release-publication + cancel-in-progress: false jobs: - check_tag_version: - name: Check Release Tag and package.json Version Consistency + prepare_release: + name: Approve Snapshot and Prepare Draft Release + if: >- + ${{ github.event_name == 'push' || + (github.event.changes.body && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login == 'github-actions[bot]' && + startsWith(github.event.pull_request.head.ref, 'publish/v') && + contains(github.event.pull_request.body, '[x] Publish this approved release')) }} runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: read + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + outputs: + release_tag: ${{ steps.source.outputs.release_tag }} + source_sha: ${{ steps.source.outputs.source_sha }} + channel: ${{ steps.source.outputs.channel }} + release_id: ${{ steps.draft.outputs.id }} steps: - - name: Checkout repository + - name: Checkout trusted release tooling uses: actions/checkout@v7 with: + ref: refs/heads/dev fetch-depth: 0 - - - name: Check if tag is from main branch - run: | - TAG_REF="${GITHUB_REF##*/}" - echo "Checking if tag $TAG_REF is from main branch..." - - TAG_COMMIT=$(git rev-list -n 1 $TAG_REF) - MAIN_COMMITS=$(git rev-list origin/main) - - if echo "$MAIN_COMMITS" | grep -q "$TAG_COMMIT"; then - echo "✅ Tag $TAG_REF is from main branch" - else - echo "❌ Tag $TAG_REF is not from main branch" - echo "This release workflow only accepts tags from main branch." - exit 1 - fi - - - name: Check tag and package.json version - run: | - TAG_REF="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" - echo "Current tag: $TAG_REF" - - PKG_VERSION=$(jq -r .version package.json) - echo "package.json version: $PKG_VERSION" - - EXPECTED_TAG="v$PKG_VERSION" - - if [[ "$TAG_REF" != "$EXPECTED_TAG" ]]; then - echo "❌ Version mismatch:" - echo " Git tag : $TAG_REF" - echo " package.json : $EXPECTED_TAG" - exit 1 - fi - - echo "✅ Tag and package.json version are consistent." - - prepare_release: - name: Prepare Draft Release - runs-on: ubuntu-latest - needs: [check_tag_version] - outputs: - release_id: ${{ steps.prepare_release.outputs.id }} - steps: - - name: Create or update draft release - id: prepare_release - uses: softprops/action-gh-release@v3 + - uses: actions/setup-node@v7 with: - tag_name: ${{ github.ref_name }} - target_commitish: ${{ github.sha }} - name: 'Clash Verge Rev ${{ github.ref_name }}' - body: 'Draft release, will be updated later.' - token: ${{ secrets.GITHUB_TOKEN }} - draft: true - prerelease: ${{ contains(github.ref_name, '-rc') }} + node-version: '26.13.0' + - name: Validate source and publish the approved snapshot + id: source + env: + RELEASE_COMMAND: ${{ github.event_name == 'push' && 'validate-tag' || 'publish' }} + run: node scripts-workflow/release.mjs "$RELEASE_COMMAND" + + - name: Create or reuse draft release + id: draft + env: + RELEASE_TAG: ${{ steps.source.outputs.release_tag }} + SOURCE_SHA: ${{ steps.source.outputs.source_sha }} + run: node scripts-workflow/release.mjs prepare-draft update_tag: - name: Update tag + name: Publish Completed Release + env: + RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + outputs: + source_sha: ${{ needs.prepare_release.outputs.source_sha }} + channel: ${{ needs.prepare_release.outputs.channel }} runs-on: ubuntu-latest needs: [ @@ -91,6 +78,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare_release.outputs.source_sha }} - name: Group changelog platforms run: bash ./scripts-workflow/group_platforms.sh @@ -103,7 +92,7 @@ jobs: - name: Set Env run: | echo "BUILDTIME=$(TZ=Asia/Shanghai date)" >> $GITHUB_ENV - TAG_REF="${GITHUB_REF##*/}" + TAG_REF="$RELEASE_TAG" echo "TAG_NAME=$TAG_REF" >> $GITHUB_ENV VERSION=$(echo "$TAG_REF" | sed 's/^v//') echo "VERSION=$VERSION" >> $GITHUB_ENV @@ -149,10 +138,16 @@ jobs: Created at ${{ env.BUILDTIME }}. EOF + - name: Verify release tag still points to the approved source + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SOURCE_SHA: ${{ needs.prepare_release.outputs.source_sha }} + run: node scripts-workflow/release.mjs verify-target + - name: Publish Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PRERELEASE: ${{ contains(github.ref_name, '-rc') }} + PRERELEASE: ${{ contains(env.RELEASE_TAG, '-rc') }} run: | release_id="${{ needs.prepare_release.outputs.release_id }}" @@ -181,6 +176,8 @@ jobs: release: name: Release Build needs: [prepare_release] + env: + RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} strategy: fail-fast: false matrix: @@ -200,6 +197,8 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare_release.outputs.source_sha }} - name: Install Rust Stable uses: dtolnay/rust-toolchain@master @@ -249,7 +248,6 @@ jobs: echo "Rust target ${{ matrix.target }} installed." - name: Tauri build - # 上游 5.24 修改了 latest.json 的生成逻辑,且依赖 tauri-plugin-update 2.10.0 暂未发布,故锁定在 0.5.23 版本 uses: tauri-apps/tauri-action@v1.0.0 env: NODE_OPTIONS: '--max_old_space_size=4096' @@ -264,11 +262,14 @@ jobs: APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} with: releaseId: ${{ needs.prepare_release.outputs.release_id }} - tagName: ${{ github.ref_name }} + tagName: ${{ env.RELEASE_TAG }} releaseBody: 'Draft release, will be updated later.' + releaseDraft: true + prerelease: ${{ needs.prepare_release.outputs.channel == 'rc' }} + releaseCommitish: ${{ needs.prepare_release.outputs.source_sha }} tauriScript: pnpm args: --target ${{ matrix.target }} - includeUpdaterJson: true + uploadUpdaterJson: ${{ needs.prepare_release.outputs.channel == 'stable' }} - name: Attest Windows bundles if: matrix.os == 'windows-latest' @@ -293,6 +294,8 @@ jobs: release-for-linux-arm: name: Release Build for Linux ARM needs: [prepare_release] + env: + RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} strategy: fail-fast: false matrix: @@ -307,6 +310,8 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare_release.outputs.source_sha }} - name: Install Rust Stable uses: dtolnay/rust-toolchain@master @@ -420,7 +425,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh release upload "${{ github.ref_name }}" \ + gh release upload "$RELEASE_TAG" \ target/${{ matrix.target }}/release/bundle/deb/*.deb \ target/${{ matrix.target }}/release/bundle/rpm/*.rpm \ --clobber @@ -428,6 +433,8 @@ jobs: release-for-fixed-webview2: name: Release Build for Fixed WebView2 needs: [prepare_release] + env: + RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} strategy: fail-fast: false matrix: @@ -442,6 +449,8 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v7 + with: + ref: ${{ needs.prepare_release.outputs.source_sha }} - name: Install Rust Stable uses: dtolnay/rust-toolchain@master @@ -532,17 +541,50 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | $files = Get-ChildItem "target\${{ matrix.target }}\release\bundle\nsis\*setup*" | Select-Object -ExpandProperty FullName - gh release upload "${{ github.ref_name }}" $files --clobber + gh release upload "$env:RELEASE_TAG" $files --clobber if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + prepare-next-dev: + name: Prepare Next Dev Version + if: ${{ needs.update_tag.outputs.channel == 'stable' }} + needs: [prepare_release, update_tag] + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + concurrency: + group: prepare-release + cancel-in-progress: false + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + RELEASE_TAG: ${{ needs.prepare_release.outputs.release_tag }} + SOURCE_SHA: ${{ needs.prepare_release.outputs.source_sha }} + steps: + - uses: actions/checkout@v7 + with: + ref: refs/heads/dev + fetch-depth: 0 + - uses: actions/setup-node@v7 + with: + node-version: '26.13.0' + - uses: pnpm/action-setup@v6 + with: + run_install: false + - name: Prepare version and changelog PR + run: | + pnpm install --frozen-lockfile --ignore-scripts + node scripts-workflow/release.mjs next-dev + release-update: - if: ${{ !contains(github.ref_name, '-rc') }} + if: ${{ needs.update_tag.outputs.channel == 'stable' }} name: Release Update runs-on: ubuntu-latest needs: [update_tag] steps: - name: Checkout repository uses: actions/checkout@v7 + with: + ref: ${{ needs.update_tag.outputs.source_sha }} - name: Install Node uses: actions/setup-node@v7 @@ -563,12 +605,14 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release-update-for-fixed-webview2: - if: ${{ !contains(github.ref_name, '-rc') }} + if: ${{ needs.update_tag.outputs.channel == 'stable' }} runs-on: ubuntu-latest needs: [update_tag] steps: - name: Checkout repository uses: actions/checkout@v7 + with: + ref: ${{ needs.update_tag.outputs.source_sha }} - name: Install Node uses: actions/setup-node@v7 @@ -589,7 +633,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} submit-to-winget: - if: ${{ !contains(github.ref_name, '-rc') }} + if: ${{ needs.update_tag.outputs.channel == 'stable' }} name: Submit to Winget runs-on: ubuntu-latest needs: [update_tag, release-update] @@ -597,6 +641,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 with: + ref: ${{ needs.update_tag.outputs.source_sha }} fetch-depth: 0 - name: Get Version run: | @@ -613,7 +658,7 @@ jobs: token: ${{ secrets.WINGET_TOKEN }} notify-telegram: - if: ${{ !contains(github.ref_name, '-rc') }} + if: ${{ needs.update_tag.outputs.channel == 'stable' }} name: Notify Telegram runs-on: ubuntu-latest needs: @@ -626,6 +671,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v7 + with: + ref: ${{ needs.update_tag.outputs.source_sha }} - name: Group changelog platforms run: bash ./scripts-workflow/group_platforms.sh diff --git a/eslint.config.ts b/eslint.config.ts index 1e1840416..2f3aa9dc0 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -129,6 +129,7 @@ export default defineConfig([ 'eslint.config.ts', `vite.config.mts`, 'scripts/*.mjs', + 'scripts-workflow/*.mjs', 'tests/*.ts', 'src/polyfills/*.js', 'tests/*.mjs', @@ -238,6 +239,13 @@ export default defineConfig([ }, }, }, + { + files: ['scripts-workflow/*.mjs'], + languageOptions: { + globals: globals.node, + }, + rules: { 'es-x/no-object-groupby': 'off' }, + }, { files: ['scripts/perf/*.{mjs,mts}'], languageOptions: { diff --git a/scripts-workflow/release.mjs b/scripts-workflow/release.mjs new file mode 100644 index 000000000..ed0a63c0d --- /dev/null +++ b/scripts-workflow/release.mjs @@ -0,0 +1,761 @@ +import { execFileSync } from 'node:child_process' +import { appendFileSync, readFileSync, writeFileSync } from 'node:fs' + +const env = process.env +const repository = env.GITHUB_REPOSITORY +const refreshLabel = 'Refresh this PR from latest dev' +const publishLabel = 'Publish this approved release' +const minorLabel = 'Use the next minor version' +const checked = (body, label = refreshLabel) => + (body || '') + .split('\n') + .some((line) => line.toLowerCase() === `- [x] ${label}`.toLowerCase()) +const versionPattern = '(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)' +const rcPattern = new RegExp(`^v${versionPattern}-rc(?:\\.(0|[1-9][0-9]*))?$`) +const stablePattern = new RegExp(`^v${versionPattern}$`) +const versionFiles = [ + 'package.json', + 'src-tauri/tauri.conf.json', + 'src-tauri/Cargo.toml', + 'Cargo.lock', +] +const read = (path) => readFileSync(path, 'utf8') +const json = (path) => JSON.parse(read(path)) +const run = (command, args, options = {}) => + execFileSync(command, args, { encoding: 'utf8', ...options }).trim() +const git = (...args) => run('git', args) +const gitFile = (sha, path) => + execFileSync('git', ['show', `${sha}:${path}`], { encoding: 'utf8' }) +const isRc = (tag) => tag.match(rcPattern)?.[0] === tag +const isRelease = (tag) => isRc(tag) || stablePattern.test(tag) +const titleFor = (tag) => `${isRc(tag) ? 'Pre-Release' : 'Release'}: ${tag}` +const branchTag = (branch, prefix = 'release/') => + branch.startsWith(prefix) && isRelease(branch.slice(prefix.length)) + ? branch.slice(prefix.length) + : undefined +const latest = (tags) => + tags.sort((a, b) => a.localeCompare(b, 'en', { numeric: true })).at(-1) +const summary = (text) => appendFileSync(env.GITHUB_STEP_SUMMARY, `${text}\n`) +const endpoint = (path) => + path === 'graphql' ? path : `repos/${repository}/${path}` +const list = (path) => + JSON.parse(run('gh', ['api', '--paginate', '--slurp', endpoint(path)])).flat() + +function api(path, body, method = 'POST') { + const response = run( + 'gh', + ['api', '--method', method, endpoint(path), '--input', '-'], + { + input: JSON.stringify(body), + }, + ) + const result = response ? JSON.parse(response) : null + if (result?.errors) throw new Error(JSON.stringify(result.errors)) + return result +} + +const get = (path) => JSON.parse(run('gh', ['api', endpoint(path)])) +const canWrite = (login) => + ['write', 'maintain', 'admin'].includes( + get(`collaborators/${login}/permission`).permission, + ) + +function output(values) { + appendFileSync( + env.GITHUB_OUTPUT, + Object.entries(values) + .map(([key, value]) => `${key}=${value}\n`) + .join(''), + ) +} + +function versionPatternFor(path) { + if (path.endsWith('.json')) return /("version"\s*:\s*")[^"]+/ + return path === 'Cargo.lock' + ? /(\[\[package\]\]\r?\nname = "clash-verge"\r?\nversion = ")[^"]+/ + : /(^version\s*=\s*")[^"]+/m +} + +function validateVersions(tag, sha) { + for (const path of versionFiles) { + const content = sha ? gitFile(sha, path) : read(path) + const version = path.endsWith('.json') + ? JSON.parse(content).version + : content.match(versionPatternFor(path))?.[0].split('"').at(-1) + if (`v${version}` !== tag) + throw new Error(`${path} version does not match ${tag}.`) + } +} + +function validateVersionCommit(tag, sha) { + validateVersions(tag, sha) + const commit = get(`commits/${sha}`) + if (!commit.commit.verification.verified || commit.parents.length !== 1) + throw new Error('The version commit must be signed and have one parent.') + const title = commit.commit.message.split('\n')[0] + if (title.replace(/ \(#\d+\)$/, '') !== titleFor(tag)) + throw new Error('The final version commit has an unexpected title.') + const parent = commit.parents[0].sha + const changed = git('diff', '--name-only', parent, sha) + .split('\n') + .filter(Boolean) + if ( + changed.some((path) => !versionFiles.includes(path)) || + git('diff', '--summary', parent, sha) + ) + throw new Error( + 'The version commit changes files outside the four version files.', + ) + for (const path of versionFiles) { + const before = gitFile(parent, path) + const after = gitFile(sha, path) + if ( + before.replace(versionPatternFor(path), '$1VERSION') !== + after.replace(versionPatternFor(path), '$1VERSION') + ) + throw new Error(`${path} contains changes other than its version.`) + } +} + +function remoteRefs() { + return git('ls-remote', '--heads', '--tags', '--refs', 'origin') + .split('\n') + .filter(Boolean) + .map((line) => line.split('\t')) +} + +function fetchBranches() { + git( + 'fetch', + '--no-tags', + 'origin', + '+refs/heads/dev:refs/remotes/origin/dev', + '+refs/heads/main:refs/remotes/origin/main', + ) +} + +function prepare() { + let refresh + if (env.GITHUB_EVENT_NAME === 'pull_request_target') { + const event = json(env.GITHUB_EVENT_PATH) + if (checked(event.changes?.body?.from) || !checked(event.pull_request.body)) + return + if (!canWrite(event.sender.login)) + throw new Error('Refreshing a version PR requires write access.') + refresh = get(`pulls/${event.pull_request.number}`) + if (refresh.state !== 'open' || !checked(refresh.body)) return + requireBotPr(refresh, 'dev', 'release/') + } + fetchBranches() + git('checkout', '--detach', 'origin/dev') + const channel = refresh + ? isRc(branchTag(refresh.head.ref)) + ? 'rc' + : 'stable' + : env.RELEASE_CHANNEL || 'rc' + if (!['rc', 'stable'].includes(channel)) + throw new Error('Unknown release channel.') + const requested = refresh + ? branchTag(refresh.head.ref) + : env.RELEASE_TAG || '' + if ( + requested && + !(channel === 'rc' ? isRc(requested) : stablePattern.test(requested)) + ) + throw new Error( + channel === 'rc' + ? 'Expected vX.Y.Z-rc or vX.Y.Z-rc.N.' + : 'Expected vX.Y.Z.', + ) + const existing = + !refresh && + list('pulls?state=open&base=dev&per_page=100').find( + (pr) => + pr.head.repo?.full_name === repository && + pr.user.login === 'github-actions[bot]' && + branchTag(pr.head.ref) && + isRc(branchTag(pr.head.ref)) === (channel === 'rc') && + (!requested || branchTag(pr.head.ref) === requested), + ) + if (existing) { + api(`issues/${existing.number}/labels`, { labels: ['release'] }) + output({ existing_pr: existing.html_url }) + summary( + `Existing version PR: ${existing.html_url}\nClose it before preparing a replacement; its candidate is unchanged.`, + ) + return + } + const base = (requested || `v${json('package.json').version}`).split( + /[-+]/, + )[0] + if (!stablePattern.test(base)) + throw new Error(`Invalid base version: ${base}`) + const refs = remoteRefs().map(([, ref]) => ref) + const tags = refs + .filter((ref) => ref.startsWith('refs/tags/')) + .map((ref) => ref.slice(10)) + const reserved = refs + .filter((ref) => ref.startsWith('refs/heads/')) + .filter((ref) => !refresh || ref !== `refs/heads/${refresh.head.ref}`) + .map( + (ref) => branchTag(ref.slice(11)) || branchTag(ref.slice(11), 'publish/'), + ) + .filter(Boolean) + const sameBase = (tag) => isRc(tag) && tag.startsWith(`${base}-rc`) + const occupied = [...tags, ...reserved] + const last = latest(occupied.filter(sameBase)) + const tag = + requested || + (channel === 'stable' + ? base + : last + ? `${base}-rc.${BigInt(last.match(rcPattern)[4] ?? 0) + 1n}` + : `${base}-rc`) + if (occupied.includes(tag)) + throw new Error(`Tag ${tag} already exists or is reserved.`) + const sha = git('rev-parse', 'HEAD') + const body = releaseBody(tag, sha, tags, false) + summary(body) + if ( + remoteRefs().some( + ([, ref]) => + ref === `refs/tags/${tag}` || ref === `refs/heads/publish/${tag}`, + ) + ) + throw new Error(`Tag ${tag} already exists or is reserved for publication.`) + if (json('package.json').version !== tag.slice(1)) + run('node', ['scripts/release-version.mjs', tag]) + validateVersions(tag) + const title = titleFor(tag) + const branch = refresh + ? `release-refresh/${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` + : `release/${tag}` + writeVersionPr({ sha, body, refresh, title, branch, files: versionFiles }) +} + +function releaseBody(tag, sha, tags, publishing) { + const previous = + (isRc(tag) && + latest( + tags.filter( + (value) => + isRc(value) && + value.startsWith(`${tag.split('-')[0]}-rc`) && + value !== tag, + ), + )) || + latest(tags.filter((value) => stablePattern.test(value) && value !== tag)) + const url = `${env.GITHUB_SERVER_URL}/${repository}` + let range = sha + let comparison = 'No previous release tag' + if (previous) { + git('fetch', '--no-tags', 'origin', `refs/tags/${previous}`) + range = `${git('rev-parse', 'FETCH_HEAD^{commit}')}..${sha}` + comparison = `Changes since **${previous}** · [Compare changes](${url}/compare/${previous}...${sha})` + } + comparison += ` · **${git('rev-list', '--count', range)} commits**` + const commits = git('log', '--format=%h %s', range) + const groups = Object.groupBy(commits ? commits.split('\n') : [], (line) => { + if (/^\S+ chore\(deps\)!?: /i.test(line)) return 'deps' + const type = line.match(/^\S+ (\w+)(?:\([^)]*\))?!?: /)?.[1]?.toLowerCase() + return type === 'doc' ? 'docs' : type || 'other' + }) + const labels = { + fix: 'Fixes', + feat: 'Features', + perf: 'Performance', + revert: 'Reverts', + refactor: 'Refactoring', + build: 'Build', + deps: 'Dependencies', + ci: 'CI', + docs: 'Documentation', + test: 'Tests', + chore: 'Maintenance', + style: 'Style', + other: 'Other', + } + const names = { + macos: 'macOS', + windows: 'Windows', + ui: 'UI', + ci: 'CI', + tun: 'TUN', + } + const order = Object.keys(labels) + const rank = (type) => + order.includes(type) ? order.indexOf(type) : order.length + const scope = + Object.entries(groups) + .sort(([a], [b]) => rank(a) - rank(b) || a.localeCompare(b)) + .map(([type, lines]) => { + const entries = lines.map((line) => { + const hash = line.slice(0, line.indexOf(' ')) + const title = line + .slice(hash.length + 1) + .replace(/\s*\((?:[\w.-]+\/[\w.-]+)?#\d+\)/g, '') + .replace(/(?:[\w.-]+\/[\w.-]+)?#\d+\b/g, '') + .replace(/\s{2,}/g, ' ') + .trim() + .replace( + /^\w+(?:\(([^)]*)\))?(!)?:\s*/, + (_, scope, breaking) => + `${breaking ? 'Breaking: ' : ''}${scope ? `${Object.hasOwn(names, scope) ? names[scope] : scope}: ` : ''}`, + ) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replace(/[\\`*_[\]]/g, '\\$&') + return `- ${title} [#${hash}](${url}/commit/${hash})` + }) + return `### ${Object.hasOwn(labels, type) ? labels[type] : type} · ${lines.length}\n\n${entries.join('\n')}` + }) + .join('\n\n') || 'No new commits.' + return `## ${titleFor(tag)} + +Source: \`dev\` · [${sha}](${url}/commit/${sha}) \n${comparison} +${ + publishing + ? ` + +After approval, select the checkbox below to publish. +Do not use the normal Squash or Rebase merge buttons. + +- [ ] ${publishLabel}` + : ` +- [ ] ${refreshLabel} + +Refreshing requires a new approval.` +} + +## Changes + +${scope} +` +} + +function writeVersionPr({ sha, body, refresh, title, branch, files }) { + // Create-only ref and expectedHeadOid prevent replacing another candidate. + api('git/refs', { ref: `refs/heads/${branch}`, sha }) + let pr + try { + const commit = api('graphql', { + query: `mutation($input: CreateCommitOnBranchInput!) { + createCommitOnBranch(input: $input) { commit { oid } } + }`, + variables: { + input: { + branch: { repositoryNameWithOwner: repository, branchName: branch }, + expectedHeadOid: sha, + message: { headline: title }, + fileChanges: { + additions: files.map((path) => ({ + path, + contents: readFileSync(path).toString('base64'), + })), + }, + }, + }, + }) + if (!commit.data?.createCommitOnBranch?.commit?.oid) + throw new Error('Signed version commit failed.') + if (refresh) { + const current = get(`pulls/${refresh.number}`) + if ( + current.state !== 'open' || + current.head.sha !== refresh.head.sha || + current.base.ref !== refresh.base.ref || + current.body !== refresh.body + ) + throw new Error( + 'The version pull request changed while preparing the update.', + ) + const oid = commit.data.createCommitOnBranch.commit.oid + updateRefs(refresh.base.repo.node_id, [ + { + name: `refs/heads/${refresh.head.ref}`, + beforeOid: refresh.head.sha, + afterOid: oid, + force: true, + }, + { + name: `refs/heads/${branch}`, + beforeOid: oid, + afterOid: '0'.repeat(40), + }, + ]) + for (const review of list( + `pulls/${refresh.number}/reviews?per_page=100`, + )) { + if (review.state === 'APPROVED') + api( + `pulls/${refresh.number}/reviews/${review.id}/dismissals`, + { + message: + 'The version PR was refreshed from dev; please review again.', + }, + 'PUT', + ) + } + pr = api(`pulls/${refresh.number}`, { title, body }, 'PATCH') + } else { + pr = api('pulls', { + head: branch, + base: 'dev', + title, + body, + }) + } + } catch (error) { + if (refresh) { + try { + api(`git/refs/heads/${branch}`, {}, 'DELETE') + } catch {} + } + throw error + } + api(`issues/${pr.number}/labels`, { labels: ['release'] }) + summary(`Version pull request: ${pr.html_url}`) +} + +function updateRefs(repositoryId, refUpdates) { + api('graphql', { + query: `mutation($input: UpdateRefsInput!) { updateRefs(input: $input) { clientMutationId } }`, + variables: { input: { repositoryId, refUpdates } }, + }) +} + +function requireBotPr(pr, base, prefix) { + const tag = branchTag(pr.head.ref, prefix) + if ( + !tag || + pr.base.ref !== base || + pr.base.repo.full_name !== repository || + pr.head.repo?.full_name !== repository || + pr.user.login !== 'github-actions[bot]' + ) + throw new Error( + 'Expected a release PR created by this repository workflow.', + ) + return tag +} + +function publishPr() { + const event = json(env.GITHUB_EVENT_PATH) + const pr = get(`pulls/${event.pull_request.number}`) + const tag = requireBotPr(pr, 'dev', 'release/') + if (!pr.merged || pr.merge_commit_sha !== event.pull_request.merge_commit_sha) + throw new Error('Expected the final merged version commit on dev.') + const sha = pr.merge_commit_sha + fetchBranches() + git('fetch', '--no-tags', 'origin', sha) + git('merge-base', '--is-ancestor', sha, 'origin/dev') + validateVersionCommit(tag, sha) + const branch = `publish/${tag}` + const existing = list( + `pulls?state=all&base=main&head=${repository.split('/')[0]}:${branch}&per_page=100`, + ) + if (existing.length) { + const publication = existing[0] + requireBotPr(publication, 'main', 'publish/') + if (publication.head.sha !== sha) + throw new Error( + 'This tag is already reserved by a different publication snapshot.', + ) + summary(`Existing publication PR: ${publication.html_url}`) + return + } + const refs = remoteRefs() + if (refs.some(([, ref]) => ref === `refs/tags/${tag}`)) + throw new Error(`Tag ${tag} already exists.`) + git('merge-base', '--is-ancestor', 'origin/main', sha) + const head = refs.find(([, ref]) => ref === `refs/heads/${branch}`)?.[0] + if (head && head !== sha) + throw new Error('Publication branch already points to another commit.') + if (!head) api('git/refs', { ref: `refs/heads/${branch}`, sha }) + const body = releaseBody( + tag, + sha, + refs + .filter(([, ref]) => ref.startsWith('refs/tags/')) + .map(([, ref]) => ref.slice(10)), + true, + ) + const publication = api('pulls', { + head: branch, + base: 'main', + title: titleFor(tag), + body, + }) + api(`issues/${publication.number}/labels`, { labels: ['release'] }) + summary(`Publication PR: ${publication.html_url}\n\n${body}`) +} + +function requireApproval(pr, sha) { + const reviews = new Map() + for (const review of list(`pulls/${pr.number}/reviews?per_page=100`)) { + if (['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(review.state)) + reviews.set(review.user.login, review) + } + let approved = false + for (const review of reviews.values()) { + if (!canWrite(review.user.login)) continue + if (review.state === 'CHANGES_REQUESTED') + throw new Error('A reviewer with write access has requested changes.') + if (review.state === 'APPROVED' && review.commit_id === sha) approved = true + } + if (!approved) + throw new Error( + 'The current PR head needs approval from a reviewer with write access.', + ) +} + +function releaseOutput(tag, sha) { + output({ + release_tag: tag, + source_sha: sha, + channel: isRc(tag) ? 'rc' : 'stable', + }) +} + +function publish() { + const event = json(env.GITHUB_EVENT_PATH) + const tag = requireBotPr(event.pull_request, 'main', 'publish/') + if ( + checked(event.changes?.body?.from, publishLabel) || + !checked(event.pull_request.body, publishLabel) + ) + throw new Error('Publishing requires a new checkbox selection.') + if (!canWrite(event.sender.login)) + throw new Error('Publishing requires write access.') + const sha = event.pull_request.head.sha + const pr = get(`pulls/${event.pull_request.number}`) + requireBotPr(pr, 'main', 'publish/') + if ( + pr.head.sha !== sha || + !checked(pr.body, publishLabel) || + !pr.body.includes(``) + ) + throw new Error( + 'The publication snapshot changed after the checkbox selection.', + ) + fetchBranches() + git('fetch', '--no-tags', 'origin', sha) + git('merge-base', '--is-ancestor', sha, 'origin/dev') + validateVersionCommit(tag, sha) + const marker = `Release workflow run: ${env.GITHUB_RUN_ID}\nSource: ${sha}\nPR: ${pr.number}` + const existing = remoteRefs().find(([, ref]) => ref === `refs/tags/${tag}`) + if (existing) { + const ref = get(`git/ref/tags/${tag}`) + const object = + ref.object.type === 'tag' && get(`git/tags/${ref.object.sha}`) + if ( + !object || + object.object.type !== 'commit' || + object.object.sha !== sha || + object.message.trim() !== marker || + Number(env.GITHUB_RUN_ATTEMPT) <= 1 + ) + throw new Error( + 'An existing tag can only be recovered by its original release run.', + ) + git('merge-base', '--is-ancestor', sha, 'origin/main') + summary(`Resuming ${tag} at ${sha} from this release run.`) + } else { + if (pr.state !== 'open' || pr.draft) + throw new Error('Expected an open, ready publication PR.') + requireApproval(pr, sha) + const main = git('rev-parse', 'origin/main') + git('merge-base', '--is-ancestor', main, sha) + const object = api('git/tags', { + tag, + message: marker, + object: sha, + type: 'commit', + }) + // The tag records run ownership in the same transaction as the fast-forward. + updateRefs(pr.base.repo.node_id, [ + { name: 'refs/heads/main', beforeOid: main, afterOid: sha, force: false }, + { + name: `refs/heads/${pr.head.ref}`, + beforeOid: sha, + afterOid: sha, + force: false, + }, + { + name: `refs/tags/${tag}`, + beforeOid: '0'.repeat(40), + afterOid: object.sha, + force: false, + }, + ]) + summary(`Fast-forwarded main and tagged ${tag} at approved commit ${sha}.`) + } + releaseOutput(tag, sha) +} + +function validateTag() { + const tag = env.GITHUB_REF_NAME + if (!isRelease(tag)) throw new Error('Expected a stable or RC release tag.') + fetchBranches() + const sha = git('rev-parse', `${env.GITHUB_SHA}^{commit}`) + const ref = get(`git/ref/tags/${tag}`) + const object = ref.object.type === 'tag' && get(`git/tags/${ref.object.sha}`) + if (object && object.message.includes('Release workflow run:')) + throw new Error( + 'Resume the original publication run for workflow-owned tags.', + ) + git('fetch', '--no-tags', 'origin', `refs/tags/${tag}`) + if (git('rev-parse', 'FETCH_HEAD^{commit}') !== sha) + throw new Error('The tag moved after this run started.') + for (const branch of ['main', 'dev']) + git('merge-base', '--is-ancestor', sha, `origin/${branch}`) + validateVersions(tag, sha) + releaseOutput(tag, sha) +} + +function prepareDraft() { + const tag = env.RELEASE_TAG + verifyTarget() + const release = list('releases?per_page=100').find( + (release) => release.tag_name === tag, + ) + if (release && !release.draft) + throw new Error('This release is already public.') + const draft = + release || + api('releases', { + tag_name: tag, + target_commitish: env.SOURCE_SHA, + name: `Clash Verge Rev ${tag}`, + body: 'Draft release, will be updated later.', + draft: true, + prerelease: isRc(tag), + }) + output({ id: draft.id }) +} + +function verifyTarget(tag = env.RELEASE_TAG, sha = env.SOURCE_SHA) { + const ref = get(`git/ref/tags/${tag}`) + const target = + ref.object.type === 'tag' + ? get(`git/tags/${ref.object.sha}`).object + : ref.object + if (target.type !== 'commit' || target.sha !== sha) + throw new Error('The release tag no longer points to the validated source.') +} + +function nextDev() { + let refresh + let tag = env.RELEASE_TAG + let source = env.SOURCE_SHA + let minor = false + if (env.GITHUB_EVENT_NAME === 'pull_request_target') { + const event = json(env.GITHUB_EVENT_PATH) + const oldBody = event.changes?.body?.from + const body = event.pull_request.body + if ( + checked(oldBody, minorLabel) === checked(body, minorLabel) && + (checked(oldBody) || !checked(body)) + ) + return + if (!canWrite(event.sender.login)) + throw new Error('Changing the next dev version requires write access.') + refresh = get(`pulls/${event.pull_request.number}`) + if (refresh.state !== 'open') return + tag = requireBotPr(refresh, 'dev', 'next-dev/') + if ( + refresh.head.sha !== event.pull_request.head.sha || + refresh.body !== body + ) + throw new Error( + 'The version update PR changed after the checkbox selection.', + ) + minor = checked(body, minorLabel) + source = body.match(//)?.[1] + } + if (!stablePattern.test(tag) || !/^[a-f0-9]{40}$/.test(source)) + throw new Error('The next dev version requires a stable release snapshot.') + verifyTarget(tag, source) + const release = get(`releases/tags/${tag}`) + if (release.draft || release.prerelease) + throw new Error('The stable release must be public before preparing dev.') + const branch = `next-dev/${tag}` + if (!refresh) { + const existing = list( + `pulls?state=all&base=dev&head=${repository.split('/')[0]}:${branch}&per_page=100`, + )[0] + if (existing) { + requireBotPr(existing, 'dev', 'next-dev/') + summary(`Existing dev version PR: ${existing.html_url}`) + return + } + } + fetchBranches() + git('fetch', '--no-tags', 'origin', source) + git('merge-base', '--is-ancestor', source, 'origin/main') + git('merge-base', '--is-ancestor', source, 'origin/dev') + git('checkout', '--detach', 'origin/dev') + const sha = git('rev-parse', 'HEAD') + validateVersions(tag, source) + validateVersions(tag, sha) + if (read('Changelog.md') !== gitFile(source, 'Changelog.md')) + throw new Error( + 'Changelog.md changed after this release. Preserve the unreleased entries and prepare the dev update manually.', + ) + if ( + !read('Changelog.md').split('\n').includes(`## ${tag}`) || + read('docs/Changelog.history.md').split('\n').includes(`## ${tag}`) + ) + throw new Error('The released changelog is missing or already archived.') + const [major, minorVersion, patch] = tag.slice(1).split('.').map(BigInt) + const patchTag = `v${major}.${minorVersion}.${patch + 1n}` + const minorTag = `v${major}.${minorVersion + 1n}.0` + const nextTag = minor ? minorTag : patchTag + run('pnpm', ['release-version', nextTag]) + run('bash', ['scripts-workflow/bump_changelog.sh']) + const changelog = read('Changelog.md') + if (!changelog.includes('## v(Version Goes Here)')) + throw new Error('The changelog template has no version placeholder.') + writeFileSync( + 'Changelog.md', + changelog.replace('## v(Version Goes Here)', `## ${nextTag}`), + ) + validateVersions(nextTag) + const title = `Dev: ${nextTag}` + const body = `## ${title} + +After [${tag}](${env.GITHUB_SERVER_URL}/${repository}/releases/tag/${tag}). + + +Patch: \`${patchTag}\` · Minor: \`${minorTag}\` + +- [${minor ? 'x' : ' '}] ${minorLabel} +- [ ] ${refreshLabel} + +Changing the version or refreshing requires a new approval. +` + writeVersionPr({ + sha, + body, + refresh, + title, + branch: refresh + ? `next-dev-refresh/${env.GITHUB_RUN_ID}-${env.GITHUB_RUN_ATTEMPT}` + : branch, + files: [...versionFiles, 'Changelog.md', 'docs/Changelog.history.md'], + }) +} + +const commands = { + prepare, + 'next-dev': nextDev, + 'publish-pr': publishPr, + publish, + 'validate-tag': validateTag, + 'prepare-draft': prepareDraft, + 'verify-target': verifyTarget, +} +const command = commands[process.argv[2]] +if (!command) throw new Error('Unknown release command.') +command() From a5b22fa512db3934666d6ed6e0742fe145c9e628 Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 2 Nov 2026 03:26:36 +0800 Subject: [PATCH 48/51] fix(webdav): allow clearing saved configuration Close #8495 --- Changelog.md | 1 + scripts-workflow/release.mjs | 83 +++++++++++++------ .../setting/mods/backup-config-viewer.tsx | 31 +++---- src/services/webdav-status.ts | 6 +- 4 files changed, 81 insertions(+), 40 deletions(-) diff --git a/Changelog.md b/Changelog.md index bdf056092..7f4578cec 100644 --- a/Changelog.md +++ b/Changelog.md @@ -13,6 +13,7 @@ - 修复 代理 页展开分组后不显示最新测速结果,需切换节点才刷新 - 修复 代理 在全局模式下无法展开节点组 - 修复 自定义 CSS 无法修改编辑器字体和字号的问题 +- 修复 WebDAV 设置清空后无法保存 **🍎 macOS** diff --git a/scripts-workflow/release.mjs b/scripts-workflow/release.mjs index ed0a63c0d..dc0083fb2 100644 --- a/scripts-workflow/release.mjs +++ b/scripts-workflow/release.mjs @@ -134,6 +134,31 @@ function fetchBranches() { ) } +function discardClosedCandidate(branch, base, prefix) { + const candidates = list( + `pulls?state=all&base=${base}&head=${repository.split('/')[0]}:${branch}&per_page=100`, + ) + const latest = candidates[0] + if (!latest || candidates.some((pr) => pr.state === 'open')) + throw new Error( + `Branch ${branch} is reserved by an active or unknown candidate.`, + ) + const pr = get(`pulls/${latest.number}`) + requireBotPr(pr, base, prefix) + if (pr.state !== 'closed' || pr.merged) + throw new Error( + `Close the unmerged candidate before retrying: ${pr.html_url}`, + ) + updateRefs(pr.base.repo.node_id, [ + { + name: `refs/heads/${branch}`, + beforeOid: pr.head.sha, + afterOid: '0'.repeat(40), + }, + ]) + summary(`Released the unused branch from closed PR: ${pr.html_url}`) +} + function prepare() { let refresh if (env.GITHUB_EVENT_NAME === 'pull_request_target') { @@ -211,8 +236,15 @@ function prepare() { : last ? `${base}-rc.${BigInt(last.match(rcPattern)[4] ?? 0) + 1n}` : `${base}-rc`) - if (occupied.includes(tag)) - throw new Error(`Tag ${tag} already exists or is reserved.`) + if (tags.includes(tag)) throw new Error(`Tag ${tag} already exists.`) + for (const [prefix, target] of [ + ['release/', 'dev'], + ['publish/', 'main'], + ]) { + const branch = `${prefix}${tag}` + if (refs.includes(`refs/heads/${branch}`) && branch !== refresh?.head.ref) + discardClosedCandidate(branch, target, prefix) + } const sha = git('rev-parse', 'HEAD') const body = releaseBody(tag, sha, tags, false) summary(body) @@ -453,7 +485,7 @@ function publishPr() { validateVersionCommit(tag, sha) const branch = `publish/${tag}` const existing = list( - `pulls?state=all&base=main&head=${repository.split('/')[0]}:${branch}&per_page=100`, + `pulls?state=open&base=main&head=${repository.split('/')[0]}:${branch}&per_page=100`, ) if (existing.length) { const publication = existing[0] @@ -567,28 +599,31 @@ function publish() { requireApproval(pr, sha) const main = git('rev-parse', 'origin/main') git('merge-base', '--is-ancestor', main, sha) - const object = api('git/tags', { + git( + '-c', + 'user.name=github-actions[bot]', + '-c', + 'user.email=41898282+github-actions[bot]@users.noreply.github.com', + 'tag', + '--annotate', + '--no-sign', + '--message', + marker, tag, - message: marker, - object: sha, - type: 'commit', - }) - // The tag records run ownership in the same transaction as the fast-forward. - updateRefs(pr.base.repo.node_id, [ - { name: 'refs/heads/main', beforeOid: main, afterOid: sha, force: false }, - { - name: `refs/heads/${pr.head.ref}`, - beforeOid: sha, - afterOid: sha, - force: false, - }, - { - name: `refs/tags/${tag}`, - beforeOid: '0'.repeat(40), - afterOid: object.sha, - force: false, - }, - ]) + sha, + ) + // GraphQL updateRefs rejects annotated tag objects; Git preserves atomicity. + git( + 'push', + '--atomic', + `--force-with-lease=refs/heads/main:${main}`, + `--force-with-lease=refs/heads/${pr.head.ref}:${sha}`, + `--force-with-lease=refs/tags/${tag}:`, + 'origin', + `${sha}:refs/heads/main`, + `${sha}:refs/heads/${pr.head.ref}`, + `refs/tags/${tag}:refs/tags/${tag}`, + ) summary(`Fast-forwarded main and tagged ${tag} at approved commit ${sha}.`) } releaseOutput(tag, sha) diff --git a/src/components/setting/mods/backup-config-viewer.tsx b/src/components/setting/mods/backup-config-viewer.tsx index c0ce70c10..84b7d579a 100644 --- a/src/components/setting/mods/backup-config-viewer.tsx +++ b/src/components/setting/mods/backup-config-viewer.tsx @@ -40,9 +40,9 @@ export const BackupConfigViewer = memo( const { register, handleSubmit, watch } = useForm({ defaultValues: { - url: webdav_url!, - username: webdav_username!, - password: webdav_password!, + url: webdav_url ?? '', + username: webdav_username ?? '', + password: webdav_password ?? '', }, }) const url = watch('url') @@ -50,9 +50,9 @@ export const BackupConfigViewer = memo( const password = watch('password') const webdavChanged = - webdav_url !== url || - webdav_username !== username || - webdav_password !== password + (webdav_url ?? '') !== url || + (webdav_username ?? '') !== username || + (webdav_password ?? '') !== password const webdavSignature = buildWebdavSignature(verge) const webdavStatus = getWebdavStatus(webdavSignature) @@ -69,9 +69,9 @@ export const BackupConfigViewer = memo( }, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit]) const checkForm = () => { - const username = usernameRef.current?.value + const username = usernameRef.current?.value.trim() const password = passwordRef.current?.value - const url = urlRef.current?.value + const url = urlRef.current?.value.trim() if (!url) { urlRef.current?.focus() @@ -95,14 +95,15 @@ export const BackupConfigViewer = memo( } const save = useLockFn(async (data: IWebDavConfig) => { - checkForm() + const trimmedUrl = data.url.trim() + const trimmedUsername = data.username.trim() + const isClearing = !trimmedUrl && !trimmedUsername && !data.password + if (!isClearing) checkForm() const signature = buildWebdavSignature({ webdav_url: data.url, webdav_username: data.username, webdav_password: data.password, }) - const trimmedUrl = data.url.trim() - const trimmedUsername = data.username.trim() try { setLoading(true) @@ -127,7 +128,7 @@ export const BackupConfigViewer = memo( ) setWebdavStatus(signature, 'unknown') showNotice.success('settings.modals.backup.messages.webdavConfigSaved') - await onSaveSuccess(signature) + if (!isClearing) await onSaveSuccess(signature) } catch (error) { showNotice.error( 'settings.modals.backup.messages.webdavConfigSaveFailed', @@ -223,9 +224,9 @@ export const BackupConfigViewer = memo( }} > {webdavChanged || - webdav_url === undefined || - webdav_username === undefined || - webdav_password === undefined ? ( + !webdav_url || + !webdav_username || + !webdav_password ? (