Merge branch 'main' into main

This commit is contained in:
ageerle
2025-12-12 11:46:19 +08:00
committed by GitHub
535 changed files with 21172 additions and 14160 deletions

View File

@@ -20,10 +20,12 @@ import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.data.NodeIODataContent;
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
@Slf4j
@@ -105,7 +107,7 @@ public class WorkflowUtil {
.mapResult(response -> {
String responseTxt = response.aiMessage().text();
log.info("llm response:{}", responseTxt);
// 传递所有输入数据 + 添加 LLM 输出
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> {
List<NodeIOData> outputs = new ArrayList<>(item.getInputs());
@@ -113,7 +115,7 @@ public class WorkflowUtil {
outputs.add(output);
item.setOutputs(outputs);
});
return Map.of("completeResult", response.aiMessage().text());
})
.startingNode(node.getUuid())

View File

@@ -171,25 +171,25 @@ public abstract class AbstractWfNode {
log.warn("No inputs available for node: {}", state.getUuid());
return "";
}
// 优先查找 output 参数LLM 节点的输出)
Optional<String> outputParam = state.getInputs()
.stream()
.filter(item -> DEFAULT_OUTPUT_PARAM_NAME.equals(item.getName()))
.map(NodeIOData::valueToString)
.findFirst();
if (outputParam.isPresent()) {
log.debug("Found output parameter for node: {}", state.getUuid());
return outputParam.get();
}
// 如果没有 output查找其他文本类型参数排除 input
String firstInputText;
if (state.getInputs().size() > 1) {
firstInputText = state.getInputs()
.stream()
.filter(item -> WfIODataTypeEnum.TEXT.getValue().equals(item.getContent().getType())
.filter(item -> WfIODataTypeEnum.TEXT.getValue().equals(item.getContent().getType())
&& !DEFAULT_INPUT_PARAM_NAME.equals(item.getName()))
.map(NodeIOData::valueToString)
.findFirst()
@@ -197,8 +197,8 @@ public abstract class AbstractWfNode {
} else {
firstInputText = state.getInputs().get(0).valueToString();
}
log.debug("Using first input text for node: {}, value: {}", state.getUuid(),
log.debug("Using first input text for node: {}, value: {}", state.getUuid(),
firstInputText.length() > 50 ? firstInputText.substring(0, 50) + "..." : firstInputText);
return firstInputText;
}

View File

@@ -33,10 +33,10 @@ public class KeywordExtractorNode extends AbstractWfNode {
* 处理关键词提取
* nodeConfig 格式:
* {
* "model_name": "deepseek-chat",
* "category": "llm",
* "top_n": 5,
* "prompt": "额外的提示词"
* "model_name": "deepseek-chat",
* "category": "llm",
* "top_n": 5,
* "prompt": "额外的提示词"
* }
*
* @return 提取的关键词列表
@@ -44,7 +44,7 @@ public class KeywordExtractorNode extends AbstractWfNode {
@Override
public NodeProcessResult onProcess() {
KeywordExtractorNodeConfig config = checkAndGetConfig(KeywordExtractorNodeConfig.class);
// 获取输入文本
String inputText = getFirstInputText();
if (StringUtils.isBlank(inputText)) {
@@ -54,51 +54,51 @@ public class KeywordExtractorNode extends AbstractWfNode {
outputs.add(NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", ""));
return NodeProcessResult.builder().content(outputs).build();
}
log.info("Keyword extractor node config: {}", config);
log.info("Input text length: {}", inputText.length());
// 构建提示词
String prompt = buildPrompt(config, inputText);
log.info("Keyword extraction prompt: {}", prompt);
// 调用 LLM 进行关键词提取
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
String modelName = config.getModelName();
String category = config.getCategory();
List<UserMessage> systemMessage = List.of(UserMessage.from(prompt));
// 使用流式调用
workflowUtil.streamingInvokeLLM(wfState, state, node, category, modelName, systemMessage);
return new NodeProcessResult();
}
/**
* 构建关键词提取的提示词
*/
private String buildPrompt(KeywordExtractorNodeConfig config, String inputText) {
StringBuilder promptBuilder = new StringBuilder();
// 基础提示词
promptBuilder.append("请从以下文本中提取 ").append(config.getTopN()).append(" 个最重要的关键词。\n\n");
// 添加自定义提示词(如果有)
if (StringUtils.isNotBlank(config.getPrompt())) {
promptBuilder.append(config.getPrompt()).append("\n\n");
}
// 输出格式要求
promptBuilder.append("要求:\n");
promptBuilder.append("1. 只返回关键词,每个关键词用逗号分隔\n");
promptBuilder.append("2. 关键词应该是名词或名词短语\n");
promptBuilder.append("3. 按重要性从高到低排序\n");
promptBuilder.append("4. 不要添加任何解释或额外的文字\n\n");
// 原始文本
promptBuilder.append("文本内容:\n");
promptBuilder.append(inputText);
return promptBuilder.toString();
}
}

View File

@@ -13,19 +13,19 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode
@Data
public class KeywordExtractorNodeConfig {
/**
* 模型分类llm, embedding 等)
*/
private String category;
/**
* 模型名称
*/
@NotNull
@JsonProperty("model_name")
private String modelName;
/**
* 提取的关键词数量
*/
@@ -33,7 +33,7 @@ public class KeywordExtractorNodeConfig {
@Max(50)
@JsonProperty("top_n")
private Integer topN = 5;
/**
* 提示词(可选)
* 用于指导关键词提取的额外说明

View File

@@ -14,6 +14,7 @@ import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
@@ -49,16 +50,16 @@ public class MailSendNode extends AbstractWfNode {
} else {
// 优先使用 output如果没有则使用 input
content = inputs.stream()
.filter(item -> "output".equals(item.getName()))
.map(NodeIOData::valueToString)
.findFirst()
.orElseGet(() -> inputs.stream()
.filter(item -> "input".equals(item.getName()))
.filter(item -> "output".equals(item.getName()))
.map(NodeIOData::valueToString)
.findFirst()
.orElse(""));
.orElseGet(() -> inputs.stream()
.filter(item -> "input".equals(item.getName()))
.map(NodeIOData::valueToString)
.findFirst()
.orElse(""));
}
// 将换行符转换为 HTML 换行
if (StringUtils.isNotBlank(content)) {
content = content.replace("\n", "<br>");
@@ -112,23 +113,23 @@ public class MailSendNode extends AbstractWfNode {
// 构造输出:统一输出为 output 参数
List<NodeIOData> outputs = new java.util.ArrayList<>();
// 优先使用 output如果没有则使用 input但重命名为 output
inputs.stream()
.filter(item -> "output".equals(item.getName()))
.findFirst()
.ifPresentOrElse(
outputs::add,
() -> inputs.stream()
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
outputs.add(outputParam);
})
);
.filter(item -> "output".equals(item.getName()))
.findFirst()
.ifPresentOrElse(
outputs::add,
() -> inputs.stream()
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
outputs.add(outputParam);
})
);
return NodeProcessResult.builder().content(outputs).build();
@@ -136,23 +137,23 @@ public class MailSendNode extends AbstractWfNode {
log.error("Failed to send email in node: {}", node.getId(), e);
// 异常时也统一输出为 output 参数,添加错误信息
List<NodeIOData> errorOutputs = new java.util.ArrayList<>();
state.getInputs().stream()
.filter(item -> "output".equals(item.getName()))
.findFirst()
.ifPresentOrElse(
errorOutputs::add,
() -> state.getInputs().stream()
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
errorOutputs.add(outputParam);
})
);
.filter(item -> "output".equals(item.getName()))
.findFirst()
.ifPresentOrElse(
errorOutputs::add,
() -> state.getInputs().stream()
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
errorOutputs.add(outputParam);
})
);
errorOutputs.add(NodeIOData.createByText("error", "mail", e.getMessage()));
return NodeProcessResult.builder().content(errorOutputs).build();
}

View File

@@ -9,6 +9,7 @@ import org.ruoyi.workflow.workflow.WfNodeState;
import org.ruoyi.workflow.workflow.WfState;
import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
import java.math.BigDecimal;
import java.util.List;
@@ -29,14 +30,14 @@ public class SwitcherNode extends AbstractWfNode {
SwitcherNodeConfig config = checkAndGetConfig(SwitcherNodeConfig.class);
List<NodeIOData> inputs = state.getInputs();
log.info("条件分支节点处理中,分支数量: {}",
log.info("条件分支节点处理中,分支数量: {}",
config.getCases() != null ? config.getCases().size() : 0);
// 按顺序评估每个分支
if (config.getCases() != null) {
for (int i = 0; i < config.getCases().size(); i++) {
SwitcherCase switcherCase = config.getCases().get(i);
log.info("评估分支 {}: uuid={}, 运算符={}",
log.info("评估分支 {}: uuid={}, 运算符={}",
i + 1, switcherCase.getUuid(), switcherCase.getOperator());
if (evaluateCase(switcherCase, inputs)) {
@@ -45,33 +46,33 @@ public class SwitcherNode extends AbstractWfNode {
log.warn("分支 {} 匹配但目标节点UUID为空跳过到下一个分支", i + 1);
continue;
}
log.info("分支 {} 匹配,跳转到节点: {}",
log.info("分支 {} 匹配,跳转到节点: {}",
i + 1, switcherCase.getTargetNodeUuid());
// 构造输出:只保留 output 和其他非 input 参数 + 添加分支匹配信息
List<NodeIOData> outputs = new java.util.ArrayList<>();
// 过滤输入:排除 input 参数(与 output 冗余),保留其他参数
inputs.stream()
.filter(item -> !"input".equals(item.getName()))
.forEach(outputs::add);
.filter(item -> !"input".equals(item.getName()))
.forEach(outputs::add);
// 如果没有 output 参数,从 input 创建 output便于后续节点使用
boolean hasOutput = outputs.stream().anyMatch(item -> "output".equals(item.getName()));
if (!hasOutput) {
inputs.stream()
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
outputs.add(outputParam);
log.debug("从输入创建输出参数供下游节点使用");
});
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
outputs.add(outputParam);
log.debug("从输入创建输出参数供下游节点使用");
});
}
outputs.add(NodeIOData.createByText("matched_case", "switcher", String.valueOf(i + 1)));
outputs.add(NodeIOData.createByText("case_uuid", "switcher", switcherCase.getUuid()));
outputs.add(NodeIOData.createByText("target_node", "switcher", switcherCase.getTargetNodeUuid()));
@@ -87,37 +88,37 @@ public class SwitcherNode extends AbstractWfNode {
// 所有分支都不满足,使用默认分支
log.info("没有分支匹配,使用默认分支: {}", config.getDefaultTargetNodeUuid());
if (StringUtils.isBlank(config.getDefaultTargetNodeUuid())) {
log.warn("默认目标节点UUID为空工作流可能在此停止");
}
String defaultTarget = config.getDefaultTargetNodeUuid() != null ?
String defaultTarget = config.getDefaultTargetNodeUuid() != null ?
config.getDefaultTargetNodeUuid() : "";
// 构造输出:只保留 output 和其他非 input 参数 + 添加默认分支信息
List<NodeIOData> outputs = new java.util.ArrayList<>();
// 过滤输入:排除 input 参数(与 output 冗余),保留其他参数
inputs.stream()
.filter(item -> !"input".equals(item.getName()))
.forEach(outputs::add);
.filter(item -> !"input".equals(item.getName()))
.forEach(outputs::add);
// 如果没有 output 参数,从 input 创建 output便于后续节点使用
boolean hasOutput = outputs.stream().anyMatch(item -> "output".equals(item.getName()));
if (!hasOutput) {
inputs.stream()
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
outputs.add(outputParam);
log.debug("从输入创建输出参数供下游节点使用");
});
.filter(item -> "input".equals(item.getName()))
.findFirst()
.ifPresent(inputParam -> {
String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null
? inputParam.getContent().getTitle() : "";
NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString());
outputs.add(outputParam);
log.debug("从输入创建输出参数供下游节点使用");
});
}
outputs.add(NodeIOData.createByText("matched_case", "switcher", "default"));
outputs.add(NodeIOData.createByText("target_node", "switcher", defaultTarget));
@@ -129,12 +130,12 @@ public class SwitcherNode extends AbstractWfNode {
} catch (Exception e) {
log.error("处理条件分支节点失败: {}", node.getUuid(), e);
List<NodeIOData> errorOutputs = List.of(
NodeIOData.createByText("status", "switcher", "error"),
NodeIOData.createByText("error", "switcher", e.getMessage())
);
return NodeProcessResult.builder()
.content(errorOutputs)
.error(true)
@@ -145,8 +146,9 @@ public class SwitcherNode extends AbstractWfNode {
/**
* 评估单个分支的条件
*
* @param switcherCase 分支配置
* @param inputs 输入数据
* @param inputs 输入数据
* @return 是否满足条件
*/
private boolean evaluateCase(SwitcherCase switcherCase, List<NodeIOData> inputs) {
@@ -158,13 +160,13 @@ public class SwitcherNode extends AbstractWfNode {
String operator = switcherCase.getOperator();
boolean isAnd = "and".equalsIgnoreCase(operator);
log.debug("使用 {} 逻辑评估 {} 个条件",
log.debug("使用 {} 逻辑评估 {} 个条件",
operator, switcherCase.getConditions().size());
for (SwitcherCase.Condition condition : switcherCase.getConditions()) {
boolean conditionResult = evaluateCondition(condition, inputs);
log.debug("条件结果: {} (参数: {}, 运算符: {}, 值: {})",
conditionResult, condition.getNodeParamName(),
log.debug("条件结果: {} (参数: {}, 运算符: {}, 值: {})",
conditionResult, condition.getNodeParamName(),
condition.getOperator(), condition.getValue());
if (isAnd && !conditionResult) {
@@ -182,29 +184,30 @@ public class SwitcherNode extends AbstractWfNode {
/**
* 评估单个条件
*
* @param condition 条件配置
* @param inputs 输入数据
* @param inputs 输入数据
* @return 是否满足条件
*/
private boolean evaluateCondition(SwitcherCase.Condition condition, List<NodeIOData> inputs) {
try {
log.info("评估条件 - 节点UUID: {}, 参数名: {}, 运算符: {}, 期望值: {}",
condition.getNodeUuid(), condition.getNodeParamName(),
log.info("评估条件 - 节点UUID: {}, 参数名: {}, 运算符: {}, 期望值: {}",
condition.getNodeUuid(), condition.getNodeParamName(),
condition.getOperator(), condition.getValue());
// 获取实际值
String actualValue = getValueFromInputs(condition.getNodeUuid(),
String actualValue = getValueFromInputs(condition.getNodeUuid(),
condition.getNodeParamName(), inputs);
if (actualValue == null) {
log.warn("无法找到节点: {}, 参数: {} 的值 - 可用输入: {}",
condition.getNodeUuid(), condition.getNodeParamName(),
log.warn("无法找到节点: {}, 参数: {} 的值 - 可用输入: {}",
condition.getNodeUuid(), condition.getNodeParamName(),
inputs.stream().map(NodeIOData::getName).toList());
actualValue = "";
}
log.info("获取到的实际值: '{}' (类型: {})", actualValue, actualValue.getClass().getSimpleName());
String expectedValue = condition.getValue() != null ? condition.getValue() : "";
OperatorEnum operator = OperatorEnum.getByName(condition.getOperator());
@@ -214,9 +217,9 @@ public class SwitcherNode extends AbstractWfNode {
}
boolean result = evaluateOperator(operator, actualValue, expectedValue);
log.info("条件评估结果: {} (实际值='{}', 运算符={}, 期望值='{}')",
log.info("条件评估结果: {} (实际值='{}', 运算符={}, 期望值='{}')",
result, actualValue, operator, expectedValue);
return result;
} catch (Exception e) {
@@ -342,7 +345,7 @@ public class SwitcherNode extends AbstractWfNode {
return false;
}
} catch (NumberFormatException e) {
log.warn("无法解析数字进行比较: 实际值={}, 期望值={}",
log.warn("无法解析数字进行比较: 实际值={}, 期望值={}",
actualValue, expectedValue);
return false;
}