From f0aba1eddd8d64c281f7c73b1cd6de01ce24d84c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Fri, 31 Jul 2026 14:23:47 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=AD=A3=E5=88=99=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 使用 RE2/J 完成安全正则执行和分层校验 - 增加全宽多行输入、说明提示和专项测试 --- easyflow-modules/easyflow-module-ai/pom.xml | 4 + .../service/WorkflowCheckService.java | 129 +++++++++ .../tech/easyflow/ai/node/ConditionNode.java | 24 +- .../easyflow/ai/node/ConditionNodeParser.java | 48 ++++ .../ai/node/ConditionRuleSupport.java | 155 +++++++++++ .../service/WorkflowCheckServiceTest.java | 131 ++++++++++ .../easyflow/ai/node/ConditionNodeTest.java | 111 ++++++++ .../src/components/nodes/ConditionNode.svelte | 246 +++++++++++++++++- .../components/utils/conditionRegex.test.ts | 24 ++ .../src/components/utils/conditionRegex.ts | 17 ++ pom.xml | 7 + 11 files changed, 879 insertions(+), 17 deletions(-) create mode 100644 easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionRuleSupport.java create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/conditionRegex.test.ts create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/conditionRegex.ts diff --git a/easyflow-modules/easyflow-module-ai/pom.xml b/easyflow-modules/easyflow-module-ai/pom.xml index 0f713e43..c866056f 100644 --- a/easyflow-modules/easyflow-module-ai/pom.xml +++ b/easyflow-modules/easyflow-module-ai/pom.xml @@ -53,6 +53,10 @@ com.easyagents easy-agents-spring-boot-starter + + com.google.re2j + re2j + diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java index e085e218..a73a74ce 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckService.java @@ -11,6 +11,8 @@ import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult; import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage; import tech.easyflow.ai.entity.PluginItem; import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.node.ConditionNode; +import tech.easyflow.ai.node.ConditionRuleSupport; import tech.easyflow.ai.node.filegeneration.FileGenerationRules; import tech.easyflow.ai.node.filegeneration.SourceFormat; import tech.easyflow.ai.node.filegeneration.TargetFormat; @@ -50,6 +52,7 @@ public class WorkflowCheckService { private static final String TYPE_START = "startNode"; private static final String TYPE_END = "endNode"; private static final String TYPE_LOOP = "loopNode"; + private static final String TYPE_CONDITION = "conditionNode"; private static final String TYPE_WORKFLOW = "workflow-node"; private static final String TYPE_PLUGIN = "plugin-node"; private static final String TYPE_MAKE_FILE = "make-file"; @@ -178,6 +181,7 @@ public class WorkflowCheckService { } } checkLoopConfigurations(nodes, nodeMap, issues, issueKeys); + checkConditionConfigurations(nodes, issues, issueKeys); List edges = new ArrayList<>(); Set edgeIds = new HashSet<>(); @@ -262,6 +266,131 @@ public class WorkflowCheckService { } } + /** + * 校验条件判断节点的可视化规则配置。 + * + * @param nodes 节点列表 + * @param issues 问题列表 + * @param issueKeys 问题去重键 + */ + private void checkConditionConfigurations( + List nodes, + List issues, + Set issueKeys) { + for (NodeView node : nodes) { + if (!TYPE_CONDITION.equals(node.type) || node.data == null) { + continue; + } + + Object branchesValue = node.data.get("branches"); + if (!(branchesValue instanceof JSONArray) + || ((JSONArray) branchesValue).isEmpty()) { + addIssue( + issues, + issueKeys, + "CONDITION_BRANCHES_EMPTY", + "条件判断节点至少需要一个分支", + node.id, + null, + node.name); + continue; + } + + JSONArray branches = (JSONArray) branchesValue; + for (int branchIndex = 0; branchIndex < branches.size(); branchIndex++) { + Object branchValue = branches.get(branchIndex); + if (!(branchValue instanceof JSONObject)) { + addIssue( + issues, + issueKeys, + "CONDITION_BRANCH_INVALID", + "第 " + (branchIndex + 1) + " 个条件分支配置无效", + node.id, + null, + node.name); + continue; + } + + JSONObject branch = (JSONObject) branchValue; + if ("expression".equalsIgnoreCase( + trimToNull(branch.getString("mode")))) { + continue; + } + Object rulesValue = branch.get("rules"); + if (rulesValue == null) { + continue; + } + if (!(rulesValue instanceof JSONArray)) { + addIssue( + issues, + issueKeys, + "CONDITION_RULES_INVALID", + conditionBranchLabel(branch, branchIndex) + "的条件规则必须是数组", + node.id, + null, + node.name); + continue; + } + + JSONArray rules = (JSONArray) rulesValue; + for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) { + Object ruleValue = rules.get(ruleIndex); + ConditionNode.ConditionRule rule = toConditionRule(ruleValue); + String error = ConditionRuleSupport.validateRule(rule); + if (error == null) { + continue; + } + addIssue( + issues, + issueKeys, + "CONDITION_RULE_INVALID", + conditionBranchLabel(branch, branchIndex) + + "第 " + (ruleIndex + 1) + + " 条条件配置无效: " + error, + node.id, + null, + node.name); + } + } + } + } + + /** + * 将 JSON 条件规则转换为运行时规则对象。 + * + * @param ruleValue 条件规则 JSON 值 + * @return 条件规则;输入无效时返回 {@code null} + */ + private ConditionNode.ConditionRule toConditionRule(Object ruleValue) { + if (!(ruleValue instanceof JSONObject)) { + return null; + } + JSONObject ruleJson = (JSONObject) ruleValue; + ConditionNode.ConditionRule rule = new ConditionNode.ConditionRule(); + rule.setId(ruleJson.getString("id")); + rule.setJoiner(ruleJson.getString("joiner")); + rule.setLeftRef(ruleJson.getString("leftRef")); + rule.setOperator(ruleJson.getString("operator")); + rule.setRightType(ruleJson.getString("rightType")); + rule.setRightValue(ruleJson.getString("rightValue")); + rule.setRightRef(ruleJson.getString("rightRef")); + return rule; + } + + /** + * 获取用于校验错误展示的条件分支名称。 + * + * @param branch 分支配置 + * @param branchIndex 分支序号 + * @return 分支名称 + */ + private String conditionBranchLabel(JSONObject branch, int branchIndex) { + String label = trimToNull(branch.getString("label")); + return StringUtils.hasText(label) + ? "条件分支[" + label + "]" + : "第 " + (branchIndex + 1) + " 个条件分支"; + } + /** * 校验普通节点启用循环后的总执行次数。 * diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNode.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNode.java index c4c5394c..34bb0fdb 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNode.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNode.java @@ -148,8 +148,19 @@ public class ConditionNode extends BaseNode { } Boolean matched = null; - for (ConditionRule rule : rules) { - boolean ruleMatched = checkRule(chain, rule); + for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) { + ConditionRule rule = rules.get(ruleIndex); + boolean ruleMatched; + try { + ruleMatched = checkRule(chain, rule); + } catch (IllegalArgumentException e) { + throw new ChainException(String.format( + "条件分支规则执行失败,分支[%s/%s],规则[%d]: %s", + branch.getId(), + branch.getLabel(), + ruleIndex + 1, + e.getMessage()), e); + } if (matched == null) { matched = ruleMatched; continue; @@ -167,8 +178,9 @@ public class ConditionNode extends BaseNode { } private boolean checkRule(Chain chain, ConditionRule rule) { - if (rule == null || StringUtil.noText(rule.getOperator())) { - return false; + String validationError = ConditionRuleSupport.validateRule(rule); + if (validationError != null) { + throw new IllegalArgumentException(validationError); } Object leftValue = resolveValue(chain, rule.getLeftRef()); @@ -201,8 +213,10 @@ public class ConditionNode extends BaseNode { return contains(leftValue, rightValue); case "notContains": return !contains(leftValue, rightValue); + case ConditionRuleSupport.OPERATOR_REGEX_MATCH: + return ConditionRuleSupport.matchesRegex(leftValue, rule.getRightValue()); default: - return false; + throw new IllegalArgumentException("不支持的条件操作符: " + operator); } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNodeParser.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNodeParser.java index 46b673cd..71fe30fe 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNodeParser.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionNodeParser.java @@ -13,6 +13,15 @@ import java.util.List; */ public class ConditionNodeParser extends BaseNodeParser { + /** + * 将 TinyFlow 条件节点配置解析为运行时节点。 + * + * @param root 节点根配置 + * @param data 节点业务配置 + * @param tinyflow 工作流配置 + * @return 条件判断运行时节点 + * @throws RuntimeException 分支或规则配置无效 + */ @Override protected ConditionNode doParse(JSONObject root, JSONObject data, JSONObject tinyflow) { ConditionNode node = new ConditionNode(); @@ -32,11 +41,50 @@ public class ConditionNodeParser extends BaseNodeParser { if (StringUtil.noText(node.getDefaultBranchId())) { throw new RuntimeException("条件判断节点必须配置默认分支"); } + validateBranches(branches); return node; } + /** + * 返回条件节点类型名称。 + * + * @return 条件节点类型名称 + */ public String getNodeName() { return "conditionNode"; } + + /** + * 校验可视化分支内的全部条件规则。 + * + * @param branches 条件分支 + * @throws RuntimeException 任一规则配置无效 + */ + private void validateBranches(List branches) { + for (int branchIndex = 0; branchIndex < branches.size(); branchIndex++) { + ConditionNode.ConditionBranch branch = branches.get(branchIndex); + if (branch == null + || "expression".equalsIgnoreCase( + StringUtil.getFirstWithText(branch.getMode(), "visual"))) { + continue; + } + List rules = branch.getRules(); + if (rules == null || rules.isEmpty()) { + continue; + } + for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) { + String error = ConditionRuleSupport.validateRule(rules.get(ruleIndex)); + if (error != null) { + String branchName = StringUtil.getFirstWithText( + branch.getLabel(), + branch.getId(), + "第 " + (branchIndex + 1) + " 个分支"); + throw new RuntimeException( + "条件分支[" + branchName + "]第 " + (ruleIndex + 1) + + " 条规则配置无效: " + error); + } + } + } + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionRuleSupport.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionRuleSupport.java new file mode 100644 index 00000000..6020735a --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/node/ConditionRuleSupport.java @@ -0,0 +1,155 @@ +package tech.easyflow.ai.node; + +import com.easyagents.flow.core.util.StringUtil; +import com.google.re2j.Pattern; +import com.google.re2j.PatternSyntaxException; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +/** + * 条件判断节点规则校验与安全正则执行支持。 + */ +public final class ConditionRuleSupport { + + public static final String OPERATOR_REGEX_MATCH = "regexMatch"; + public static final int MAX_REGEX_LENGTH = 512; + + private static final int MAX_REGEX_CACHE_SIZE = 256; + private static final Set SUPPORTED_OPERATORS = Set.of( + "eq", + "ne", + "gt", + "gte", + "lt", + "lte", + "isEmpty", + "isNotEmpty", + "contains", + "notContains", + OPERATOR_REGEX_MATCH); + private static final Map REGEX_CACHE = + new LinkedHashMap(32, 0.75F, true) { + private static final long serialVersionUID = 1L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_REGEX_CACHE_SIZE; + } + }; + + /** + * 禁止创建工具类实例。 + */ + private ConditionRuleSupport() { + } + + /** + * 校验一条可视化条件规则。 + * + * @param rule 条件规则 + * @return 校验错误;返回 {@code null} 表示通过 + */ + public static String validateRule(ConditionNode.ConditionRule rule) { + if (rule == null) { + return "条件规则不能为空"; + } + + String operator = rule.getOperator(); + if (StringUtil.noText(operator)) { + return "条件操作符不能为空"; + } + if (!SUPPORTED_OPERATORS.contains(operator)) { + return "不支持的条件操作符: " + operator; + } + if (!OPERATOR_REGEX_MATCH.equals(operator)) { + return null; + } + if (!"fixed".equals(rule.getRightType())) { + return "正则表达式必须使用固定值"; + } + return validateRegex(rule.getRightValue()); + } + + /** + * 校验正则表达式的长度和 RE2/J 语法。 + * + * @param regex 正则表达式 + * @return 校验错误;返回 {@code null} 表示通过 + */ + public static String validateRegex(String regex) { + try { + requireRegex(regex); + return null; + } catch (IllegalArgumentException e) { + return e.getMessage(); + } + } + + /** + * 使用 RE2/J 在标量左值中查找正则匹配。 + * + * @param leftValue 条件左值 + * @param regex 正则表达式 + * @return 任意位置存在匹配时返回 {@code true} + * @throws IllegalArgumentException 正则无效或左值类型不支持 + */ + public static boolean matchesRegex(Object leftValue, String regex) { + Pattern pattern = requireRegex(regex); + if (leftValue == null) { + return false; + } + if (!(leftValue instanceof CharSequence) + && !(leftValue instanceof Number) + && !(leftValue instanceof Boolean) + && !(leftValue instanceof Character)) { + throw new IllegalArgumentException( + "正则匹配仅支持字符串、数字或布尔值,当前类型: " + + leftValue.getClass().getSimpleName()); + } + return pattern.matcher(String.valueOf(leftValue)).find(); + } + + /** + * 校验并获取正则模式。 + * + * @param regex 正则表达式 + * @return 已编译 RE2/J 模式 + * @throws IllegalArgumentException 正则为空、过长或语法无效 + */ + private static Pattern requireRegex(String regex) { + if (StringUtil.noText(regex)) { + throw new IllegalArgumentException("正则表达式不能为空"); + } + if (regex.length() > MAX_REGEX_LENGTH) { + throw new IllegalArgumentException( + "正则表达式不能超过 " + MAX_REGEX_LENGTH + " 个字符"); + } + try { + return compileRegex(regex); + } catch (PatternSyntaxException e) { + throw new IllegalArgumentException( + "正则表达式语法错误: " + e.getMessage(), e); + } + } + + /** + * 获取已编译正则,并以有界 LRU 缓存复用结果。 + * + * @param regex 正则表达式 + * @return 已编译 RE2/J 模式 + * @throws PatternSyntaxException 正则语法无效 + */ + private static Pattern compileRegex(String regex) { + synchronized (REGEX_CACHE) { + Pattern cached = REGEX_CACHE.get(regex); + if (cached != null) { + return cached; + } + Pattern compiled = Pattern.compile(regex); + REGEX_CACHE.put(regex, compiled); + return compiled; + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java index d9190cff..48dbf070 100644 --- a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/easyagentsflow/service/WorkflowCheckServiceTest.java @@ -8,6 +8,7 @@ import org.junit.Test; import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult; import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage; import tech.easyflow.ai.entity.Workflow; +import tech.easyflow.ai.node.ConditionNodeParser; import tech.easyflow.ai.node.MakeFileNodeParser; import tech.easyflow.ai.node.SearchDatasetNodeParser; import tech.easyflow.ai.node.WorkflowNodeParser; @@ -21,6 +22,92 @@ import java.util.Map; public class WorkflowCheckServiceTest { + /** + * 验证保存阶段接受合法的正则条件规则。 + */ + @Test + public void testSaveShouldPassValidRegexCondition() throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + String content = workflowJson( + array(node( + "condition-1", + "conditionNode", + null, + conditionData("regexMatch", "fixed", "(?i)^[a-z]+-\\d+$"))), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertTrue(result.isPassed()); + } + + /** + * 验证保存阶段拒绝 RE2/J 不支持的正则语法。 + */ + @Test + public void testSaveShouldBlockUnsupportedRegexSyntax() throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + String content = workflowJson( + array(node( + "condition-1", + "conditionNode", + null, + conditionData("regexMatch", "fixed", "(?=VIP)VIP"))), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + assertHasCode(result, "CONDITION_RULE_INVALID"); + Assert.assertTrue(result.getIssues().stream() + .anyMatch(issue -> issue.getMessage().contains("VIP 分支") + && issue.getMessage().contains("第 1 条"))); + + WorkflowCheckResult preExecuteResult = service.checkContent( + content, WorkflowCheckStage.PRE_EXECUTE, null); + Assert.assertFalse(preExecuteResult.isPassed()); + assertHasCode(preExecuteResult, "CONDITION_RULE_INVALID"); + } + + /** + * 验证保存阶段拒绝变量正则和未知操作符。 + */ + @Test + public void testSaveShouldBlockDynamicRegexAndUnknownOperator() throws Exception { + WorkflowCheckService service = newService(new HashMap<>()); + String content = workflowJson( + array( + node( + "condition-ref", + "conditionNode", + null, + conditionData( + "regexMatch", + "ref", + "start.regex")), + node( + "condition-unknown", + "conditionNode", + null, + conditionData( + "unknown", + "fixed", + "VIP"))), + new JSONArray()); + + WorkflowCheckResult result = service.checkContent( + content, WorkflowCheckStage.SAVE, null); + + Assert.assertFalse(result.isPassed()); + Assert.assertEquals( + 2, + result.getIssues().stream() + .filter(issue -> "CONDITION_RULE_INVALID".equals(issue.getCode())) + .count()); + } + /** * 验证普通节点循环次数必须处于 1~300。 */ @@ -579,6 +666,7 @@ public class WorkflowCheckServiceTest { parser.addNodeParser("workflow-node", new WorkflowNodeParser()); parser.addNodeParser("search-dataset-node", new SearchDatasetNodeParser()); parser.addNodeParser("make-file", new MakeFileNodeParser()); + parser.addNodeParser("conditionNode", new ConditionNodeParser()); setField(service, "chainParser", parser); setField(service, "workflowService", mockWorkflowService(workflowStore)); setField(service, "workflowDatacenterContentService", new WorkflowDatacenterContentService()); @@ -680,6 +768,49 @@ public class WorkflowCheckServiceTest { return node(id, "search-dataset-node", parentId, data); } + /** + * 创建包含一个普通分支和 Else 分支的条件节点配置。 + * + * @param operator 条件操作符 + * @param rightType 右值类型 + * @param rightValue 右值 + * @return 条件节点数据 + */ + private static JSONObject conditionData( + String operator, + String rightType, + String rightValue) { + JSONObject rule = new JSONObject(); + rule.put("id", "rule-1"); + rule.put("joiner", "AND"); + rule.put("leftRef", "start.value"); + rule.put("operator", operator); + rule.put("rightType", rightType); + rule.put("rightValue", rightValue); + if ("ref".equals(rightType)) { + rule.put("rightRef", rightValue); + } + + JSONObject branch = new JSONObject(); + branch.put("id", "branch-vip"); + branch.put("label", "VIP 分支"); + branch.put("mode", "visual"); + branch.put("rules", array(rule)); + + JSONObject defaultBranch = new JSONObject(); + defaultBranch.put("id", "branch-else"); + defaultBranch.put("label", "Else"); + defaultBranch.put("mode", "visual"); + defaultBranch.put("rules", new JSONArray()); + + JSONObject data = data("条件判断"); + data.put("branchMode", "first_match"); + data.put("branches", array(branch, defaultBranch)); + data.put("defaultBranchId", "branch-else"); + data.put("defaultBranchLabel", "Else"); + return data; + } + /** * 创建显式循环节点数据。 * diff --git a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/node/ConditionNodeTest.java b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/node/ConditionNodeTest.java index 6ea90aa7..3fa4891a 100644 --- a/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/node/ConditionNodeTest.java +++ b/easyflow-modules/easyflow-module-ai/src/test/java/tech/easyflow/ai/node/ConditionNodeTest.java @@ -150,6 +150,117 @@ public class ConditionNodeTest { Assert.assertEquals(false, result.get("matchedByDefault")); } + /** + * 验证正则默认查找匹配,并支持锚点和内联大小写标志。 + */ + @Test + public void testRegexMatchFindAnchorsAndInlineFlag() { + ConditionNode node = new ConditionNode(); + ConditionNode.ConditionBranch hit = visualBranch( + "branch_regex", + "正则分支", + visualRule( + "ctx.orderCode", + ConditionRuleSupport.OPERATOR_REGEX_MATCH, + "fixed", + "(?i)[a-z]+-\\d+", + null)); + ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支"); + node.setBranches(Arrays.asList(hit, def)); + node.setDefaultBranchId(def.getId()); + node.setDefaultBranchLabel(def.getLabel()); + + Map partialResult = node.execute( + createChain(Map.of("ctx", Map.of("orderCode", "订单ABC-123完成")))); + Assert.assertEquals("branch_regex", partialResult.get("matchedBranchId")); + + hit.setRules(Collections.singletonList( + visualRule( + "ctx.orderCode", + ConditionRuleSupport.OPERATOR_REGEX_MATCH, + "fixed", + "(?i)^[a-z]+-\\d+$", + null))); + Map anchoredMiss = node.execute( + createChain(Map.of("ctx", Map.of("orderCode", "订单ABC-123完成")))); + Assert.assertEquals("branch_default", anchoredMiss.get("matchedBranchId")); + + Map anchoredHit = node.execute( + createChain(Map.of("ctx", Map.of("orderCode", "ABC-123")))); + Assert.assertEquals("branch_regex", anchoredHit.get("matchedBranchId")); + } + + /** + * 验证正则支持数字和布尔标量,并覆盖语法和长度边界。 + */ + @Test + public void testRegexMatchScalarAndValidationBoundaries() { + Assert.assertTrue(ConditionRuleSupport.matchesRegex(123, "^12\\d$")); + Assert.assertTrue(ConditionRuleSupport.matchesRegex(true, "^true$")); + Assert.assertFalse(ConditionRuleSupport.matchesRegex(null, ".*")); + Assert.assertTrue( + ConditionRuleSupport.validateRegex(" ") + .contains("不能为空")); + Assert.assertTrue( + ConditionRuleSupport.validateRegex("(a)\\1") + .contains("语法错误")); + Assert.assertTrue( + ConditionRuleSupport.validateRegex( + "a".repeat(ConditionRuleSupport.MAX_REGEX_LENGTH + 1)) + .contains(String.valueOf(ConditionRuleSupport.MAX_REGEX_LENGTH))); + } + + /** + * 验证正则拒绝集合左值并返回可定位错误。 + */ + @Test + public void testRegexMatchShouldRejectCollectionValue() { + ConditionNode node = new ConditionNode(); + ConditionNode.ConditionBranch hit = visualBranch( + "branch_regex", + "正则分支", + visualRule( + "ctx.values", + ConditionRuleSupport.OPERATOR_REGEX_MATCH, + "fixed", + "\\d+", + null)); + ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支"); + node.setBranches(Arrays.asList(hit, def)); + node.setDefaultBranchId(def.getId()); + + ChainException exception = Assert.assertThrows( + ChainException.class, + () -> node.execute(createChain( + Map.of("ctx", Map.of("values", Arrays.asList(1, 2)))))); + + Assert.assertTrue(exception.getMessage().contains("正则分支")); + Assert.assertTrue(exception.getMessage().contains("规则[1]")); + Assert.assertTrue(exception.getMessage().contains("仅支持字符串、数字或布尔值")); + } + + /** + * 验证未知条件操作符不会静默进入默认分支。 + */ + @Test + public void testUnknownOperatorShouldFailExplicitly() { + ConditionNode node = new ConditionNode(); + ConditionNode.ConditionBranch hit = visualBranch( + "branch_invalid", + "异常分支", + visualRule("ctx.value", "unknown", "fixed", "x", null)); + ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支"); + node.setBranches(Arrays.asList(hit, def)); + node.setDefaultBranchId(def.getId()); + + ChainException exception = Assert.assertThrows( + ChainException.class, + () -> node.execute(createChain(Map.of("ctx", Map.of("value", "x"))))); + + Assert.assertTrue(exception.getMessage().contains("不支持的条件操作符")); + Assert.assertTrue(exception.getMessage().contains("异常分支")); + } + @Test public void testManagedEdgeConditionRouting() { ConditionNode node = new ConditionNode(); diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/ConditionNode.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/ConditionNode.svelte index ef60a43b..ccf3cdb5 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/ConditionNode.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/nodes/ConditionNode.svelte @@ -1,6 +1,7 @@