From aa553805c0f6cb0035f78afc8524e1990de28b47 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Thu, 26 Feb 2026 14:20:57 +0800 Subject: [PATCH 1/8] Refactor node pre check --- v2rayN/ServiceLib/GlobalUsings.cs | 1 + .../Builder/CoreConfigContextBuilder.cs | 266 +++++++++++++ .../Handler/Builder/NodeValidator.cs | 180 +++++++++ .../ServiceLib/Handler/CoreConfigHandler.cs | 129 +------ .../Manager/ActionPrecheckManager.cs | 352 ------------------ v2rayN/ServiceLib/Manager/CoreManager.cs | 17 +- .../ServiceLib/Manager/GroupProfileManager.cs | 15 +- v2rayN/ServiceLib/Models/CoreConfigContext.cs | 1 + .../ViewModels/MainWindowViewModel.cs | 21 +- .../ViewModels/ProfilesViewModel.cs | 24 +- 10 files changed, 503 insertions(+), 503 deletions(-) create mode 100644 v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs create mode 100644 v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs delete mode 100644 v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs diff --git a/v2rayN/ServiceLib/GlobalUsings.cs b/v2rayN/ServiceLib/GlobalUsings.cs index d38ccc06..4553cbc4 100644 --- a/v2rayN/ServiceLib/GlobalUsings.cs +++ b/v2rayN/ServiceLib/GlobalUsings.cs @@ -24,6 +24,7 @@ global using ServiceLib.Common; global using ServiceLib.Enums; global using ServiceLib.Events; global using ServiceLib.Handler; +global using ServiceLib.Handler.Builder; global using ServiceLib.Handler.Fmt; global using ServiceLib.Handler.SysProxy; global using ServiceLib.Helper; diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs new file mode 100644 index 00000000..7c1e93f1 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -0,0 +1,266 @@ +namespace ServiceLib.Handler.Builder; + +public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult) +{ + public bool Success => ValidatorResult.Success; +} + +public class CoreConfigContextBuilder +{ + public static async Task Build(Config config, ProfileItem node) + { + var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box + ? ECoreType.sing_box + : ECoreType.Xray; + var context = new CoreConfigContext() + { + Node = node, + RunCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType), + AllProxiesMap = [], + AppConfig = config, + FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType), + IsTunEnabled = config.TunModeItem.EnableTun, + SimpleDnsItem = config.SimpleDNSItem, + ProtectDomainList = [], + TunProtectSsPort = 0, + ProxyRelaySsPort = 0, + RawDnsItem = await AppManager.Instance.GetDNSItem(coreType), + RoutingItem = await ConfigHandler.GetDefaultRouting(config), + }; + var validatorResult = NodeValidatorResult.Empty(); + var (actNode, nodeValidatorResult) = await FillNodeContext(context, node); + if (!nodeValidatorResult.Success) + { + return new CoreConfigContextBuilderResult(context, nodeValidatorResult); + } + context = context with { Node = actNode }; + validatorResult.Warnings.AddRange(nodeValidatorResult.Warnings); + if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) + { + var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet); + foreach (var ruleItem in rules.Where(ruleItem => !Global.OutboundTags.Contains(ruleItem.OutboundTag))) + { + var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); + if (ruleOutboundNode == null) + { + continue; + } + + var (actRuleNode, ruleNodeValidatorResult) = await FillNodeContext(context, ruleOutboundNode, false); + validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Warnings.Select(w => + $"Routing rule {ruleItem.Remarks} outbound node {ruleItem.OutboundTag} warning: {w}")); + if (!ruleNodeValidatorResult.Success) + { + validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e => + $"Routing rule {ruleItem.Remarks} outbound node {ruleItem.OutboundTag} error: {e}. Fallback to proxy node only.")); + ruleItem.OutboundTag = Global.ProxyTag; + continue; + } + + context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = actRuleNode; + } + } + + return new CoreConfigContextBuilderResult(context, validatorResult); + } + + public static async Task<(ProfileItem, NodeValidatorResult)> FillNodeContext(CoreConfigContext context, + ProfileItem node, + bool includeSubChain = true) + { + if (node.IndexId.IsNullOrEmpty()) + { + return (node, NodeValidatorResult.Empty()); + } + + if (includeSubChain) + { + var virtualChainNode = await BuildVirtualSubChainNode(node); + if (virtualChainNode != null) + { + context.AllProxiesMap[virtualChainNode.IndexId] = virtualChainNode; + return await FillNodeContext(context, virtualChainNode, false); + } + } + + var fillResult = await FillNodeContextPrivate(context, node); + return (node, fillResult); + } + + private static async Task BuildVirtualSubChainNode(ProfileItem node) + { + if (node.Subid.IsNullOrEmpty()) + { + return null; + } + + var subItem = await AppManager.Instance.GetSubItem(node.Subid); + if (subItem == null + || (subItem.PrevProfile.IsNullOrEmpty() + && subItem.NextProfile.IsNullOrEmpty())) + { + return null; + } + + var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (prevNode is null && nextNode is null) + { + return null; + } + + // Build new proxy chain node + var chainNode = new ProfileItem() + { + IndexId = $"inner-{Utils.GetGuid(false)}", + ConfigType = EConfigType.ProxyChain, + CoreType = node.CoreType ?? ECoreType.Xray, + }; + List childItems = [prevNode?.IndexId, node.IndexId, nextNode?.IndexId]; + var chainExtraItem = chainNode.GetProtocolExtra() with + { + GroupType = chainNode.ConfigType.ToString(), + ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())), + }; + chainNode.SetProtocolExtra(chainExtraItem); + return chainNode; + } + + private static async Task FillNodeContextPrivate(CoreConfigContext context, ProfileItem node) + { + if (node.ConfigType.IsGroupType()) + { + return await FillGroupNodeContextPrivate(context, node); + } + else + { + return FillNormalNodeContextPrivate(context, node); + } + } + + private static NodeValidatorResult FillNormalNodeContextPrivate(CoreConfigContext context, ProfileItem node) + { + if (node.ConfigType.IsGroupType()) + { + return NodeValidatorResult.Empty(); + } + + var nodeValidatorResult = NodeValidator.Validate(node, context.RunCoreType); + if (!nodeValidatorResult.Success) + { + return nodeValidatorResult; + } + + context.AllProxiesMap[node.IndexId] = node; + + var address = node.Address; + if (Utils.IsDomain(address)) + { + context.ProtectDomainList.Add(address); + } + + if (!node.EchConfigList.IsNullOrEmpty()) + { + var echQuerySni = node.Sni; + if (node.StreamSecurity == Global.StreamSecurity + && node.EchConfigList?.Contains("://") == true) + { + var idx = node.EchConfigList.IndexOf('+'); + echQuerySni = idx > 0 ? node.EchConfigList[..idx] : node.Sni; + } + + if (Utils.IsDomain(echQuerySni)) + { + context.ProtectDomainList.Add(echQuerySni); + } + } + + return nodeValidatorResult; + } + + private static async Task FillGroupNodeContextPrivate(CoreConfigContext context, + ProfileItem node) + { + if (!node.ConfigType.IsGroupType()) + { + return NodeValidatorResult.Empty(); + } + + HashSet ancestors = [node.IndexId]; + HashSet globalVisited = [node.IndexId]; + return await FillGroupNodeContextPrivate(context, node, globalVisited, ancestors); + } + + private static async Task FillGroupNodeContextPrivate( + CoreConfigContext context, + ProfileItem node, + HashSet globalVisitedGroup, + HashSet ancestorsGroup) + { + var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node); + List childIndexIdList = []; + var childNodeValidatorResult = NodeValidatorResult.Empty(); + foreach (var childNode in groupChildList) + { + if (ancestorsGroup.Contains(childNode.IndexId)) + { + childNodeValidatorResult.Errors.Add( + $"Group {node.Remarks} has a cycle dependency on child node {childNode.Remarks}. Skipping this node."); + continue; + } + + if (globalVisitedGroup.Contains(childNode.IndexId)) + { + childIndexIdList.Add(childNode.IndexId); + continue; + } + + if (!childNode.ConfigType.IsGroupType()) + { + var childNodeResult = FillNormalNodeContextPrivate(context, childNode); + childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w => + $"Group {node.Remarks} child node {childNode.Remarks} warning: {w}")); + childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e => + $"Group {node.Remarks} child node {childNode.Remarks} error: {e}. Skipping this node.")); + if (!childNodeResult.Success) + { + continue; + } + + globalVisitedGroup.Add(childNode.IndexId); + childIndexIdList.Add(childNode.IndexId); + continue; + } + + globalVisitedGroup.Add(childNode.IndexId); + var newAncestorsGroup = new HashSet(ancestorsGroup) { childNode.IndexId }; + var childGroupResult = + await FillGroupNodeContextPrivate(context, childNode, globalVisitedGroup, newAncestorsGroup); + childNodeValidatorResult.Warnings.AddRange(childGroupResult.Warnings.Select(w => + $"Group {node.Remarks} child group node {childNode.Remarks} warning: {w}")); + childNodeValidatorResult.Errors.AddRange(childGroupResult.Errors.Select(e => + $"Group {node.Remarks} child group node {childNode.Remarks} error: {e}. Skipping this node.")); + if (!childGroupResult.Success) + { + continue; + } + + childIndexIdList.Add(childNode.IndexId); + } + + if (childIndexIdList.Count == 0) + { + childNodeValidatorResult.Errors.Add($"Group {node.Remarks} has no valid child node."); + return childNodeValidatorResult; + } + else + { + childNodeValidatorResult.Warnings.AddRange(childNodeValidatorResult.Errors); + childNodeValidatorResult.Errors.Clear(); + } + + node.SetProtocolExtra(node.GetProtocolExtra() with { ChildItems = Utils.List2String(childIndexIdList), }); + context.AllProxiesMap[node.IndexId] = node; + return childNodeValidatorResult; + } +} diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs new file mode 100644 index 00000000..bfffb599 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -0,0 +1,180 @@ +namespace ServiceLib.Handler.Builder; + +public record NodeValidatorResult(List Errors, List Warnings) +{ + public bool Success => Errors.Count == 0; + + public static NodeValidatorResult Empty() + { + return new NodeValidatorResult([], []); + } +} + +public class NodeValidator +{ + // Static validator rules + private static readonly HashSet SingboxUnsupportedTransports = + [nameof(ETransport.kcp), nameof(ETransport.xhttp)]; + + private static readonly HashSet SingboxTransportSupportedProtocols = + [EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks]; + + private static readonly HashSet SingboxShadowsocksAllowedTransports = + [nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)]; + + public static NodeValidatorResult Validate(ProfileItem item, ECoreType coreType) + { + var v = new ValidationContext(); + ValidateNodeAndCoreSupport(item, coreType, v); + return v.ToResult(); + } + + private class ValidationContext + { + public List Errors { get; } = []; + public List Warnings { get; } = []; + + public void Error(string message) + { + Errors.Add(message); + } + + public void Warning(string message) + { + Warnings.Add(message); + } + + public void Assert(bool condition, string errorMsg) + { + if (!condition) + { + Error(errorMsg); + } + } + + public NodeValidatorResult ToResult() + { + return new NodeValidatorResult(Errors, Warnings); + } + } + + private static void ValidateNodeAndCoreSupport(ProfileItem item, ECoreType coreType, ValidationContext v) + { + if (item.ConfigType is EConfigType.Custom) + { + return; + } + + if (item.ConfigType.IsGroupType()) + { + // Group logic is handled in ValidateGroupNode + return; + } + + // Basic Property Validation + v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Address")); + v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.InvalidProperty, "Port")); + + // Network & Core Logic + var net = item.GetNetwork(); + if (coreType == ECoreType.sing_box) + { + var transportError = ValidateSingboxTransport(item.ConfigType, net); + if (transportError != null) + v.Error(transportError); + + if (!Global.SingboxSupportConfigType.Contains(item.ConfigType)) + { + v.Error(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.sing_box), item.ConfigType)); + } + } + else if (coreType is ECoreType.Xray) + { + if (!Global.XraySupportConfigType.Contains(item.ConfigType)) + { + v.Error(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType)); + } + } + + // Protocol Specifics + var protocolExtra = item.GetProtocolExtra(); + switch (item.ConfigType) + { + case EConfigType.VMess: + v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password), + string.Format(ResUI.InvalidProperty, "Password")); + break; + + case EConfigType.VLESS: + // Example of converting a non-critical issue to Warning if desired + if (item.Password.Length <= 30 && !Utils.IsGuidByParse(item.Password)) + { + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + } + else + { + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + } + + v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty), + string.Format(ResUI.InvalidProperty, "Flow")); + break; + + case EConfigType.Shadowsocks: + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + v.Assert( + !string.IsNullOrEmpty(protocolExtra.SsMethod) && + Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod), + string.Format(ResUI.InvalidProperty, "SsMethod")); + break; + } + + // TLS & Security + if (item.StreamSecurity == Global.StreamSecurity) + { + if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 && + !item.CertSha.IsNullOrEmpty()) + { + v.Error(string.Format(ResUI.InvalidProperty, "TLS Certificate")); + } + } + + if (item.StreamSecurity == Global.StreamSecurityReality) + { + v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "PublicKey")); + } + + if (item.Network == nameof(ETransport.xhttp) && !item.Extra.IsNullOrEmpty()) + { + if (JsonUtils.ParseJson(item.Extra) is null) + { + v.Error(string.Format(ResUI.InvalidProperty, "XHTTP Extra")); + } + } + } + + private static string? ValidateSingboxTransport(EConfigType configType, string net) + { + // sing-box does not support xhttp / kcp transports + if (SingboxUnsupportedTransports.Contains(net)) + { + return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net); + } + + // sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks + if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp)) + { + return string.Format(ResUI.CoreNotSupportProtocolTransport, + nameof(ECoreType.sing_box), configType.ToString(), net); + } + + // sing-box shadowsocks only supports tcp/ws/quic transports + if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net)) + { + return string.Format(ResUI.CoreNotSupportProtocolTransport, + nameof(ECoreType.sing_box), configType.ToString(), net); + } + + return null; + } +} diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index a38faafa..37e1e268 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -93,13 +93,14 @@ public static class CoreConfigHandler public static async Task GenerateClientSpeedtestConfig(Config config, string fileName, List selecteds, ECoreType coreType) { var result = new RetResult(); - var context = await BuildCoreConfigContext(config, new()); + var builderResult = await CoreConfigContextBuilder.Build(config, new()); + var context = builderResult.Context; var ids = selecteds.Where(serverTestItem => !serverTestItem.IndexId.IsNullOrEmpty()) .Select(serverTestItem => serverTestItem.IndexId); var nodes = await AppManager.Instance.GetProfileItemsByIndexIds(ids); foreach (var node in nodes) { - var actNode = await FillNodeContext(context, node, true); + var (actNode, _) = await CoreConfigContextBuilder.FillNodeContext(context, node, true); if (node.IndexId == actNode.IndexId) { continue; @@ -146,128 +147,4 @@ public static class CoreConfigHandler await File.WriteAllTextAsync(fileName, result.Data.ToString()); return result; } - - public static async Task BuildCoreConfigContext(Config config, ProfileItem node) - { - var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray; - var context = new CoreConfigContext() - { - Node = node, - AllProxiesMap = [], - AppConfig = config, - FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType), - IsTunEnabled = config.TunModeItem.EnableTun, - SimpleDnsItem = config.SimpleDNSItem, - ProtectDomainList = [], - TunProtectSsPort = 0, - ProxyRelaySsPort = 0, - RawDnsItem = await AppManager.Instance.GetDNSItem(coreType), - RoutingItem = await ConfigHandler.GetDefaultRouting(config), - }; - context = context with - { - Node = await FillNodeContext(context, node) - }; - if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) - { - var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet); - foreach (var ruleItem in rules.Where(ruleItem => !Global.OutboundTags.Contains(ruleItem.OutboundTag))) - { - var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); - if (ruleOutboundNode != null) - { - var ruleOutboundNodeAct = await FillNodeContext(context, ruleOutboundNode, false); - context.AllProxiesMap[$"remark:{ruleItem.OutboundTag}"] = ruleOutboundNodeAct; - } - } - } - return context; - } - - private static async Task FillNodeContext(CoreConfigContext context, ProfileItem node, bool includeSubChain = true) - { - if (node.IndexId.IsNullOrEmpty()) - { - return node; - } - var newItems = new List { node }; - if (node.ConfigType.IsGroupType()) - { - var (groupChildList, _) = await GroupProfileManager.GetChildProfileItems(node); - foreach (var childItem in groupChildList.Where(childItem => !context.AllProxiesMap.ContainsKey(childItem.IndexId))) - { - await FillNodeContext(context, childItem, false); - } - node.SetProtocolExtra(node.GetProtocolExtra() with - { - ChildItems = Utils.List2String(groupChildList.Select(n => n.IndexId).ToList()), - }); - newItems.AddRange(groupChildList); - } - context.AllProxiesMap[node.IndexId] = node; - - foreach (var item in newItems) - { - var address = item.Address; - if (Utils.IsDomain(address)) - { - context.ProtectDomainList.Add(address); - } - - if (item.EchConfigList.IsNullOrEmpty()) - { - continue; - } - - var echQuerySni = item.Sni; - if (item.StreamSecurity == Global.StreamSecurity - && item.EchConfigList?.Contains("://") == true) - { - var idx = item.EchConfigList.IndexOf('+'); - echQuerySni = idx > 0 ? item.EchConfigList[..idx] : item.Sni; - } - if (!Utils.IsDomain(echQuerySni)) - { - continue; - } - context.ProtectDomainList.Add(echQuerySni); - } - - if (!includeSubChain || node.Subid.IsNullOrEmpty()) - { - return node; - } - - var subItem = await AppManager.Instance.GetSubItem(node.Subid); - if (subItem == null) - { - return node; - } - var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); - if (prevNode is null && nextNode is null) - { - return node; - } - - var prevNodeAct = prevNode is null ? null : await FillNodeContext(context, prevNode, false); - var nextNodeAct = nextNode is null ? null : await FillNodeContext(context, nextNode, false); - - // Build new proxy chain node - var chainNode = new ProfileItem() - { - IndexId = $"inner-{Utils.GetGuid(false)}", - ConfigType = EConfigType.ProxyChain, - CoreType = node.CoreType ?? ECoreType.Xray, - }; - List childItems = [prevNodeAct?.IndexId, node.IndexId, nextNodeAct?.IndexId]; - var chainExtraItem = chainNode.GetProtocolExtra() with - { - GroupType = chainNode.ConfigType.ToString(), - ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())), - }; - chainNode.SetProtocolExtra(chainExtraItem); - context.AllProxiesMap[chainNode.IndexId] = chainNode; - return chainNode; - } } diff --git a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs deleted file mode 100644 index ee3cd5a0..00000000 --- a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs +++ /dev/null @@ -1,352 +0,0 @@ -namespace ServiceLib.Manager; - -/// -/// Centralized pre-checks before sensitive actions (set active profile, generate config, etc.). -/// -public class ActionPrecheckManager -{ - private static readonly Lazy _instance = new(); - public static ActionPrecheckManager Instance => _instance.Value; - - // sing-box supported transports for different protocol types - private static readonly HashSet SingboxUnsupportedTransports = [nameof(ETransport.kcp), nameof(ETransport.xhttp)]; - - private static readonly HashSet SingboxTransportSupportedProtocols = - [EConfigType.VMess, EConfigType.VLESS, EConfigType.Trojan, EConfigType.Shadowsocks]; - - private static readonly HashSet SingboxShadowsocksAllowedTransports = - [nameof(ETransport.tcp), nameof(ETransport.ws), nameof(ETransport.quic)]; - - public async Task> Check(string? indexId) - { - if (indexId.IsNullOrEmpty()) - { - return [ResUI.PleaseSelectServer]; - } - - var item = await AppManager.Instance.GetProfileItem(indexId); - if (item is null) - { - return [ResUI.PleaseSelectServer]; - } - - return await Check(item); - } - - public async Task> Check(ProfileItem? item) - { - if (item is null) - { - return [ResUI.PleaseSelectServer]; - } - - var errors = new List(); - - errors.AddRange(await ValidateCurrentNodeAndCoreSupport(item)); - errors.AddRange(await ValidateRelatedNodesExistAndValid(item)); - - return errors; - } - - private async Task> ValidateCurrentNodeAndCoreSupport(ProfileItem item) - { - if (item.ConfigType == EConfigType.Custom) - { - return []; - } - - var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); - return await ValidateNodeAndCoreSupport(item, coreType); - } - - private async Task> ValidateNodeAndCoreSupport(ProfileItem item, ECoreType? coreType = null) - { - var errors = new List(); - - coreType ??= AppManager.Instance.GetCoreType(item, item.ConfigType); - - if (item.ConfigType is EConfigType.Custom) - { - errors.Add(string.Format(ResUI.NotSupportProtocol, item.ConfigType.ToString())); - return errors; - } - else if (item.ConfigType.IsGroupType()) - { - var groupErrors = await ValidateGroupNode(item, coreType); - errors.AddRange(groupErrors); - return errors; - } - else if (!item.IsComplex()) - { - var normalErrors = await ValidateNormalNode(item, coreType); - errors.AddRange(normalErrors); - return errors; - } - - return errors; - } - - private async Task> ValidateNormalNode(ProfileItem item, ECoreType? coreType = null) - { - var errors = new List(); - - if (item.Address.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Address")); - return errors; - } - - if (item.Port is <= 0 or > 65535) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Port")); - return errors; - } - - var net = item.GetNetwork(); - - if (coreType == ECoreType.sing_box) - { - var transportError = ValidateSingboxTransport(item.ConfigType, net); - if (transportError != null) - { - errors.Add(transportError); - } - - if (!Global.SingboxSupportConfigType.Contains(item.ConfigType)) - { - errors.Add(string.Format(ResUI.CoreNotSupportProtocol, - nameof(ECoreType.sing_box), item.ConfigType.ToString())); - } - } - else if (coreType is ECoreType.Xray) - { - // Xray core does not support these protocols - if (!Global.XraySupportConfigType.Contains(item.ConfigType)) - { - errors.Add(string.Format(ResUI.CoreNotSupportProtocol, - nameof(ECoreType.Xray), item.ConfigType.ToString())); - } - } - - var protocolExtra = item.GetProtocolExtra(); - - switch (item.ConfigType) - { - case EConfigType.VMess: - if (item.Password.IsNullOrEmpty() || !Utils.IsGuidByParse(item.Password)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Password")); - } - - break; - - case EConfigType.VLESS: - if (item.Password.IsNullOrEmpty() || (!Utils.IsGuidByParse(item.Password) && item.Password.Length > 30)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Password")); - } - - if (!Global.Flows.Contains(protocolExtra.Flow ?? string.Empty)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Flow")); - } - - break; - - case EConfigType.Shadowsocks: - if (item.Password.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "Password")); - } - - if (string.IsNullOrEmpty(protocolExtra.SsMethod) || !Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod)) - { - errors.Add(string.Format(ResUI.InvalidProperty, "SsMethod")); - } - - break; - } - - if (item.StreamSecurity == Global.StreamSecurity) - { - // check certificate validity - if (!item.Cert.IsNullOrEmpty() - && (CertPemManager.ParsePemChain(item.Cert).Count == 0) - && !item.CertSha.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate")); - } - } - - if (item.StreamSecurity == Global.StreamSecurityReality) - { - if (item.PublicKey.IsNullOrEmpty()) - { - errors.Add(string.Format(ResUI.InvalidProperty, "PublicKey")); - } - } - - if (item.Network == nameof(ETransport.xhttp) - && !item.Extra.IsNullOrEmpty()) - { - // check xhttp extra json validity - var xhttpExtra = JsonUtils.ParseJson(item.Extra); - if (xhttpExtra is null) - { - errors.Add(string.Format(ResUI.InvalidProperty, "XHTTP Extra")); - } - } - - return errors; - } - - private async Task> ValidateGroupNode(ProfileItem item, ECoreType? coreType = null) - { - var errors = new List(); - - var hasCycle = await GroupProfileManager.HasCycle(item.IndexId, item.GetProtocolExtra()); - if (hasCycle) - { - errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks)); - return errors; - } - - var (childItems, _) = await GroupProfileManager.GetChildProfileItems(item); - - foreach (var childItem in childItems) - { - var childErrors = new List(); - - if (childItem is null) - { - childErrors.Add(string.Format(ResUI.NodeTagNotExist, "")); - continue; - } - - if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain) - { - childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks)); - continue; - } - - childErrors.AddRange(await ValidateNodeAndCoreSupport(childItem, coreType)); - errors.AddRange(childErrors.Select(s => s.Insert(0, $"{childItem.Remarks}: "))); - } - return errors; - } - - private static string? ValidateSingboxTransport(EConfigType configType, string net) - { - // sing-box does not support xhttp / kcp transports - if (SingboxUnsupportedTransports.Contains(net)) - { - return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net); - } - - // sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks - if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp)) - { - return string.Format(ResUI.CoreNotSupportProtocolTransport, - nameof(ECoreType.sing_box), configType.ToString(), net); - } - - // sing-box shadowsocks only supports tcp/ws/quic transports - if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net)) - { - return string.Format(ResUI.CoreNotSupportProtocolTransport, - nameof(ECoreType.sing_box), configType.ToString(), net); - } - - return null; - } - - private async Task> ValidateRelatedNodesExistAndValid(ProfileItem? item) - { - var errors = new List(); - errors.AddRange(await ValidateProxyChainedNodeExistAndValid(item)); - errors.AddRange(await ValidateRoutingNodeExistAndValid(item)); - return errors; - } - - private async Task> ValidateProxyChainedNodeExistAndValid(ProfileItem? item) - { - var errors = new List(); - if (item is null) - { - return errors; - } - - // prev node and next node - var subItem = await AppManager.Instance.GetSubItem(item.Subid); - if (subItem is null) - { - return errors; - } - - var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); - var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); - - await CollectProxyChainedNodeValidation(prevNode, subItem.PrevProfile, coreType, errors); - await CollectProxyChainedNodeValidation(nextNode, subItem.NextProfile, coreType, errors); - - return errors; - } - - private async Task CollectProxyChainedNodeValidation(ProfileItem? node, string tag, ECoreType coreType, List errors) - { - if (node is not null) - { - var nodeErrors = await ValidateNodeAndCoreSupport(node, coreType); - errors.AddRange(nodeErrors.Select(s => ResUI.ProxyChainedPrefix + $"{node.Remarks}: " + s)); - } - else if (tag.IsNotEmpty()) - { - errors.Add(ResUI.ProxyChainedPrefix + string.Format(ResUI.NodeTagNotExist, tag)); - } - } - - private async Task> ValidateRoutingNodeExistAndValid(ProfileItem? item) - { - var errors = new List(); - - if (item is null) - { - return errors; - } - - var coreType = AppManager.Instance.GetCoreType(item, item.ConfigType); - var routing = await ConfigHandler.GetDefaultRouting(AppManager.Instance.Config); - if (routing == null) - { - return errors; - } - - var rules = JsonUtils.Deserialize>(routing.RuleSet); - foreach (var ruleItem in rules ?? []) - { - if (!ruleItem.Enabled) - { - continue; - } - - var outboundTag = ruleItem.OutboundTag; - if (outboundTag.IsNullOrEmpty() || Global.OutboundTags.Contains(outboundTag)) - { - continue; - } - - var tagItem = await AppManager.Instance.GetProfileItemViaRemarks(outboundTag); - if (tagItem is null) - { - errors.Add(ResUI.RoutingRuleOutboundPrefix + string.Format(ResUI.NodeTagNotExist, outboundTag)); - continue; - } - - var tagErrors = await ValidateNodeAndCoreSupport(tagItem, coreType); - errors.AddRange(tagErrors.Select(s => ResUI.RoutingRuleOutboundPrefix + $"{tagItem.Remarks}: " + s)); - } - - return errors; - } -} diff --git a/v2rayN/ServiceLib/Manager/CoreManager.cs b/v2rayN/ServiceLib/Manager/CoreManager.cs index 159af85e..cd30450f 100644 --- a/v2rayN/ServiceLib/Manager/CoreManager.cs +++ b/v2rayN/ServiceLib/Manager/CoreManager.cs @@ -57,26 +57,27 @@ public class CoreManager } } - public async Task LoadCore(ProfileItem? node) + public async Task LoadCore(CoreConfigContext? context) { - if (node == null) + if (context == null) { await UpdateFunc(false, ResUI.CheckServerSettings); return; } + var contextMod = context; + var node = contextMod.Node; var fileName = Utils.GetBinConfigPath(Global.CoreConfigFileName); - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, node); - var preContext = ConfigHandler.GetPreSocksCoreConfigContext(context); + var preContext = ConfigHandler.GetPreSocksCoreConfigContext(contextMod); if (preContext is not null) { - context = context with + contextMod = contextMod with { TunProtectSsPort = preContext.TunProtectSsPort, ProxyRelaySsPort = preContext.ProxyRelaySsPort, }; } - var result = await CoreConfigHandler.GenerateClientConfig(context, fileName); + var result = await CoreConfigHandler.GenerateClientConfig(contextMod, fileName); if (result.Success != true) { await UpdateFunc(true, result.Msg); @@ -95,7 +96,7 @@ public class CoreManager await WindowsUtils.RemoveTunDevice(); } - await CoreStart(context); + await CoreStart(contextMod); await CoreStartPreService(preContext); if (_processService != null) { @@ -132,7 +133,7 @@ public class CoreManager var fileName = string.Format(Global.CoreSpeedtestConfigFileName, Utils.GetGuid(false)); var configPath = Utils.GetBinConfigPath(fileName); - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, node); + var (context, _) = await CoreConfigContextBuilder.Build(_config, node); var result = await CoreConfigHandler.GenerateClientSpeedtestConfig(_config, context, testItem, configPath); if (result.Success != true) { diff --git a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs index 5e87f0e6..510667dd 100644 --- a/v2rayN/ServiceLib/Manager/GroupProfileManager.cs +++ b/v2rayN/ServiceLib/Manager/GroupProfileManager.cs @@ -143,26 +143,27 @@ public class GroupProfileManager .ToList() ?? []; } - public static async Task> GetAllChildProfileItems(ProfileItem profileItem) + public static async Task> GetAllChildProfileItems(ProfileItem profileItem) { - var allChildItems = new List(); + var itemMap = new Dictionary(); var visited = new HashSet(); - await CollectChildItems(profileItem, allChildItems, visited); + await CollectChildItems(profileItem, itemMap, visited); - return allChildItems; + return itemMap; } - private static async Task CollectChildItems(ProfileItem profileItem, List allChildItems, HashSet visited) + private static async Task CollectChildItems(ProfileItem profileItem, Dictionary itemMap, + HashSet visited) { var (childItems, _) = await GetChildProfileItems(profileItem); foreach (var child in childItems.Where(child => visited.Add(child.IndexId))) { - allChildItems.Add(child); + itemMap[child.IndexId] = child; if (child.ConfigType.IsGroupType()) { - await CollectChildItems(child, allChildItems, visited); + await CollectChildItems(child, itemMap, visited); } } } diff --git a/v2rayN/ServiceLib/Models/CoreConfigContext.cs b/v2rayN/ServiceLib/Models/CoreConfigContext.cs index a0a43a20..a9a49f96 100644 --- a/v2rayN/ServiceLib/Models/CoreConfigContext.cs +++ b/v2rayN/ServiceLib/Models/CoreConfigContext.cs @@ -3,6 +3,7 @@ namespace ServiceLib.Models; public record CoreConfigContext { public required ProfileItem Node { get; init; } + public required ECoreType RunCoreType { get; init; } public RoutingItem? RoutingItem { get; init; } public DNSItem? RawDnsItem { get; init; } public SimpleDNSItem SimpleDnsItem { get; init; } = new(); diff --git a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs index 8f5e9029..becabf25 100644 --- a/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/MainWindowViewModel.cs @@ -540,7 +540,14 @@ public class MainWindowViewModel : MyReactiveObject { SetReloadEnabled(false); - var msgs = await ActionPrecheckManager.Instance.Check(_config.IndexId); + var profileItem = await ConfigHandler.GetDefaultServer(_config); + if (profileItem == null) + { + NoticeManager.Instance.Enqueue(ResUI.CheckServerSettings); + return; + } + var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, profileItem); + var msgs = new List([..validatorResult.Errors, ..validatorResult.Warnings]); if (msgs.Count > 0) { foreach (var msg in msgs) @@ -548,12 +555,15 @@ public class MainWindowViewModel : MyReactiveObject NoticeManager.Instance.SendMessage(msg); } NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); - return; + if (!validatorResult.Success) + { + return; + } } await Task.Run(async () => { - await LoadCore(); + await LoadCore(context); await SysProxyHandler.UpdateSysProxy(_config, false); await Task.Delay(1000); }); @@ -594,10 +604,9 @@ public class MainWindowViewModel : MyReactiveObject RxApp.MainThreadScheduler.Schedule(() => BlReloadEnabled = enabled); } - private async Task LoadCore() + private async Task LoadCore(CoreConfigContext? context) { - var node = await ConfigHandler.GetDefaultServer(_config); - await CoreManager.Instance.LoadCore(node); + await CoreManager.Instance.LoadCore(context); } #endregion core job diff --git a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs index 8b4e7eb0..1cc6f46e 100644 --- a/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/ProfilesViewModel.cs @@ -788,7 +788,8 @@ public class ProfilesViewModel : MyReactiveObject return; } - var msgs = await ActionPrecheckManager.Instance.Check(item); + var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item); + var msgs = new List([..validatorResult.Errors, ..validatorResult.Warnings]); if (msgs.Count > 0) { foreach (var msg in msgs) @@ -796,12 +797,14 @@ public class ProfilesViewModel : MyReactiveObject NoticeManager.Instance.SendMessage(msg); } NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); - return; + if (!validatorResult.Success) + { + return; + } } if (blClipboard) { - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item); var result = await CoreConfigHandler.GenerateClientConfig(context, null); if (result.Success != true) { @@ -825,7 +828,20 @@ public class ProfilesViewModel : MyReactiveObject { return; } - var context = await CoreConfigHandler.BuildCoreConfigContext(_config, item); + var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item); + var msgs = new List([..validatorResult.Errors, ..validatorResult.Warnings]); + if (msgs.Count > 0) + { + foreach (var msg in msgs) + { + NoticeManager.Instance.SendMessage(msg); + } + NoticeManager.Instance.Enqueue(Utils.List2String(msgs.Take(10).ToList(), true)); + if (!validatorResult.Success) + { + return; + } + } var result = await CoreConfigHandler.GenerateClientConfig(context, fileName); if (result.Success != true) { From aa59e9b91567c3be515ad2c07b51b9efcd7c6925 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 09:56:06 +0800 Subject: [PATCH 2/8] Rename method --- .../Builder/CoreConfigContextBuilder.cs | 62 ++++++++++++++----- .../ServiceLib/Handler/CoreConfigHandler.cs | 2 +- 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index 7c1e93f1..b2b6e30d 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -7,6 +7,10 @@ public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeVali public class CoreConfigContextBuilder { + /// + /// Builds a for the given node, resolves its proxy map, + /// and processes outbound nodes referenced by routing rules. + /// public static async Task Build(Config config, ProfileItem node) { var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box @@ -28,7 +32,7 @@ public class CoreConfigContextBuilder RoutingItem = await ConfigHandler.GetDefaultRouting(config), }; var validatorResult = NodeValidatorResult.Empty(); - var (actNode, nodeValidatorResult) = await FillNodeContext(context, node); + var (actNode, nodeValidatorResult) = await ResolveNodeAsync(context, node); if (!nodeValidatorResult.Success) { return new CoreConfigContextBuilderResult(context, nodeValidatorResult); @@ -46,7 +50,7 @@ public class CoreConfigContextBuilder continue; } - var (actRuleNode, ruleNodeValidatorResult) = await FillNodeContext(context, ruleOutboundNode, false); + var (actRuleNode, ruleNodeValidatorResult) = await ResolveNodeAsync(context, ruleOutboundNode, false); validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Warnings.Select(w => $"Routing rule {ruleItem.Remarks} outbound node {ruleItem.OutboundTag} warning: {w}")); if (!ruleNodeValidatorResult.Success) @@ -64,7 +68,11 @@ public class CoreConfigContextBuilder return new CoreConfigContextBuilderResult(context, validatorResult); } - public static async Task<(ProfileItem, NodeValidatorResult)> FillNodeContext(CoreConfigContext context, + /// + /// Resolves a node into the context, optionally wrapping it in a subscription-level proxy chain. + /// Returns the effective (possibly replaced) node and the validation result. + /// + public static async Task<(ProfileItem, NodeValidatorResult)> ResolveNodeAsync(CoreConfigContext context, ProfileItem node, bool includeSubChain = true) { @@ -75,19 +83,24 @@ public class CoreConfigContextBuilder if (includeSubChain) { - var virtualChainNode = await BuildVirtualSubChainNode(node); + var virtualChainNode = await BuildSubscriptionChainNodeAsync(node); if (virtualChainNode != null) { context.AllProxiesMap[virtualChainNode.IndexId] = virtualChainNode; - return await FillNodeContext(context, virtualChainNode, false); + return await ResolveNodeAsync(context, virtualChainNode, false); } } - var fillResult = await FillNodeContextPrivate(context, node); + var fillResult = await RegisterNodeAsync(context, node); return (node, fillResult); } - private static async Task BuildVirtualSubChainNode(ProfileItem node) + /// + /// If the node's subscription defines prev/next profiles, creates a virtual + /// node that wraps them together. + /// Returns null when no chain is needed. + /// + private static async Task BuildSubscriptionChainNodeAsync(ProfileItem node) { if (node.Subid.IsNullOrEmpty()) { @@ -126,19 +139,27 @@ public class CoreConfigContextBuilder return chainNode; } - private static async Task FillNodeContextPrivate(CoreConfigContext context, ProfileItem node) + /// + /// Dispatches registration to either or + /// based on the node's config type. + /// + private static async Task RegisterNodeAsync(CoreConfigContext context, ProfileItem node) { if (node.ConfigType.IsGroupType()) { - return await FillGroupNodeContextPrivate(context, node); + return await RegisterGroupNodeAsync(context, node); } else { - return FillNormalNodeContextPrivate(context, node); + return RegisterSingleNodeAsync(context, node); } } - private static NodeValidatorResult FillNormalNodeContextPrivate(CoreConfigContext context, ProfileItem node) + /// + /// Validates a single (non-group) node and, on success, adds it to the proxy map + /// and records any domain addresses that should bypass the proxy. + /// + private static NodeValidatorResult RegisterSingleNodeAsync(CoreConfigContext context, ProfileItem node) { if (node.ConfigType.IsGroupType()) { @@ -178,7 +199,11 @@ public class CoreConfigContextBuilder return nodeValidatorResult; } - private static async Task FillGroupNodeContextPrivate(CoreConfigContext context, + /// + /// Entry point for registering a group node. Initialises the visited/ancestor sets + /// and delegates to . + /// + private static async Task RegisterGroupNodeAsync(CoreConfigContext context, ProfileItem node) { if (!node.ConfigType.IsGroupType()) @@ -188,10 +213,15 @@ public class CoreConfigContextBuilder HashSet ancestors = [node.IndexId]; HashSet globalVisited = [node.IndexId]; - return await FillGroupNodeContextPrivate(context, node, globalVisited, ancestors); + return await TraverseGroupNodeAsync(context, node, globalVisited, ancestors); } - private static async Task FillGroupNodeContextPrivate( + /// + /// Recursively walks the children of a group node, registering valid leaf nodes + /// and nested groups. Detects cycles via and + /// deduplicates shared nodes via . + /// + private static async Task TraverseGroupNodeAsync( CoreConfigContext context, ProfileItem node, HashSet globalVisitedGroup, @@ -217,7 +247,7 @@ public class CoreConfigContextBuilder if (!childNode.ConfigType.IsGroupType()) { - var childNodeResult = FillNormalNodeContextPrivate(context, childNode); + var childNodeResult = RegisterSingleNodeAsync(context, childNode); childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w => $"Group {node.Remarks} child node {childNode.Remarks} warning: {w}")); childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e => @@ -235,7 +265,7 @@ public class CoreConfigContextBuilder globalVisitedGroup.Add(childNode.IndexId); var newAncestorsGroup = new HashSet(ancestorsGroup) { childNode.IndexId }; var childGroupResult = - await FillGroupNodeContextPrivate(context, childNode, globalVisitedGroup, newAncestorsGroup); + await TraverseGroupNodeAsync(context, childNode, globalVisitedGroup, newAncestorsGroup); childNodeValidatorResult.Warnings.AddRange(childGroupResult.Warnings.Select(w => $"Group {node.Remarks} child group node {childNode.Remarks} warning: {w}")); childNodeValidatorResult.Errors.AddRange(childGroupResult.Errors.Select(e => diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index 37e1e268..6991db83 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -100,7 +100,7 @@ public static class CoreConfigHandler var nodes = await AppManager.Instance.GetProfileItemsByIndexIds(ids); foreach (var node in nodes) { - var (actNode, _) = await CoreConfigContextBuilder.FillNodeContext(context, node, true); + var (actNode, _) = await CoreConfigContextBuilder.ResolveNodeAsync(context, node, true); if (node.IndexId == actNode.IndexId) { continue; From 07a5619e60a90b72149f2ade2330b0f781bdd9da Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 10:22:30 +0800 Subject: [PATCH 3/8] I18n --- .../Builder/CoreConfigContextBuilder.cs | 16 +- .../Handler/Builder/NodeValidator.cs | 34 +-- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 216 ++++++++++-------- v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 62 ++--- v2rayN/ServiceLib/Resx/ResUI.fr.resx | 60 ++--- v2rayN/ServiceLib/Resx/ResUI.hu.resx | 62 ++--- v2rayN/ServiceLib/Resx/ResUI.resx | 62 ++--- v2rayN/ServiceLib/Resx/ResUI.ru.resx | 62 ++--- v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 62 ++--- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 62 ++--- .../ViewModels/AddGroupServerViewModel.cs | 7 - 11 files changed, 379 insertions(+), 326 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index b2b6e30d..d932c5ef 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -52,11 +52,11 @@ public class CoreConfigContextBuilder var (actRuleNode, ruleNodeValidatorResult) = await ResolveNodeAsync(context, ruleOutboundNode, false); validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Warnings.Select(w => - $"Routing rule {ruleItem.Remarks} outbound node {ruleItem.OutboundTag} warning: {w}")); + string.Format(ResUI.MsgRoutingRuleOutboundNodeWarning, ruleItem.Remarks, ruleItem.OutboundTag, w))); if (!ruleNodeValidatorResult.Success) { validatorResult.Warnings.AddRange(ruleNodeValidatorResult.Errors.Select(e => - $"Routing rule {ruleItem.Remarks} outbound node {ruleItem.OutboundTag} error: {e}. Fallback to proxy node only.")); + string.Format(ResUI.MsgRoutingRuleOutboundNodeError, ruleItem.Remarks, ruleItem.OutboundTag, e))); ruleItem.OutboundTag = Global.ProxyTag; continue; } @@ -235,7 +235,7 @@ public class CoreConfigContextBuilder if (ancestorsGroup.Contains(childNode.IndexId)) { childNodeValidatorResult.Errors.Add( - $"Group {node.Remarks} has a cycle dependency on child node {childNode.Remarks}. Skipping this node."); + string.Format(ResUI.MsgGroupCycleDependency, node.Remarks, childNode.Remarks)); continue; } @@ -249,9 +249,9 @@ public class CoreConfigContextBuilder { var childNodeResult = RegisterSingleNodeAsync(context, childNode); childNodeValidatorResult.Warnings.AddRange(childNodeResult.Warnings.Select(w => - $"Group {node.Remarks} child node {childNode.Remarks} warning: {w}")); + string.Format(ResUI.MsgGroupChildNodeWarning, node.Remarks, childNode.Remarks, w))); childNodeValidatorResult.Errors.AddRange(childNodeResult.Errors.Select(e => - $"Group {node.Remarks} child node {childNode.Remarks} error: {e}. Skipping this node.")); + string.Format(ResUI.MsgGroupChildNodeError, node.Remarks, childNode.Remarks, e))); if (!childNodeResult.Success) { continue; @@ -267,9 +267,9 @@ public class CoreConfigContextBuilder var childGroupResult = await TraverseGroupNodeAsync(context, childNode, globalVisitedGroup, newAncestorsGroup); childNodeValidatorResult.Warnings.AddRange(childGroupResult.Warnings.Select(w => - $"Group {node.Remarks} child group node {childNode.Remarks} warning: {w}")); + string.Format(ResUI.MsgGroupChildGroupNodeWarning, node.Remarks, childNode.Remarks, w))); childNodeValidatorResult.Errors.AddRange(childGroupResult.Errors.Select(e => - $"Group {node.Remarks} child group node {childNode.Remarks} error: {e}. Skipping this node.")); + string.Format(ResUI.MsgGroupChildGroupNodeError, node.Remarks, childNode.Remarks, e))); if (!childGroupResult.Success) { continue; @@ -280,7 +280,7 @@ public class CoreConfigContextBuilder if (childIndexIdList.Count == 0) { - childNodeValidatorResult.Errors.Add($"Group {node.Remarks} has no valid child node."); + childNodeValidatorResult.Errors.Add(string.Format(ResUI.MsgGroupNoValidChildNode, node.Remarks)); return childNodeValidatorResult; } else diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs index bfffb599..ea4876c6 100644 --- a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Handler.Builder; +namespace ServiceLib.Handler.Builder; public record NodeValidatorResult(List Errors, List Warnings) { @@ -72,8 +72,8 @@ public class NodeValidator } // Basic Property Validation - v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Address")); - v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.InvalidProperty, "Port")); + v.Assert(!item.Address.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Address")); + v.Assert(item.Port is > 0 and <= 65535, string.Format(ResUI.MsgInvalidProperty, "Port")); // Network & Core Logic var net = item.GetNetwork(); @@ -85,14 +85,14 @@ public class NodeValidator if (!Global.SingboxSupportConfigType.Contains(item.ConfigType)) { - v.Error(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.sing_box), item.ConfigType)); + v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.sing_box), item.ConfigType)); } } else if (coreType is ECoreType.Xray) { if (!Global.XraySupportConfigType.Contains(item.ConfigType)) { - v.Error(string.Format(ResUI.CoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType)); + v.Error(string.Format(ResUI.MsgCoreNotSupportProtocol, nameof(ECoreType.Xray), item.ConfigType)); } } @@ -102,30 +102,30 @@ public class NodeValidator { case EConfigType.VMess: v.Assert(!item.Password.IsNullOrEmpty() && Utils.IsGuidByParse(item.Password), - string.Format(ResUI.InvalidProperty, "Password")); + string.Format(ResUI.MsgInvalidProperty, "Password")); break; case EConfigType.VLESS: // Example of converting a non-critical issue to Warning if desired if (item.Password.Length <= 30 && !Utils.IsGuidByParse(item.Password)) { - v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password")); } else { - v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password")); } v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty), - string.Format(ResUI.InvalidProperty, "Flow")); + string.Format(ResUI.MsgInvalidProperty, "Flow")); break; case EConfigType.Shadowsocks: - v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "Password")); + v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password")); v.Assert( !string.IsNullOrEmpty(protocolExtra.SsMethod) && Global.SsSecuritiesInSingbox.Contains(protocolExtra.SsMethod), - string.Format(ResUI.InvalidProperty, "SsMethod")); + string.Format(ResUI.MsgInvalidProperty, "SsMethod")); break; } @@ -135,20 +135,20 @@ public class NodeValidator if (!item.Cert.IsNullOrEmpty() && CertPemManager.ParsePemChain(item.Cert).Count == 0 && !item.CertSha.IsNullOrEmpty()) { - v.Error(string.Format(ResUI.InvalidProperty, "TLS Certificate")); + v.Error(string.Format(ResUI.MsgInvalidProperty, "TLS Certificate")); } } if (item.StreamSecurity == Global.StreamSecurityReality) { - v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.InvalidProperty, "PublicKey")); + v.Assert(!item.PublicKey.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "PublicKey")); } if (item.Network == nameof(ETransport.xhttp) && !item.Extra.IsNullOrEmpty()) { if (JsonUtils.ParseJson(item.Extra) is null) { - v.Error(string.Format(ResUI.InvalidProperty, "XHTTP Extra")); + v.Error(string.Format(ResUI.MsgInvalidProperty, "XHTTP Extra")); } } } @@ -158,20 +158,20 @@ public class NodeValidator // sing-box does not support xhttp / kcp transports if (SingboxUnsupportedTransports.Contains(net)) { - return string.Format(ResUI.CoreNotSupportNetwork, nameof(ECoreType.sing_box), net); + return string.Format(ResUI.MsgCoreNotSupportNetwork, nameof(ECoreType.sing_box), net); } // sing-box does not support non-tcp transports for protocols other than vmess/trojan/vless/shadowsocks if (!SingboxTransportSupportedProtocols.Contains(configType) && net != nameof(ETransport.tcp)) { - return string.Format(ResUI.CoreNotSupportProtocolTransport, + return string.Format(ResUI.MsgCoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), configType.ToString(), net); } // sing-box shadowsocks only supports tcp/ws/quic transports if (configType == EConfigType.Shadowsocks && !SingboxShadowsocksAllowedTransports.Contains(net)) { - return string.Format(ResUI.CoreNotSupportProtocolTransport, + return string.Format(ResUI.MsgCoreNotSupportProtocolTransport, nameof(ECoreType.sing_box), configType.ToString(), net); } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 8f798007..10a1aed3 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -132,33 +132,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Core '{0}' does not support network type '{1}'. 的本地化字符串。 - /// - public static string CoreNotSupportNetwork { - get { - return ResourceManager.GetString("CoreNotSupportNetwork", resourceCulture); - } - } - - /// - /// 查找类似 Core '{0}' does not support protocol '{1}'. 的本地化字符串。 - /// - public static string CoreNotSupportProtocol { - get { - return ResourceManager.GetString("CoreNotSupportProtocol", resourceCulture); - } - } - - /// - /// 查找类似 Core '{0}' does not support protocol '{1}' when using transport '{2}'. 的本地化字符串。 - /// - public static string CoreNotSupportProtocolTransport { - get { - return ResourceManager.GetString("CoreNotSupportProtocolTransport", resourceCulture); - } - } - /// /// 查找类似 Note that custom configuration relies entirely on your own configuration and does not work with all settings. If you want to use the system proxy, please modify the listening port manually. 的本地化字符串。 /// @@ -312,24 +285,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Group '{0}' is empty. Please add at least one node. 的本地化字符串。 - /// - public static string GroupEmpty { - get { - return ResourceManager.GetString("GroupEmpty", resourceCulture); - } - } - - /// - /// 查找类似 {0} Group cannot reference itself or have a circular reference 的本地化字符串。 - /// - public static string GroupSelfReference { - get { - return ResourceManager.GetString("GroupSelfReference", resourceCulture); - } - } - /// /// 查找类似 This is not the correct configuration, please check 的本地化字符串。 /// @@ -357,15 +312,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 The {0} property is invalid, please check. 的本地化字符串。 - /// - public static string InvalidProperty { - get { - return ResourceManager.GetString("InvalidProperty", resourceCulture); - } - } - /// /// 查找类似 Invalid address (URL) 的本地化字符串。 /// @@ -1914,6 +1860,33 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Core '{0}' does not support network type '{1}' 的本地化字符串。 + /// + public static string MsgCoreNotSupportNetwork { + get { + return ResourceManager.GetString("MsgCoreNotSupportNetwork", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}' 的本地化字符串。 + /// + public static string MsgCoreNotSupportProtocol { + get { + return ResourceManager.GetString("MsgCoreNotSupportProtocol", resourceCulture); + } + } + + /// + /// 查找类似 Core '{0}' does not support protocol '{1}' when using transport '{2}' 的本地化字符串。 + /// + public static string MsgCoreNotSupportProtocolTransport { + get { + return ResourceManager.GetString("MsgCoreNotSupportProtocolTransport", resourceCulture); + } + } + /// /// 查找类似 Downloaded GeoFile: {0} successfully 的本地化字符串。 /// @@ -1959,6 +1932,60 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Group {0} child group node {1} error: {2}. Skipping this node. 的本地化字符串。 + /// + public static string MsgGroupChildGroupNodeError { + get { + return ResourceManager.GetString("MsgGroupChildGroupNodeError", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} child group node {1} warning: {2} 的本地化字符串。 + /// + public static string MsgGroupChildGroupNodeWarning { + get { + return ResourceManager.GetString("MsgGroupChildGroupNodeWarning", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} child node {1} error: {2}. Skipping this node. 的本地化字符串。 + /// + public static string MsgGroupChildNodeError { + get { + return ResourceManager.GetString("MsgGroupChildNodeError", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} child node {1} warning: {2} 的本地化字符串。 + /// + public static string MsgGroupChildNodeWarning { + get { + return ResourceManager.GetString("MsgGroupChildNodeWarning", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} has a cycle dependency on child node {1}. Skipping this node. 的本地化字符串。 + /// + public static string MsgGroupCycleDependency { + get { + return ResourceManager.GetString("MsgGroupCycleDependency", resourceCulture); + } + } + + /// + /// 查找类似 Group {0} has no valid child node. 的本地化字符串。 + /// + public static string MsgGroupNoValidChildNode { + get { + return ResourceManager.GetString("MsgGroupNoValidChildNode", resourceCulture); + } + } + /// /// 查找类似 Information 的本地化字符串。 /// @@ -1968,6 +1995,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 The {0} property is invalid, please check 的本地化字符串。 + /// + public static string MsgInvalidProperty { + get { + return ResourceManager.GetString("MsgInvalidProperty", resourceCulture); + } + } + /// /// 查找类似 Please enter the URL 的本地化字符串。 /// @@ -1977,6 +2013,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Not support protocol '{0}' 的本地化字符串。 + /// + public static string MsgNotSupportProtocol { + get { + return ResourceManager.GetString("MsgNotSupportProtocol", resourceCulture); + } + } + /// /// 查找类似 No valid subscriptions set 的本地化字符串。 /// @@ -1995,6 +2040,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. 的本地化字符串。 + /// + public static string MsgRoutingRuleOutboundNodeError { + get { + return ResourceManager.GetString("MsgRoutingRuleOutboundNodeError", resourceCulture); + } + } + + /// + /// 查找类似 Routing rule {0} outbound node {1} warning: {2} 的本地化字符串。 + /// + public static string MsgRoutingRuleOutboundNodeWarning { + get { + return ResourceManager.GetString("MsgRoutingRuleOutboundNodeWarning", resourceCulture); + } + } + /// /// 查找类似 Filter, press Enter to execute 的本地化字符串。 /// @@ -2103,15 +2166,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Node alias '{0}' does not exist. 的本地化字符串。 - /// - public static string NodeTagNotExist { - get { - return ResourceManager.GetString("NodeTagNotExist", resourceCulture); - } - } - /// /// 查找类似 Non-VMess or SS protocol 的本地化字符串。 /// @@ -2139,15 +2193,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Not support protocol '{0}'. 的本地化字符串。 - /// - public static string NotSupportProtocol { - get { - return ResourceManager.GetString("NotSupportProtocol", resourceCulture); - } - } - /// /// 查找类似 Scan completed, no valid QR code found 的本地化字符串。 /// @@ -2229,24 +2274,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Policy group: 的本地化字符串。 - /// - public static string PolicyGroupPrefix { - get { - return ResourceManager.GetString("PolicyGroupPrefix", resourceCulture); - } - } - - /// - /// 查找类似 Proxy chained: 的本地化字符串。 - /// - public static string ProxyChainedPrefix { - get { - return ResourceManager.GetString("ProxyChainedPrefix", resourceCulture); - } - } - /// /// 查找类似 Global hotkey {0} registration failed, reason: {1} 的本地化字符串。 /// @@ -2310,15 +2337,6 @@ namespace ServiceLib.Resx { } } - /// - /// 查找类似 Routing rule outbound: 的本地化字符串。 - /// - public static string RoutingRuleOutboundPrefix { - get { - return ResourceManager.GetString("RoutingRuleOutboundPrefix", resourceCulture); - } - } - /// /// 查找类似 Run as Admin 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 9aba2fed..c8aec9ae 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1539,38 +1539,20 @@ Multi-Configuration Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + Core '{0}' does not support network type '{1}' - - Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + Core '{0}' does not support protocol '{1}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,28 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index 2deed9fb..f606225b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1536,38 +1536,20 @@ Xray basculement (multi-sélection) - - Le cœur « {0} » ne prend pas en charge le type de réseau « {1} ». + + Le cœur « {0} » ne prend pas en charge le type de réseau « {1} » - - Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} ». + + Le cœur « {0} » ne prend pas en charge le protocole « {1} » avec le mode de transport « {2} » - - Le cœur « {0} » ne prend pas en charge le protocole « {1} ». + + Le cœur « {0} » ne prend pas en charge le protocole « {1} » - - Chaîne de proxy : - - - Règle de routage sortante : - - - Groupe de stratégie : - - - L’alias « {0} » n’existe pas. - - - Le groupe « {0} » est vide. Veuillez ajouter au moins une configuration. - - + La propriété {0} est invalide, veuillez vérifier - - Le groupe {0} ne peut pas se référencer lui-même ni créer de référence circulaire - - - Protocole « {0} » non pris en charge. + + Protocole « {0} » non pris en charge Si le système n’a pas de zone de notif., n’activez pas cette option @@ -1665,4 +1647,28 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index e69a0a54..3eb3b357 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1539,38 +1539,20 @@ Multi-Configuration Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + Core '{0}' does not support network type '{1}' - - Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + Core '{0}' does not support protocol '{1}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,28 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index cc304154..a2d9e691 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1539,38 +1539,20 @@ Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + Core '{0}' does not support network type '{1}' - - Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + Core '{0}' does not support protocol '{1}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,28 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 6f093f72..4e851e20 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1539,38 +1539,20 @@ Multi-Configuration Fallback by Xray - - Core '{0}' does not support network type '{1}'. + + Core '{0}' does not support network type '{1}' - - Core '{0}' does not support protocol '{1}' when using transport '{2}'. + + Core '{0}' does not support protocol '{1}' when using transport '{2}' - - Core '{0}' does not support protocol '{1}'. + + Core '{0}' does not support protocol '{1}' - - Proxy chained: + + The {0} property is invalid, please check - - Routing rule outbound: - - - Policy group: - - - Node alias '{0}' does not exist. - - - Group '{0}' is empty. Please add at least one node. - - - The {0} property is invalid, please check. - - - {0} Group cannot reference itself or have a circular reference - - - Not support protocol '{0}'. + + Not support protocol '{0}' If the system does not have a tray function, please do not enable it @@ -1668,4 +1650,28 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + \ 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..21e199ac 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1536,38 +1536,20 @@ 多选故障转移 Xray - - 核心 '{0}' 不支持网络类型 '{1}'。 + + 核心 '{0}' 不支持网络类型 '{1}' - - 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}'。 + + 核心 '{0}' 在使用传输方式 '{2}' 时不支持协议 '{1}' - - 核心 '{0}' 不支持协议 '{1}'。 + + 核心 '{0}' 不支持协议 '{1}' - - 代理链: + + {0} 属性无效,请检查 - - 路由规则出站: - - - 策略组: - - - 别名 '{0}' 不存在。 - - - 组“{0}”为空。请至少添加一个配置。 - - - {0}属性无效,请检查 - - - {0} 分组不能引用自身或循环引用 - - - 不支持协议 '{0}'。 + + 不支持协议 '{0}' 如果系统没有托盘功能,请不要开启 @@ -1665,4 +1647,28 @@ Finalmask + + 路由规则 {0} 出站节点 {1} 警告:{2} + + + 路由规则 {0} 出站节点 {1} 错误:{2}。已回退为仅使用代理节点。 + + + 节点组 {0} 与子节点 {1} 存在循环依赖,已跳过该节点。 + + + 节点组 {0} 子节点 {1} 警告:{2} + + + 节点组 {0} 子节点 {1} 错误:{2}。已跳过该节点。 + + + 节点组 {0} 子节点组 {1} 警告:{2} + + + 节点组 {0} 子节点组 {1} 错误:{2}。已跳过该节点。 + + + 节点组 {0} 下没有有效的子节点。 + \ 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..5b74197c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1536,38 +1536,20 @@ 多選容錯移轉 Xray - - 核心 '{0}' 不支援網路類型 '{1}'. + + 核心 '{0}' 不支援網路類型 '{1}' - - 核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}'. + + 核心 '{0}' 在使用傳輸方式 '{2}' 時不支援協定 '{1}' - - 核心 '{0}' 不支援協定 '{1}'. + + 核心 '{0}' 不支援協定 '{1}' - - 代理鏈: + + {0} 屬性無效,請檢查 - - 路由規則出站: - - - 策略組: - - - 別名 '{0}' 不存在。 - - - 組“{0}”為空.請至少添加一個配置。 - - - {0}屬性無效,請檢查 - - - {0} 分組不能引用自身或循環引用 - - - 不支援協定 '{0}'. + + 不支援協定 '{0}' 如果系統沒有託盤功能,請不要開啟 @@ -1665,4 +1647,28 @@ Finalmask + + Routing rule {0} outbound node {1} warning: {2} + + + Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. + + + Group {0} has a cycle dependency on child node {1}. Skipping this node. + + + Group {0} child node {1} warning: {2} + + + Group {0} child node {1} error: {2}. Skipping this node. + + + Group {0} child group node {1} warning: {2} + + + Group {0} child group node {1} error: {2}. Skipping this node. + + + Group {0} has no valid child node. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs index 6e4d021c..d30917ec 100644 --- a/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddGroupServerViewModel.cs @@ -234,13 +234,6 @@ public class AddGroupServerViewModel : MyReactiveObject SelectedSource.SetProtocolExtra(protocolExtra); - var hasCycle = await GroupProfileManager.HasCycle(SelectedSource.IndexId, protocolExtra); - if (hasCycle) - { - NoticeManager.Instance.Enqueue(string.Format(ResUI.GroupSelfReference, remarks)); - return; - } - if (await ConfigHandler.AddServerCommon(_config, SelectedSource) == 0) { NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); From 50d8d993368c8e1b4d856eea196e742ea8e4bef1 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 10:39:09 +0800 Subject: [PATCH 4/8] Add remark not found warnings --- .../Builder/CoreConfigContextBuilder.cs | 68 ++++++++++++++----- v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 36 ++++++++++ v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 12 ++++ v2rayN/ServiceLib/Resx/ResUI.fr.resx | 12 ++++ v2rayN/ServiceLib/Resx/ResUI.hu.resx | 12 ++++ v2rayN/ServiceLib/Resx/ResUI.resx | 12 ++++ v2rayN/ServiceLib/Resx/ResUI.ru.resx | 12 ++++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 12 ++++ v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 12 ++++ 9 files changed, 172 insertions(+), 16 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index d932c5ef..44ab2b40 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -1,4 +1,4 @@ -namespace ServiceLib.Handler.Builder; +namespace ServiceLib.Handler.Builder; public record CoreConfigContextBuilderResult(CoreConfigContext Context, NodeValidatorResult ValidatorResult) { @@ -44,9 +44,17 @@ public class CoreConfigContextBuilder var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet); foreach (var ruleItem in rules.Where(ruleItem => !Global.OutboundTags.Contains(ruleItem.OutboundTag))) { + if (ruleItem.OutboundTag.IsNullOrEmpty()) + { + validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleEmptyOutboundTag, ruleItem.Remarks)); + ruleItem.OutboundTag = Global.ProxyTag; + continue; + } var ruleOutboundNode = await AppManager.Instance.GetProfileItemViaRemarks(ruleItem.OutboundTag); if (ruleOutboundNode == null) { + validatorResult.Warnings.Add(string.Format(ResUI.MsgRoutingRuleOutboundNodeNotFound, ruleItem.Remarks, ruleItem.OutboundTag)); + ruleItem.OutboundTag = Global.ProxyTag; continue; } @@ -83,43 +91,71 @@ public class CoreConfigContextBuilder if (includeSubChain) { - var virtualChainNode = await BuildSubscriptionChainNodeAsync(node); + var (virtualChainNode, chainValidatorResult) = await BuildSubscriptionChainNodeAsync(node); if (virtualChainNode != null) { context.AllProxiesMap[virtualChainNode.IndexId] = virtualChainNode; - return await ResolveNodeAsync(context, virtualChainNode, false); + var (resolvedNode, resolvedResult) = await ResolveNodeAsync(context, virtualChainNode, false); + resolvedResult.Warnings.InsertRange(0, chainValidatorResult.Warnings); + return (resolvedNode, resolvedResult); + } + // Chain not built but warnings may still exist (e.g. missing profiles) + if (chainValidatorResult.Warnings.Count > 0) + { + var fillResult = await RegisterNodeAsync(context, node); + fillResult.Warnings.InsertRange(0, chainValidatorResult.Warnings); + return (node, fillResult); } } - var fillResult = await RegisterNodeAsync(context, node); - return (node, fillResult); + var registerResult = await RegisterNodeAsync(context, node); + return (node, registerResult); } /// /// If the node's subscription defines prev/next profiles, creates a virtual /// node that wraps them together. - /// Returns null when no chain is needed. + /// Returns null as the chain item when no chain is needed. + /// Any warnings (e.g. missing prev/next profile) are returned in the validator result. /// - private static async Task BuildSubscriptionChainNodeAsync(ProfileItem node) + private static async Task<(ProfileItem? ChainNode, NodeValidatorResult ValidatorResult)> BuildSubscriptionChainNodeAsync(ProfileItem node) { + var result = NodeValidatorResult.Empty(); + if (node.Subid.IsNullOrEmpty()) { - return null; + return (null, result); } var subItem = await AppManager.Instance.GetSubItem(node.Subid); - if (subItem == null - || (subItem.PrevProfile.IsNullOrEmpty() - && subItem.NextProfile.IsNullOrEmpty())) + if (subItem == null) { - return null; + return (null, result); + } + + ProfileItem? prevNode = null; + ProfileItem? nextNode = null; + + if (!subItem.PrevProfile.IsNullOrEmpty()) + { + prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); + if (prevNode == null) + { + result.Warnings.Add(string.Format(ResUI.MsgSubscriptionPrevProfileNotFound, subItem.PrevProfile)); + } + } + if (!subItem.NextProfile.IsNullOrEmpty()) + { + nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); + if (nextNode == null) + { + result.Warnings.Add(string.Format(ResUI.MsgSubscriptionNextProfileNotFound, subItem.NextProfile)); + } } - var prevNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.PrevProfile); - var nextNode = await AppManager.Instance.GetProfileItemViaRemarks(subItem.NextProfile); if (prevNode is null && nextNode is null) { - return null; + return (null, result); } // Build new proxy chain node @@ -136,7 +172,7 @@ public class CoreConfigContextBuilder ChildItems = string.Join(",", childItems.Where(x => !x.IsNullOrEmpty())), }; chainNode.SetProtocolExtra(chainExtraItem); - return chainNode; + return (chainNode, result); } /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 10a1aed3..5e35d3bb 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -2040,6 +2040,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule {0} has an empty outbound tag. Fallback to proxy node only. 的本地化字符串。 + /// + public static string MsgRoutingRuleEmptyOutboundTag { + get { + return ResourceManager.GetString("MsgRoutingRuleEmptyOutboundTag", resourceCulture); + } + } + /// /// 查找类似 Routing rule {0} outbound node {1} error: {2}. Fallback to proxy node only. 的本地化字符串。 /// @@ -2049,6 +2058,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Routing rule {0} outbound node {1} not found. Fallback to proxy node only. 的本地化字符串。 + /// + public static string MsgRoutingRuleOutboundNodeNotFound { + get { + return ResourceManager.GetString("MsgRoutingRuleOutboundNodeNotFound", resourceCulture); + } + } + /// /// 查找类似 Routing rule {0} outbound node {1} warning: {2} 的本地化字符串。 /// @@ -2112,6 +2130,24 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Subscription next proxy {0} not found. Skipping. 的本地化字符串。 + /// + public static string MsgSubscriptionNextProfileNotFound { + get { + return ResourceManager.GetString("MsgSubscriptionNextProfileNotFound", resourceCulture); + } + } + + /// + /// 查找类似 Subscription previous proxy {0} not found. Skipping. 的本地化字符串。 + /// + public static string MsgSubscriptionPrevProfileNotFound { + get { + return ResourceManager.GetString("MsgSubscriptionPrevProfileNotFound", resourceCulture); + } + } + /// /// 查找类似 Unpacking... 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index c8aec9ae..0df75d77 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1674,4 +1674,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group {0} has no valid child node. + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index f606225b..459e3b26 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1671,4 +1671,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group {0} has no valid child node. + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 3eb3b357..dd7d22f4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1674,4 +1674,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group {0} has no valid child node. + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index a2d9e691..fa53ccc8 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1674,4 +1674,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group {0} has no valid child node. + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 4e851e20..b33a10e3 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1674,4 +1674,16 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if Group {0} has no valid child node. + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ 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 21e199ac..be76525e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1671,4 +1671,16 @@ 节点组 {0} 下没有有效的子节点。 + + 路由规则 {0} 的出站标签为空,已回退为仅使用代理节点。 + + + 路由规则 {0} 的出站节点 {1} 未找到,已回退为仅使用代理节点。 + + + 订阅前置节点 {0} 未找到,已跳过。 + + + 订阅后置节点 {0} 未找到,已跳过。 + \ 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 5b74197c..4132a27b 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1671,4 +1671,16 @@ Group {0} has no valid child node. + + Routing rule {0} has an empty outbound tag. Fallback to proxy node only. + + + Routing rule {0} outbound node {1} not found. Fallback to proxy node only. + + + Subscription previous proxy {0} not found. Skipping. + + + Subscription next proxy {0} not found. Skipping. + \ No newline at end of file From 71594cf59418eb010b807b002119c0aa4fa3a4c9 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 10:45:34 +0800 Subject: [PATCH 5/8] Fix --- .../Handler/Builder/CoreConfigContextBuilder.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index 44ab2b40..a4585d2c 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -13,13 +13,12 @@ public class CoreConfigContextBuilder /// public static async Task Build(Config config, ProfileItem node) { - var coreType = AppManager.Instance.GetCoreType(node, node.ConfigType) == ECoreType.sing_box - ? ECoreType.sing_box - : ECoreType.Xray; + var runCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType); + var coreType = runCoreType == ECoreType.sing_box ? ECoreType.sing_box : ECoreType.Xray; var context = new CoreConfigContext() { Node = node, - RunCoreType = AppManager.Instance.GetCoreType(node, node.ConfigType), + RunCoreType = runCoreType, AllProxiesMap = [], AppConfig = config, FullConfigTemplate = await AppManager.Instance.GetFullConfigTemplateItem(coreType), @@ -298,7 +297,6 @@ public class CoreConfigContextBuilder continue; } - globalVisitedGroup.Add(childNode.IndexId); var newAncestorsGroup = new HashSet(ancestorsGroup) { childNode.IndexId }; var childGroupResult = await TraverseGroupNodeAsync(context, childNode, globalVisitedGroup, newAncestorsGroup); @@ -311,6 +309,7 @@ public class CoreConfigContextBuilder continue; } + globalVisitedGroup.Add(childNode.IndexId); childIndexIdList.Add(childNode.IndexId); } From 446f98983dbc9ca68a9aed91cb792de828440e00 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 15:36:57 +0800 Subject: [PATCH 6/8] Fix --- .../Handler/Builder/CoreConfigContextBuilder.cs | 2 +- .../ServiceLib/Handler/Builder/NodeValidator.cs | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index a4585d2c..f58e066c 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -40,7 +40,7 @@ public class CoreConfigContextBuilder validatorResult.Warnings.AddRange(nodeValidatorResult.Warnings); if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) { - var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet); + var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet) ?? []; foreach (var ruleItem in rules.Where(ruleItem => !Global.OutboundTags.Contains(ruleItem.OutboundTag))) { if (ruleItem.OutboundTag.IsNullOrEmpty()) diff --git a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs index ea4876c6..a6adf1c2 100644 --- a/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs +++ b/v2rayN/ServiceLib/Handler/Builder/NodeValidator.cs @@ -106,16 +106,11 @@ public class NodeValidator break; case EConfigType.VLESS: - // Example of converting a non-critical issue to Warning if desired - if (item.Password.Length <= 30 && !Utils.IsGuidByParse(item.Password)) - { - v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password")); - } - else - { - v.Assert(!item.Password.IsNullOrEmpty(), string.Format(ResUI.MsgInvalidProperty, "Password")); - } - + v.Assert( + !item.Password.IsNullOrEmpty() + && (Utils.IsGuidByParse(item.Password) || item.Password.Length <= 30), + string.Format(ResUI.MsgInvalidProperty, "Password") + ); v.Assert(Global.Flows.Contains(protocolExtra.Flow ?? string.Empty), string.Format(ResUI.MsgInvalidProperty, "Flow")); break; From 8232e0adf980e2fdcbd348a7cb3b689a685962da Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 15:39:03 +0800 Subject: [PATCH 7/8] Fix --- v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs index f58e066c..23a495f8 100644 --- a/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs +++ b/v2rayN/ServiceLib/Handler/Builder/CoreConfigContextBuilder.cs @@ -41,7 +41,7 @@ public class CoreConfigContextBuilder if (!(context.RoutingItem?.RuleSet.IsNullOrEmpty() ?? true)) { var rules = JsonUtils.Deserialize>(context.RoutingItem?.RuleSet) ?? []; - foreach (var ruleItem in rules.Where(ruleItem => !Global.OutboundTags.Contains(ruleItem.OutboundTag))) + foreach (var ruleItem in rules.Where(ruleItem => ruleItem.Enabled && !Global.OutboundTags.Contains(ruleItem.OutboundTag))) { if (ruleItem.OutboundTag.IsNullOrEmpty()) { From 5fbb651c63dd8b471b1d037b2ddd09173172cbca Mon Sep 17 00:00:00 2001 From: DHR60 Date: Fri, 27 Feb 2026 07:44:12 +0000 Subject: [PATCH 8/8] Update v2rayN/ServiceLib/Handler/CoreConfigHandler.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- v2rayN/ServiceLib/Handler/CoreConfigHandler.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs index 6991db83..1806ad26 100644 --- a/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/CoreConfigHandler.cs @@ -93,7 +93,11 @@ public static class CoreConfigHandler public static async Task GenerateClientSpeedtestConfig(Config config, string fileName, List selecteds, ECoreType coreType) { var result = new RetResult(); - var builderResult = await CoreConfigContextBuilder.Build(config, new()); + var dummyNode = new ProfileItem + { + CoreType = coreType + }; + var builderResult = await CoreConfigContextBuilder.Build(config, dummyNode); var context = builderResult.Context; var ids = selecteds.Where(serverTestItem => !serverTestItem.IndexId.IsNullOrEmpty()) .Select(serverTestItem => serverTestItem.IndexId);