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(); + } +}