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,