Add region group

This commit is contained in:
DHR60 2026-02-27 17:43:20 +08:00
parent 10a53fc48e
commit 90bc0e98ed
15 changed files with 136 additions and 3 deletions

View file

@ -90,8 +90,6 @@ public class Global
public const int Hysteria2DefaultHopInt = 10; public const int Hysteria2DefaultHopInt = 10;
public const string PolicyGroupDefaultAllFilter = "^(?!.*(剩余|过期|到期|重置)).*$";
public static readonly List<string> IEProxyProtocols = public static readonly List<string> IEProxyProtocols =
[ [
"{ip}:{http_port}", "{ip}:{http_port}",

View file

@ -1163,6 +1163,16 @@ public static class ConfigHandler
return 0; return 0;
} }
private const string PolicyGroupDefaultAllFilter = "^(?!.*(?:剩余|过期|到期|重置)).*$";
/// <summary>
/// Combines a region pattern with PolicyGroupDefaultAllFilter so results must
/// match the region keyword AND not contain expiry/traffic noise words.
/// Result pattern: ^(?!.*(?:剩余|过期|到期|重置)).*(?:regionPattern).*$
/// </summary>
private static string CombineWithDefaultAllFilter(string regionPattern)
=> $"^(?!.*(?:剩余|过期|到期|重置)).*(?:{regionPattern}).*$";
/// <summary> /// <summary>
/// Create a group server that combines multiple servers for load balancing /// Create a group server that combines multiple servers for load balancing
/// Generates a PolicyGroup profile with references to the sub-items /// Generates a PolicyGroup profile with references to the sub-items
@ -1195,7 +1205,7 @@ public static class ConfigHandler
MultipleLoad = EMultipleLoad.LeastPing, MultipleLoad = EMultipleLoad.LeastPing,
GroupType = profile.ConfigType.ToString(), GroupType = profile.ConfigType.ToString(),
SubChildItems = subId, SubChildItems = subId,
Filter = Global.PolicyGroupDefaultAllFilter, Filter = PolicyGroupDefaultAllFilter,
}; };
profile.SetProtocolExtra(extraItem); profile.SetProtocolExtra(extraItem);
var ret = await AddServerCommon(config, profile, true); var ret = await AddServerCommon(config, profile, true);
@ -1204,6 +1214,76 @@ public static class ConfigHandler
return result; return result;
} }
private static readonly Dictionary<string, string> 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<RetResult> AddGroupRegionServer(Config config, SubItem? subItem)
{
var result = new RetResult();
List<string> 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;
}
/// <summary> /// <summary>
/// Get a SOCKS server profile for pre-SOCKS functionality /// 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 /// Used when TUN mode is enabled or when a custom config has a pre-SOCKS port

View file

@ -1077,6 +1077,15 @@ namespace ServiceLib.Resx {
} }
} }
/// <summary>
/// 查找类似 Group by Region 的本地化字符串。
/// </summary>
public static string menuGenRegionGroup {
get {
return ResourceManager.GetString("menuGenRegionGroup", resourceCulture);
}
}
/// <summary> /// <summary>
/// 查找类似 Global Hotkey Setting 的本地化字符串。 /// 查找类似 Global Hotkey Setting 的本地化字符串。
/// </summary> /// </summary>

View file

@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuAllServers" xml:space="preserve"> <data name="menuAllServers" xml:space="preserve">
<value>All configurations</value> <value>All configurations</value>
</data> </data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root> </root>

View file

@ -1647,4 +1647,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuAllServers" xml:space="preserve"> <data name="menuAllServers" xml:space="preserve">
<value>All configurations</value> <value>All configurations</value>
</data> </data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root> </root>

View file

@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuAllServers" xml:space="preserve"> <data name="menuAllServers" xml:space="preserve">
<value>All configurations</value> <value>All configurations</value>
</data> </data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root> </root>

View file

@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuAllServers" xml:space="preserve"> <data name="menuAllServers" xml:space="preserve">
<value>All configurations</value> <value>All configurations</value>
</data> </data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root> </root>

View file

@ -1650,4 +1650,7 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if
<data name="menuAllServers" xml:space="preserve"> <data name="menuAllServers" xml:space="preserve">
<value>All configurations</value> <value>All configurations</value>
</data> </data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root> </root>

View file

@ -1647,4 +1647,7 @@
<data name="menuAllServers" xml:space="preserve"> <data name="menuAllServers" xml:space="preserve">
<value>全部配置项</value> <value>全部配置项</value>
</data> </data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>按地区分组</value>
</data>
</root> </root>

View file

@ -1647,4 +1647,7 @@
<data name="menuAllServers" xml:space="preserve"> <data name="menuAllServers" xml:space="preserve">
<value>All configurations</value> <value>All configurations</value>
</data> </data>
<data name="menuGenRegionGroup" xml:space="preserve">
<value>Group by Region</value>
</data>
</root> </root>

View file

@ -45,6 +45,7 @@ public class ProfilesViewModel : MyReactiveObject
public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; } public ReactiveCommand<Unit, Unit> SetDefaultServerCmd { get; }
public ReactiveCommand<Unit, Unit> ShareServerCmd { get; } public ReactiveCommand<Unit, Unit> ShareServerCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupAllServerCmd { get; } public ReactiveCommand<Unit, Unit> GenGroupAllServerCmd { get; }
public ReactiveCommand<Unit, Unit> GenGroupRegionServerCmd { get; }
//servers move //servers move
public ReactiveCommand<Unit, Unit> MoveTopCmd { get; } public ReactiveCommand<Unit, Unit> MoveTopCmd { get; }
@ -133,6 +134,10 @@ public class ProfilesViewModel : MyReactiveObject
{ {
await GenGroupAllServer(); await GenGroupAllServer();
}, canEditRemove); }, canEditRemove);
GenGroupRegionServerCmd = ReactiveCommand.CreateFromTask(async () =>
{
await GenGroupRegionServer();
}, canEditRemove);
//servers move //servers move
MoveTopCmd = ReactiveCommand.CreateFromTask(async () => MoveTopCmd = ReactiveCommand.CreateFromTask(async () =>
@ -623,6 +628,19 @@ public class ProfilesViewModel : MyReactiveObject
await RefreshServers(); 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<string>;
_pendingSelectIndexId = indexIdList?.FirstOrDefault();
await RefreshServers();
}
public async Task SortServer(string colName) public async Task SortServer(string colName)
{ {
if (colName.IsNullOrEmpty()) if (colName.IsNullOrEmpty())

View file

@ -195,6 +195,7 @@
<Separator /> <Separator />
<MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}"> <MenuItem Header="{x:Static resx:ResUI.menuGenGroupServer}">
<MenuItem x:Name="menuGenGroupAllServer" Header="{x:Static resx:ResUI.menuAllServers}" /> <MenuItem x:Name="menuGenGroupAllServer" Header="{x:Static resx:ResUI.menuAllServers}" />
<MenuItem x:Name="menuGenGroupRegionServer" Header="{x:Static resx:ResUI.menuGenRegionGroup}" />
</MenuItem> </MenuItem>
</ContextMenu> </ContextMenu>
</DataGrid.ContextMenu> </DataGrid.ContextMenu>

View file

@ -61,6 +61,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).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.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).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 //servers move
//this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); //this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables);

View file

@ -245,6 +245,10 @@
x:Name="menuGenGroupAllServer" x:Name="menuGenGroupAllServer"
Height="{StaticResource MenuItemHeight}" Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuAllServers}" /> Header="{x:Static resx:ResUI.menuAllServers}" />
<MenuItem
x:Name="menuGenGroupRegionServer"
Height="{StaticResource MenuItemHeight}"
Header="{x:Static resx:ResUI.menuGenRegionGroup}" />
</MenuItem> </MenuItem>
</ContextMenu> </ContextMenu>
</DataGrid.ContextMenu> </DataGrid.ContextMenu>

View file

@ -55,6 +55,7 @@ public partial class ProfilesView
this.BindCommand(ViewModel, vm => vm.SetDefaultServerCmd, v => v.menuSetDefaultServer).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.ShareServerCmd, v => v.menuShareServer).DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.GenGroupAllServerCmd, v => v.menuGenGroupAllServer).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 //servers move
this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables); this.OneWayBind(ViewModel, vm => vm.SubItems, v => v.cmbMoveToGroup.ItemsSource).DisposeWith(disposables);