mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-03-13 20:53:42 +08:00
@@ -21,4 +21,14 @@ public class NodeProcessResult {
|
||||
* 条件执行时使用
|
||||
*/
|
||||
private String nextNodeUuid;
|
||||
|
||||
/**
|
||||
* 是否发生错误
|
||||
*/
|
||||
private boolean error = false;
|
||||
|
||||
/**
|
||||
* 错误或提示信息
|
||||
*/
|
||||
private String message;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||
import org.ruoyi.workflow.workflow.node.EndNode;
|
||||
import org.ruoyi.workflow.workflow.node.answer.LLMAnswerNode;
|
||||
import org.ruoyi.workflow.workflow.node.keywordExtractor.KeywordExtractorNode;
|
||||
import org.ruoyi.workflow.workflow.node.mailSend.MailSendNode;
|
||||
import org.ruoyi.workflow.workflow.node.start.StartNode;
|
||||
import org.ruoyi.workflow.workflow.node.switcher.SwitcherNode;
|
||||
|
||||
public class WfNodeFactory {
|
||||
public static AbstractWfNode create(WorkflowComponent wfComponent, WorkflowNode nodeDefinition,
|
||||
@@ -14,7 +17,10 @@ public class WfNodeFactory {
|
||||
switch (WfComponentNameEnum.getByName(wfComponent.getName())) {
|
||||
case START -> wfNode = new StartNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case LLM_ANSWER -> wfNode = new LLMAnswerNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case KEYWORD_EXTRACTOR -> wfNode = new KeywordExtractorNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case END -> wfNode = new EndNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case MAIL_SEND -> wfNode = new MailSendNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
case SWITCHER -> wfNode = new SwitcherNode(wfComponent, nodeDefinition, wfState, nodeState);
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,10 @@ 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
|
||||
@@ -35,22 +33,48 @@ public class WorkflowUtil {
|
||||
@Resource
|
||||
private ChatServiceFactory chatServiceFactory;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static String renderTemplate(String template, List<NodeIOData> values) {
|
||||
// 🔒 关键修复:如果 template 为 null,直接返回 null 或空字符串
|
||||
if (template == null) {
|
||||
return null; // 或 return ""; 根据业务需求
|
||||
}
|
||||
|
||||
String result = template;
|
||||
|
||||
// 防御 values 为 null
|
||||
if (values == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (NodeIOData next : values) {
|
||||
if (next == null || next.getName() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String name = next.getName();
|
||||
NodeIODataContent<?> dataContent = next.getContent();
|
||||
if (dataContent.getType().equals(WfIODataTypeEnum.FILES.getValue())) {
|
||||
List<String> value = (List<String>) dataContent.getValue();
|
||||
result = result.replace("{" + name + "}", String.join(",", value));
|
||||
} else if (dataContent.getType().equals(WfIODataTypeEnum.OPTIONS.getValue())) {
|
||||
Map<String, Object> value = (Map<String, Object>) dataContent.getValue();
|
||||
result = result.replace("{" + name + "}", value.toString());
|
||||
} else {
|
||||
result = result.replace("{" + name + "}", dataContent.getValue().toString());
|
||||
if (dataContent == null || dataContent.getValue() == null) {
|
||||
// 变量值为 null,替换为空字符串
|
||||
result = result.replace("{" + name + "}", "");
|
||||
continue;
|
||||
}
|
||||
|
||||
String replacement;
|
||||
if (dataContent.getType().equals(WfIODataTypeEnum.FILES.getValue())) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> value = (List<String>) dataContent.getValue();
|
||||
replacement = String.join(",", value);
|
||||
} else if (dataContent.getType().equals(WfIODataTypeEnum.OPTIONS.getValue())) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> value = (Map<String, Object>) dataContent.getValue();
|
||||
replacement = value.toString();
|
||||
} else {
|
||||
replacement = dataContent.getValue().toString();
|
||||
}
|
||||
|
||||
result = result.replace("{" + name + "}", replacement);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -81,8 +105,15 @@ public class WorkflowUtil {
|
||||
.mapResult(response -> {
|
||||
String responseTxt = response.aiMessage().text();
|
||||
log.info("llm response:{}", responseTxt);
|
||||
NodeIOData output = NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", responseTxt);
|
||||
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> item.getOutputs().add(output));
|
||||
|
||||
// 传递所有输入数据 + 添加 LLM 输出
|
||||
wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> {
|
||||
List<NodeIOData> outputs = new ArrayList<>(item.getInputs());
|
||||
NodeIOData output = NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", responseTxt);
|
||||
outputs.add(output);
|
||||
item.setOutputs(outputs);
|
||||
});
|
||||
|
||||
return Map.of("completeResult", response.aiMessage().text());
|
||||
})
|
||||
.startingNode(node.getUuid())
|
||||
@@ -141,9 +172,10 @@ public class WorkflowUtil {
|
||||
* @return
|
||||
*/
|
||||
private Message getMessage(String role, Object value) {
|
||||
log.info("Creating message with role: {}, content: {}", role, value); // 🔥
|
||||
Message message = new Message();
|
||||
message.setContent(String.valueOf(value));
|
||||
message.setRole(role);
|
||||
message.setContent(value);
|
||||
return message;
|
||||
}
|
||||
|
||||
@@ -154,9 +186,13 @@ public class WorkflowUtil {
|
||||
* @param messages
|
||||
*/
|
||||
private void addSystemMessage(List<UserMessage> systemMessage, List<Message> messages) {
|
||||
log.info("addSystemMessage received: {}", systemMessage); // 🔥 加这一行
|
||||
|
||||
if (CollUtil.isEmpty(systemMessage)) {
|
||||
return;
|
||||
}
|
||||
systemMessage.stream().map(userMsg -> getMessage("system", userMsg.singleText())).forEach(messages::add);
|
||||
systemMessage.stream()
|
||||
.map(userMsg -> getMessage("system", userMsg.singleText()))
|
||||
.forEach(messages::add);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,17 +166,40 @@ public abstract class AbstractWfNode {
|
||||
protected abstract NodeProcessResult onProcess();
|
||||
|
||||
protected String getFirstInputText() {
|
||||
// 检查输入是否为空
|
||||
if (state.getInputs() == null || state.getInputs().isEmpty()) {
|
||||
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()) && !DEFAULT_INPUT_PARAM_NAME.equals(item.getName()))
|
||||
.filter(item -> WfIODataTypeEnum.TEXT.getValue().equals(item.getContent().getType())
|
||||
&& !DEFAULT_INPUT_PARAM_NAME.equals(item.getName()))
|
||||
.map(NodeIOData::valueToString)
|
||||
.findFirst()
|
||||
.orElse("");
|
||||
} else {
|
||||
firstInputText = state.getInputs().get(0).valueToString();
|
||||
}
|
||||
|
||||
log.debug("Using first input text for node: {}, value: {}", state.getUuid(),
|
||||
firstInputText.length() > 50 ? firstInputText.substring(0, 50) + "..." : firstInputText);
|
||||
return firstInputText;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.ruoyi.workflow.workflow.node.keywordExtractor;
|
||||
|
||||
import dev.langchain4j.data.message.UserMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.util.SpringUtil;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME;
|
||||
|
||||
/**
|
||||
* 【节点】关键词提取节点
|
||||
* 使用 LLM 从文本中提取关键词
|
||||
*/
|
||||
@Slf4j
|
||||
public class KeywordExtractorNode extends AbstractWfNode {
|
||||
|
||||
public KeywordExtractorNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
|
||||
super(wfComponent, nodeDef, wfState, nodeState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理关键词提取
|
||||
* nodeConfig 格式:
|
||||
* {
|
||||
* "model_name": "deepseek-chat",
|
||||
* "category": "llm",
|
||||
* "top_n": 5,
|
||||
* "prompt": "额外的提示词"
|
||||
* }
|
||||
*
|
||||
* @return 提取的关键词列表
|
||||
*/
|
||||
@Override
|
||||
public NodeProcessResult onProcess() {
|
||||
KeywordExtractorNodeConfig config = checkAndGetConfig(KeywordExtractorNodeConfig.class);
|
||||
|
||||
// 获取输入文本
|
||||
String inputText = getFirstInputText();
|
||||
if (StringUtils.isBlank(inputText)) {
|
||||
log.warn("Keyword extractor node has no input text, node: {}", state.getUuid());
|
||||
// 返回空结果
|
||||
List<NodeIOData> outputs = new ArrayList<>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package org.ruoyi.workflow.workflow.node.keywordExtractor;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 关键词提取节点配置
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@Data
|
||||
public class KeywordExtractorNodeConfig {
|
||||
|
||||
/**
|
||||
* 模型分类(如:llm, embedding 等)
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@NotNull
|
||||
@JsonProperty("model_name")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 提取的关键词数量
|
||||
*/
|
||||
@Min(1)
|
||||
@Max(50)
|
||||
@JsonProperty("top_n")
|
||||
private Integer topN = 5;
|
||||
|
||||
/**
|
||||
* 提示词(可选)
|
||||
* 用于指导关键词提取的额外说明
|
||||
*/
|
||||
private String prompt;
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.ruoyi.workflow.workflow.node.mailSend;
|
||||
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
import org.ruoyi.workflow.workflow.WfNodeState;
|
||||
import org.ruoyi.workflow.workflow.WfState;
|
||||
import org.ruoyi.workflow.workflow.WorkflowUtil;
|
||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||
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;
|
||||
|
||||
@Slf4j
|
||||
public class MailSendNode extends AbstractWfNode {
|
||||
|
||||
public MailSendNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
|
||||
super(wfComponent, nodeDef, wfState, nodeState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeProcessResult onProcess() {
|
||||
try {
|
||||
MailSendNodeConfig config = checkAndGetConfig(MailSendNodeConfig.class);
|
||||
List<NodeIOData> inputs = state.getInputs();
|
||||
|
||||
// 安全获取模板(使用 defaultString 避免 null)
|
||||
String subjectTemplate = StringUtils.defaultString(config.getSubject());
|
||||
String contentTemplate = StringUtils.defaultString(config.getContent());
|
||||
String toMailsTemplate = StringUtils.defaultString(config.getToMails());
|
||||
String ccMailsTemplate = StringUtils.defaultString(config.getCcMails());
|
||||
|
||||
// 渲染收件人和主题
|
||||
String toMails = WorkflowUtil.renderTemplate(toMailsTemplate, inputs);
|
||||
String ccMails = WorkflowUtil.renderTemplate(ccMailsTemplate, inputs);
|
||||
String subject = WorkflowUtil.renderTemplate(subjectTemplate, inputs);
|
||||
|
||||
// 内容:优先使用配置的内容模板,否则使用 output 或 input 参数
|
||||
String content;
|
||||
if (StringUtils.isNotBlank(contentTemplate)) {
|
||||
content = WorkflowUtil.renderTemplate(contentTemplate, inputs);
|
||||
} 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()))
|
||||
.map(NodeIOData::valueToString)
|
||||
.findFirst()
|
||||
.orElse(""));
|
||||
}
|
||||
|
||||
// 将换行符转换为 HTML 换行
|
||||
if (StringUtils.isNotBlank(content)) {
|
||||
content = content.replace("\n", "<br>");
|
||||
} else {
|
||||
content = ""; // 安全兜底
|
||||
}
|
||||
|
||||
// 校验必要字段
|
||||
if (config.getSender() == null) {
|
||||
throw new IllegalArgumentException("发件人配置(sender)不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(toMails)) {
|
||||
throw new IllegalArgumentException("收件人邮箱(to_mails)不能为空或未解析出有效值");
|
||||
}
|
||||
|
||||
// 创建邮件发送器
|
||||
JavaMailSender mailSender = createMailSender(config);
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(config.getSender().getMail(), config.getSender().getName());
|
||||
|
||||
// 设置收件人
|
||||
String[] toArray = Arrays.stream(toMails.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.toArray(String[]::new);
|
||||
if (toArray.length == 0) {
|
||||
throw new IllegalArgumentException("收件人邮箱列表为空");
|
||||
}
|
||||
helper.setTo(toArray);
|
||||
|
||||
// 设置抄送(如有)
|
||||
if (StringUtils.isNotBlank(ccMails)) {
|
||||
String[] ccArray = Arrays.stream(ccMails.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::isNotBlank)
|
||||
.toArray(String[]::new);
|
||||
if (ccArray.length > 0) {
|
||||
helper.setCc(ccArray);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置主题和内容(支持 HTML)
|
||||
helper.setSubject(subject);
|
||||
helper.setText(content, true);
|
||||
|
||||
// 发送
|
||||
mailSender.send(message);
|
||||
log.info("Email sent successfully to: {}", toMails);
|
||||
|
||||
// 构造输出:统一输出为 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);
|
||||
})
|
||||
);
|
||||
|
||||
return NodeProcessResult.builder().content(outputs).build();
|
||||
|
||||
} catch (Exception e) {
|
||||
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);
|
||||
})
|
||||
);
|
||||
|
||||
errorOutputs.add(NodeIOData.createByText("error", "mail", e.getMessage()));
|
||||
return NodeProcessResult.builder().content(errorOutputs).build();
|
||||
}
|
||||
}
|
||||
|
||||
private JavaMailSender createMailSender(MailSendNodeConfig config) {
|
||||
JavaMailSenderImpl sender = new JavaMailSenderImpl();
|
||||
sender.setHost(config.getSmtp().getHost());
|
||||
sender.setPort(config.getSmtp().getPort());
|
||||
sender.setUsername(config.getSender().getMail());
|
||||
sender.setPassword(config.getSender().getPassword());
|
||||
|
||||
Properties props = new Properties();
|
||||
props.put("mail.smtp.auth", "true");
|
||||
props.put("mail.smtp.ssl.enable", "true"); // QQ 邮箱 465 必须开 SSL
|
||||
props.put("mail.smtp.ssl.protocols", "TLSv1.2");
|
||||
sender.setJavaMailProperties(props);
|
||||
|
||||
return sender;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.ruoyi.workflow.workflow.node.mailSend;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MailSendNodeConfig {
|
||||
private SmtpConfig smtp;
|
||||
private SenderConfig sender;
|
||||
private String subject;
|
||||
private String content;
|
||||
|
||||
@JsonProperty("to_mails")
|
||||
private String toMails; // 支持 "a@x.com,b@y.com"
|
||||
|
||||
@JsonProperty("cc_mails")
|
||||
private String ccMails; // 可选
|
||||
private Integer senderType;
|
||||
|
||||
@Data
|
||||
public static class SmtpConfig {
|
||||
private String host;
|
||||
private Integer port; // 465 或 587
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class SenderConfig {
|
||||
private String mail;
|
||||
private String name;
|
||||
private String password; // 授权码
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package org.ruoyi.workflow.workflow.node.switcher;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 条件分支节点
|
||||
* 根据配置的条件规则,选择不同的分支路径执行
|
||||
*/
|
||||
@Slf4j
|
||||
public class SwitcherNode extends AbstractWfNode {
|
||||
|
||||
public SwitcherNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) {
|
||||
super(wfComponent, nodeDef, wfState, nodeState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeProcessResult onProcess() {
|
||||
try {
|
||||
SwitcherNodeConfig config = checkAndGetConfig(SwitcherNodeConfig.class);
|
||||
List<NodeIOData> inputs = state.getInputs();
|
||||
|
||||
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={}, 运算符={}",
|
||||
i + 1, switcherCase.getUuid(), switcherCase.getOperator());
|
||||
|
||||
if (evaluateCase(switcherCase, inputs)) {
|
||||
// 检查目标节点UUID是否为空
|
||||
if (StringUtils.isBlank(switcherCase.getTargetNodeUuid())) {
|
||||
log.warn("分支 {} 匹配但目标节点UUID为空,跳过到下一个分支", i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 如果没有 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("从输入创建输出参数供下游节点使用");
|
||||
});
|
||||
}
|
||||
|
||||
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()));
|
||||
|
||||
// WorkflowEngine 会自动将 nextNodeUuid 放入 resultMap 的 "next" 键中
|
||||
return NodeProcessResult.builder()
|
||||
.content(outputs)
|
||||
.nextNodeUuid(switcherCase.getTargetNodeUuid())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 所有分支都不满足,使用默认分支
|
||||
log.info("没有分支匹配,使用默认分支: {}", config.getDefaultTargetNodeUuid());
|
||||
|
||||
if (StringUtils.isBlank(config.getDefaultTargetNodeUuid())) {
|
||||
log.warn("默认目标节点UUID为空,工作流可能在此停止");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 如果没有 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("从输入创建输出参数供下游节点使用");
|
||||
});
|
||||
}
|
||||
|
||||
outputs.add(NodeIOData.createByText("matched_case", "switcher", "default"));
|
||||
outputs.add(NodeIOData.createByText("target_node", "switcher", defaultTarget));
|
||||
|
||||
// WorkflowEngine 会自动将 nextNodeUuid 放入 resultMap 的 "next" 键中
|
||||
return NodeProcessResult.builder()
|
||||
.content(outputs)
|
||||
.nextNodeUuid(config.getDefaultTargetNodeUuid())
|
||||
.build();
|
||||
|
||||
} 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)
|
||||
.message("条件分支节点错误: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估单个分支的条件
|
||||
* @param switcherCase 分支配置
|
||||
* @param inputs 输入数据
|
||||
* @return 是否满足条件
|
||||
*/
|
||||
private boolean evaluateCase(SwitcherCase switcherCase, List<NodeIOData> inputs) {
|
||||
if (switcherCase.getConditions() == null || switcherCase.getConditions().isEmpty()) {
|
||||
log.warn("分支 {} 没有条件,跳过", switcherCase.getUuid());
|
||||
return false;
|
||||
}
|
||||
|
||||
String operator = switcherCase.getOperator();
|
||||
boolean isAnd = "and".equalsIgnoreCase(operator);
|
||||
|
||||
log.debug("使用 {} 逻辑评估 {} 个条件",
|
||||
operator, switcherCase.getConditions().size());
|
||||
|
||||
for (SwitcherCase.Condition condition : switcherCase.getConditions()) {
|
||||
boolean conditionResult = evaluateCondition(condition, inputs);
|
||||
log.debug("条件结果: {} (参数: {}, 运算符: {}, 值: {})",
|
||||
conditionResult, condition.getNodeParamName(),
|
||||
condition.getOperator(), condition.getValue());
|
||||
|
||||
if (isAnd && !conditionResult) {
|
||||
// AND 逻辑:任何一个条件不满足就返回 false
|
||||
return false;
|
||||
} else if (!isAnd && conditionResult) {
|
||||
// OR 逻辑:任何一个条件满足就返回 true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// AND 逻辑:所有条件都满足返回 true
|
||||
// OR 逻辑:所有条件都不满足返回 false
|
||||
return isAnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估单个条件
|
||||
* @param condition 条件配置
|
||||
* @param inputs 输入数据
|
||||
* @return 是否满足条件
|
||||
*/
|
||||
private boolean evaluateCondition(SwitcherCase.Condition condition, List<NodeIOData> inputs) {
|
||||
try {
|
||||
log.info("评估条件 - 节点UUID: {}, 参数名: {}, 运算符: {}, 期望值: {}",
|
||||
condition.getNodeUuid(), condition.getNodeParamName(),
|
||||
condition.getOperator(), condition.getValue());
|
||||
|
||||
// 获取实际值
|
||||
String actualValue = getValueFromInputs(condition.getNodeUuid(),
|
||||
condition.getNodeParamName(), inputs);
|
||||
|
||||
if (actualValue == null) {
|
||||
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());
|
||||
|
||||
if (operator == null) {
|
||||
log.warn("未知运算符: {},视为false", condition.getOperator());
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean result = evaluateOperator(operator, actualValue, expectedValue);
|
||||
log.info("条件评估结果: {} (实际值='{}', 运算符={}, 期望值='{}')",
|
||||
result, actualValue, operator, expectedValue);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("评估条件时出错: {}", condition, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从输入数据中获取指定节点的参数值
|
||||
*/
|
||||
private String getValueFromInputs(String nodeUuid, String paramName, List<NodeIOData> inputs) {
|
||||
log.debug("从节点UUID '{}' 搜索参数 '{}'", nodeUuid, paramName);
|
||||
|
||||
// 首先尝试从当前输入中查找
|
||||
log.debug("检查当前输入 (数量: {})", inputs.size());
|
||||
for (NodeIOData input : inputs) {
|
||||
log.debug(" - 输入: 名称='{}', 值='{}'", input.getName(), input.valueToString());
|
||||
if (paramName.equals(input.getName())) {
|
||||
log.info("在当前输入中找到参数 '{}': '{}'", paramName, input.valueToString());
|
||||
return input.valueToString();
|
||||
}
|
||||
}
|
||||
|
||||
// 如果当前输入中没有,尝试从工作流状态中查找指定节点的输出
|
||||
if (StringUtils.isNotBlank(nodeUuid)) {
|
||||
List<NodeIOData> nodeOutputs = wfState.getIOByNodeUuid(nodeUuid);
|
||||
log.debug("检查节点 '{}' 的输出 (数量: {})", nodeUuid, nodeOutputs.size());
|
||||
for (NodeIOData output : nodeOutputs) {
|
||||
log.debug(" - 输出: 名称='{}', 值='{}'", output.getName(), output.valueToString());
|
||||
if (paramName.equals(output.getName())) {
|
||||
log.info("在节点 '{}' 的输出中找到参数 '{}': '{}'", nodeUuid, paramName, output.valueToString());
|
||||
return output.valueToString();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.debug("节点UUID为空,跳过工作流状态搜索");
|
||||
}
|
||||
|
||||
log.warn("在输入或节点 '{}' 的输出中未找到参数 '{}'", nodeUuid, paramName);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据运算符评估条件
|
||||
*/
|
||||
private boolean evaluateOperator(OperatorEnum operator, String actualValue, String expectedValue) {
|
||||
switch (operator) {
|
||||
case CONTAINS:
|
||||
return actualValue.contains(expectedValue);
|
||||
|
||||
case NOT_CONTAINS:
|
||||
return !actualValue.contains(expectedValue);
|
||||
|
||||
case START_WITH:
|
||||
return actualValue.startsWith(expectedValue);
|
||||
|
||||
case END_WITH:
|
||||
return actualValue.endsWith(expectedValue);
|
||||
|
||||
case EMPTY:
|
||||
return StringUtils.isBlank(actualValue);
|
||||
|
||||
case NOT_EMPTY:
|
||||
return StringUtils.isNotBlank(actualValue);
|
||||
|
||||
case EQUAL:
|
||||
return actualValue.equals(expectedValue);
|
||||
|
||||
case NOT_EQUAL:
|
||||
return !actualValue.equals(expectedValue);
|
||||
|
||||
case GREATER:
|
||||
case GREATER_OR_EQUAL:
|
||||
case LESS:
|
||||
case LESS_OR_EQUAL:
|
||||
return evaluateNumericComparison(operator, actualValue, expectedValue);
|
||||
|
||||
default:
|
||||
log.warn("不支持的运算符: {}", operator);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估数值比较
|
||||
*/
|
||||
private boolean evaluateNumericComparison(OperatorEnum operator, String actualValue, String expectedValue) {
|
||||
try {
|
||||
BigDecimal actual = new BigDecimal(actualValue.trim());
|
||||
BigDecimal expected = new BigDecimal(expectedValue.trim());
|
||||
int comparison = actual.compareTo(expected);
|
||||
|
||||
switch (operator) {
|
||||
case GREATER:
|
||||
return comparison > 0;
|
||||
case GREATER_OR_EQUAL:
|
||||
return comparison >= 0;
|
||||
case LESS:
|
||||
return comparison < 0;
|
||||
case LESS_OR_EQUAL:
|
||||
return comparison <= 0;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法解析数字进行比较: 实际值={}, 期望值={}",
|
||||
actualValue, expectedValue);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user