feat: 增加条件节点正则匹配

- 使用 RE2/J 完成安全正则执行和分层校验

- 增加全宽多行输入、说明提示和专项测试
This commit is contained in:
2026-07-31 14:23:47 +08:00
parent 41b056b7e3
commit f0aba1eddd
11 changed files with 879 additions and 17 deletions

View File

@@ -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());
}
/**
* 验证普通节点循环次数必须处于 1300。
*/
@@ -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;
}
/**
* 创建显式循环节点数据。
*

View File

@@ -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<String, Object> 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<String, Object> anchoredMiss = node.execute(
createChain(Map.of("ctx", Map.of("orderCode", "订单ABC-123完成"))));
Assert.assertEquals("branch_default", anchoredMiss.get("matchedBranchId"));
Map<String, Object> 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();