From 0a115f289ee9235d138365c0d76addd9fc45b0d5 Mon Sep 17 00:00:00 2001 From: zengxb <648669796@qq.com> Date: Tue, 24 Feb 2026 10:34:26 +0800 Subject: [PATCH] =?UTF-8?q?context:=E9=80=9A=E4=B9=89=E4=B8=87=E7=9B=B8?= =?UTF-8?q?=E6=96=87=E7=94=9F=E5=9B=BE=E8=8A=82=E7=82=B9=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E4=BB=A5=E5=8F=8A=E5=8F=91=E9=80=81=E9=82=AE=E7=AE=B1=E5=92=8C?= =?UTF-8?q?HTTP=E8=AF=B7=E6=B1=82=E8=8A=82=E7=82=B9=E8=B0=83=E7=A0=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/script/sql/workFlow-v3.0.sql | 16 ++- .../chat/Service/IImageGenerationService.java | 20 +++ .../domain/entity/image/ImageContext.java | 45 +++++++ .../chat/factory/ImageServiceFactory.java | 45 +++++++ .../workflow/workflow/WfNodeFactory.java | 4 +- .../ruoyi/workflow/workflow/WorkflowUtil.java | 32 +++++ .../node/answer/LLMAnswerNodeConfig.java | 6 - .../HumanFeedbackNode.java | 3 +- .../workflow/node/image/ImageNode.java | 62 +++++++++ .../workflow/node/image/ImageNodeConfig.java | 37 ++++++ .../java/org/ruoyi/enums/ImageModeType.java | 23 ++++ .../impl/provider/QianWenChatServiceImpl.java | 124 ++++++++++++------ .../image/AbstractImageGenerationService.java | 43 ++++++ .../provider/TongYiWanxImageServiceImpl.java | 73 +++++++++++ 14 files changed, 476 insertions(+), 57 deletions(-) create mode 100644 ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IImageGenerationService.java create mode 100644 ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/image/ImageContext.java create mode 100644 ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ImageServiceFactory.java rename ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/{ => humanFeedBack}/HumanFeedbackNode.java (95%) create mode 100644 ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNode.java create mode 100644 ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNodeConfig.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ImageModeType.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/AbstractImageGenerationService.java create mode 100644 ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/provider/TongYiWanxImageServiceImpl.java diff --git a/docs/script/sql/workFlow-v3.0.sql b/docs/script/sql/workFlow-v3.0.sql index b97366f2..f7573338 100644 --- a/docs/script/sql/workFlow-v3.0.sql +++ b/docs/script/sql/workFlow-v3.0.sql @@ -1,6 +1,10 @@ -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (17, '5cd68dccbbb411f0bb7840c2ba9a7fbc', 'Start', '开始', '流程由此开始', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (18, '5cd6ac69bbb411f0bb7840c2ba9a7fbc', 'End', '结束', '流程由此结束', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (19, '5cd6c8eabbb411f0bb7840c2ba9a7fbc', 'Answer', '生成回答', '调用大语言模型回答问题', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (25, '0b4369bb60dc46d6bd84ceb4e36184dc', 'KeywordExtractor', '关键词提取', '从文本中提取关键词', 0, 1, '2025-12-26 16:30:05', '2025-12-26 16:30:05', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (26, 'bb00fc2f52c74fec82ee3f99725b56bb', 'Switcher', '条件分支', '根据条件执行不同分支', 0, 1, '2025-12-26 16:30:46', '2025-12-26 16:30:46', 0, '000000'); -INSERT INTO `mysql`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (36, 'f37dbcb8f0d5464d90fbb22774490a56', 'HumanFeedback', '人类', '人机沟通', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); +INSERT INTO `ruoyi-ai-v3`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (17, '5cd68dccbbb411f0bb7840c2ba9a7fbc', 'Start', '开始', '流程由此开始', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); +INSERT INTO `ruoyi-ai-v3`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (18, '5cd6ac69bbb411f0bb7840c2ba9a7fbc', 'End', '结束', '流程由此结束', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); +INSERT INTO `ruoyi-ai-v3`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (19, '5cd6c8eabbb411f0bb7840c2ba9a7fbc', 'Answer', '生成回答', '调用大语言模型回答问题', 0, 1, '2025-11-07 16:32:49', '2025-11-07 16:32:49', 0, '000000'); +INSERT INTO `ruoyi-ai-v3`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (20, '0b4369bb60dc46d6bd84ceb4e36184dc', 'KeywordExtractor', '关键词提取', '从文本中提取关键词', 0, 1, '2025-12-26 16:30:05', '2025-12-26 16:30:05', 0, '000000'); +INSERT INTO `ruoyi-ai-v3`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (21, 'bb00fc2f52c74fec82ee3f99725b56bb', 'Switcher', '条件分支', '根据条件执行不同分支', 0, 1, '2025-12-26 16:30:46', '2025-12-26 16:30:46', 0, '000000'); +INSERT INTO `ruoyi-ai-v3`.`t_workflow_component` (`id`, `uuid`, `name`, `title`, `remark`, `display_order`, `is_enable`, `create_time`, `update_time`, `is_deleted`, `tenant_id`) VALUES (22, 'f37dbcb8f0d5464d90fbb22774490a56', 'HumanFeedback', '人类', '人机沟通', 0, 1, '2025-12-30 17:37:14', '2025-12-30 17:37:14', 0, '000000'); +INSERT INTO `ruoyi-ai-v3`.`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 `ruoyi-ai-v3`.`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 `ruoyi-ai-v3`.`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 `ruoyi-ai-v3`.`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', 'Bearer sk-f4550b0e138c488cbfcafe3d61f800a5', 103, 1, '2026-02-14 14:57:11', 1, '2026-02-14 14:57:11', '通义万相文生图', 0); diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IImageGenerationService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IImageGenerationService.java new file mode 100644 index 00000000..237afed1 --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IImageGenerationService.java @@ -0,0 +1,20 @@ +package org.ruoyi.common.chat.Service; + +import jakarta.validation.Valid; +import org.ruoyi.common.chat.domain.entity.image.ImageContext; + +/** + * 公共文生图接口 + */ +public interface IImageGenerationService { + + /** + * 根据文字生成图片 + */ + String generateImage(@Valid ImageContext imageContext); + + /** + * 获取服务提供商名称 + */ + String getProviderName(); +} diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/image/ImageContext.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/image/ImageContext.java new file mode 100644 index 00000000..4bf42c61 --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/image/ImageContext.java @@ -0,0 +1,45 @@ +package org.ruoyi.common.chat.domain.entity.image; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; + +/** + * 文生图对话上下文对象 + * + * @author zengxb + * @date 2026-02-14 + */ +@Data +@EqualsAndHashCode(callSuper = false) +@Builder +public class ImageContext { + + /** + * 模型管理视图对象 + */ + @NotNull(message = "模型管理视图对象不能为空") + private ChatModelVo chatModelVo; + + /** + * 提示词 + */ + @NotNull(message = "提示词不能为空") + private String prompt; + + /** + * 图片尺寸大小 + */ + private String size; + + /** + * 随机数种子 + */ + @Min(value = 0, message = "随机数种子不能小于0") + @Max(value = 2147483647, message = "随机数种子不能大于2147483647") + private Integer seed; +} diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ImageServiceFactory.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ImageServiceFactory.java new file mode 100644 index 00000000..e7ba4160 --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ImageServiceFactory.java @@ -0,0 +1,45 @@ +package org.ruoyi.common.chat.factory; + +import org.ruoyi.common.chat.Service.IImageGenerationService; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 文生图服务工厂类 + * + * @author zengxb + * @date 2026-02-14 + */ +@Component +public class ImageServiceFactory implements ApplicationContextAware { + + private final Map imageSerivceMap = new ConcurrentHashMap<>(); + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + // 初始化时收集所有IImageGenerationService的实现 + Map serviceMap = applicationContext.getBeansOfType(IImageGenerationService.class); + for (IImageGenerationService service : serviceMap.values()) { + if (service != null ) { + imageSerivceMap.put(service.getProviderName(), service); + } + } + } + + + /** + * 获取原始服务(不包装代理) + */ + public IImageGenerationService getOriginalService(String category) { + IImageGenerationService service = imageSerivceMap.get(category); + if (service == null) { + throw new IllegalArgumentException("不支持的模型类别: " + category); + } + return service; + } +} diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java index 94aafe8b..ce22efca 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java @@ -4,9 +4,10 @@ import org.ruoyi.workflow.entity.WorkflowComponent; 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.HumanFeedbackNode; +import org.ruoyi.workflow.workflow.node.humanFeedBack.HumanFeedbackNode; import org.ruoyi.workflow.workflow.node.answer.LLMAnswerNode; import org.ruoyi.workflow.workflow.node.httpRequest.HttpRequestNode; +import org.ruoyi.workflow.workflow.node.image.ImageNode; import org.ruoyi.workflow.workflow.node.keywordExtractor.KeywordExtractorNode; import org.ruoyi.workflow.workflow.node.knowledgeRetrieval.KnowledgeRetrievalNode; import org.ruoyi.workflow.workflow.node.mailSend.MailSendNode; @@ -21,6 +22,7 @@ public class WfNodeFactory { 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 TONGYI_WANX -> wfNode = new ImageNode(wfComponent, nodeDefinition, wfState, nodeState); case KNOWLEDGE_RETRIEVER -> wfNode = new KnowledgeRetrievalNode(wfComponent, nodeDefinition, wfState, nodeState); case END -> wfNode = new EndNode(wfComponent, nodeDefinition, wfState, nodeState); case MAIL_SEND -> wfNode = new MailSendNode(wfComponent, nodeDefinition, wfState, nodeState); 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 176ed7d6..6715b3b7 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 @@ -13,10 +13,13 @@ import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator; import org.bsc.langgraph4j.state.AgentState; import org.ruoyi.common.chat.Service.IChatModelService; import org.ruoyi.common.chat.Service.IChatService; +import org.ruoyi.common.chat.Service.IImageGenerationService; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.entity.chat.ChatContext; +import org.ruoyi.common.chat.domain.entity.image.ImageContext; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.factory.ChatServiceFactory; +import org.ruoyi.common.chat.factory.ImageServiceFactory; import org.ruoyi.workflow.base.NodeInputConfigTypeHandler; import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.enums.WfIODataTypeEnum; @@ -38,6 +41,9 @@ public class WorkflowUtil { @Resource private ChatServiceFactory chatServiceFactory; + @Resource + private ImageServiceFactory imageServiceFactory; + @Resource private IChatModelService chatModelService; @@ -213,4 +219,30 @@ public class WorkflowUtil { .filter(item -> nameSet.contains(item.getName())) .map(item -> getMessage("user", item.getContent().getValue().toString())).toList(); } + + /** + * 调用LLM 根据文字生成图片 + */ + public String buildTextToImage(String modelName, String prompt, String size, Integer seed){ + log.info("Generate image invoke, modelName: {}", modelName); + // 根据模型名称查询模型信息 + ChatModelVo chatModelVo = chatModelService.selectModelByName(modelName); + if (chatModelVo == null) { + throw new IllegalArgumentException("模型不存在: " + modelName); + } + // 根据模型名称找到模型实体 + String modelVoCategory = chatModelVo.getCategory(); + // 根据 category 获取对应的 IImageGenerationService(不使用计费代理,工作流场景单独计费) + IImageGenerationService imageService = imageServiceFactory.getOriginalService(modelVoCategory); + // 构建文生图上下文对象 + ImageContext imageContext = ImageContext.builder() + .chatModelVo(chatModelVo) + .prompt(prompt) + .size(size) + .seed(seed) + .build(); + // 调用LLM 生成图片 + return imageService.generateImage(imageContext); + } + } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNodeConfig.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNodeConfig.java index fc90009f..64b45b0a 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNodeConfig.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNodeConfig.java @@ -12,12 +12,6 @@ public class LLMAnswerNodeConfig { @NotBlank private String prompt; - /** - * TODO - */ - // @NotBlank - private String category; - @NotNull @JsonProperty("model_name") private String modelName; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/HumanFeedbackNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/humanFeedBack/HumanFeedbackNode.java similarity index 95% rename from ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/HumanFeedbackNode.java rename to ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/humanFeedBack/HumanFeedbackNode.java index f41e1938..ea5b918d 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/HumanFeedbackNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/humanFeedBack/HumanFeedbackNode.java @@ -1,4 +1,4 @@ -package org.ruoyi.workflow.workflow.node; +package org.ruoyi.workflow.workflow.node.humanFeedBack; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -8,6 +8,7 @@ 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 static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*; 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 new file mode 100644 index 00000000..095e4acd --- /dev/null +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNode.java @@ -0,0 +1,62 @@ +package org.ruoyi.workflow.workflow.node.image; + +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 static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.NODE_PROCESS_STATUS_SUCCESS; + +/** + * 【节点】文生图
+ * 节点内容固定格式:ImageNodeConfig + */ +@Slf4j +public class ImageNode extends AbstractWfNode { + + public ImageNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) { + super(wfComponent, nodeDef, wfState, nodeState); + } + + /** + * nodeConfig格式: + * {"prompt": "{input}","model_name":"wan2.5-t2i-preview","size":"1024*1024"} + * + * @return 图片地址URL + */ + @Override + public NodeProcessResult onProcess() { + ImageNodeConfig nodeConfigObj = checkAndGetConfig(ImageNodeConfig.class); + String inputText = getFirstInputText(); + log.info("Image node config:{}", nodeConfigObj); + String prompt = inputText; + if (StringUtils.isNotBlank(nodeConfigObj.getPrompt())) { + prompt = WorkflowUtil.renderTemplate(nodeConfigObj.getPrompt(), state.getInputs()); + } + log.info("Image prompt:{}", prompt); + // 获取工作流实例 + WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); + // 获取模型名称 + String modelName = nodeConfigObj.getModelName(); + // 获取图片大小 + String size = nodeConfigObj.getSize(); + // 获取随机数种子 + Integer seed = nodeConfigObj.getSeed(); + // 调用LLM生成图片(后续可以将图片保存到OSS中) + String imageUrl = workflowUtil.buildTextToImage(modelName, prompt, size, seed); + // 创建节点参数对象 + NodeIOData nodeIOData = NodeIOData.createByText("output", "image", imageUrl); + // 添加到输出列表以便给后续节点使用 + state.getOutputs().add(nodeIOData); + // 设置为成功状态 + state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS); + return new NodeProcessResult(); + } +} diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNodeConfig.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNodeConfig.java new file mode 100644 index 00000000..381c9016 --- /dev/null +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNodeConfig.java @@ -0,0 +1,37 @@ +package org.ruoyi.workflow.workflow.node.image; + +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 ImageNodeConfig { + + /** + * 提示词 + */ + private String prompt; + + /** + * 模型名称 + */ + @NotNull + @JsonProperty("model_name") + private String modelName; + + /** + * 图片尺寸大小 + */ + private String size; + + /** + * 随机数种子 + */ + @Min(value = 0, message = "随机数种子不能小于0") + @Max(value = 2147483647, message = "随机数种子不能大于2147483647") + private Integer seed; +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ImageModeType.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ImageModeType.java new file mode 100644 index 00000000..2faf77e7 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ImageModeType.java @@ -0,0 +1,23 @@ +package org.ruoyi.enums; + +import lombok.Getter; + +/** + * 文生图模型分类 + * + * @author Zengxb + * @date 2026-02-14 + */ +@Getter +public enum ImageModeType { + + TONGYI_WANX("Tongyiwanx", "万相"); + + private final String code; + private final String description; + + ImageModeType(String code, String description) { + this.code = code; + this.description = description; + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java index ab583520..e962e2f7 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/QianWenChatServiceImpl.java @@ -1,6 +1,5 @@ package org.ruoyi.service.chat.impl.provider; -import dev.langchain4j.agent.tool.ToolSpecification; import dev.langchain4j.agentic.AgenticServices; import dev.langchain4j.agentic.supervisor.SupervisorAgent; import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; @@ -30,6 +29,8 @@ import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; /** * qianWenAI服务调用 @@ -44,14 +45,20 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { @Autowired private McpSseConfig mcpSseConfig; - /** - * 千问开发者默认地址 - */ - private static final String QWEN_API_HOST = "https://dashscope.aliyuncs.com/api/v1"; - // 添加文档解析的前缀字段 private static final String UPLOAD_FILE_API_PREFIX = "fileid"; + // 缓存不同API Key和模型的MCP智能体实例 + private final ConcurrentHashMap supervisorCache = new ConcurrentHashMap<>(); + + // 缓存不同API Key和模型的MCP客户端实例 + private final ConcurrentHashMap mcpClientCache = new ConcurrentHashMap<>(); + + // 缓存不同API Key和模型的MCP工具提供者实例 + private final ConcurrentHashMap toolProviderCache = new ConcurrentHashMap<>(); + // 用于线程安全的锁 + private final ReentrantLock cacheLock = new ReentrantLock(); + @Override protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo,ChatRequest chatRequest) { return QwenStreamingChatModel.builder() @@ -102,17 +109,16 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { } /** - * 调用MCP服务(智能体) - * @param userMessage 用户信息 - * @param chatModelVo 模型信息 - * @return 返回LLM信息 + * 获取缓存键 */ - protected String doAgent(String userMessage,ChatModelVo chatModelVo) { - // 判断是否开启MCP服务 - if (!mcpSseConfig.isEnabled()) { - return ""; - } + private String getCacheKey(ChatModelVo chatModelVo) { + return chatModelVo.getApiKey() + ":" + chatModelVo.getModelName(); + } + /** + * 初始化MCP客户端连接 + */ + private McpClient initializeMcpClient() { // 步骤1:根据SSE对外暴露端点连接 McpTransport httpMcpTransport = new StreamableHttpMcpTransport.Builder(). url(mcpSseConfig.getUrl()). @@ -120,42 +126,74 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { build(); // 步骤2:开启客户端连接 - McpClient mcpClient = new DefaultMcpClient.Builder() + return new DefaultMcpClient.Builder() .transport(httpMcpTransport) .build(); + } - // 获取所有mcp工具 - List toolSpecifications = mcpClient.listTools(); - System.out.println(toolSpecifications); + /** + * 调用MCP服务(智能体) + * @param userMessage 用户信息 + * @param chatModelVo 模型信息 + * @return 返回LLM信息 + */ + protected String doAgent(String userMessage, ChatModelVo chatModelVo) { + // 判断是否开启MCP服务 + if (!mcpSseConfig.isEnabled()) { + return ""; + } + // 生成缓存键 + String cacheKey = getCacheKey(chatModelVo); + // 尝试从缓存获取监督智能体 + SupervisorAgent cachedSupervisor = supervisorCache.get(cacheKey); + if (cachedSupervisor != null) { + // 如果已存在缓存的监督智能体,直接使用 + return cachedSupervisor.invoke(userMessage); + } + cacheLock.lock(); + try { + // 双重检查,防止并发情况下的重复初始化 + cachedSupervisor = supervisorCache.get(cacheKey); + if (cachedSupervisor != null) { + return cachedSupervisor.invoke(userMessage); + } - // 步骤3:将mcp对象包装 - ToolProvider toolProvider = McpToolProvider.builder() - .mcpClients(List.of(mcpClient)) - .build(); + // 获取或初始化MCP客户端 + McpClient mcpClient = mcpClientCache.computeIfAbsent(cacheKey, k -> initializeMcpClient()); - // 步骤4:加载LLM模型对话 - QwenChatModel qwenChatModel = QwenChatModel.builder() - .baseUrl(QWEN_API_HOST) - .apiKey(chatModelVo.getApiKey()) - .modelName(chatModelVo.getModelName()) - .build(); + // 步骤3:将mcp对象包装 + ToolProvider toolProvider = toolProviderCache.computeIfAbsent(cacheKey, k -> McpToolProvider.builder() + .mcpClients(List.of(mcpClient)) + .build()); - // 步骤5:将MCP对象由智能体Agent管控 - McpAgent mcpAgent = AgenticServices.agentBuilder(McpAgent.class) - .chatModel(qwenChatModel) - .toolProvider(toolProvider) - .build(); + // 步骤4:加载LLM模型对话 + QwenChatModel qwenChatModel = QwenChatModel.builder() + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .build(); - // 步骤6:将所有MCP对象由超级智能体管控 - SupervisorAgent supervisor = AgenticServices - .supervisorBuilder() - .chatModel(qwenChatModel) - .subAgents(mcpAgent) - .responseStrategy(SupervisorResponseStrategy.LAST) - .build(); + // 步骤5:将MCP对象由智能体Agent管控 + McpAgent mcpAgent = AgenticServices.agentBuilder(McpAgent.class) + .chatModel(qwenChatModel) + .toolProvider(toolProvider) + .build(); - // 步骤7:调用大模型LLM - return supervisor.invoke(userMessage); + // 步骤6:将所有MCP对象由超级智能体管控 + SupervisorAgent supervisor = AgenticServices + .supervisorBuilder() + .chatModel(qwenChatModel) + .subAgents(mcpAgent) + .responseStrategy(SupervisorResponseStrategy.LAST) + .build(); + + // 缓存监督智能体 + supervisorCache.put(cacheKey, supervisor); + + // 步骤7:调用大模型LLM + return supervisor.invoke(userMessage); + } finally { + cacheLock.unlock(); + } } @Override diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/AbstractImageGenerationService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/AbstractImageGenerationService.java new file mode 100644 index 00000000..3312239e --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/AbstractImageGenerationService.java @@ -0,0 +1,43 @@ +package org.ruoyi.service.image; + +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.Service.IImageGenerationService; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.domain.entity.image.ImageContext; +import org.springframework.validation.annotation.Validated; + +@Slf4j +@Validated +public abstract class AbstractImageGenerationService implements IImageGenerationService { + + /** + * 根据文字生成图片 + * @param imageContext 文生图上下文对象 + * @return 生成的图片URL + */ + @Override + public String generateImage(ImageContext imageContext){ + // 获取模型管理视图对象 + ChatModelVo chatModelVo = imageContext.getChatModelVo(); + // 获取提示词 + String prompt = imageContext.getPrompt(); + // 获取图片尺寸大小 + String size = imageContext.getSize(); + // 获取随机数种子 + Integer seed = imageContext.getSeed(); + return doGenerateImage(chatModelVo, prompt, size, seed); + } + + /** + * 执行生成图片(钩子方法 - 子类必须实现) + * + * @param prompt 提示词 + */ + protected abstract String doGenerateImage(ChatModelVo chatModelVo, String prompt, String size, Integer seed); + + /** + * 构建具体厂商的 ImageModel(原生SDK 非langchain4j-dashscope版) + * 子类必须实现此方法,返回对应厂商的模型实例 + */ + protected abstract Object buildImageModel(ChatModelVo chatModelVo); +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/provider/TongYiWanxImageServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/provider/TongYiWanxImageServiceImpl.java new file mode 100644 index 00000000..090ca327 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/image/provider/TongYiWanxImageServiceImpl.java @@ -0,0 +1,73 @@ +package org.ruoyi.service.image.provider; + +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam; +import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult; +import com.alibaba.dashscope.exception.ApiException; +import com.alibaba.dashscope.exception.NoApiKeyException; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.core.utils.StringUtils; +import org.ruoyi.enums.ImageModeType; +import org.ruoyi.service.image.AbstractImageGenerationService; +import org.springframework.stereotype.Service; + +/** + * 万相文生图AI调用 + * + * @author Zengxb + * @date 2026/02/14 + */ +@Service +@Slf4j +public class TongYiWanxImageServiceImpl extends AbstractImageGenerationService { + + /** + * 默认图片数量(1张) + */ + private final static int IMAGE_DEFAULT_SIZE = 1; + + /** + * 默认图片分辨率(1280*1280) + */ + private final static String IMAGE_DEFAULT_RESOLUTION = "1280*1280"; + + @Override + protected String doGenerateImage(ChatModelVo chatModelVo, String prompt, String size, Integer seed) { + // 构建万相模型对象 + var param = (ImageSynthesisParam) buildImageModel(chatModelVo); + // 设置图片大小和提示词以及随机数种子 + param.setSize(StringUtils.isEmpty(size) ? IMAGE_DEFAULT_RESOLUTION : size); + param.setPrompt(prompt); + param.setSeed(seed); + // 同步调用 AI 大模型,生成图片 + var imageSynthesis = new ImageSynthesis(); + ImageSynthesisResult result; + try { + log.info("同步调用通义万相文生图接口中...."); + result = imageSynthesis.call(param); + } catch (ApiException | NoApiKeyException e) { + log.error("同步调用通义万相文生图接口失败", e); + return ""; + } + // 直接提取图片URL + var output = result.getOutput(); + var results = output.getResults(); + return results.isEmpty() ? "" : results.get(0).get("url"); + } + + @Override + protected Object buildImageModel(ChatModelVo chatModelVo) { + return ImageSynthesisParam.builder() + .prompt("") + .apiKey(chatModelVo.getApiKey()) + .model(chatModelVo.getModelName()) + .n(IMAGE_DEFAULT_SIZE) + .build(); + } + + @Override + public String getProviderName() { + return ImageModeType.TONGYI_WANX.getCode(); + } +}