diff --git a/docs/script/sql/ruoyi-ai-v3_mysql8.sql b/docs/script/sql/ruoyi-ai-v3_mysql8.sql
index 6d85a2ef..58d14837 100644
--- a/docs/script/sql/ruoyi-ai-v3_mysql8.sql
+++ b/docs/script/sql/ruoyi-ai-v3_mysql8.sql
@@ -3657,7 +3657,16 @@ INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `di
INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (23, 'af9d6d7b9c9b47f990ad25ec84912b73', 'Tongyiwanx', '阿里图像生成', '使用通义万相生成图像', 0, 1, '2025-12-26 16:32:25', '2025-12-26 16:32:25', 0, '000000');
INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (24, 'a1e2c9d4b8f04e1a9c3d6f8e2a7b1c9d', 'MailSend', '发送邮箱', '发送邮箱', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000');
INSERT INTO `t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (25, 'f1e2d3c4b5a67890f1e2d3c4b5a6f1e2', 'HttpRequest', '请求节点', '请求节点', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000');
-INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_price`, `model_type`, `model_show`, `model_free`, `priority`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) VALUES (2022565766560468994, 'Tongyiwanx', 'wan2.5-t2i-preview', 'Tongyiwanx', 'wan2.5-t2i-preview', 1, '1', 'Y', 'Y', 1, 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation', 'skxxxx', 103, 1, '2026-02-14 14:57:11', 1, '2026-02-14 14:57:11', '通义万相文生图', 0);
+INSERT INTO `chat_model` (`id`, `category`, `model_name`, `provider_code`, `model_describe`, `model_price`, `model_type`, `model_show`, `model_free`, `priority`, `api_host`, `api_key`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`, `tenant_id`) VALUES (2022565766560468994, 'image', 'wan2.5-t2i-preview', 'Tongyiwanx', 'wan2.5-t2i-preview', 1, '1', 'Y', 'Y', 1, 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation', 'skxxxx', 103, 1, '2026-02-14 14:57:11', 1, '2026-02-14 14:57:11', '通义万相文生图', 0);
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2021046920636690433, '流程管理', 0, 0, 'flow', '', NULL, 1, 0, 'M', '0', '0', NULL, 'ph:user-fill', 103, 1, '2026-02-10 10:21:50', 1, '2026-02-10 15:59:28', '');
INSERT INTO `sys_menu` (`menu_id`, `menu_name`, `parent_id`, `order_num`, `path`, `component`, `query_param`, `is_frame`, `is_cache`, `menu_type`, `visible`, `status`, `perms`, `icon`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2021047050391678978, '工作流编排', 2021046920636690433, 0, 'aiflowengine', 'aiflow/index', NULL, 1, 0, 'C', '0', '0', '', 'ph:user-fill', 103, 1, '2026-02-10 10:22:21', 1, '2026-02-10 16:04:41', '');
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027192921483309058, '000000', 'HTTP请求节点响应模板', 'node.httpRequest.template', '✅ HTTP请求节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:23:51', 1, '2026-02-27 09:31:41', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027193296990957569, '000000', '文生图节点响应模板', 'node.image.template', '🎨 文生图节点:结束响应 - 图片URL: ', 'Y', 103, 1, '2026-02-27 09:25:20', 1, '2026-02-27 09:31:52', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027193820393959425, '000000', '发送邮箱节点响应模板', 'node.mailsend.template', '📧 发送邮箱节点:结束响应 - ', 'Y', 103, 1, '2026-02-27 09:27:25', 1, '2026-02-27 09:32:05', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027194134438277122, '000000', '结束节点响应模板', 'node.end.template', '🔚 流程已执行完毕,如果您有其他需求,请随时重新发起请求。', 'Y', 103, 1, '2026-02-27 09:28:40', 1, '2026-02-27 09:32:53', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027206492573335554, '000000', '人机交互节点响应模板', 'node.humanFeedback.template', '👤 人机交互节点:等待用户操作 - ', 'Y', 103, 1, '2026-02-27 10:17:46', 1, '2026-02-27 10:17:46', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027208880369647617, '000000', '条件分支节点响应模板', 'node.switch.template', '🔀 条件分支节点:触发 -> 跳转到节点 ', 'Y', 103, 1, '2026-02-27 10:27:15', 1, '2026-02-27 10:35:54', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027213914603995137, '000000', '大模型回答节点响应模板', 'node.llmAnswer.template', '🤖 LLM 节点 生成回答:', 'Y', 103, 1, '2026-02-27 10:47:16', 1, '2026-02-27 10:52:40', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027214387000066050, '000000', '关键词提取响应模板', 'node.keywordExtractor.template', '🔑 关键词提取节点 处理完成 : ', 'Y', 103, 1, '2026-02-27 10:49:08', 1, '2026-02-27 10:52:08', NULL);
+INSERT INTO `sys_config` (`config_id`, `tenant_id`, `config_name`, `config_key`, `config_value`, `config_type`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (2027217577397391361, '000000', '工作流异常响应模板', 'node.exception.template', '🛑 工作流发生异常:', 'N', 103, 1, '2026-02-27 11:01:49', 1, '2026-02-27 11:02:01', NULL);
diff --git a/pom.xml b/pom.xml
index 2e5a27ab..4fda3ba8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -56,6 +56,7 @@
1.11.0
1.11.0-beta19
+ 1.1.0-beta7
1.5.3
1.19.6
1.0.7
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/node/LLmMailSendNodeConfigDto.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/node/LLmMailSendNodeConfigDto.java
new file mode 100644
index 00000000..277c4424
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/node/LLmMailSendNodeConfigDto.java
@@ -0,0 +1,38 @@
+package org.ruoyi.workflow.dto.node;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 为大模型返回信息封装的信息DTO(发送邮箱)
+ */
+@Data
+public class LLmMailSendNodeConfigDto {
+
+ /**
+ * 主题
+ */
+ private String subject;
+
+ /**
+ * 内容
+ */
+ private String content;
+
+ /**
+ * 收件邮箱
+ */
+ @JsonProperty("to_mails")
+ private String toMails;
+
+ /**
+ * 抄送邮箱
+ */
+ @JsonProperty("cc_mails")
+ private String ccMails;
+
+ /**
+ * 发送类型
+ */
+ private Integer senderType;
+}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java
index 4ae71e3b..9fef1230 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java
@@ -4,9 +4,15 @@ import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
import org.ruoyi.common.chat.enums.RoleType;
import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.common.core.exception.ServiceException;
+import org.ruoyi.common.core.service.ConfigService;
import org.ruoyi.common.core.utils.SpringUtils;
+import org.ruoyi.common.core.utils.StringUtils;
+import org.ruoyi.workflow.entity.WorkflowNode;
+import org.ruoyi.workflow.helper.SSEEmitterHelper;
import org.ruoyi.workflow.workflow.WfState;
import org.ruoyi.workflow.workflow.WorkflowUtil;
+import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
/**
* 工作流消息工具类
@@ -17,6 +23,34 @@ import org.ruoyi.workflow.workflow.WorkflowUtil;
@Slf4j
public class WorkflowMessageUtil {
+
+ /**
+ * 通知并存储消息(对话使用)
+ * @param wfState 工作流实例状态
+ * @param sseEmitter SSE连接对象
+ * @param node 工作流节点
+ * @param message 消息
+ */
+ public static void notifyAndStoreMessage(WfState wfState, SseEmitter sseEmitter, WorkflowNode node, String message){
+ saveWorkflowMessage(wfState, message);
+ sendEmitterMessage(sseEmitter, node, message);
+ }
+
+
+ /**
+ * 获取节点的响应模板
+ * @param configKey 参数Key
+ * @return 返回模板样式
+ */
+ public static String getNodeMessageTemplate(String configKey){
+ ConfigService configService = SpringUtil.getBean(ConfigService.class);
+ String configValue = configService.getConfigValue(configKey);
+ if (StringUtils.isEmpty(configValue)) {
+ throw new ServiceException("请先配置该节点的响应模板");
+ }
+ return configValue;
+ }
+
/**
* 保存工作流消息公共方法(对话使用)
* @param wfState 工作流实例状态
@@ -34,4 +68,15 @@ public class WorkflowMessageUtil {
}
}
+ /**
+ * 发送SSE消息
+ * @param sseEmitter 连接对象
+ * @param node 工作流定义
+ * @param message 消息
+ */
+ public static void sendEmitterMessage(SseEmitter sseEmitter, WorkflowNode node, String message) {
+ String nodeUuid = node.getUuid();
+ SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter,"[NODE_CHUNK_" + nodeUuid + "]", message);
+ }
+
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java
index 20a3e783..379bbb2b 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java
@@ -31,6 +31,7 @@ import org.ruoyi.workflow.workflow.data.NodeIOData;
import org.ruoyi.workflow.workflow.def.WfNodeIO;
import org.ruoyi.workflow.workflow.def.WfNodeParamRef;
import org.ruoyi.workflow.workflow.node.AbstractWfNode;
+import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.*;
@@ -54,6 +55,7 @@ public class WorkflowEngine {
@Setter
private SseEmitter sseEmitter;
private User user;
+ @Getter
private WfState wfState;
private WfRuntimeResp wfRuntimeResp;
@@ -125,7 +127,10 @@ public class WorkflowEngine {
String nextNode = stateSnapshot.config().nextNode().orElse("");
//还有下个节点,表示进入中断状态,等待用户输入后继续执�?
if (StringUtils.isNotBlank(nextNode) && !nextNode.equalsIgnoreCase(END)) {
- String intTip = WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes);
+ // 获取提示模板
+ String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.HUMAN_FEED_BACK.getValue());
+ // 获取人机交互提示信息
+ String intTip = nodeMessageTemplate + WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes);
//将等待输入信息[事件与提示词]发送到到客户端
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_WAIT_FEEDBACK_BY_" + nextNode + "]", intTip);
// 保存提示信息到Chat信息记录中(对话使用)
@@ -136,7 +141,17 @@ public class WorkflowEngine {
workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
} else {
WorkflowRuntime updatedRuntime = workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
+ // 保存成功会话信息
+ wfNodes.stream().filter(item -> stateSnapshot.node().equals(item.getUuid()))
+ .findFirst().ifPresent(wfNode -> {
+ // 获取节点模板提示词信息
+ String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.END.getValue());
+ // 发送SSE消息驱动事件和保存会话
+ WorkflowMessageUtil.notifyAndStoreMessage(wfState, sseEmitter, wfNode, nodeMessageTemplate);
+ });
+ // 发送结束消息
sseEmitterHelper.sendComplete(user.getId(), sseEmitter, updatedRuntime.getOutput());
+ // 发送驱动消息事件
InterruptedFlow.RUNTIME_TO_GRAPH.remove(wfState.getUuid());
}
}
@@ -163,10 +178,12 @@ public class WorkflowEngine {
private void errorWhenExe(Exception e) {
log.error("error", e);
+ String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.EXCEPTION.getValue());
String errorMsg = e.getMessage();
if (errorMsg.contains("parallel node doesn't support conditional branch")) {
errorMsg = "并行节点中不能包含条件分�?";
}
+ errorMsg = nodeMessageTemplate + errorMsg;
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg);
workflowRuntimeService.updateStatus(wfRuntimeResp.getId(), WORKFLOW_PROCESS_STATUS_FAIL, errorMsg);
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java
index 95885ed1..bb99f201 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java
@@ -103,6 +103,8 @@ public class WorkflowStarter implements IWorkFlowStarterService {
// 如果SSE连接对象不为空传入该对象(Chat调用工作流对话使用)
if (null != sseEmitter){
workflowEngine.setSseEmitter(sseEmitter);
+ // 为了让每个节点都可以发送模板消息 保持SSE对象一致(以防出现向已关闭的SSE对象发送消息)
+ workflowEngine.getWfState().setSseEmitter(sseEmitter);
}
workflowEngine.resume(userInput);
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java
index c34962c0..53eb41cf 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java
@@ -112,7 +112,7 @@ public class WorkflowUtil extends AbstractChatMessageService {
}
public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName,
- List systemMessage) {
+ List systemMessage, String nodeMessageTemplate) {
log.info("stream invoke, modelName: {}", modelName);
// 根据模型名称查询模型信息
@@ -153,8 +153,10 @@ public class WorkflowUtil extends AbstractChatMessageService {
// 会话ID不为空时插入数据库
if (sessionId != null){
+ // 获取模板消息拼接信息体
+ String message = nodeMessageTemplate + responseTxt;
// 保存助手回复消息
- saveChatMessage(chatRequest, userId, responseTxt, RoleType.ASSISTANT.getName(), chatModelVo);
+ saveChatMessage(chatRequest, userId, message, RoleType.ASSISTANT.getName(), chatModelVo);
log.info("{}消息结束,已保存到数据库", getProviderName());
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java
index 700071f0..80f8aa60 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java
@@ -6,15 +6,11 @@ import lombok.Data;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.SerializationUtils;
-import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
-import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
-import org.ruoyi.common.chat.enums.RoleType;
import org.ruoyi.common.core.exception.base.BaseException;
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
-import org.ruoyi.workflow.helper.SSEEmitterHelper;
import org.ruoyi.workflow.util.JsonUtil;
import org.ruoyi.workflow.util.SpringUtil;
import org.ruoyi.workflow.util.WorkflowMessageUtil;
@@ -229,16 +225,16 @@ public abstract class AbstractWfNode {
/**
* 会话消息保存方法
*/
- public void saveSessionMessage(WfState wfState, String message) {
- WorkflowMessageUtil.saveWorkflowMessage(wfState, message);
+ public void notifyAndStoreMessage(WfState wfState, String message) {
+ WorkflowMessageUtil.notifyAndStoreMessage(wfState, wfState.getSseEmitter(), node, message);
}
/**
- * 发送SSe消息
- * @param message 信息
+ * 获取节点的响应模板
+ * @param configKey 参数Key
+ * @return 返回模板样式
*/
- public void sendSseEvent(String message){
- String nodeUuid = node.getUuid();
- SSEEmitterHelper.parseAndSendPartialMsg(wfState.getSseEmitter(), "[NODE_CHUNK_" + nodeUuid + "]", message);
+ public String getNodeMessageTemplate(String configKey){
+ return WorkflowMessageUtil.getNodeMessageTemplate(configKey);
}
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java
index e0c2f31c..2539226b 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java
@@ -6,11 +6,13 @@ 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.util.WorkflowMessageUtil;
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.node.AbstractWfNode;
+import org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import java.util.List;
@@ -46,7 +48,11 @@ public class LLMAnswerNode extends AbstractWfNode {
String modelName = nodeConfigObj.getModelName();
// 转换系统信息结构
List systemMessage = List.of(new SystemMessage(prompt));
- workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage);
+ // 获取节点模板提示词信息
+ String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.LLM_RESPONSE.getValue());
+ // 发送SSE驱动事件消息
+ WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
+ workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate);
return new NodeProcessResult();
}
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/enmus/NodeMessageTemplateEnum.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/enmus/NodeMessageTemplateEnum.java
new file mode 100644
index 00000000..1b39443f
--- /dev/null
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/enmus/NodeMessageTemplateEnum.java
@@ -0,0 +1,25 @@
+package org.ruoyi.workflow.workflow.node.enmus;
+
+import lombok.Getter;
+
+/**
+ * 节点消息模板ConfigKey枚举
+ */
+@Getter
+public enum NodeMessageTemplateEnum {
+ HTTP_REQUEST("node.httpRequest.template"),
+ MAIL_SEND("node.mailsend.template"),
+ IMAGE("node.image.template"),
+ HUMAN_FEED_BACK("node.humanFeedback.template"),
+ SWITCH("node.switch.template"),
+ LLM_RESPONSE("node.llmAnswer.template"),
+ KEYWORD_EXTRACTOR("node.keywordExtractor.template"),
+ EXCEPTION("node.exception.template"),
+ END("node.end.template");
+
+ private final String value;
+
+ NodeMessageTemplateEnum(String value) {
+ this.value = value;
+ }
+}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/httpRequest/HttpRequestNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/httpRequest/HttpRequestNode.java
index 10248aeb..8cd360c2 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/httpRequest/HttpRequestNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/httpRequest/HttpRequestNode.java
@@ -12,6 +12,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 org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -32,6 +33,8 @@ public class HttpRequestNode extends AbstractWfNode {
@Override
public NodeProcessResult onProcess() {
+ // 获取节点模板提示词信息
+ String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.HTTP_REQUEST.getValue());
try {
HttpRequestNodeConfig config = checkAndGetConfig(HttpRequestNodeConfig.class);
List inputs = state.getInputs();
@@ -63,11 +66,9 @@ public class HttpRequestNode extends AbstractWfNode {
List outputs = new ArrayList<>();
outputs.add(NodeIOData.createByText("output", "HTTP响应", response));
- // 保存成功会话信息
- String message = "HTTP响应:" + response;
- saveSessionMessage(wfState, message);
- // 发送驱动消息事件
- sendSseEvent(message);
+ // 保存成功会话信息且发送驱动消息事件
+ String message = nodeMessageTemplate + response;
+ notifyAndStoreMessage(wfState, message);
return NodeProcessResult.builder().content(outputs).build();
} catch (Exception e) {
@@ -78,11 +79,9 @@ public class HttpRequestNode extends AbstractWfNode {
errorOutputs.add(NodeIOData.createByText("output", "错误", ""));
errorOutputs.add(NodeIOData.createByText("error", "HTTP请求错误", e.getMessage()));
- // 保存失败会话信息
- String message = "HTTP响应失败:" + e.getMessage();
- saveSessionMessage(wfState, message);
- // 发送驱动消息事件
- sendSseEvent(message);
+ // 保存失败会话信息且发送驱动消息事件
+ String message = nodeMessageTemplate + e.getMessage();
+ notifyAndStoreMessage(wfState, message);
return NodeProcessResult.builder().content(errorOutputs).build();
}
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNode.java
index 4b887ad7..6f5b639b 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNode.java
@@ -11,6 +11,7 @@ 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.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.NODE_PROCESS_STATUS_SUCCESS;
@@ -51,11 +52,11 @@ public class ImageNode extends AbstractWfNode {
Integer seed = nodeConfigObj.getSeed();
// 调用LLM生成图片(后续可以将图片保存到OSS中)
String imageUrl = workflowUtil.buildTextToImage(modelName, prompt, size, seed);
- // 保存成功信息
- String message = "图片生成地址:" + imageUrl;
- saveSessionMessage(wfState, message);
- // 发送驱动消息事件
- sendSseEvent(message);
+ // 获取节点模板提示词信息
+ String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.IMAGE.getValue());
+ // 保存成功信息且发送驱动消息事件
+ String message = nodeMessageTemplate + imageUrl;
+ notifyAndStoreMessage(wfState, message);
// 创建节点参数对象
NodeIOData nodeIOData = NodeIOData.createByText("output", "image", imageUrl);
// 添加到输出列表以便给后续节点使用
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java
index 5ad7147a..a75c79f3 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/keywordExtractor/KeywordExtractorNode.java
@@ -1,18 +1,19 @@
package org.ruoyi.workflow.workflow.node.keywordExtractor;
import dev.langchain4j.data.message.SystemMessage;
-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.util.WorkflowMessageUtil;
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.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import java.util.ArrayList;
import java.util.List;
@@ -67,8 +68,12 @@ public class KeywordExtractorNode extends AbstractWfNode {
WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class);
String modelName = config.getModelName();
List systemMessage = List.of(new SystemMessage(prompt));
+ // 获取节点模板提示词信息
+ String nodeMessageTemplate = WorkflowMessageUtil.getNodeMessageTemplate(NodeMessageTemplateEnum.KEYWORD_EXTRACTOR.getValue());
+ // 发送SSE事件消息
+ WorkflowMessageUtil.sendEmitterMessage(wfState.getSseEmitter(), node, nodeMessageTemplate);
// 使用流式调用
- workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage);
+ workflowUtil.streamingInvokeLLM(wfState, state, node, modelName, systemMessage, nodeMessageTemplate);
return new NodeProcessResult();
}
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java
index f17dc744..d934c79b 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/knowledgeRetrieval/KnowledgeRetrievalNode.java
@@ -161,7 +161,8 @@ public class KnowledgeRetrievalNode extends AbstractWfNode {
tempState,
tempNode,
modelName,
- systemMessage
+ systemMessage,
+ ""
);
// 等待LLM响应完成(最多等待30秒)
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/mailSend/MailSendNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/mailSend/MailSendNode.java
index 0ae6a197..71688064 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/mailSend/MailSendNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/mailSend/MailSendNode.java
@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSONValidator;
import jakarta.mail.internet.MimeMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
+import org.ruoyi.workflow.dto.node.LLmMailSendNodeConfigDto;
import org.ruoyi.workflow.entity.WorkflowComponent;
import org.ruoyi.workflow.entity.WorkflowNode;
import org.ruoyi.workflow.workflow.NodeProcessResult;
@@ -13,6 +14,8 @@ 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.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
+import org.springframework.beans.BeanUtils;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
@@ -30,6 +33,8 @@ public class MailSendNode extends AbstractWfNode {
@Override
public NodeProcessResult onProcess() {
+ // 获取节点模板提示词信息
+ String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.MAIL_SEND.getValue());
try {
MailSendNodeConfig config = checkAndGetConfig(MailSendNodeConfig.class);
List inputs = state.getInputs();
@@ -37,7 +42,9 @@ public class MailSendNode extends AbstractWfNode {
String input = getDataFromInput(inputs);
// 判断是否为JSON格式(LLM输出转换 由LLM生成格式)
if (StringUtils.isNotBlank(input) && isJson(input)) {
- config = JSONObject.parseObject(input, MailSendNodeConfig.class);
+ LLmMailSendNodeConfigDto lLmMailSendNodeConfigDto = JSONObject.parseObject(input, LLmMailSendNodeConfigDto.class);
+ // 保留原本Sender和Smtp对象
+ BeanUtils.copyProperties(lLmMailSendNodeConfigDto, config);
}
// 安全获取模板(使用 defaultString 避免 null)
@@ -111,11 +118,9 @@ public class MailSendNode extends AbstractWfNode {
mailSender.send(message);
log.info("Email sent successfully to: {}", toMails);
- // 保存成功会话信息
- String resultMessage = "发送邮箱成功";
- saveSessionMessage(wfState, resultMessage);
- // 发送驱动消息事件
- sendSseEvent(resultMessage);
+ // 保存成功会话信息且发送驱动消息事件
+ String resultMessage = nodeMessageTemplate + "发送邮箱成功";
+ notifyAndStoreMessage(wfState, resultMessage);
// 构造输出:统一输出为 output 参数
List outputs = new java.util.ArrayList<>();
@@ -144,11 +149,9 @@ public class MailSendNode extends AbstractWfNode {
// 异常时也统一输出为 output 参数,添加错误信息
List errorOutputs = new java.util.ArrayList<>();
- // 保存失败会话信息
- String resultMessage = "发送邮箱失败: " + e.getMessage();
- saveSessionMessage(wfState, resultMessage);
- // 发送驱动消息事件
- sendSseEvent(resultMessage);
+ // 保存失败会话信息且发送驱动消息事件
+ String resultMessage = nodeMessageTemplate + "发送邮箱失败: " + e.getMessage();
+ notifyAndStoreMessage(wfState, resultMessage);
state.getInputs().stream()
.filter(item -> "output".equals(item.getName()))
diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java
index 6c27509d..f67d1585 100644
--- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java
+++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNode.java
@@ -15,6 +15,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 org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum;
import java.math.BigDecimal;
import java.util.List;
@@ -43,6 +44,9 @@ public class SwitcherNode extends AbstractWfNode {
log.info("条件分支节点处理中,分支数量: {}",
config.getCases() != null ? config.getCases().size() : 0);
+ // 获取提示模板
+ String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.SWITCH.getValue());
+
// 按顺序评估每个分支
if (config.getCases() != null) {
for (int i = 0; i < config.getCases().size(); i++) {
@@ -52,13 +56,17 @@ public class SwitcherNode extends AbstractWfNode {
if (evaluateCase(switcherCase, inputs)) {
// 检查目标节点UUID是否为空
- if (StringUtils.isBlank(switcherCase.getTargetNodeUuid())) {
+ String targetNodeUuid = switcherCase.getTargetNodeUuid();
+ if (StringUtils.isBlank(targetNodeUuid)) {
log.warn("分支 {} 匹配但目标节点UUID为空,跳过到下一个分支", i + 1);
continue;
}
+ // 根据目标节点UUID获取对应节点名称
+ findNodeAndNotify(targetNodeUuid, nodeMessageTemplate);
+
log.info("分支 {} 匹配,跳转到节点: {}",
- i + 1, switcherCase.getTargetNodeUuid());
+ i + 1, targetNodeUuid);
// 构造输出:只保留 output 和其他非 input 参数 + 添加分支匹配信息
List outputs = new java.util.ArrayList<>();
@@ -85,12 +93,12 @@ public class SwitcherNode extends AbstractWfNode {
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()));
+ outputs.add(NodeIOData.createByText("target_node", "switcher", targetNodeUuid));
// WorkflowEngine 会自动将 nextNodeUuid 放入 resultMap 的 "next" 键中
return NodeProcessResult.builder()
.content(outputs)
- .nextNodeUuid(switcherCase.getTargetNodeUuid())
+ .nextNodeUuid(targetNodeUuid)
.build();
}
}
@@ -99,6 +107,9 @@ public class SwitcherNode extends AbstractWfNode {
// 所有分支都不满足,使用默认分支
log.info("没有分支匹配,使用默认分支: {}", config.getDefaultTargetNodeUuid());
+ // 根据默认目标节点UUID获取对应节点名称
+ findNodeAndNotify(config.getDefaultTargetNodeUuid(), nodeMessageTemplate);
+
if (StringUtils.isBlank(config.getDefaultTargetNodeUuid())) {
log.warn("默认目标节点UUID为空,工作流可能在此停止");
}
@@ -154,6 +165,21 @@ public class SwitcherNode extends AbstractWfNode {
}
}
+ /**
+ * 根据节点ID查询对应节点
+ * @param targetNodeUuid 节点UUID
+ * @param nodeMessageTemplate 节点消息模板
+ */
+ private void findNodeAndNotify(String targetNodeUuid, String nodeMessageTemplate) {
+ // 根据目标节点UUID获取对应节点名称
+ WorkflowNode workflowNode = workflowNodeService.lambdaQuery().eq(WorkflowNode::getUuid, targetNodeUuid).one();
+ if (null != workflowNode){
+ // 获取节点名称
+ String message = nodeMessageTemplate + workflowNode.getTitle();
+ notifyAndStoreMessage(wfState, message);
+ }
+ }
+
/**
* 评估单个分支的条件
*
diff --git a/ruoyi-modules/ruoyi-chat/pom.xml b/ruoyi-modules/ruoyi-chat/pom.xml
index ae85abd9..35a3535f 100644
--- a/ruoyi-modules/ruoyi-chat/pom.xml
+++ b/ruoyi-modules/ruoyi-chat/pom.xml
@@ -48,6 +48,12 @@
${langchain4j.community.version}
+
+ dev.langchain4j
+ langchain4j-community-zhipu-ai
+ ${langchain4j.community.zhipu.ai.version}
+
+
diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java
new file mode 100644
index 00000000..222ee3cc
--- /dev/null
+++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java
@@ -0,0 +1,43 @@
+package org.ruoyi.service.chat.impl.provider;
+
+import dev.langchain4j.community.model.zhipu.ZhipuAiStreamingChatModel;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.model.chat.StreamingChatModel;
+import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.ruoyi.common.chat.domain.dto.request.ChatRequest;
+import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo;
+import org.ruoyi.enums.ChatModeType;
+import org.ruoyi.service.chat.impl.AbstractStreamingChatService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 智谱AI服务调用
+ *
+ * @author zengxb
+ * @date 2026/02/26
+ */
+@Service
+@Slf4j
+public class ZhiPuChatServiceImpl extends AbstractStreamingChatService {
+ @Override
+ protected void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler) {
+ StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo,chatRequest);
+ streamingChatModel.chat(messagesWithMemory, handler);
+ }
+
+ @Override
+ protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) {
+ return ZhipuAiStreamingChatModel.builder()
+ .apiKey(chatModelVo.getApiKey())
+ .model(chatModelVo.getModelName())
+ .build();
+ }
+
+ @Override
+ public String getProviderName() {
+ return ChatModeType.ZHI_PU.getCode();
+ }
+}