From 8f2e83618f697e56f0c9b07993f4a7331eeff7a6 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 15:32:57 +0800 Subject: [PATCH 1/4] Perf Policy Group generate --- v2rayN/ServiceLib/Global.cs | 2 + v2rayN/ServiceLib/Handler/ConfigHandler.cs | 41 +++------- .../ServiceLib/Manager/GroupProfileManager.cs | 2 +- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 78 +++---------------- v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 30 ++----- v2rayN/ServiceLib/Resx/ResUI.fr.resx | 30 ++----- v2rayN/ServiceLib/Resx/ResUI.hu.resx | 30 ++----- v2rayN/ServiceLib/Resx/ResUI.resx | 30 ++----- v2rayN/ServiceLib/Resx/ResUI.ru.resx | 30 ++----- v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 30 ++----- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 30 ++----- .../ViewModels/ProfilesViewModel.cs | 56 ++----------- .../v2rayN.Desktop/Views/ProfilesView.axaml | 10 +-- .../Views/ProfilesView.axaml.cs | 7 +- v2rayN/v2rayN/Views/ProfilesView.xaml | 27 +------ v2rayN/v2rayN/Views/ProfilesView.xaml.cs | 7 +- 16 files changed, 80 insertions(+), 360 deletions(-) diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index 90721217..c11ec6bf 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -90,6 +90,8 @@ public class Global public const int Hysteria2DefaultHopInt = 10; + public const string PolicyGroupDefaultAllFilter = "^(?!.*(剩余|过期|到期|重置)).*$"; + public static readonly List IEProxyProtocols = [ "{ip}:{http_port}", diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index b0bde1df..0cc9e4dd 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -1165,46 +1165,23 @@ public static class ConfigHandler /// /// Create a group server that combines multiple servers for load balancing - /// Generates a configuration file that references multiple servers + /// Generates a PolicyGroup profile with references to the sub-items /// /// Current configuration - /// Selected servers to combine - /// Core type to use (Xray or sing_box) - /// Load balancing algorithm + /// Sub-item for grouping /// Result object with success state and data - public static async Task AddGroupServer4Multiple(Config config, List selecteds, ECoreType coreType, EMultipleLoad multipleLoad, string? subId) + public static async Task AddGroupAllServer(Config config, SubItem? subItem) { var result = new RetResult(); var indexId = Utils.GetGuid(false); - var childProfileIndexId = Utils.List2String(selecteds.Select(p => p.IndexId).ToList()); + var subId = subItem?.Id; - var remark = subId.IsNullOrEmpty() ? string.Empty : $"{(await AppManager.Instance.GetSubItem(subId))?.Remarks} "; - if (coreType == ECoreType.Xray) - { - remark += multipleLoad switch - { - EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerXrayLeastPing, - EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerXrayFallback, - EMultipleLoad.Random => ResUI.menuGenGroupMultipleServerXrayRandom, - EMultipleLoad.RoundRobin => ResUI.menuGenGroupMultipleServerXrayRoundRobin, - EMultipleLoad.LeastLoad => ResUI.menuGenGroupMultipleServerXrayLeastLoad, - _ => ResUI.menuGenGroupMultipleServerXrayRoundRobin, - }; - } - else if (coreType == ECoreType.sing_box) - { - remark += multipleLoad switch - { - EMultipleLoad.LeastPing => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, - EMultipleLoad.Fallback => ResUI.menuGenGroupMultipleServerSingBoxFallback, - _ => ResUI.menuGenGroupMultipleServerSingBoxLeastPing, - }; - } + var remark = subItem is null ? ResUI.TbConfigTypePolicyGroup : $"{subItem.Remarks} - {ResUI.TbConfigTypePolicyGroup}"; var profile = new ProfileItem { IndexId = indexId, - CoreType = coreType, + CoreType = ECoreType.Xray, ConfigType = EConfigType.PolicyGroup, Remarks = remark, IsSub = false @@ -1215,8 +1192,10 @@ public static class ConfigHandler } var extraItem = new ProtocolExtraItem { - ChildItems = childProfileIndexId, - MultipleLoad = multipleLoad, + MultipleLoad = EMultipleLoad.LeastPing, + GroupType = profile.ConfigType.ToString(), + SubChildItems = subId, + Filter = Global.PolicyGroupDefaultAllFilter, }; profile.SetProtocolExtra(extraItem); var ret = await AddServerCommon(config, profile, true); diff --git a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs index 5e87f0e6..7d2fdbff 100644 --- a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs +++ b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs @@ -128,7 +128,7 @@ public class GroupProfileManager private static async Task> GetSubChildProfileItems(ProtocolExtraItem? extra) { - if (extra == null || extra.SubChildItems.IsNullOrEmpty()) + if (extra == null || extra.Filter.IsNullOrEmpty()) { return []; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 8f798007..6c3e48e5 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -888,6 +888,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 All configurations 的本地化字符串。 + /// + public static string menuAllServers { + get { + return ResourceManager.GetString("menuAllServers", resourceCulture); + } + } + /// /// 查找类似 Backup and Restore 的本地化字符串。 /// @@ -1060,74 +1069,11 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Generate Policy Group from Multiple Profiles 的本地化字符串。 + /// 查找类似 Generate Policy Group 的本地化字符串。 /// - public static string menuGenGroupMultipleServer { + public static string menuGenGroupServer { get { - return ResourceManager.GetString("menuGenGroupMultipleServer", resourceCulture); - } - } - - /// - /// 查找类似 Fallback by sing-box 的本地化字符串。 - /// - public static string menuGenGroupMultipleServerSingBoxFallback { - get { - return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxFallback", resourceCulture); - } - } - - /// - /// 查找类似 LeastPing by sing-box 的本地化字符串。 - /// - public static string menuGenGroupMultipleServerSingBoxLeastPing { - get { - return ResourceManager.GetString("menuGenGroupMultipleServerSingBoxLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Fallback by Xray 的本地化字符串。 - /// - public static string menuGenGroupMultipleServerXrayFallback { - get { - return ResourceManager.GetString("menuGenGroupMultipleServerXrayFallback", resourceCulture); - } - } - - /// - /// 查找类似 LeastLoad by Xray 的本地化字符串。 - /// - public static string menuGenGroupMultipleServerXrayLeastLoad { - get { - return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastLoad", resourceCulture); - } - } - - /// - /// 查找类似 LeastPing by Xray 的本地化字符串。 - /// - public static string menuGenGroupMultipleServerXrayLeastPing { - get { - return ResourceManager.GetString("menuGenGroupMultipleServerXrayLeastPing", resourceCulture); - } - } - - /// - /// 查找类似 Random by Xray 的本地化字符串。 - /// - public static string menuGenGroupMultipleServerXrayRandom { - get { - return ResourceManager.GetString("menuGenGroupMultipleServerXrayRandom", resourceCulture); - } - } - - /// - /// 查找类似 RoundRobin by Xray 的本地化字符串。 - /// - public static string menuGenGroupMultipleServerXrayRoundRobin { - get { - return ResourceManager.GetString("menuGenGroupMultipleServerXrayRoundRobin", resourceCulture); + return ResourceManager.GetString("menuGenGroupServer", resourceCulture); } } diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 9aba2fed..20f48b38 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1371,24 +1371,6 @@ مخفی و پورت می شود، با کاما (،) جدا می شود - - Generate Policy Group from Multiple Profiles - - - چند سرور تصادفی توسط Xray - - - چند سرور RoundRobin توسط Xray - - - چند سرور LeastPing توسط Xray - - - چند سرور LeastLoad توسط Xray - - - LeastPing چند سرور توسط sing-box - صادر کردن سرور @@ -1533,12 +1515,6 @@ Fallback - - Multi-Configuration Fallback by sing-box - - - Multi-Configuration Fallback by Xray - Core '{0}' does not support network type '{1}'. @@ -1668,4 +1644,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Generate Policy Group + + + All configurations + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 3dfbcc09..4c5459b8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1368,24 +1368,6 @@ Écrase le port ; pour plusieurs groupes, séparer par virgules (,) - - Générer un groupe de stratégie depuis plusieurs profils - - - Xray aléatoire (multi-sélection) - - - Xray équilibrage (tourniquet) multi-sélection - - - Xray latence minimale (multi-sélection) - - - Xray le plus stable (multi-sélection) - - - sing-box latence minimale (multi-sélection) - Exporter @@ -1530,12 +1512,6 @@ Basculement (failover) - - sing-box basculement (multi-sélection) - - - Xray basculement (multi-sélection) - Le cœur « {0} » ne prend pas en charge le type de réseau « {1} ». @@ -1665,4 +1641,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Generate Policy Group + + + All configurations + diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index e69a0a54..f90611ab 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1371,24 +1371,6 @@ A portot lefedi, vesszővel (,) elválasztva - - Generate Policy Group from Multiple Profiles - - - Több konfiguráció véletlenszerűen Xray szerint - - - Több konfiguráció RoundRobin Xray szerint - - - Több konfiguráció legkisebb pinggel Xray szerint - - - Több konfiguráció legkisebb terheléssel Xray szerint - - - Több konfiguráció legkisebb pinggel sing-box szerint - Konfiguráció exportálása @@ -1533,12 +1515,6 @@ Fallback - - Multi-Configuration Fallback by sing-box - - - Multi-Configuration Fallback by Xray - Core '{0}' does not support network type '{1}'. @@ -1668,4 +1644,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Generate Policy Group + + + All configurations + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index cc304154..98132ca6 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1371,24 +1371,6 @@ Will cover the port, separate with commas (,) - - Generate Policy Group from Multiple Profiles - - - Random by Xray - - - RoundRobin by Xray - - - LeastPing by Xray - - - LeastLoad by Xray - - - LeastPing by sing-box - Export @@ -1533,12 +1515,6 @@ Fallback - - Fallback by sing-box - - - Fallback by Xray - Core '{0}' does not support network type '{1}'. @@ -1668,4 +1644,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Generate Policy Group + + + All configurations + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 6f093f72..04a2168d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1371,24 +1371,6 @@ Заменит указанный порт, перечисляйте через запятую (,) - - Generate Policy Group from Multiple Profiles - - - Случайный (Xray) - - - Круговой (Xray) - - - Минимальное RTT (минимальное время туда-обратно) (Xray) - - - Минимальная нагрузка (Xray) - - - Минимальное RTT (минимальное время туда-обратно) (sing-box) - Экспортировать конфигурацию @@ -1533,12 +1515,6 @@ Fallback - - Multi-Configuration Fallback by sing-box - - - Multi-Configuration Fallback by Xray - Core '{0}' does not support network type '{1}'. @@ -1668,4 +1644,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Generate Policy Group + + + All configurations + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index c0f8665d..5c30f020 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1368,24 +1368,6 @@ 会覆盖端口,多组时用逗号 (,) 隔开 - - 多选生成策略组 - - - 多选随机 Xray - - - 多选负载均衡 Xray - - - 多选最低延迟 Xray - - - 多选最稳定 Xray - - - 多选最低延迟 sing-box - 导出 @@ -1530,12 +1512,6 @@ 故障转移 - - 多选故障转移 sing-box - - - 多选故障转移 Xray - 核心 '{0}' 不支持网络类型 '{1}'。 @@ -1665,4 +1641,10 @@ Finalmask + + 一键生成策略组 + + + 全部配置项 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 9002c3af..6cbccf45 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1368,24 +1368,6 @@ 會覆蓋埠,多組時用逗號 (,) 隔開 - - 多選生成策略組 - - - 多選隨機 Xray - - - 多選負載平衡 Xray - - - 多選最低延遲 Xray - - - 多選最穩定 Xray - - - 多選最低延遲 sing-box - 匯出 @@ -1530,12 +1512,6 @@ 容錯移轉 - - 多選容錯移轉 sing-box - - - 多選容錯移轉 Xray - 核心 '{0}' 不支援網路類型 '{1}'. @@ -1665,4 +1641,10 @@ Finalmask + + Generate Policy Group + + + All configurations + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 8b4e7eb0..8aa06cfe 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -43,13 +43,7 @@ public class ProfilesViewModel : MyReactiveObject public ReactiveCommand CopyServerCmd { get; } public ReactiveCommand SetDefaultServerCmd { get; } public ReactiveCommand ShareServerCmd { get; } - public ReactiveCommand GenGroupMultipleServerXrayRandomCmd { get; } - public ReactiveCommand GenGroupMultipleServerXrayRoundRobinCmd { get; } - public ReactiveCommand GenGroupMultipleServerXrayLeastPingCmd { get; } - public ReactiveCommand GenGroupMultipleServerXrayLeastLoadCmd { get; } - public ReactiveCommand GenGroupMultipleServerXrayFallbackCmd { get; } - public ReactiveCommand GenGroupMultipleServerSingBoxLeastPingCmd { get; } - public ReactiveCommand GenGroupMultipleServerSingBoxFallbackCmd { get; } + public ReactiveCommand GenGroupAllServerCmd { get; } //servers move public ReactiveCommand MoveTopCmd { get; } @@ -134,33 +128,9 @@ public class ProfilesViewModel : MyReactiveObject { await ShareServerAsync(); }, canEditRemove); - GenGroupMultipleServerXrayRandomCmd = ReactiveCommand.CreateFromTask(async () => + GenGroupAllServerCmd = ReactiveCommand.CreateFromTask(async () => { - await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Random); - }, canEditRemove); - GenGroupMultipleServerXrayRoundRobinCmd = ReactiveCommand.CreateFromTask(async () => - { - await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.RoundRobin); - }, canEditRemove); - GenGroupMultipleServerXrayLeastPingCmd = ReactiveCommand.CreateFromTask(async () => - { - await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastPing); - }, canEditRemove); - GenGroupMultipleServerXrayLeastLoadCmd = ReactiveCommand.CreateFromTask(async () => - { - await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.LeastLoad); - }, canEditRemove); - GenGroupMultipleServerXrayFallbackCmd = ReactiveCommand.CreateFromTask(async () => - { - await GenGroupMultipleServer(ECoreType.Xray, EMultipleLoad.Fallback); - }, canEditRemove); - GenGroupMultipleServerSingBoxLeastPingCmd = ReactiveCommand.CreateFromTask(async () => - { - await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.LeastPing); - }, canEditRemove); - GenGroupMultipleServerSingBoxFallbackCmd = ReactiveCommand.CreateFromTask(async () => - { - await GenGroupMultipleServer(ECoreType.sing_box, EMultipleLoad.Fallback); + await GenGroupAllServer(); }, canEditRemove); //servers move @@ -641,29 +611,15 @@ public class ProfilesViewModel : MyReactiveObject await _updateView?.Invoke(EViewAction.ShareServer, url); } - private async Task GenGroupMultipleServer(ECoreType coreType, EMultipleLoad multipleLoad) + private async Task GenGroupAllServer() { - var lstSelected = await GetProfileItems(true); - if (lstSelected == null) - { - return; - } - - var ret = await ConfigHandler.AddGroupServer4Multiple(_config, lstSelected, coreType, multipleLoad, SelectedSub?.Id); + var ret = await ConfigHandler.AddGroupAllServer(_config, SelectedSub); if (ret.Success != true) { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); return; } - if (ret?.Data?.ToString() == _config.IndexId) - { - await RefreshServers(); - Reload(); - } - else - { - await SetDefaultServer(ret?.Data?.ToString()); - } + await RefreshServers(); } public async Task SortServer(string colName) diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml index 116f35fa..71569feb 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml @@ -193,14 +193,8 @@ - - - - - - - - + + diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs index aaebb518..3ce58944 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs @@ -60,12 +60,7 @@ public partial class ProfilesView : ReactiveUserControl this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).DisposeWith(disposables); //servers move //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml b/v2rayN/v2rayN/Views/ProfilesView.xaml index 6cf0d159..3cfd59a7 100644 --- a/v2rayN/v2rayN/Views/ProfilesView.xaml +++ b/v2rayN/v2rayN/Views/ProfilesView.xaml @@ -240,32 +240,11 @@ Header="{x:Static resx:ResUI.menuExport2ShareUrlBase64}" /> - + - - - - - - + Header="{x:Static resx:ResUI.menuAllServers}" /> diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs index 58b468a4..aeeaaa64 100644 --- a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs +++ b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs @@ -54,12 +54,7 @@ public partial class ProfilesView this.BindCommand(ViewModel, vm => vm.CopyServerCmd, v => v.menuCopyServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRandomCmd, v => v.menuGenGroupMultipleServerXrayRandom).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayRoundRobinCmd, v => v.menuGenGroupMultipleServerXrayRoundRobin).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastPingCmd, v => v.menuGenGroupMultipleServerXrayLeastPing).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerXrayLeastLoadCmd, v => v.menuGenGroupMultipleServerXrayLeastLoad).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxLeastPingCmd, v => v.menuGenGroupMultipleServerSingBoxLeastPing).DisposeWith(disposables); - this.BindCommand(ViewModel, vm => vm.GenGroupMultipleServerSingBoxFallbackCmd, v => v.menuGenGroupMultipleServerSingBoxFallback).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).DisposeWith(disposables); //servers move this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); From 10a53fc48ef4a2c0275f0f92ebbe0f0f19b840c3 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 17:00:39 +0800 Subject: [PATCH 2/4] Scroll to new group node --- v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 8aa06cfe..f62573c0 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -8,6 +8,7 @@ public class ProfilesViewModel : MyReactiveObject private string _serverFilter = string.Empty; private Dictionary _dicHeaderSort = new(); private SpeedtestService? _speedtestService; + private string? _pendingSelectIndexId; #endregion private prop @@ -362,15 +363,14 @@ public class ProfilesViewModel : MyReactiveObject ProfileItems.AddRange(lstModel); if (lstModel.Count > 0) { - var selected = lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); - if (selected != null) + ProfileItemModel? selected = null; + if (!_pendingSelectIndexId.IsNullOrEmpty()) { - SelectedProfile = selected; - } - else - { - SelectedProfile = lstModel.First(); + selected = lstModel.FirstOrDefault(t => t.IndexId == _pendingSelectIndexId); + _pendingSelectIndexId = null; } + selected ??= lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); + SelectedProfile = selected ?? lstModel.First(); } await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null); @@ -619,6 +619,7 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.Enqueue(ResUI.OperationFailed); return; } + _pendingSelectIndexId = ret.Data?.ToString(); await RefreshServers(); } From 90bc0e98eddaf908e966ba85ec6ad4de44814720 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 17:43:20 +0800 Subject: [PATCH 3/4] Add region group --- v2rayN/ServiceLib/Global.cs | 2 - v2rayN/ServiceLib/Handler/ConfigHandler.cs | 82 ++++++++++++++++++- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 9 ++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.fr.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.hu.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.ru.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 3 + v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 3 + .../ViewModels/ProfilesViewModel.cs | 18 ++++ .../v2rayN.Desktop/Views/ProfilesView.axaml | 1 + .../Views/ProfilesView.axaml.cs | 1 + v2rayN/v2rayN/Views/ProfilesView.xaml | 4 + v2rayN/v2rayN/Views/ProfilesView.xaml.cs | 1 + 15 files changed, 136 insertions(+), 3 deletions(-) diff --git a/v2rayN/ServiceLib/Global.cs b/v2rayN/ServiceLib/Global.cs index c11ec6bf..90721217 100644 --- a/v2rayN/ServiceLib/Global.cs +++ b/v2rayN/ServiceLib/Global.cs @@ -90,8 +90,6 @@ public class Global public const int Hysteria2DefaultHopInt = 10; - public const string PolicyGroupDefaultAllFilter = "^(?!.*(剩余|过期|到期|重置)).*$"; - public static readonly List IEProxyProtocols = [ "{ip}:{http_port}", diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 0cc9e4dd..78e74f1c 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -1163,6 +1163,16 @@ public static class ConfigHandler return 0; } + private const string PolicyGroupDefaultAllFilter = "^(?!.*(?:剩余|过期|到期|重置)).*$"; + + /// + /// Combines a region pattern with PolicyGroupDefaultAllFilter so results must + /// match the region keyword AND not contain expiry/traffic noise words. + /// Result pattern: ^(?!.*(?:剩余|过期|到期|重置)).*(?:regionPattern).*$ + /// + private static string CombineWithDefaultAllFilter(string regionPattern) + => $"^(?!.*(?:剩余|过期|到期|重置)).*(?:{regionPattern}).*$"; + /// /// Create a group server that combines multiple servers for load balancing /// Generates a PolicyGroup profile with references to the sub-items @@ -1195,7 +1205,7 @@ public static class ConfigHandler MultipleLoad = EMultipleLoad.LeastPing, GroupType = profile.ConfigType.ToString(), SubChildItems = subId, - Filter = Global.PolicyGroupDefaultAllFilter, + Filter = PolicyGroupDefaultAllFilter, }; profile.SetProtocolExtra(extraItem); var ret = await AddServerCommon(config, profile, true); @@ -1204,6 +1214,76 @@ public static class ConfigHandler return result; } + private static readonly Dictionary PolicyGroupRegionFilters = new() + { + { "JP", "日本|[Jj][Pp]|🇯🇵" }, + { "US", "美国|[Uu][Ss]|🇺🇸" }, + { "HK", "香港|[Hh][Kk]|🇭🇰" }, + { "TW", "台湾|[Tt][Ww]|🇹🇼" }, + { "KR", "韩国|[Kk][Rr]|🇰🇷" }, + { "SG", "新加坡|[Ss][Gg]|🇸🇬" }, + { "DE", "德国|[Dd][Ee]|🇩🇪" }, + { "FR", "法国|[Ff][Rr]|🇫🇷" }, + { "GB", "英国|[Gg][Bb]|🇬🇧" }, + { "CA", "加拿大|[Cc][Aa]|🇨🇦" }, + { "AU", "澳大利亚|[Aa][Uu]|🇦🇺" }, + { "RU", "俄罗斯|[Rr][Uu]|🇷🇺" }, + { "BR", "巴西|[Bb][Rr]|🇧🇷" }, + { "IN", "印度|[Ii][Nn]|🇮🇳" }, + { "VN", "越南|[Vv][Nn]|🇻🇳" }, + { "ID", "印度尼西亚|[Ii][Dd]|🇮🇩" }, + { "MX", "墨西哥|[Mm][Xx]|🇲🇽" } + }; + + public static async Task AddGroupRegionServer(Config config, SubItem? subItem) + { + var result = new RetResult(); + List indexIdList = []; + + foreach (var regionFilter in PolicyGroupRegionFilters) + { + var indexId = Utils.GetGuid(false); + var subId = subItem?.Id; + + var remark = subItem is null ? ResUI.TbConfigTypePolicyGroup : $"{subItem.Remarks} - {ResUI.TbConfigTypePolicyGroup} - {regionFilter.Key}"; + var profile = new ProfileItem + { + IndexId = indexId, + CoreType = ECoreType.Xray, + ConfigType = EConfigType.PolicyGroup, + Remarks = remark, + IsSub = false + }; + if (!subId.IsNullOrEmpty()) + { + profile.Subid = subId; + } + var extraItem = new ProtocolExtraItem + { + MultipleLoad = EMultipleLoad.LeastPing, + GroupType = profile.ConfigType.ToString(), + SubChildItems = subId, + Filter = CombineWithDefaultAllFilter(regionFilter.Value), + }; + profile.SetProtocolExtra(extraItem); + + var childProfile = await GroupProfileManager.GetChildProfileItemsByProtocolExtra(extraItem); + if (childProfile.Count == 0) + { + continue; + } + + var ret = await AddServerCommon(config, profile, true); + if (ret == 0) + { + indexIdList.Add(indexId); + } + } + result.Success = indexIdList.Count > 0; + result.Data = indexIdList; + return result; + } + /// /// Get a SOCKS server profile for pre-SOCKS functionality /// Used when TUN mode is enabled or when a custom config has a pre-SOCKS port diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 6c3e48e5..d1dd3b75 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -1077,6 +1077,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Group by Region 的本地化字符串。 + /// + public static string menuGenRegionGroup { + get { + return ResourceManager.GetString("menuGenRegionGroup", resourceCulture); + } + } + /// /// 查找类似 Global Hotkey Setting 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 20f48b38..31d8d7f7 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if All configurations + + Group by Region + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 4c5459b8..87fbb37c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1647,4 +1647,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if All configurations + + Group by Region + diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index f90611ab..2c5c44f7 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if All configurations + + Group by Region + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 98132ca6..16cef1b7 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if All configurations + + Group by Region + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 04a2168d..4a29c8c9 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if All configurations + + Group by Region + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index 5c30f020..f74d83ef 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1647,4 +1647,7 @@ 全部配置项 + + 按地区分组 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 6cbccf45..ddb087f0 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1647,4 +1647,7 @@ All configurations + + Group by Region + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index f62573c0..ad21883e 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -45,6 +45,7 @@ public class ProfilesViewModel : MyReactiveObject public ReactiveCommand SetDefaultServerCmd { get; } public ReactiveCommand ShareServerCmd { get; } public ReactiveCommand GenGroupAllServerCmd { get; } + public ReactiveCommand GenGroupRegionServerCmd { get; } //servers move public ReactiveCommand MoveTopCmd { get; } @@ -133,6 +134,10 @@ public class ProfilesViewModel : MyReactiveObject { await GenGroupAllServer(); }, canEditRemove); + GenGroupRegionServerCmd = ReactiveCommand.CreateFromTask(async () => + { + await GenGroupRegionServer(); + }, canEditRemove); //servers move MoveTopCmd = ReactiveCommand.CreateFromTask(async () => @@ -623,6 +628,19 @@ public class ProfilesViewModel : MyReactiveObject await RefreshServers(); } + private async Task GenGroupRegionServer() + { + var ret = await ConfigHandler.AddGroupRegionServer(_config, SelectedSub); + if (ret.Success != true) + { + NoticeManager.Instance.Enqueue(ResUI.OperationFailed); + return; + } + var indexIdList = ret.Data as List; + _pendingSelectIndexId = indexIdList?.FirstOrDefault(); + await RefreshServers(); + } + public async Task SortServer(string colName) { if (colName.IsNullOrEmpty()) diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml index 71569feb..e0751ca1 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml @@ -195,6 +195,7 @@ + diff --git a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs index 3ce58944..4ca8e793 100644 --- a/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs +++ b/v2rayN/v2rayN.Desktop/Views/ProfilesView.axaml.cs @@ -61,6 +61,7 @@ public partial class ProfilesView : ReactiveUserControl this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupRegionServerCmd, v => v.menuGenGroupRegionServer).DisposeWith(disposables); //servers move //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml b/v2rayN/v2rayN/Views/ProfilesView.xaml index 3cfd59a7..fd984306 100644 --- a/v2rayN/v2rayN/Views/ProfilesView.xaml +++ b/v2rayN/v2rayN/Views/ProfilesView.xaml @@ -245,6 +245,10 @@ x:Name="menuGenGroupAllServer" Height="{StaticResource MenuItemHeight}" Header="{x:Static resx:ResUI.menuAllServers}" /> + diff --git a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs index aeeaaa64..46def3e2 100644 --- a/v2rayN/v2rayN/Views/ProfilesView.xaml.cs +++ b/v2rayN/v2rayN/Views/ProfilesView.xaml.cs @@ -55,6 +55,7 @@ public partial class ProfilesView this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables); this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).DisposeWith(disposables); + this.BindCommand(ViewModel, vm => vm.GenGroupRegionServerCmd, v => v.menuGenGroupRegionServer).DisposeWith(disposables); //servers move this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); From b4082ec1ec6eca158452e168965f76f6197c0298 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 17:45:06 +0800 Subject: [PATCH 4/4] I18n --- v2rayN/ServiceLib/Handler/ConfigHandler.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 78e74f1c..f984b99c 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -1163,15 +1163,22 @@ public static class ConfigHandler return 0; } - private const string PolicyGroupDefaultAllFilter = "^(?!.*(?:剩余|过期|到期|重置)).*$"; + // Matches subscription-info noise words in both Chinese and English. + // Chinese: 剩余(remaining), 过期/到期(expired/expiry), 重置(reset) + // English: remaining, expir(e/ed/y), reset + private const string PolicyGroupExcludeKeywords = @"剩余|过期|到期|重置|[Rr]emaining|[Ee]xpir|[Rr]eset"; + + private const string PolicyGroupDefaultAllFilter = $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*$"; /// /// Combines a region pattern with PolicyGroupDefaultAllFilter so results must - /// match the region keyword AND not contain expiry/traffic noise words. - /// Result pattern: ^(?!.*(?:剩余|过期|到期|重置)).*(?:regionPattern).*$ + /// match the region keyword AND not contain expiry/traffic noise words (CN + EN). + /// Result pattern: ^(?!.*(?:...excludeKeywords...)).*(?:regionPattern).*$ /// private static string CombineWithDefaultAllFilter(string regionPattern) - => $"^(?!.*(?:剩余|过期|到期|重置)).*(?:{regionPattern}).*$"; + { + return $"^(?!.*(?:{PolicyGroupExcludeKeywords})).*(?:{regionPattern}).*$"; + } /// /// Create a group server that combines multiple servers for load balancing