From 84bbe790e8fff315d73bdbe6c73f784f66db071e Mon Sep 17 00:00:00 2001 From: wonfen Date: Tue, 3 Nov 2026 23:28:15 +0800 Subject: [PATCH] fix(proxies): move an unpicked GLOBAL off DIRECT to its first group Keep DIRECT and REJECT listed in global mode. --- Changelog.md | 2 + src-tauri/src/config/profiles.rs | 53 +++++++++++++++++++++++++ src-tauri/src/feat/clash.rs | 6 +++ src/components/proxy/use-render-list.ts | 17 +------- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/Changelog.md b/Changelog.md index 64b93631d..d6db6c761 100644 --- a/Changelog.md +++ b/Changelog.md @@ -25,6 +25,7 @@ 🐞 修复问题 - 修复检测更新后又发布新版时,应用内更新失败 +- 修复全局模式默认停在 `DIRECT` 或 `REJECT` 导致流量未经代理,现自动切换到第一个策略组 @@ -37,5 +38,6 @@
🚀 优化改进 +- 优化全局模式代理列表:仅显示 `GLOBAL` 节点,选中其中的策略组时才显示该组
diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index 23826cdc1..24d38d004 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -614,6 +614,53 @@ async fn select_node_with_timeout(group_name: &String, node: &String) -> Result< .with_context(|| format!("failed to select node [{node}] for group [{group_name}]")) } +// mihomo lists these first in GLOBAL, so an unpicked GLOBAL starts on DIRECT. +const GLOBAL_BUILTINS: [&str; 2] = ["DIRECT", "REJECT"]; + +/// GLOBAL's first visible group, else its first node, while GLOBAL is still on a built-in. +fn global_exit_target(proxies: &Proxies) -> Option<&str> { + let global = proxies.proxies.get("GLOBAL")?; + if !global.now.as_deref().is_some_and(|now| GLOBAL_BUILTINS.contains(&now)) { + return None; + } + let members = global.all.as_deref()?; + let is_visible_group = |name: &str| { + proxies.proxies.get(name).is_some_and(|proxy| { + proxy.hidden != Some(true) + && matches!( + proxy.proxy_type, + ProxyType::Selector | ProxyType::URLTest | ProxyType::Fallback | ProxyType::LoadBalance + ) + }) + }; + members + .iter() + .find(|name| is_visible_group(name)) + .or_else(|| members.iter().find(|name| !GLOBAL_BUILTINS.contains(&name.as_str()))) + .map(|name| name.as_str()) +} + +/// Moves GLOBAL off a built-in unless the profile records any GLOBAL choice. +pub(crate) async fn leave_default_global_exit() -> Result<()> { + let profiles = Config::profiles().await.data_arc(); + let has_record = profiles + .current + .as_ref() + .and_then(|current| profiles.get_item(current).ok()) + .and_then(|item| item.selected.as_deref()) + .is_some_and(|selected| selected.iter().any(|item| item.name.as_deref() == Some("GLOBAL"))); + if has_record { + return Ok(()); + } + let snapshot = fetch_proxies_with_timeout().await?; + let Some(target) = global_exit_target(&snapshot) else { + return Ok(()); + }; + select_node_with_timeout(&String::from("GLOBAL"), &String::from(target)).await?; + logging!(info, Type::Config, "moved GLOBAL off its built-in default to {target}"); + Ok(()) +} + fn remaining_activations( activations: &[(String, String)], completed: &HashMap, @@ -1019,6 +1066,12 @@ fn activate_selected_nodes_with(repair: SelectionRepair) -> tokio::sync::oneshot .clone() .unwrap_or_default(); + if Config::clash().await.latest_arc().get_mode().as_deref() == Some("global") + && let Err(err) = leave_default_global_exit().await + { + logging!(warn, Type::Config, "failed to move GLOBAL off its default: {err:#}"); + } + if selected.is_empty() { if is_activation_current(generation) { announce(Refresh::Clash); diff --git a/src-tauri/src/feat/clash.rs b/src-tauri/src/feat/clash.rs index 6fdcb681c..7b6d4aa21 100644 --- a/src-tauri/src/feat/clash.rs +++ b/src-tauri/src/feat/clash.rs @@ -66,6 +66,12 @@ pub async fn change_clash_mode(mode: String) -> Result<(), String> { return Err(err.to_string().into()); } + if mode.as_str() == "global" + && let Err(err) = crate::config::profiles::leave_default_global_exit().await + { + logging!(warn, Type::Core, "failed to move GLOBAL off its default: {err:#}"); + } + let clash = Config::clash().await; let save_result = { let _config_write = Config::lock_config_write().await; diff --git a/src/components/proxy/use-render-list.ts b/src/components/proxy/use-render-list.ts index 0e94054b5..3823d91cf 100644 --- a/src/components/proxy/use-render-list.ts +++ b/src/components/proxy/use-render-list.ts @@ -79,18 +79,6 @@ const resolveOccurrences = (view: ProxyViewV1, group: ProxyGroupView) => member: resolveMember(view, member), })) -// mihomo selects DIRECT in GLOBAL by default, so the selected built-in stays visible. -const HIDDEN_GLOBAL_BUILTINS = new Set(['DIRECT', 'REJECT']) - -const isHiddenGlobalBuiltin = ( - group: ProxyGroupView, - { member }: ResolvedMemberOccurrence, -) => - member.kind === 'node' && - member.node.source.kind === 'core' && - HIDDEN_GLOBAL_BUILTINS.has(member.node.source.proxyName) && - member.ref.name !== group.now - const memberKey = ( group: ProxyGroupView, occurrence: ResolvedMemberOccurrence, @@ -307,11 +295,8 @@ export const useRenderList = ( ] if (headState.open || isFlat) { - const resolved = resolveOccurrences(proxyView, group) const occurrences = filterSort( - isFlat - ? resolved.filter((item) => !isHiddenGlobalBuiltin(group, item)) - : resolved, + resolveOccurrences(proxyView, group), group.name, headState.filterText, headState.sortType,