feat: 工作流增加条件判断节点,重构部分UI

This commit is contained in:
2026-03-04 17:35:49 +08:00
parent 67d42a80b9
commit 27376a5f33
14 changed files with 2481 additions and 31 deletions

View File

@@ -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() {

View File

@@ -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;
}
}
}

View File

@@ -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";
}
}