feat: 工作流增加条件判断节点,重构部分UI
This commit is contained in:
@@ -75,6 +75,8 @@ public class TinyFlowConfigService {
|
||||
SearchDatacenterNodeParser searchDatacenterNodeParser = new SearchDatacenterNodeParser();
|
||||
// 工作流节点
|
||||
WorkflowNodeParser workflowNodeParser = new WorkflowNodeParser();
|
||||
// 条件判断节点
|
||||
ConditionNodeParser conditionNodeParser = new ConditionNodeParser();
|
||||
|
||||
chainParser.addNodeParser(docNodeParser.getNodeName(), docNodeParser);
|
||||
chainParser.addNodeParser(makeFileNodeParser.getNodeName(), makeFileNodeParser);
|
||||
@@ -84,6 +86,7 @@ public class TinyFlowConfigService {
|
||||
chainParser.addNodeParser(saveDaveParser.getNodeName(), saveDaveParser);
|
||||
chainParser.addNodeParser(searchDatacenterNodeParser.getNodeName(), searchDatacenterNodeParser);
|
||||
chainParser.addNodeParser(workflowNodeParser.getNodeName(), workflowNodeParser);
|
||||
chainParser.addNodeParser(conditionNodeParser.getNodeName(), conditionNodeParser);
|
||||
}
|
||||
|
||||
public void setSearchEngineProvider() {
|
||||
|
||||
@@ -0,0 +1,496 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainException;
|
||||
import com.easyagents.flow.core.node.BaseNode;
|
||||
import com.easyagents.flow.core.util.JsConditionUtil;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 条件判断节点:首个命中(if / else-if)语义。
|
||||
*/
|
||||
public class ConditionNode extends BaseNode {
|
||||
private static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\{\\{\\s*([^{}]+?)\\s*}}");
|
||||
|
||||
private String branchMode = "first_match";
|
||||
private String defaultBranchId;
|
||||
private String defaultBranchLabel;
|
||||
private List<ConditionBranch> branches = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
if (branches == null || branches.isEmpty()) {
|
||||
throw new ChainException("条件判断节点未配置任何分支");
|
||||
}
|
||||
|
||||
ConditionBranch defaultBranch = resolveDefaultBranch();
|
||||
|
||||
for (ConditionBranch branch : branches) {
|
||||
if (branch == null) {
|
||||
continue;
|
||||
}
|
||||
if (defaultBranch != null && Objects.equals(defaultBranch.getId(), branch.getId())) {
|
||||
continue;
|
||||
}
|
||||
if (isBranchMatched(chain, branch)) {
|
||||
return buildMatchedResult(branch, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultBranch == null) {
|
||||
throw new ChainException("条件判断节点缺少默认分支,请检查配置");
|
||||
}
|
||||
|
||||
return buildMatchedResult(defaultBranch, true);
|
||||
}
|
||||
|
||||
private ConditionBranch resolveDefaultBranch() {
|
||||
if (branches == null || branches.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (StringUtil.hasText(defaultBranchId)) {
|
||||
for (ConditionBranch branch : branches) {
|
||||
if (branch != null && Objects.equals(defaultBranchId, branch.getId())) {
|
||||
return branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (StringUtil.hasText(defaultBranchLabel)) {
|
||||
for (ConditionBranch branch : branches) {
|
||||
if (branch != null && Objects.equals(defaultBranchLabel, branch.getLabel())) {
|
||||
return branch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return branches.get(branches.size() - 1);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMatchedResult(ConditionBranch branch, boolean matchedByDefault) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("matchedBranchId", branch.getId());
|
||||
result.put("matchedBranchLabel", branch.getLabel());
|
||||
result.put("matchedByDefault", matchedByDefault);
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isBranchMatched(Chain chain, ConditionBranch branch) {
|
||||
if (branch == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String mode = StringUtil.getFirstWithText(branch.getMode(), "visual");
|
||||
if ("expression".equalsIgnoreCase(mode)) {
|
||||
return checkByExpression(chain, branch);
|
||||
}
|
||||
|
||||
return checkByVisualRules(chain, branch);
|
||||
}
|
||||
|
||||
private boolean checkByExpression(Chain chain, ConditionBranch branch) {
|
||||
String expression = branch.getExpression();
|
||||
if (StringUtil.noText(expression)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
String resolvedExpression = resolveExpressionTemplate(expression, chain);
|
||||
return JsConditionUtil.eval(resolvedExpression, chain, new HashMap<>());
|
||||
} catch (Exception e) {
|
||||
throw new ChainException(String.format("条件分支表达式执行失败,分支[%s/%s]: %s",
|
||||
branch.getId(), branch.getLabel(), e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveExpressionTemplate(String expression, Chain chain) {
|
||||
Matcher matcher = TEMPLATE_PARAM_PATTERN.matcher(expression);
|
||||
StringBuffer output = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
String path = matcher.group(1) == null ? "" : matcher.group(1).trim();
|
||||
Object value = StringUtil.noText(path) ? null : chain.getState().resolveValue(path);
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(toJsLiteral(value)));
|
||||
}
|
||||
matcher.appendTail(output);
|
||||
return output.toString();
|
||||
}
|
||||
|
||||
private String toJsLiteral(Object value) {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
if (value instanceof Number || value instanceof Boolean) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
if (value instanceof Character || value.getClass().isEnum()) {
|
||||
return JSON.toJSONString(String.valueOf(value));
|
||||
}
|
||||
return JSON.toJSONString(value);
|
||||
}
|
||||
|
||||
private boolean checkByVisualRules(Chain chain, ConditionBranch branch) {
|
||||
List<ConditionRule> rules = branch.getRules();
|
||||
if (rules == null || rules.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Boolean matched = null;
|
||||
for (ConditionRule rule : rules) {
|
||||
boolean ruleMatched = checkRule(chain, rule);
|
||||
if (matched == null) {
|
||||
matched = ruleMatched;
|
||||
continue;
|
||||
}
|
||||
|
||||
String joiner = StringUtil.getFirstWithText(rule.getJoiner(), "AND");
|
||||
if ("OR".equalsIgnoreCase(joiner)) {
|
||||
matched = matched || ruleMatched;
|
||||
} else {
|
||||
matched = matched && ruleMatched;
|
||||
}
|
||||
}
|
||||
|
||||
return Boolean.TRUE.equals(matched);
|
||||
}
|
||||
|
||||
private boolean checkRule(Chain chain, ConditionRule rule) {
|
||||
if (rule == null || StringUtil.noText(rule.getOperator())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object leftValue = resolveValue(chain, rule.getLeftRef());
|
||||
String operator = rule.getOperator();
|
||||
|
||||
if ("isEmpty".equals(operator)) {
|
||||
return isEmpty(leftValue);
|
||||
}
|
||||
|
||||
if ("isNotEmpty".equals(operator)) {
|
||||
return !isEmpty(leftValue);
|
||||
}
|
||||
|
||||
Object rightValue = resolveRightValue(chain, rule);
|
||||
|
||||
switch (operator) {
|
||||
case "eq":
|
||||
return equalsSmart(leftValue, rightValue);
|
||||
case "ne":
|
||||
return !equalsSmart(leftValue, rightValue);
|
||||
case "gt":
|
||||
return compareNumber(leftValue, rightValue, it -> it > 0);
|
||||
case "gte":
|
||||
return compareNumber(leftValue, rightValue, it -> it >= 0);
|
||||
case "lt":
|
||||
return compareNumber(leftValue, rightValue, it -> it < 0);
|
||||
case "lte":
|
||||
return compareNumber(leftValue, rightValue, it -> it <= 0);
|
||||
case "contains":
|
||||
return contains(leftValue, rightValue);
|
||||
case "notContains":
|
||||
return !contains(leftValue, rightValue);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Object resolveRightValue(Chain chain, ConditionRule rule) {
|
||||
if (rule == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ("ref".equals(rule.getRightType())) {
|
||||
return resolveValue(chain, rule.getRightRef());
|
||||
}
|
||||
|
||||
return rule.getRightValue();
|
||||
}
|
||||
|
||||
private Object resolveValue(Chain chain, String path) {
|
||||
if (chain == null || StringUtil.noText(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chain.getState().resolveValue(path);
|
||||
}
|
||||
|
||||
private boolean isEmpty(Object value) {
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value instanceof CharSequence) {
|
||||
return StringUtil.noText(value.toString());
|
||||
}
|
||||
|
||||
if (value instanceof Collection) {
|
||||
return ((Collection<?>) value).isEmpty();
|
||||
}
|
||||
|
||||
if (value instanceof Map) {
|
||||
return ((Map<?, ?>) value).isEmpty();
|
||||
}
|
||||
|
||||
if (value.getClass().isArray()) {
|
||||
return java.lang.reflect.Array.getLength(value) == 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean contains(Object leftValue, Object rightValue) {
|
||||
if (leftValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (leftValue instanceof String) {
|
||||
return rightValue != null && ((String) leftValue).contains(String.valueOf(rightValue));
|
||||
}
|
||||
|
||||
if (leftValue instanceof Collection) {
|
||||
Collection<?> collection = (Collection<?>) leftValue;
|
||||
for (Object item : collection) {
|
||||
if (equalsSmart(item, rightValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (leftValue.getClass().isArray()) {
|
||||
int length = java.lang.reflect.Array.getLength(leftValue);
|
||||
for (int i = 0; i < length; i++) {
|
||||
Object item = java.lang.reflect.Array.get(leftValue, i);
|
||||
if (equalsSmart(item, rightValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean compareNumber(Object leftValue, Object rightValue, java.util.function.IntPredicate compareFunc) {
|
||||
BigDecimal leftNumber = toBigDecimal(leftValue);
|
||||
BigDecimal rightNumber = toBigDecimal(rightValue);
|
||||
|
||||
if (leftNumber == null || rightNumber == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return compareFunc.test(leftNumber.compareTo(rightNumber));
|
||||
}
|
||||
|
||||
private boolean equalsSmart(Object leftValue, Object rightValue) {
|
||||
if (leftValue == null || rightValue == null) {
|
||||
return leftValue == rightValue;
|
||||
}
|
||||
|
||||
BigDecimal leftNumber = toBigDecimal(leftValue);
|
||||
BigDecimal rightNumber = toBigDecimal(rightValue);
|
||||
if (leftNumber != null && rightNumber != null) {
|
||||
return leftNumber.compareTo(rightNumber) == 0;
|
||||
}
|
||||
|
||||
if (leftValue instanceof Boolean || rightValue instanceof Boolean) {
|
||||
return toBoolean(leftValue) == toBoolean(rightValue);
|
||||
}
|
||||
|
||||
return Objects.equals(String.valueOf(leftValue), String.valueOf(rightValue));
|
||||
}
|
||||
|
||||
private boolean toBoolean(Object value) {
|
||||
if (value == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
|
||||
String stringValue = String.valueOf(value).trim();
|
||||
if (StringUtil.noText(stringValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return "true".equalsIgnoreCase(stringValue)
|
||||
|| "1".equals(stringValue)
|
||||
|| "yes".equalsIgnoreCase(stringValue)
|
||||
|| "y".equalsIgnoreCase(stringValue);
|
||||
}
|
||||
|
||||
private BigDecimal toBigDecimal(Object value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value instanceof Number) {
|
||||
return new BigDecimal(String.valueOf(value));
|
||||
}
|
||||
|
||||
String text = String.valueOf(value).trim();
|
||||
if (StringUtil.noText(text)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new BigDecimal(text);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getBranchMode() {
|
||||
return branchMode;
|
||||
}
|
||||
|
||||
public void setBranchMode(String branchMode) {
|
||||
this.branchMode = branchMode;
|
||||
}
|
||||
|
||||
public String getDefaultBranchId() {
|
||||
return defaultBranchId;
|
||||
}
|
||||
|
||||
public void setDefaultBranchId(String defaultBranchId) {
|
||||
this.defaultBranchId = defaultBranchId;
|
||||
}
|
||||
|
||||
public String getDefaultBranchLabel() {
|
||||
return defaultBranchLabel;
|
||||
}
|
||||
|
||||
public void setDefaultBranchLabel(String defaultBranchLabel) {
|
||||
this.defaultBranchLabel = defaultBranchLabel;
|
||||
}
|
||||
|
||||
public List<ConditionBranch> getBranches() {
|
||||
return branches;
|
||||
}
|
||||
|
||||
public void setBranches(List<ConditionBranch> branches) {
|
||||
this.branches = branches;
|
||||
}
|
||||
|
||||
public static class ConditionBranch {
|
||||
private String id;
|
||||
private String label;
|
||||
private String mode;
|
||||
private String expression;
|
||||
private List<ConditionRule> rules = new ArrayList<>();
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getMode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public void setMode(String mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public String getExpression() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
public void setExpression(String expression) {
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
public List<ConditionRule> getRules() {
|
||||
return rules;
|
||||
}
|
||||
|
||||
public void setRules(List<ConditionRule> rules) {
|
||||
this.rules = rules;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConditionRule {
|
||||
private String id;
|
||||
private String joiner;
|
||||
private String leftRef;
|
||||
private String operator;
|
||||
private String rightType;
|
||||
private String rightValue;
|
||||
private String rightRef;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getJoiner() {
|
||||
return joiner;
|
||||
}
|
||||
|
||||
public void setJoiner(String joiner) {
|
||||
this.joiner = joiner;
|
||||
}
|
||||
|
||||
public String getLeftRef() {
|
||||
return leftRef;
|
||||
}
|
||||
|
||||
public void setLeftRef(String leftRef) {
|
||||
this.leftRef = leftRef;
|
||||
}
|
||||
|
||||
public String getOperator() {
|
||||
return operator;
|
||||
}
|
||||
|
||||
public void setOperator(String operator) {
|
||||
this.operator = operator;
|
||||
}
|
||||
|
||||
public String getRightType() {
|
||||
return rightType;
|
||||
}
|
||||
|
||||
public void setRightType(String rightType) {
|
||||
this.rightType = rightType;
|
||||
}
|
||||
|
||||
public String getRightValue() {
|
||||
return rightValue;
|
||||
}
|
||||
|
||||
public void setRightValue(String rightValue) {
|
||||
this.rightValue = rightValue;
|
||||
}
|
||||
|
||||
public String getRightRef() {
|
||||
return rightRef;
|
||||
}
|
||||
|
||||
public void setRightRef(String rightRef) {
|
||||
this.rightRef = rightRef;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.parser.BaseNodeParser;
|
||||
import com.easyagents.flow.core.util.StringUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 条件判断节点解析器。
|
||||
*/
|
||||
public class ConditionNodeParser extends BaseNodeParser<ConditionNode> {
|
||||
|
||||
@Override
|
||||
protected ConditionNode doParse(JSONObject root, JSONObject data, JSONObject tinyflow) {
|
||||
ConditionNode node = new ConditionNode();
|
||||
node.setBranchMode(StringUtil.getFirstWithText(data.getString("branchMode"), "first_match"));
|
||||
node.setDefaultBranchId(data.getString("defaultBranchId"));
|
||||
node.setDefaultBranchLabel(data.getString("defaultBranchLabel"));
|
||||
|
||||
JSONArray branchesJson = data.getJSONArray("branches");
|
||||
List<ConditionNode.ConditionBranch> branches = branchesJson == null
|
||||
? Collections.emptyList()
|
||||
: branchesJson.toJavaList(ConditionNode.ConditionBranch.class);
|
||||
node.setBranches(branches);
|
||||
|
||||
if (branches.isEmpty()) {
|
||||
throw new RuntimeException("条件判断节点至少需要一个分支");
|
||||
}
|
||||
if (StringUtil.noText(node.getDefaultBranchId())) {
|
||||
throw new RuntimeException("条件判断节点必须配置默认分支");
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public String getNodeName() {
|
||||
return "conditionNode";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package tech.easyflow.ai.node;
|
||||
|
||||
import com.easyagents.flow.core.chain.*;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class ConditionNodeTest {
|
||||
|
||||
@Test
|
||||
public void testFirstMatch() {
|
||||
ConditionNode node = new ConditionNode();
|
||||
|
||||
ConditionNode.ConditionBranch b1 = visualBranch("branch_a", "分支A",
|
||||
visualRule("ctx.score", "gte", "fixed", "80", null));
|
||||
ConditionNode.ConditionBranch b2 = visualBranch("branch_b", "分支B",
|
||||
visualRule("ctx.score", "gte", "fixed", "60", null));
|
||||
ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支");
|
||||
|
||||
node.setBranches(Arrays.asList(b1, b2, def));
|
||||
node.setDefaultBranchId(def.getId());
|
||||
node.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
Chain chain = createChain(Map.of("ctx", Map.of("score", 90)));
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
|
||||
Assert.assertEquals("branch_a", result.get("matchedBranchId"));
|
||||
Assert.assertEquals("分支A", result.get("matchedBranchLabel"));
|
||||
Assert.assertEquals(false, result.get("matchedByDefault"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultBranchWhenNoMatch() {
|
||||
ConditionNode node = new ConditionNode();
|
||||
|
||||
ConditionNode.ConditionBranch b1 = visualBranch("branch_a", "分支A",
|
||||
visualRule("ctx.level", "eq", "fixed", "vip", null));
|
||||
ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支");
|
||||
|
||||
node.setBranches(Arrays.asList(b1, def));
|
||||
node.setDefaultBranchId(def.getId());
|
||||
node.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
Chain chain = createChain(Map.of("ctx", Map.of("level", "normal")));
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
|
||||
Assert.assertEquals("branch_default", result.get("matchedBranchId"));
|
||||
Assert.assertEquals("默认分支", result.get("matchedBranchLabel"));
|
||||
Assert.assertEquals(true, result.get("matchedByDefault"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVisualOperators() {
|
||||
ConditionNode node = new ConditionNode();
|
||||
|
||||
ConditionNode.ConditionBranch b1 = visualBranch("branch_hit", "命中分支",
|
||||
visualRule("ctx.amount", "gt", "fixed", "100", null),
|
||||
visualRule("ctx.tags", "contains", "fixed", "vip", null),
|
||||
visualRule("ctx.title", "notContains", "fixed", "blacklist", null),
|
||||
visualRule("ctx.emptyText", "isEmpty", "fixed", "", null),
|
||||
visualRule("ctx.note", "isNotEmpty", "fixed", "", null));
|
||||
ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支");
|
||||
|
||||
node.setBranches(Arrays.asList(b1, def));
|
||||
node.setDefaultBranchId(def.getId());
|
||||
node.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
Map<String, Object> ctx = new HashMap<>();
|
||||
ctx.put("amount", 120);
|
||||
ctx.put("tags", Arrays.asList("vip", "new"));
|
||||
ctx.put("title", "white-user");
|
||||
ctx.put("emptyText", "");
|
||||
ctx.put("note", "ok");
|
||||
|
||||
Chain chain = createChain(Map.of("ctx", ctx));
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
Assert.assertEquals("branch_hit", result.get("matchedBranchId"));
|
||||
Assert.assertEquals(false, result.get("matchedByDefault"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testVisualRuleJoinerOr() {
|
||||
ConditionNode node = new ConditionNode();
|
||||
|
||||
ConditionNode.ConditionBranch b1 = visualBranch("branch_or", "OR 分支",
|
||||
visualRule("ctx.score", "gt", "fixed", "100", null, "AND"),
|
||||
visualRule("ctx.level", "eq", "fixed", "vip", null, "OR"));
|
||||
ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支");
|
||||
|
||||
node.setBranches(Arrays.asList(b1, def));
|
||||
node.setDefaultBranchId(def.getId());
|
||||
node.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
Chain chain = createChain(Map.of("ctx", Map.of("score", 80, "level", "vip")));
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
|
||||
Assert.assertEquals("branch_or", result.get("matchedBranchId"));
|
||||
Assert.assertEquals(false, result.get("matchedByDefault"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpressionModeAndInvalidExpression() {
|
||||
ConditionNode node = new ConditionNode();
|
||||
|
||||
ConditionNode.ConditionBranch b1 = expressionBranch("branch_expr", "表达式分支",
|
||||
"{{amount}} >= 100 && {{level}} === 'vip'");
|
||||
ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支");
|
||||
node.setBranches(Arrays.asList(b1, def));
|
||||
node.setDefaultBranchId(def.getId());
|
||||
node.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
Chain chain = createChain(Map.of("amount", 120, "level", "vip"));
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
Assert.assertEquals("branch_expr", result.get("matchedBranchId"));
|
||||
|
||||
ConditionNode invalidNode = new ConditionNode();
|
||||
ConditionNode.ConditionBranch bad = expressionBranch("branch_bad", "异常分支", "amount >");
|
||||
invalidNode.setBranches(Arrays.asList(bad, def));
|
||||
invalidNode.setDefaultBranchId(def.getId());
|
||||
invalidNode.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
ChainException ex = Assert.assertThrows(ChainException.class, () -> invalidNode.execute(chain));
|
||||
Assert.assertTrue(ex.getMessage().contains("branch_bad"));
|
||||
Assert.assertTrue(ex.getMessage().contains("异常分支"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpressionModeWithTemplatePath() {
|
||||
ConditionNode node = new ConditionNode();
|
||||
|
||||
ConditionNode.ConditionBranch b1 = expressionBranch(
|
||||
"branch_expr_tpl",
|
||||
"模板分支",
|
||||
"{{ctx.order.amount}} >= 10 && {{ctx.user.level}} === 'vip'"
|
||||
);
|
||||
ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支");
|
||||
node.setBranches(Arrays.asList(b1, def));
|
||||
node.setDefaultBranchId(def.getId());
|
||||
node.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
Map<String, Object> ctx = new HashMap<>();
|
||||
ctx.put("order", Map.of("amount", 18));
|
||||
ctx.put("user", Map.of("level", "vip"));
|
||||
Chain chain = createChain(Map.of("ctx", ctx));
|
||||
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
Assert.assertEquals("branch_expr_tpl", result.get("matchedBranchId"));
|
||||
Assert.assertEquals(false, result.get("matchedByDefault"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManagedEdgeConditionRouting() {
|
||||
ConditionNode node = new ConditionNode();
|
||||
|
||||
ConditionNode.ConditionBranch b1 = visualBranch("branch_yes", "通过",
|
||||
visualRule("ctx.allow", "eq", "fixed", "true", null));
|
||||
ConditionNode.ConditionBranch def = defaultBranch("branch_default", "默认分支");
|
||||
node.setBranches(Arrays.asList(b1, def));
|
||||
node.setDefaultBranchId(def.getId());
|
||||
node.setDefaultBranchLabel(def.getLabel());
|
||||
|
||||
Chain chain = createChain(Map.of("ctx", Map.of("allow", true)));
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
|
||||
Edge yesEdge = new Edge("e_yes");
|
||||
yesEdge.setCondition(new JsCodeCondition("matchedBranchId === 'branch_yes'"));
|
||||
Assert.assertTrue(yesEdge.getCondition().check(chain, yesEdge, result));
|
||||
|
||||
Edge noEdge = new Edge("e_no");
|
||||
noEdge.setCondition(new JsCodeCondition("matchedBranchId === 'branch_other'"));
|
||||
Assert.assertFalse(noEdge.getCondition().check(chain, noEdge, result));
|
||||
}
|
||||
|
||||
private static Chain createChain(Map<String, Object> memory) {
|
||||
Chain chain = new Chain(new ChainDefinition(), UUID.randomUUID().toString());
|
||||
chain.setChainStateRepository(new InMemoryChainStateRepository());
|
||||
if (memory != null && !memory.isEmpty()) {
|
||||
chain.getState().getMemory().putAll(memory);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
private static ConditionNode.ConditionBranch defaultBranch(String id, String label) {
|
||||
ConditionNode.ConditionBranch branch = new ConditionNode.ConditionBranch();
|
||||
branch.setId(id);
|
||||
branch.setLabel(label);
|
||||
branch.setMode("visual");
|
||||
branch.setRules(Collections.emptyList());
|
||||
return branch;
|
||||
}
|
||||
|
||||
private static ConditionNode.ConditionBranch expressionBranch(String id, String label, String expression) {
|
||||
ConditionNode.ConditionBranch branch = new ConditionNode.ConditionBranch();
|
||||
branch.setId(id);
|
||||
branch.setLabel(label);
|
||||
branch.setMode("expression");
|
||||
branch.setExpression(expression);
|
||||
branch.setRules(Collections.emptyList());
|
||||
return branch;
|
||||
}
|
||||
|
||||
private static ConditionNode.ConditionBranch visualBranch(
|
||||
String id, String label, ConditionNode.ConditionRule... rules) {
|
||||
ConditionNode.ConditionBranch branch = new ConditionNode.ConditionBranch();
|
||||
branch.setId(id);
|
||||
branch.setLabel(label);
|
||||
branch.setMode("visual");
|
||||
branch.setRules(Arrays.asList(rules));
|
||||
return branch;
|
||||
}
|
||||
|
||||
private static ConditionNode.ConditionRule visualRule(
|
||||
String leftRef, String operator, String rightType, String rightValue, String rightRef) {
|
||||
return visualRule(leftRef, operator, rightType, rightValue, rightRef, "AND");
|
||||
}
|
||||
|
||||
private static ConditionNode.ConditionRule visualRule(
|
||||
String leftRef, String operator, String rightType, String rightValue, String rightRef, String joiner) {
|
||||
ConditionNode.ConditionRule rule = new ConditionNode.ConditionRule();
|
||||
rule.setId(UUID.randomUUID().toString());
|
||||
rule.setJoiner(joiner);
|
||||
rule.setLeftRef(leftRef);
|
||||
rule.setOperator(operator);
|
||||
rule.setRightType(rightType);
|
||||
rule.setRightValue(rightValue);
|
||||
rule.setRightRef(rightRef);
|
||||
return rule;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user