diff --git a/docs/script/sql/ruoyi-ai-v3_mysql8.sql b/docs/script/sql/ruoyi-ai-v3_mysql8.sql index 26217b07..805c932d 100644 --- a/docs/script/sql/ruoyi-ai-v3_mysql8.sql +++ b/docs/script/sql/ruoyi-ai-v3_mysql8.sql @@ -3728,3 +3728,26 @@ INSERT INTO `test_tree` VALUES (12, '000000', 10, 108, 3, '子节点88', 0, 103, INSERT INTO `test_tree` VALUES (13, '000000', 10, 108, 3, '子节点99', 0, 103, '2026-02-03 05:14:54', 1, NULL, NULL, 0); SET FOREIGN_KEY_CHECKS = 1; + +INSERT INTO `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 `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 `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 `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 `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 `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 `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, '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 dbd40c94..372c2770 100644 --- a/pom.xml +++ b/pom.xml @@ -56,11 +56,10 @@ 1.11.0 1.11.0-beta19 + 1.1.0-beta7 1.5.3 1.19.6 1.0.7 - - 1.27.1 1.1.0 @@ -397,13 +396,6 @@ ${revision} - - - org.ruoyi - ruoyi-mcp - ${revision} - - com.github.binarywang @@ -418,13 +410,6 @@ ${jackson-dataformat-xml.version} - - - org.apache.commons - commons-compress - ${commons-compress.version} - - diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 153e82da..4675462c 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -110,12 +110,6 @@ ruoyi-aiflow - - - org.ruoyi - ruoyi-mcp - - de.codecentric spring-boot-admin-starter-client diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index 20e1ee87..03b7bc24 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -58,9 +58,16 @@ spring: driverClassName: com.mysql.cj.jdbc.Driver # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562 # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题) - url: jdbc:mysql://127.0.0.1:3306/ruoyi-ai?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + url: jdbc:mysql://127.0.0.1:3306/ruoyi_ai_agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true username: root password: root + agent: + url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true + # url: jdbc:mysql://localhost:3306/agent_db + username: root + password: root + driverClassName: com.mysql.cj.jdbc.Driver + hikari: # 最大连接池数量 maxPoolSize: 20 @@ -77,17 +84,13 @@ spring: # 多久检查一次连接的活性 keepaliveTime: 30000 -agent: - mysql: - url: jdbc:mysql://localhost:3306/ruoyi-ai-agent?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true - username: root - password: root + --- # 上传文件地址 sys: upload: path: D:\\DownLoad - + --- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉) spring.data: redis: @@ -265,3 +268,4 @@ justauth: client-secret: 1f7d08**********5b7**********29e redirect-uri: ${justauth.address}/social-callback?source=gitea +AGENT_ALLOWED_TABLES: "abtest_rule,abtest_project,agent_ban_log,agent_ban_logs,agent_install_sub_task,agent_install_sum_task,agent_install_task" \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index ca371e3d..2a17ad30 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -216,8 +216,6 @@ springdoc: packages-to-scan: org.ruoyi.generator - group: 5.工作流模块 packages-to-scan: org.ruoyi.workflow - - group: 6.MCP模块 - packages-to-scan: org.ruoyi.mcp # 防止XSS攻击 xss: @@ -359,14 +357,3 @@ knowledge: cache-enabled: true # 缓存过期时间(分钟) cache-expire-minutes: 60 - ---- # MCP 模块配置 -app: - mcp: - client: - # 请求超时时间(秒) - request-timeout: 30 - # 连接超时时间(秒) - connection-timeout: 10 - # 最大重试次数 - max-retries: 3 diff --git a/ruoyi-common/ruoyi-common-chat/pom.xml b/ruoyi-common/ruoyi-common-chat/pom.xml index 0db3c41c..06e38185 100644 --- a/ruoyi-common/ruoyi-common-chat/pom.xml +++ b/ruoyi-common/ruoyi-common-chat/pom.xml @@ -62,6 +62,12 @@ org.ruoyi ruoyi-common-tenant + + + io.swagger.core.v3 + swagger-annotations + ${swagger-annotations.version} + diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/base/ThreadContext.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/base/ThreadContext.java similarity index 94% rename from ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/base/ThreadContext.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/base/ThreadContext.java index d74eb104..7605307d 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/base/ThreadContext.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/base/ThreadContext.java @@ -1,14 +1,14 @@ -package org.ruoyi.workflow.base; +package org.ruoyi.common.chat.base; import cn.dev33.satoken.stp.StpUtil; import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.chat.entity.User; +import org.ruoyi.common.chat.enums.UserStatusEnum; import org.ruoyi.common.core.domain.model.LoginUser; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.common.satoken.utils.LoginHelper; -import org.ruoyi.workflow.entity.User; -import org.ruoyi.workflow.enums.UserStatusEnum; -import static org.ruoyi.workflow.enums.ErrorEnum.A_USER_NOT_FOUND; +import static org.ruoyi.common.chat.enums.ErrorEnum.A_USER_NOT_FOUND; /** * 线程上下文适配器,统一接入 Sa-Token 登录态。 diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatMessageBo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatMessageBo.java similarity index 90% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatMessageBo.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatMessageBo.java index 9d44fcaa..2cbb051b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/bo/chat/ChatMessageBo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatMessageBo.java @@ -1,13 +1,14 @@ -package org.ruoyi.domain.bo.chat; +package org.ruoyi.common.chat.domain.bo.chat; -import org.ruoyi.common.core.validate.AddGroup; -import org.ruoyi.common.core.validate.EditGroup; -import org.ruoyi.domain.entity.chat.ChatMessage; -import org.ruoyi.common.mybatis.core.domain.BaseEntity; import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import jakarta.validation.constraints.*; +import org.ruoyi.common.chat.entity.chat.ChatMessage; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import org.ruoyi.common.mybatis.core.domain.BaseEntity; + /** * 聊天消息业务对象 chat_message diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatModelBo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatModelBo.java index 8807503c..4d97dd05 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatModelBo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/bo/chat/ChatModelBo.java @@ -4,7 +4,7 @@ import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotNull; import lombok.Data; import lombok.EqualsAndHashCode; -import org.ruoyi.common.chat.domain.entity.chat.ChatModel; +import org.ruoyi.common.chat.entity.chat.ChatModel; import org.ruoyi.common.core.validate.EditGroup; import org.ruoyi.common.mybatis.core.domain.BaseEntity; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java index 1b36ae42..8d13339e 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ChatRequest.java @@ -21,6 +21,21 @@ public class ChatRequest { @NotEmpty(message = "传入的模型不能为空") private String model; + /** + * 工作流请求体 + */ + private WorkFlowRunner workFlowRunner; + + /** + * 人机交互信息体 + */ + private ReSumeRunner reSumeRunner; + + /** + * 是否启用工作流 + */ + private Boolean enableWorkFlow; + /** * 会话id */ @@ -41,6 +56,11 @@ public class ChatRequest { */ private Long uuid; + /** + * 是否为人机交互用户继续输入 + */ + private Boolean isResume; + /** * 是否启用深度思考 */ diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ReSumeRunner.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ReSumeRunner.java new file mode 100644 index 00000000..65b39fd0 --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/ReSumeRunner.java @@ -0,0 +1,19 @@ +package org.ruoyi.common.chat.domain.dto.request; + +import lombok.Data; + +/** + * 人机交互输入信息 + */ +@Data +public class ReSumeRunner { + /** + * 运行节点UUID + */ + private String runtimeUuid; + + /** + * 人机交互用户输入信息 + */ + private String feedbackContent; +} diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/WorkFlowRunner.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/WorkFlowRunner.java new file mode 100644 index 00000000..fff727ec --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/dto/request/WorkFlowRunner.java @@ -0,0 +1,15 @@ +package org.ruoyi.common.chat.domain.dto.request; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; + +import java.util.List; + +/** + * 工作流请求体信息 + */ +@Data +public class WorkFlowRunner { + private List inputs; + private String uuid; +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatMessageVo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatMessageVo.java similarity index 94% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatMessageVo.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatMessageVo.java index 7a548865..17f4d707 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/chat/ChatMessageVo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatMessageVo.java @@ -1,18 +1,17 @@ -package org.ruoyi.domain.vo.chat; +package org.ruoyi.common.chat.domain.vo.chat; -import org.ruoyi.domain.entity.chat.ChatMessage; import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; -import org.ruoyi.common.excel.annotation.ExcelDictFormat; -import org.ruoyi.common.excel.convert.ExcelDictConvert; import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; +import org.ruoyi.common.chat.entity.chat.ChatMessage; +import org.ruoyi.common.excel.annotation.ExcelDictFormat; +import org.ruoyi.common.excel.convert.ExcelDictConvert; import java.io.Serial; import java.io.Serializable; - /** * 聊天消息视图对象 chat_message * diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java index 77ba3328..f79f9f23 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/vo/chat/ChatModelVo.java @@ -5,7 +5,7 @@ import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; import lombok.Data; -import org.ruoyi.common.chat.domain.entity.chat.ChatModel; +import org.ruoyi.common.chat.entity.chat.ChatModel; import java.io.Serial; import java.io.Serializable; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/BaseEntity.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/BaseEntity.java similarity index 95% rename from ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/BaseEntity.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/BaseEntity.java index b657eb16..e9630ff2 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/BaseEntity.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/BaseEntity.java @@ -1,4 +1,4 @@ -package org.ruoyi.workflow.entity; +package org.ruoyi.common.chat.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/User.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/User.java similarity index 95% rename from ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/User.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/User.java index 20ac4dc0..dadedf9a 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/User.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/User.java @@ -1,11 +1,11 @@ -package org.ruoyi.workflow.entity; +package org.ruoyi.common.chat.entity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; -import org.ruoyi.workflow.enums.UserStatusEnum; +import org.ruoyi.common.chat.enums.UserStatusEnum; import java.time.LocalDateTime; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatContext.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatContext.java similarity index 96% rename from ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatContext.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatContext.java index 13a5b4b9..f90abd82 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatContext.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatContext.java @@ -1,4 +1,4 @@ -package org.ruoyi.common.chat.domain.entity.chat; +package org.ruoyi.common.chat.entity.chat; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import jakarta.validation.constraints.NotNull; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatMessage.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatMessage.java similarity index 87% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatMessage.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatMessage.java index 905f74fb..c0055bd4 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/entity/chat/ChatMessage.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatMessage.java @@ -1,9 +1,10 @@ -package org.ruoyi.domain.entity.chat; +package org.ruoyi.common.chat.entity.chat; -import org.ruoyi.common.tenant.core.TenantEntity; -import com.baomidou.mybatisplus.annotation.*; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.tenant.core.TenantEntity; import java.io.Serial; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatModel.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatModel.java similarity index 96% rename from ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatModel.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatModel.java index 9edee32f..4fc63bfd 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/domain/entity/chat/ChatModel.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/chat/ChatModel.java @@ -1,4 +1,4 @@ -package org.ruoyi.common.chat.domain.entity.chat; +package org.ruoyi.common.chat.entity.chat; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/image/ImageContext.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/image/ImageContext.java new file mode 100644 index 00000000..60be2739 --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/entity/image/ImageContext.java @@ -0,0 +1,45 @@ +package org.ruoyi.common.chat.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-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/BaseEnum.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/BaseEnum.java similarity index 84% rename from ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/BaseEnum.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/BaseEnum.java index 66a2fb63..902adf45 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/BaseEnum.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/BaseEnum.java @@ -1,4 +1,4 @@ -package org.ruoyi.workflow.enums; +package org.ruoyi.common.chat.enums; import com.baomidou.mybatisplus.annotation.IEnum; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/ErrorEnum.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/ErrorEnum.java similarity index 99% rename from ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/ErrorEnum.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/ErrorEnum.java index f9de801e..06095cec 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/ErrorEnum.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/ErrorEnum.java @@ -1,4 +1,4 @@ -package org.ruoyi.workflow.enums; +package org.ruoyi.common.chat.enums; import lombok.Getter; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/RoleType.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/RoleType.java similarity index 84% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/RoleType.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/RoleType.java index cb550975..daaaaaa2 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/RoleType.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/RoleType.java @@ -1,4 +1,4 @@ -package org.ruoyi.enums; +package org.ruoyi.common.chat.enums; import lombok.AllArgsConstructor; import lombok.Getter; @@ -18,6 +18,7 @@ public enum RoleType { ASSISTANT("assistant"), FUNCTION("function"), TOOL("tool"), + WORKFLOW("workFlow") ; private final String name; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/UserStatusEnum.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/UserStatusEnum.java similarity index 93% rename from ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/UserStatusEnum.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/UserStatusEnum.java index 2be9a950..2b2cca43 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/UserStatusEnum.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/enums/UserStatusEnum.java @@ -1,4 +1,4 @@ -package org.ruoyi.workflow.enums; +package org.ruoyi.common.chat.enums; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java index ee884357..6e91254a 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/factory/ChatServiceFactory.java @@ -1,6 +1,6 @@ package org.ruoyi.common.chat.factory; -import org.ruoyi.common.chat.Service.IChatService; +import org.ruoyi.common.chat.service.chat.IChatService; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; 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..52d3731b --- /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.image.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-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatModelService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatModelService.java similarity index 97% rename from ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatModelService.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatModelService.java index edb7ed52..3c362a52 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatModelService.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatModelService.java @@ -1,4 +1,4 @@ -package org.ruoyi.common.chat.Service; +package org.ruoyi.common.chat.service.chat; import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatService.java similarity index 79% rename from ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatService.java index 01a02567..de9f13ca 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/Service/IChatService.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chat/IChatService.java @@ -1,7 +1,7 @@ -package org.ruoyi.common.chat.Service; +package org.ruoyi.common.chat.service.chat; import jakarta.validation.Valid; -import org.ruoyi.common.chat.domain.entity.chat.ChatContext; +import org.ruoyi.common.chat.entity.chat.ChatContext; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; /** diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/AbstractChatMessageService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/AbstractChatMessageService.java new file mode 100644 index 00000000..687605eb --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/AbstractChatMessageService.java @@ -0,0 +1,59 @@ +package org.ruoyi.common.chat.service.chatMessage; + +import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; +import org.ruoyi.common.chat.domain.dto.request.ChatRequest; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * 聊天信息抽象基类 - 保存聊天信息 + * + * @author Zengxb + * @date 2026-02-24 + */ +public abstract class AbstractChatMessageService { + + /** + * 创建日志对象 + */ + Logger log = LoggerFactory.getLogger(AbstractChatMessageService.class); + + @Autowired + private IChatMessageService chatMessageService; + + /** + * 保存聊天信息 + */ + public void saveChatMessage(ChatRequest chatRequest, Long userId, String content, String role, ChatModelVo chatModelVo){ + try { + // 验证必要的上下文信息 + if (chatRequest == null || userId == null) { + log.warn("缺少必要的聊天上下文信息,无法保存消息"); + return; + } + + // 创建ChatMessageBo对象 + ChatMessageBo messageBO = new ChatMessageBo(); + messageBO.setUserId(userId); + messageBO.setSessionId(chatRequest.getSessionId()); + messageBO.setContent(content); + messageBO.setRole(role); + messageBO.setModelName(chatRequest.getModel()); + messageBO.setBillingType(chatModelVo.getModelType()); + messageBO.setRemark(null); + + chatMessageService.insertByBo(messageBO); + } catch (Exception e) { + log.error("保存{}聊天消息时出错: {}", getProviderName(), e.getMessage(), e); + } + } + + /** + * 获取服务提供商名称 + */ + protected String getProviderName(){ + return "默认工作流大模型"; + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/IChatMessageService.java similarity index 92% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java rename to ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/IChatMessageService.java index 3289b5f2..98e31a40 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/IChatMessageService.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/chatMessage/IChatMessageService.java @@ -1,10 +1,10 @@ -package org.ruoyi.service.chat; +package org.ruoyi.common.chat.service.chatMessage; +import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; +import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo; import org.ruoyi.common.mybatis.core.page.PageQuery; import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.domain.bo.chat.ChatMessageBo; -import org.ruoyi.domain.vo.chat.ChatMessageVo; import java.util.Collection; import java.util.List; diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/image/IImageGenerationService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/image/IImageGenerationService.java new file mode 100644 index 00000000..3be0a84f --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/image/IImageGenerationService.java @@ -0,0 +1,20 @@ +package org.ruoyi.common.chat.service.image; + +import jakarta.validation.Valid; +import org.ruoyi.common.chat.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/service/workFlow/IWorkFlowStarterService.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/workFlow/IWorkFlowStarterService.java new file mode 100644 index 00000000..4001b67f --- /dev/null +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/service/workFlow/IWorkFlowStarterService.java @@ -0,0 +1,33 @@ +package org.ruoyi.common.chat.service.workFlow; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.ruoyi.common.chat.entity.User; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.List; + +/** + * 工作流启动Service接口 + * + * @author Zengxb + * @date 2026-02-24 + */ +public interface IWorkFlowStarterService { + + /** + * 启动工作流 + * @param user 用户 + * @param workflowUuid 工作流UUID + * @param userInputs 用户输入信息 + * @return 流式输出结果 + */ + SseEmitter streaming(User user, String workflowUuid, List userInputs, Long sessionId); + + /** + * 恢复工作流 + * @param runtimeUuid 运行时UUID + * @param userInput 用户输入 + * @param sseEmitter SSE连接对象 + */ + void resumeFlow(String runtimeUuid, String userInput, SseEmitter sseEmitter); +} diff --git a/ruoyi-common/ruoyi-common-excel/pom.xml b/ruoyi-common/ruoyi-common-excel/pom.xml index e48189f9..51a34a08 100644 --- a/ruoyi-common/ruoyi-common-excel/pom.xml +++ b/ruoyi-common/ruoyi-common-excel/pom.xml @@ -25,12 +25,6 @@ cn.idev.excel fastexcel - - - - org.apache.commons - commons-compress - diff --git a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java index d111c875..4e7a5481 100644 --- a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java +++ b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/core/SseEmitterManager.java @@ -65,7 +65,7 @@ public class SseEmitterManager { emitter.onCompletion(() -> { SseEmitter remove = emitters.remove(token); if (remove != null) { - remove.complete(); +// remove.complete(); } }); emitter.onTimeout(() -> { diff --git a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java index b4c224d5..ec4e95ca 100644 --- a/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java +++ b/ruoyi-common/ruoyi-common-sse/src/main/java/org/ruoyi/common/sse/utils/SseMessageUtils.java @@ -6,6 +6,9 @@ import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.sse.core.SseEmitterManager; import org.ruoyi.common.sse.dto.SseMessageDto; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; /** * SSE工具类 diff --git a/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java b/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java index 3d81e0d0..03a682db 100644 --- a/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java +++ b/ruoyi-common/ruoyi-common-web/src/main/java/org/ruoyi/common/web/handler/GlobalExceptionHandler.java @@ -121,23 +121,16 @@ public class GlobalExceptionHandler { /** * 拦截未知的运行时异常 - * 注意:对于文件下载/导出等场景,IOException 可能是正常流程的一部分, - * 需要排除 export/download 等路径,避免干扰文件导出 */ + @ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR) @ExceptionHandler(IOException.class) - public R handleIoException(IOException e, HttpServletRequest request) { + public void handleIoException(IOException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); if (requestURI.contains("sse")) { // sse 经常性连接中断 例如关闭浏览器 直接屏蔽 - return null; - } - // 排除文件下载/导出相关的 IOException,让异常正常传播以便上层处理 - if (requestURI.contains("/export") || requestURI.contains("/download")) { - // 重新抛出,让调用方处理 - throw new RuntimeException("文件导出/下载IO异常: " + e.getMessage(), e); + return; } log.error("请求地址'{}',连接中断", requestURI, e); - return R.fail(e.getMessage()); } /** @@ -153,13 +146,6 @@ public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public R handleRuntimeException(RuntimeException e, HttpServletRequest request) { String requestURI = request.getRequestURI(); - // 对于文件导出相关异常,不进行封装处理,让原始异常信息传播 - Throwable cause = e.getCause(); - if (requestURI.contains("/export") || requestURI.contains("/download")) { - log.error("请求地址'{}',文件导出/下载异常.", requestURI, e); - // 对于文件导出,直接返回异常信息,不进行额外封装 - return R.fail(cause != null ? cause.getMessage() : e.getMessage()); - } log.error("请求地址'{}',发生未知异常.", requestURI, e); return R.fail(e.getMessage()); } diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index 73fe7257..5db99df1 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -15,7 +15,6 @@ ruoyi-demo ruoyi-generator ruoyi-job - ruoyi-mcp ruoyi-system ruoyi-wechat ruoyi-workflow diff --git a/ruoyi-modules/ruoyi-aiflow/pom.xml b/ruoyi-modules/ruoyi-aiflow/pom.xml index 34d46d62..d7a147c8 100644 --- a/ruoyi-modules/ruoyi-aiflow/pom.xml +++ b/ruoyi-modules/ruoyi-aiflow/pom.xml @@ -81,12 +81,6 @@ - - io.swagger.core.v3 - swagger-annotations - ${swagger-annotations.version} - - com.google.api-client google-api-client diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/base/BaseResponse.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/base/BaseResponse.java index d6220abd..c84389bc 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/base/BaseResponse.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/base/BaseResponse.java @@ -1,7 +1,7 @@ package org.ruoyi.workflow.base; import lombok.Data; -import org.ruoyi.workflow.enums.ErrorEnum; +import org.ruoyi.common.chat.enums.ErrorEnum; import java.io.Serializable; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowController.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowController.java index d92f79d8..61037520 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowController.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowController.java @@ -5,8 +5,8 @@ import io.swagger.v3.oas.annotations.Operation; import jakarta.annotation.Resource; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; +import org.ruoyi.common.chat.base.ThreadContext; import org.ruoyi.common.core.domain.R; -import org.ruoyi.workflow.base.ThreadContext; import org.ruoyi.workflow.dto.workflow.*; import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.service.WorkflowComponentService; @@ -72,7 +72,7 @@ public class WorkflowController { @Operation(summary = "流式响应") @PostMapping(value = "/run", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter sseAsk(@RequestBody WorkflowRunReq runReq) { - return workflowStarter.streaming(ThreadContext.getCurrentUser(), runReq.getUuid(), runReq.getInputs()); + return workflowStarter.streaming(ThreadContext.getCurrentUser(), runReq.getUuid(), runReq.getInputs(),runReq.getSessionId()); } @GetMapping("/mine/search") diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowRuntimeController.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowRuntimeController.java index 34fc0065..2d384aaa 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowRuntimeController.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/controller/WorkflowRuntimeController.java @@ -30,7 +30,7 @@ public class WorkflowRuntimeController { @Operation(summary = "接收用户输入以继续执行剩余流程") @PostMapping(value = "/resume/{runtimeUuid}") public R resume(@PathVariable String runtimeUuid, @RequestBody WorkflowResumeReq resumeReq) { - workflowStarter.resumeFlow(runtimeUuid, resumeReq.getFeedbackContent()); + workflowStarter.resumeFlow(runtimeUuid, resumeReq.getFeedbackContent(), resumeReq.getSseEmitter()); return R.ok(); } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResumeReq.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResumeReq.java index 27680932..c1b64829 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResumeReq.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResumeReq.java @@ -1,8 +1,10 @@ package org.ruoyi.workflow.dto.workflow; import lombok.Data; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @Data public class WorkflowResumeReq { private String feedbackContent; + private SseEmitter sseEmitter; } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowRunReq.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowRunReq.java index 54a32a03..40ca0fcc 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowRunReq.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowRunReq.java @@ -9,6 +9,7 @@ import java.util.List; public class WorkflowRunReq { private List inputs; private String uuid; + private Long sessionId; } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/Workflow.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/Workflow.java index 4334b069..4a43e674 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/Workflow.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/Workflow.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.chat.entity.BaseEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowComponent.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowComponent.java index d75e92c8..c60b06bc 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowComponent.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowComponent.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.chat.entity.BaseEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowEdge.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowEdge.java index 0a9bffda..6c44c70d 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowEdge.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowEdge.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.chat.entity.BaseEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowNode.java index cc15504a..e4261aab 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowNode.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.chat.entity.BaseEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntime.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntime.java index 557de3f4..dada5dea 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntime.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntime.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.chat.entity.BaseEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntimeNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntimeNode.java index 271d9eb7..a472b5a1 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntimeNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntimeNode.java @@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.EqualsAndHashCode; +import org.ruoyi.common.chat.entity.BaseEntity; import java.io.Serial; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/AiModelStatus.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/AiModelStatus.java index 0dec4a94..f49c3f88 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/AiModelStatus.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/AiModelStatus.java @@ -2,6 +2,7 @@ package org.ruoyi.workflow.enums; import lombok.AllArgsConstructor; import lombok.Getter; +import org.ruoyi.common.chat.enums.BaseEnum; @Getter @AllArgsConstructor diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/WfIODataTypeEnum.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/WfIODataTypeEnum.java index 8f8989c6..2ca638dc 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/WfIODataTypeEnum.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/enums/WfIODataTypeEnum.java @@ -2,6 +2,7 @@ package org.ruoyi.workflow.enums; import lombok.AllArgsConstructor; import lombok.Getter; +import org.ruoyi.common.chat.enums.BaseEnum; import java.util.Arrays; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/helper/SSEEmitterHelper.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/helper/SSEEmitterHelper.java index 4d432c07..61b3dda0 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/helper/SSEEmitterHelper.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/helper/SSEEmitterHelper.java @@ -5,9 +5,9 @@ import com.google.common.cache.CacheBuilder; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.chat.entity.User; import org.ruoyi.workflow.cosntant.AdiConstant; import org.ruoyi.workflow.cosntant.RedisKeyConstant; -import org.ruoyi.workflow.entity.User; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowComponentService.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowComponentService.java index 04e3b03c..c1790738 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowComponentService.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowComponentService.java @@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.workflow.dto.workflow.WfComponentReq; import org.ruoyi.workflow.dto.workflow.WfComponentSearchReq; import org.ruoyi.workflow.entity.WorkflowComponent; -import org.ruoyi.workflow.enums.ErrorEnum; import org.ruoyi.workflow.mapper.WorkflowComponentMapper; import org.ruoyi.workflow.util.PrivilegeUtil; import org.ruoyi.workflow.util.UuidUtil; @@ -26,7 +26,7 @@ import java.util.List; import static org.ruoyi.workflow.cosntant.RedisKeyConstant.WORKFLOW_COMPONENTS; import static org.ruoyi.workflow.cosntant.RedisKeyConstant.WORKFLOW_COMPONENT_START_KEY; -import static org.ruoyi.workflow.enums.ErrorEnum.C_WF_COMPONENT_DELETED_FAIL_BY_USED; +import static org.ruoyi.common.chat.enums.ErrorEnum.C_WF_COMPONENT_DELETED_FAIL_BY_USED; @Slf4j @Service diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowEdgeService.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowEdgeService.java index 63cc3530..624482b8 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowEdgeService.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowEdgeService.java @@ -5,10 +5,10 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.workflow.dto.workflow.WfEdgeReq; import org.ruoyi.workflow.entity.WorkflowEdge; -import org.ruoyi.workflow.enums.ErrorEnum; import org.ruoyi.workflow.mapper.WorkflowEdgeMapper; import org.ruoyi.workflow.util.MPPageUtil; import org.ruoyi.workflow.util.UuidUtil; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowNodeService.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowNodeService.java index e99bd8fd..f56ac479 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowNodeService.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowNodeService.java @@ -6,12 +6,12 @@ import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.workflow.dto.workflow.WfNodeDto; import org.ruoyi.workflow.entity.Workflow; import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.entity.WorkflowNode; -import org.ruoyi.workflow.enums.ErrorEnum; import org.ruoyi.workflow.enums.WfIODataTypeEnum; import org.ruoyi.workflow.mapper.WorkflowNodeMapper; import org.ruoyi.workflow.util.JsonUtil; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeNodeService.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeNodeService.java index 43c73207..63aa6d4b 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeNodeService.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeNodeService.java @@ -4,9 +4,9 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.workflow.base.ThreadContext; +import org.ruoyi.common.chat.base.ThreadContext; +import org.ruoyi.common.chat.entity.User; import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto; -import org.ruoyi.workflow.entity.User; import org.ruoyi.workflow.entity.WorkflowRuntimeNode; import org.ruoyi.workflow.mapper.WorkflowRuntimeNodeMapper; import org.ruoyi.workflow.util.JsonUtil; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeService.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeService.java index 330974a9..ddd3f3fb 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeService.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeService.java @@ -7,13 +7,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; -import org.ruoyi.workflow.base.ThreadContext; +import org.ruoyi.common.chat.base.ThreadContext; +import org.ruoyi.common.chat.entity.User; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto; import org.ruoyi.workflow.dto.workflow.WfRuntimeResp; -import org.ruoyi.workflow.entity.User; import org.ruoyi.workflow.entity.Workflow; import org.ruoyi.workflow.entity.WorkflowRuntime; -import org.ruoyi.workflow.enums.ErrorEnum; import org.ruoyi.workflow.mapper.WorkflowRunMapper; import org.ruoyi.workflow.util.JsonUtil; import org.ruoyi.workflow.util.MPPageUtil; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowService.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowService.java index 439ffba4..926dc12b 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowService.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/service/WorkflowService.java @@ -6,15 +6,15 @@ import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.chat.base.ThreadContext; +import org.ruoyi.common.chat.entity.User; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; -import org.ruoyi.workflow.base.ThreadContext; import org.ruoyi.workflow.dto.workflow.WfEdgeReq; import org.ruoyi.workflow.dto.workflow.WfNodeDto; import org.ruoyi.workflow.dto.workflow.WorkflowResp; import org.ruoyi.workflow.dto.workflow.WorkflowUpdateReq; -import org.ruoyi.workflow.entity.User; import org.ruoyi.workflow.entity.Workflow; -import org.ruoyi.workflow.enums.ErrorEnum; import org.ruoyi.workflow.mapper.WorkflowMapper; import org.ruoyi.workflow.util.MPPageUtil; import org.ruoyi.workflow.util.PrivilegeUtil; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/PrivilegeUtil.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/PrivilegeUtil.java index 7ad0dc7a..7480cbfe 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/PrivilegeUtil.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/PrivilegeUtil.java @@ -1,9 +1,9 @@ package org.ruoyi.workflow.util; import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; +import org.ruoyi.common.chat.base.ThreadContext; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; -import org.ruoyi.workflow.base.ThreadContext; -import org.ruoyi.workflow.enums.ErrorEnum; import static org.ruoyi.workflow.cosntant.AdiConstant.*; 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 new file mode 100644 index 00000000..9fef1230 --- /dev/null +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/util/WorkflowMessageUtil.java @@ -0,0 +1,82 @@ +package org.ruoyi.workflow.util; + +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; + +/** + * 工作流消息工具类 + * + * @author Zengxb + * @date 2026-02-26 + */ +@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 工作流实例状态 + * @param message 消息 + */ + public static void saveWorkflowMessage(WfState wfState, String message) { + Long sessionId = wfState.getSessionId(); + Long userId = wfState.getUserId(); + + if (sessionId != null && userId != null) { + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setSessionId(sessionId); + WorkflowUtil workflowUtil = SpringUtils.getBean(WorkflowUtil.class); + workflowUtil.saveChatMessage(chatRequest, userId, message, RoleType.WORKFLOW.getName(), new ChatModelVo()); + } + } + + /** + * 发送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/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/WfNodeIODataUtil.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeIODataUtil.java index 61828568..9bedc711 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeIODataUtil.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfNodeIODataUtil.java @@ -4,8 +4,8 @@ import cn.hutool.core.collection.CollUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.commons.collections4.CollectionUtils; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; -import org.ruoyi.workflow.enums.ErrorEnum; import org.ruoyi.workflow.enums.WfIODataTypeEnum; import org.ruoyi.workflow.util.JsonUtil; import org.ruoyi.workflow.workflow.data.NodeIOData; diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java index 97962a71..d0595803 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WfState.java @@ -4,8 +4,8 @@ import lombok.Getter; import lombok.Setter; import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator; import org.bsc.langgraph4j.state.AgentState; +import org.ruoyi.common.chat.entity.User; import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto; -import org.ruoyi.workflow.entity.User; import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.workflow.data.NodeIOData; import org.ruoyi.workflow.workflow.node.AbstractWfNode; @@ -28,6 +28,7 @@ public class WfState { private Long userId; private String tokenValue; private SseEmitter sseEmitter; + private Long sessionId; //Source node uuid => target node uuid list private Map> edges = new HashMap<>(); @@ -59,13 +60,14 @@ public class WfState { */ private Set interruptNodes = new HashSet<>(); - public WfState(User user, List input, String uuid, Long userId, String tokenValue, SseEmitter sseEmitter) { + public WfState(User user, List input, String uuid, Long userId, String tokenValue, SseEmitter sseEmitter, Long sessionId) { this.input = input; this.user = user; this.uuid = uuid; this.userId = userId; this.tokenValue = tokenValue; this.sseEmitter = sseEmitter; + this.sessionId = sessionId; } /** 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 f84b2164..87ab1baf 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 @@ -3,6 +3,8 @@ package org.ruoyi.workflow.workflow; import cn.hutool.core.collection.CollStreamUtil; import cn.hutool.core.collection.CollUtil; import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -13,20 +15,23 @@ import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator; import org.bsc.langgraph4j.state.AgentState; import org.bsc.langgraph4j.state.StateSnapshot; import org.bsc.langgraph4j.streaming.StreamingOutput; +import org.ruoyi.common.chat.entity.User; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.workflow.base.NodeInputConfigTypeHandler; import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto; import org.ruoyi.workflow.dto.workflow.WfRuntimeResp; import org.ruoyi.workflow.entity.*; -import org.ruoyi.workflow.enums.ErrorEnum; import org.ruoyi.workflow.helper.SSEEmitterHelper; import org.ruoyi.workflow.service.WorkflowRuntimeNodeService; import org.ruoyi.workflow.service.WorkflowRuntimeService; import org.ruoyi.workflow.util.JsonUtil; +import org.ruoyi.workflow.util.WorkflowMessageUtil; 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.*; @@ -34,7 +39,7 @@ import java.util.function.Function; import static org.bsc.langgraph4j.StateGraph.END; import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*; -import static org.ruoyi.workflow.enums.ErrorEnum.*; +import static org.ruoyi.common.chat.enums.ErrorEnum.*; @Slf4j public class WorkflowEngine { @@ -45,9 +50,12 @@ public class WorkflowEngine { private final SSEEmitterHelper sseEmitterHelper; private final WorkflowRuntimeService workflowRuntimeService; private final WorkflowRuntimeNodeService workflowRuntimeNodeService; + @Getter private CompiledGraph app; + @Setter private SseEmitter sseEmitter; private User user; + @Getter private WfState wfState; private WfRuntimeResp wfRuntimeResp; @@ -68,7 +76,7 @@ public class WorkflowEngine { this.workflowRuntimeNodeService = workflowRuntimeNodeService; } - public void run(User user, List userInputs, SseEmitter sseEmitter, Long userId, String tokenValue) { + public void run(User user, List userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId) { this.user = user; this.sseEmitter = sseEmitter; log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); @@ -86,7 +94,7 @@ public class WorkflowEngine { Pair> startAndEnds = findStartAndEndNode(); WorkflowNode startNode = startAndEnds.getLeft(); List wfInputs = getAndCheckUserInput(userInputs, startNode); - this.wfState = new WfState(user, wfInputs, runtimeUuid,userId, tokenValue, sseEmitter); + this.wfState = new WfState(user, wfInputs, runtimeUuid,userId, tokenValue, sseEmitter, sessionId); workflowRuntimeService.updateInput(this.wfRuntimeResp.getId(), wfState); @@ -119,16 +127,31 @@ 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信息记录中(对话使用) + WorkflowMessageUtil.saveWorkflowMessage(wfState, intTip); InterruptedFlow.RUNTIME_TO_GRAPH.put(wfState.getUuid(), this); //更新状�? wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_WAITING_INPUT); 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()); } } @@ -155,10 +178,14 @@ 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; + // 保存会话信息且发送驱动消息事件 + WorkflowMessageUtil.saveWorkflowMessage(wfState, errorMsg); sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg); workflowRuntimeService.updateStatus(wfRuntimeResp.getId(), WORKFLOW_PROCESS_STATUS_FAIL, errorMsg); } @@ -241,6 +268,7 @@ public class WorkflowEngine { Map strMap = new HashMap<>(); strMap.put("ck", chunk); // SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", strMap.toString()); + SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk); } else { AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream() @@ -349,8 +377,4 @@ public class WorkflowEngine { return Pair.of(startNode, endNodes); } - - public CompiledGraph getApp() { - return app; - } } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowGraphBuilder.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowGraphBuilder.java index aa11b4b5..55241376 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowGraphBuilder.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/WorkflowGraphBuilder.java @@ -5,11 +5,11 @@ import org.apache.commons.lang3.StringUtils; import org.bsc.langgraph4j.GraphStateException; import org.bsc.langgraph4j.StateGraph; import org.bsc.langgraph4j.serializer.std.ObjectStreamStateSerializer; +import org.ruoyi.common.chat.enums.ErrorEnum; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.workflow.entity.WorkflowComponent; import org.ruoyi.workflow.entity.WorkflowEdge; import org.ruoyi.workflow.entity.WorkflowNode; -import org.ruoyi.workflow.enums.ErrorEnum; import java.util.*; import java.util.function.Function; 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 4cb377a1..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 @@ -4,6 +4,8 @@ import cn.dev33.satoken.stp.StpUtil; import com.fasterxml.jackson.databind.node.ObjectNode; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.entity.User; +import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService; import org.ruoyi.common.core.exception.base.BaseException; import org.ruoyi.common.satoken.utils.LoginHelper; import org.ruoyi.common.sse.core.SseEmitterManager; @@ -12,16 +14,16 @@ import org.ruoyi.workflow.helper.SSEEmitterHelper; import org.ruoyi.workflow.service.*; import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.List; -import static org.ruoyi.workflow.enums.ErrorEnum.*; +import static org.ruoyi.common.chat.enums.ErrorEnum.*; @Slf4j -@Component -public class WorkflowStarter { +@Service +public class WorkflowStarter implements IWorkFlowStarterService { @Lazy @Resource @@ -51,8 +53,7 @@ public class WorkflowStarter { @Resource private SseEmitterManager sseEmitterManager; - - public SseEmitter streaming(User user, String workflowUuid, List userInputs) { + public SseEmitter streaming(User user, String workflowUuid, List userInputs, Long sessionId) { // 获取用户ID Long userId = LoginHelper.getUserId(); // 获取登录Token @@ -70,12 +71,12 @@ public class WorkflowStarter { sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo()); return sseEmitter; } - self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue); + self.asyncRun(user, workflow, userInputs, sseEmitter, userId, tokenValue, sessionId); return sseEmitter; } @Async - public void asyncRun(User user, Workflow workflow, List userInputs, SseEmitter sseEmitter, Long userId, String tokenValue) { + public void asyncRun(User user, Workflow workflow, List userInputs, SseEmitter sseEmitter, Long userId, String tokenValue, Long sessionId) { log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); List components = workflowComponentService.getAllEnable(); List nodes = workflowNodeService.lambdaQuery() @@ -89,17 +90,22 @@ public class WorkflowStarter { WorkflowEngine workflowEngine = new WorkflowEngine(workflow, sseEmitterHelper, components, nodes, edges, workflowRuntimeService, workflowRuntimeNodeService); - workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue); + workflowEngine.run(user, userInputs, sseEmitter, userId, tokenValue, sessionId); } @Async - public void resumeFlow(String runtimeUuid, String userInput) { + public void resumeFlow(String runtimeUuid, String userInput, SseEmitter sseEmitter) { WorkflowEngine workflowEngine = InterruptedFlow.RUNTIME_TO_GRAPH.get(runtimeUuid); if (null == workflowEngine) { log.error("工作流恢复执行时失败,runtime:{}", runtimeUuid); throw new BaseException(A_WF_RESUME_FAIL.getInfo()); } + // 如果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 176ed7d6..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 @@ -11,12 +11,17 @@ import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; 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.enums.RoleType; +import org.ruoyi.common.chat.service.chat.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatService; +import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService; +import org.ruoyi.common.chat.service.image.IImageGenerationService; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.domain.entity.chat.ChatContext; +import org.ruoyi.common.chat.entity.chat.ChatContext; +import org.ruoyi.common.chat.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; @@ -25,6 +30,7 @@ import org.ruoyi.workflow.workflow.data.NodeIOData; import org.ruoyi.workflow.workflow.data.NodeIODataContent; import org.ruoyi.workflow.workflow.def.WfNodeParamRef; import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.*; @@ -32,12 +38,15 @@ import java.util.*; import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME; @Slf4j -@Component -public class WorkflowUtil { +@Service +public class WorkflowUtil extends AbstractChatMessageService { @Resource private ChatServiceFactory chatServiceFactory; + @Resource + private ImageServiceFactory imageServiceFactory; + @Resource private IChatModelService chatModelService; @@ -103,7 +112,7 @@ public class WorkflowUtil { } public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String modelName, - List systemMessage) { + List systemMessage, String nodeMessageTemplate) { log.info("stream invoke, modelName: {}", modelName); // 根据模型名称查询模型信息 @@ -112,16 +121,45 @@ public class WorkflowUtil { throw new IllegalArgumentException("模型不存在: " + modelName); } - // 根据模型名称找到模型实体 - String modelVoCategory = chatModelVo.getCategory(); + // 路由服务提供商 + String category = chatModelVo.getProviderCode(); // 根据 category 获取对应的 ChatService(不使用计费代理,工作流场景单独计费) - IChatService chatService = chatServiceFactory.getOriginalService(modelVoCategory); + IChatService chatService = chatServiceFactory.getOriginalService(category); + // 获取用户信息和Token以及SSe连接对象(对话接口需要使用) + Long sessionId = wfState.getSessionId(); + Long userId = wfState.getUserId(); + String tokenValue = wfState.getTokenValue(); + SseEmitter sseEmitter = wfState.getSseEmitter(); + + // 构建 ruoyi-ai 的 ChatRequest + List chatMessages = new ArrayList<>(); + addUserMessage(node, state.getInputs(), chatMessages); + chatMessages.addAll(systemMessage); + + // 定义模型调用对象 + ChatRequest chatRequest = new ChatRequest(); + // 目前工作流深度思考成员变量只能写死 + chatRequest.setSessionId(sessionId); + chatRequest.setEnableThinking(false); + chatRequest.setModel(modelName); + chatRequest.setChatMessages(chatMessages); + + // 构建流式生成器 StreamingChatGenerator streamingGenerator = StreamingChatGenerator.builder() .mapResult(response -> { String responseTxt = response.aiMessage().text(); log.info("llm response:{}", responseTxt); + // 会话ID不为空时插入数据库 + if (sessionId != null){ + // 获取模板消息拼接信息体 + String message = nodeMessageTemplate + responseTxt; + // 保存助手回复消息 + saveChatMessage(chatRequest, userId, message, RoleType.ASSISTANT.getName(), chatModelVo); + log.info("{}消息结束,已保存到数据库", getProviderName()); + } + // 传递所有输入数据 + 添加 LLM 输出 wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> { List outputs = new ArrayList<>(item.getInputs()); @@ -136,24 +174,9 @@ public class WorkflowUtil { .startingState(state) .build(); - // 获取用户信息和Token以及SSe连接对象(对话接口需要使用) - Long userId = wfState.getUserId(); - String tokenValue = wfState.getTokenValue(); - SseEmitter sseEmitter = wfState.getSseEmitter(); + // 构建流式回调响应器 StreamingChatResponseHandler handler = streamingGenerator.handler(); - // 构建 ruoyi-ai 的 ChatRequest - List chatMessages = new ArrayList<>(); - addUserMessage(node, state.getInputs(), chatMessages); - chatMessages.addAll(systemMessage); - - // 定义模型调用对象 - ChatRequest chatRequest = new ChatRequest(); - // 目前工作流深度思考成员变量只能写死 - chatRequest.setEnableThinking(false); - chatRequest.setModel(modelName); - chatRequest.setChatMessages(chatMessages); - //构建聊天对话上下文参数 ChatContext chatContext = ChatContext.builder() .chatModelVo(chatModelVo) @@ -213,4 +236,29 @@ 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 category = chatModelVo.getProviderCode(); + // 根据 category 获取对应的 IImageGenerationService(不使用计费代理,工作流场景单独计费) + IImageGenerationService imageService = imageServiceFactory.getOriginalService(category); + // 构建文生图上下文对象 + 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/AbstractWfNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java index e0cef0fd..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 @@ -13,10 +13,8 @@ import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.enums.WfIODataTypeEnum; import org.ruoyi.workflow.util.JsonUtil; import org.ruoyi.workflow.util.SpringUtil; -import org.ruoyi.workflow.workflow.NodeProcessResult; -import org.ruoyi.workflow.workflow.WfNodeInputConfig; -import org.ruoyi.workflow.workflow.WfNodeState; -import org.ruoyi.workflow.workflow.WfState; +import org.ruoyi.workflow.util.WorkflowMessageUtil; +import org.ruoyi.workflow.workflow.*; import org.ruoyi.workflow.workflow.data.NodeIOData; import org.ruoyi.workflow.workflow.def.WfNodeIO; import org.ruoyi.workflow.workflow.def.WfNodeParamRef; @@ -30,8 +28,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*; -import static org.ruoyi.workflow.enums.ErrorEnum.A_WF_NODE_CONFIG_ERROR; -import static org.ruoyi.workflow.enums.ErrorEnum.A_WF_NODE_CONFIG_NOT_FOUND; +import static org.ruoyi.common.chat.enums.ErrorEnum.A_WF_NODE_CONFIG_ERROR; +import static org.ruoyi.common.chat.enums.ErrorEnum.A_WF_NODE_CONFIG_NOT_FOUND; /** * 节点实例-运行时 @@ -224,4 +222,19 @@ public abstract class AbstractWfNode { return nodeConfig; } + /** + * 会话消息保存方法 + */ + public void notifyAndStoreMessage(WfState wfState, String message) { + WorkflowMessageUtil.notifyAndStoreMessage(wfState, wfState.getSseEmitter(), node, message); + } + + /** + * 获取节点的响应模板 + * @param configKey 参数Key + * @return 返回模板样式 + */ + 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/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/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 16fc5f71..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,6 +66,9 @@ public class HttpRequestNode extends AbstractWfNode { List outputs = new ArrayList<>(); outputs.add(NodeIOData.createByText("output", "HTTP响应", response)); + // 保存成功会话信息且发送驱动消息事件 + String message = nodeMessageTemplate + response; + notifyAndStoreMessage(wfState, message); return NodeProcessResult.builder().content(outputs).build(); } catch (Exception e) { @@ -73,6 +79,9 @@ public class HttpRequestNode extends AbstractWfNode { errorOutputs.add(NodeIOData.createByText("output", "错误", "")); errorOutputs.add(NodeIOData.createByText("error", "HTTP请求错误", e.getMessage())); + // 保存失败会话信息且发送驱动消息事件 + 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/HumanFeedbackNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/humanFeedBack/HumanFeedbackNode.java similarity index 93% 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..bf14b716 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; @@ -7,7 +7,9 @@ import org.ruoyi.workflow.entity.WorkflowNode; import org.ruoyi.workflow.workflow.NodeProcessResult; import org.ruoyi.workflow.workflow.WfNodeState; import org.ruoyi.workflow.workflow.WfState; +import org.ruoyi.workflow.workflow.WorkflowUtil; import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.node.AbstractWfNode; import 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..6f5b639b --- /dev/null +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/image/ImageNode.java @@ -0,0 +1,68 @@ +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 org.ruoyi.workflow.workflow.node.enmus.NodeMessageTemplateEnum; + +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); + // 获取节点模板提示词信息 + String nodeMessageTemplate = getNodeMessageTemplate(NodeMessageTemplateEnum.IMAGE.getValue()); + // 保存成功信息且发送驱动消息事件 + String message = nodeMessageTemplate + imageUrl; + notifyAndStoreMessage(wfState, message); + // 创建节点参数对象 + 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-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 b3f85796..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 @@ -6,12 +6,14 @@ 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; @@ -66,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 c006cbe4..f2482096 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 @@ -1,5 +1,8 @@ package org.ruoyi.workflow.workflow.node.mailSend; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.JSONValidator; import jakarta.mail.internet.MimeMessage; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; @@ -11,6 +14,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 org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; @@ -28,9 +32,20 @@ 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(); + // 获取输入信息 + String input = getDataFromInput(inputs); + // 判断是否为JSON格式(LLM输出转换 由LLM生成格式) + if (StringUtils.isNotBlank(input) && isJson(input)) { + JSONObject inputJson = JSON.parseObject(input); + JSONObject configJson = (JSONObject) JSON.toJSON(config); + configJson.putAll(inputJson); + config = configJson.toJavaObject(MailSendNodeConfig.class); + } // 安全获取模板(使用 defaultString 避免 null) String subjectTemplate = StringUtils.defaultString(config.getSubject()); @@ -49,15 +64,7 @@ public class MailSendNode extends AbstractWfNode { content = WorkflowUtil.renderTemplate(contentTemplate, inputs); } else { // 优先使用 output,如果没有则使用 input - content = inputs.stream() - .filter(item -> "output".equals(item.getName())) - .map(NodeIOData::valueToString) - .findFirst() - .orElseGet(() -> inputs.stream() - .filter(item -> "input".equals(item.getName())) - .map(NodeIOData::valueToString) - .findFirst() - .orElse("")); + content = getDataFromInput(inputs); } // 将换行符转换为 HTML 换行 @@ -84,9 +91,9 @@ public class MailSendNode extends AbstractWfNode { // 设置收件人 String[] toArray = Arrays.stream(toMails.split(",")) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .toArray(String[]::new); + .map(String::trim) + .filter(StringUtils::isNotBlank) + .toArray(String[]::new); if (toArray.length == 0) { throw new IllegalArgumentException("收件人邮箱列表为空"); } @@ -95,9 +102,9 @@ public class MailSendNode extends AbstractWfNode { // 设置抄送(如有) if (StringUtils.isNotBlank(ccMails)) { String[] ccArray = Arrays.stream(ccMails.split(",")) - .map(String::trim) - .filter(StringUtils::isNotBlank) - .toArray(String[]::new); + .map(String::trim) + .filter(StringUtils::isNotBlank) + .toArray(String[]::new); if (ccArray.length > 0) { helper.setCc(ccArray); } @@ -111,25 +118,29 @@ public class MailSendNode extends AbstractWfNode { mailSender.send(message); log.info("Email sent successfully to: {}", toMails); + // 保存成功会话信息且发送驱动消息事件 + String resultMessage = nodeMessageTemplate + "发送邮箱成功"; + notifyAndStoreMessage(wfState, resultMessage); + // 构造输出:统一输出为 output 参数 List outputs = new java.util.ArrayList<>(); // 优先使用 output,如果没有则使用 input(但重命名为 output) inputs.stream() - .filter(item -> "output".equals(item.getName())) - .findFirst() - .ifPresentOrElse( - outputs::add, - () -> inputs.stream() - .filter(item -> "input".equals(item.getName())) - .findFirst() - .ifPresent(inputParam -> { - String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null - ? inputParam.getContent().getTitle() : ""; - NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString()); - outputs.add(outputParam); - }) - ); + .filter(item -> "output".equals(item.getName())) + .findFirst() + .ifPresentOrElse( + outputs::add, + () -> inputs.stream() + .filter(item -> "input".equals(item.getName())) + .findFirst() + .ifPresent(inputParam -> { + String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null + ? inputParam.getContent().getTitle() : ""; + NodeIOData outputParam = NodeIOData.createByText("output", title, resultMessage); + outputs.add(outputParam); + }) + ); return NodeProcessResult.builder().content(outputs).build(); @@ -138,23 +149,27 @@ public class MailSendNode extends AbstractWfNode { // 异常时也统一输出为 output 参数,添加错误信息 List errorOutputs = new java.util.ArrayList<>(); - state.getInputs().stream() - .filter(item -> "output".equals(item.getName())) - .findFirst() - .ifPresentOrElse( - errorOutputs::add, - () -> state.getInputs().stream() - .filter(item -> "input".equals(item.getName())) - .findFirst() - .ifPresent(inputParam -> { - String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null - ? inputParam.getContent().getTitle() : ""; - NodeIOData outputParam = NodeIOData.createByText("output", title, inputParam.valueToString()); - errorOutputs.add(outputParam); - }) - ); + // 保存失败会话信息且发送驱动消息事件 + String resultMessage = nodeMessageTemplate + "发送邮箱失败: " + e.getMessage(); + notifyAndStoreMessage(wfState, resultMessage); - errorOutputs.add(NodeIOData.createByText("error", "mail", e.getMessage())); + state.getInputs().stream() + .filter(item -> "output".equals(item.getName())) + .findFirst() + .ifPresentOrElse( + errorOutputs::add, + () -> state.getInputs().stream() + .filter(item -> "input".equals(item.getName())) + .findFirst() + .ifPresent(inputParam -> { + String title = inputParam.getContent() != null && inputParam.getContent().getTitle() != null + ? inputParam.getContent().getTitle() : ""; + NodeIOData outputParam = NodeIOData.createByText("output", title, resultMessage); + errorOutputs.add(outputParam); + }) + ); + + errorOutputs.add(NodeIOData.createByText("error", "mail", resultMessage)); return NodeProcessResult.builder().content(errorOutputs).build(); } } @@ -174,4 +189,40 @@ public class MailSendNode extends AbstractWfNode { return sender; } + + /** + * 获取信息 + * @param inputs 用户输入 + * @return 返回输入信息 + */ + public String getDataFromInput(List inputs) { + return inputs.stream() + .filter(item -> "output".equals(item.getName())) + .map(NodeIOData::valueToString) + .findFirst() + .orElseGet(() -> inputs.stream() + .filter(item -> "input".equals(item.getName())) + .map(NodeIOData::valueToString) + .findFirst() + .orElse("")); + } + + /** + * 判断字符串是否为合法的 JSON 格式 + * + * @param str 待检测的字符串 + * @return true 表示是合法 JSON (包括 JSONObject, JSONArray, 或基本类型值) + */ + public static boolean isJson(String str) { + if (str == null || str.trim().isEmpty()) { + return false; + } + // 使用 try-with-resources 正确处理 JSONValidator 资源关闭 + try (JSONValidator validator = JSONValidator.from(str.trim())) { + return validator.getType() == JSONValidator.Type.Object; + } catch (Exception e) { + log.warn("JSON格式校验失败: {}", e.getMessage()); + return false; + } + } } diff --git a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNode.java b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNode.java index 6f5df29e..e9ccc2be 100644 --- a/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNode.java +++ b/ruoyi-modules/ruoyi-aiflow/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNode.java @@ -16,8 +16,8 @@ import org.ruoyi.workflow.workflow.node.AbstractWfNode; import java.util.List; import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME; -import static org.ruoyi.workflow.enums.ErrorEnum.A_WF_NODE_CONFIG_ERROR; -import static org.ruoyi.workflow.enums.ErrorEnum.A_WF_NODE_CONFIG_NOT_FOUND; +import static org.ruoyi.common.chat.enums.ErrorEnum.A_WF_NODE_CONFIG_ERROR; +import static org.ruoyi.common.chat.enums.ErrorEnum.A_WF_NODE_CONFIG_NOT_FOUND; @Slf4j public class StartNode extends AbstractWfNode { 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 26c66ef5..35a3535f 100644 --- a/ruoyi-modules/ruoyi-chat/pom.xml +++ b/ruoyi-modules/ruoyi-chat/pom.xml @@ -19,11 +19,6 @@ ruoyi-common-chat
- - org.ruoyi - ruoyi-mcp - - org.ruoyi ruoyi-common-sensitive @@ -53,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/agent/McpAgent.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java index 6fc613f2..1f5d0c6a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/McpAgent.java @@ -7,35 +7,40 @@ import dev.langchain4j.service.V; public interface McpAgent { /** - * 系统提示词:通用工具调用智能体 - * 不限定具体工具类型,让 LangChain4j 自动传递工具描述给 LLM + * 系统提示词:定义智能体身份、核心职责、强制遵守的规则 + * 适配SSE流式特性,明确工具全来自远端MCP服务,仅做代理调用和结果整理 */ @SystemMessage(""" - 你是一个AI助手,可以通过调用各种工具来帮助用户完成不同的任务。 + 你是专业的MCP服务工具代理智能体,核心能力是通过HTTP SSE流式传输协议,调用本地http://localhost:8085/sse地址上MCP服务端注册的所有工具。 + 你的核心工作职责: + 1. 准确理解用户的自然语言请求,判断需要调用MCP服务端的哪一个/哪些工具; + 2. 通过绑定的工具提供者,向MCP服务端发起工具调用请求,传递完整的工具执行参数; + 3. 实时接收MCP服务端通过SSE流式返回的工具执行结果,保证结果片段的完整性; + 4. 将流式结果按原始顺序整理为清晰、易懂的自然语言答案,返回给用户。 - 【工具使用规则】 - 1. 根据用户的请求,判断需要使用哪些工具 - 2. 仔细阅读每个工具的描述,确保理解工具的功能和参数要求 - 3. 使用正确的参数调用工具 - 4. 如果工具执行失败,向用户友好地说明错误原因,并尝试提供替代方案 - 5. 对于复杂任务,可以分步骤使用多个工具完成 - 6. 将工具执行结果以清晰易懂的方式呈现给用户 - - 【响应格式】 - - 直接回答用户的问题 - - 如果使用了工具,说明使用了什么工具以及结果 - - 如果遇到错误,提供友好的错误信息和解决建议 + 【强制遵守的核心规则 - 无例外】 + 1. 所有工具调用必须通过远端MCP服务执行,严禁尝试本地执行任何业务逻辑; + 2. 处理SSE流式结果时,严格保留结果片段的返回顺序,不得打乱或遗漏; + 3. 若MCP服务返回错误(如工具未找到、参数错误、执行失败),直接将错误信息友好反馈给用户,无需额外推理; + 4. 工具执行结果若为结构化数据(如JSON、表格),需格式化后返回,提升可读性。 """) + /** + * 用户消息模板:{{query}}为参数占位符,与方法入参的@V("query")绑定 + */ @UserMessage(""" + 请通过调用MCP服务端的工具,处理用户的以下请求: {{query}} """) - - @Agent("通用工具调用智能体") /** - * 智能体对外调用入口 - * @param query 用户的自然语言请求 - * @return 处理结果 + * 智能体标识:用于日志打印、监控追踪、多智能体协作时的身份识别 + */ + @Agent("MCP服务SSE流式代理智能体-连接本地8085端口") + /** + * 智能体对外调用入口方法 + * @param query 用户的自然语言请求(如:生成订单数据柱状图、查询今日天气) + * @V("query") 将方法入参值绑定到@UserMessage的{{query}}占位符中 + * @return 整理后的MCP工具执行结果(流式结果会自动拼接为完整字符串) */ String callMcpTool(@V("query") String query); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/config/AgentMysqlConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/config/AgentMysqlConfig.java index b5d2f9e0..dfcc28b6 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/config/AgentMysqlConfig.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/config/AgentMysqlConfig.java @@ -1,44 +1,44 @@ -package org.ruoyi.agent.config; +// package org.ruoyi.agent.config; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import javax.sql.DataSource; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +// import com.zaxxer.hikari.HikariConfig; +// import com.zaxxer.hikari.HikariDataSource; +// import javax.sql.DataSource; +// import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +// import org.springframework.boot.context.properties.EnableConfigurationProperties; +// import org.springframework.context.annotation.Bean; +// import org.springframework.context.annotation.Configuration; -/** - * Agent MySQL 数据源配置 - * 为 Agent 配置独立的 MySQL 数据库连接池(HikariCP) - * - * 仅在 agent.mysql.enabled=true 时启用 - */ -@Configuration -@EnableConfigurationProperties(AgentMysqlProperties.class) -@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") -public class AgentMysqlConfig { +// /** +// * Agent MySQL 数据源配置 +// * 为 Agent 配置独立的 MySQL 数据库连接池(HikariCP) +// * +// * 仅在 agent.mysql.enabled=true 时启用 +// */ +// @Configuration +// @EnableConfigurationProperties(AgentMysqlProperties.class) +// @ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") +// public class AgentMysqlConfig { - /** - * 创建 Agent 专用的数据源 - * 与项目主数据源隔离,独立管理 - * - * @param properties Agent MySQL 配置属性 - * @return HikariCP 数据源 - */ - @Bean("agentDataSource") - public DataSource agentDataSource(AgentMysqlProperties properties) { - HikariConfig config = new HikariConfig(); - config.setJdbcUrl(properties.getUrl()); - config.setUsername(properties.getUsername()); - config.setPassword(properties.getPassword()); - config.setDriverClassName("com.mysql.cj.jdbc.Driver"); - config.setMaximumPoolSize(properties.getMaxPoolSize()); - config.setMinimumIdle(properties.getMinIdle()); - config.setConnectionTimeout(30000); - config.setIdleTimeout(600000); - config.setMaxLifetime(1800000); +// /** +// * 创建 Agent 专用的数据源 +// * 与项目主数据源隔离,独立管理 +// * +// * @param properties Agent MySQL 配置属性 +// * @return HikariCP 数据源 +// */ +// @Bean("agentDataSource") +// public DataSource agentDataSource(AgentMysqlProperties properties) { +// HikariConfig config = new HikariConfig(); +// config.setJdbcUrl(properties.getUrl()); +// config.setUsername(properties.getUsername()); +// config.setPassword(properties.getPassword()); +// config.setDriverClassName("com.mysql.cj.jdbc.Driver"); +// config.setMaximumPoolSize(properties.getMaxPoolSize()); +// config.setMinimumIdle(properties.getMinIdle()); +// config.setConnectionTimeout(30000); +// config.setIdleTimeout(600000); +// config.setMaxLifetime(1800000); - return new HikariDataSource(config); - } -} +// return new HikariDataSource(config); +// } +// } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/domain/TableStructure.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/domain/TableStructure.java index d1c3bca2..7ff66c21 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/domain/TableStructure.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/domain/TableStructure.java @@ -16,7 +16,8 @@ public class TableStructure { * 表名 */ private String tableName; - + + private String tableType; // 添加此字段:BASE TABLE 或 VIEW /** * 表注释/说明 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaInitializer.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaInitializer.java index 14bd86e3..ce75074e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaInitializer.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaInitializer.java @@ -1,19 +1,19 @@ package org.ruoyi.agent.manager; -import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; +import lombok.extern.slf4j.Slf4j; + /** * 架构初始化器 * 在应用启动完成后自动初始化表结构缓存 */ @Slf4j @Component -@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") +// @ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") public class TableSchemaInitializer { @Autowired(required = false) diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java index 9bdab429..a02eb78d 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/manager/TableSchemaManager.java @@ -1,16 +1,30 @@ package org.ruoyi.agent.manager; -import lombok.extern.slf4j.Slf4j; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import javax.sql.DataSource; + import org.ruoyi.agent.domain.ColumnInfo; import org.ruoyi.agent.domain.TableStructure; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; -import javax.sql.DataSource; -import java.sql.*; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; + +import com.baomidou.dynamic.datasource.annotation.DS; + +import lombok.extern.slf4j.Slf4j; /** * 表结构管理器 @@ -24,12 +38,15 @@ import java.util.stream.Collectors; */ @Slf4j @Component -@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") +@DS("agent") public class TableSchemaManager { - + @Autowired(required = false) private DataSource agentDataSource; + @Value("${AGENT_ALLOWED_TABLES}") + private String allowedTables; + /** * 表结构缓存 (表名 -> 表结构) * 使用 ConcurrentHashMap 支持高并发访问 @@ -55,12 +72,12 @@ public class TableSchemaManager { if (initialized) { return; } - try { log.info("Initializing database schema cache..."); loadAllowedTableSchemas(); initialized = true; log.info("Schema cache initialized with {} tables", schemaCache.size()); + } catch (Exception e) { log.error("Failed to initialize schema cache", e); } @@ -103,6 +120,7 @@ public class TableSchemaManager { try (ResultSet tableRs = metaData.getTables(conn.getCatalog(), null, tableName, new String[]{"TABLE"})) { if (tableRs.next()) { table.setTableComment(tableRs.getString("REMARKS")); + table.setTableType(tableRs.getString("TABLE_TYPE")); } } @@ -183,7 +201,6 @@ public class TableSchemaManager { * 获取所有允许的表名 */ public List getAllowedTableNames() { - String allowedTables = System.getenv("AGENT_ALLOWED_TABLES"); if (allowedTables == null || allowedTables.trim().isEmpty()) { log.warn("AGENT_ALLOWED_TABLES not configured"); return new ArrayList<>(); @@ -224,7 +241,7 @@ public class TableSchemaManager { * 检查表是否在允许列表中 */ private boolean isTableAllowed(String tableName) { - String allowedTables = System.getenv("AGENT_ALLOWED_TABLES"); + // String allowedTables = System.getenv("AGENT_ALLOWED_TABLES"); if (allowedTables == null || allowedTables.trim().isEmpty()) { return false; } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java index 914fa409..2cc33a67 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/ExecuteSqlQueryTool.java @@ -1,12 +1,5 @@ package org.ruoyi.agent.tool; -import dev.langchain4j.agent.tool.Tool; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -16,17 +9,26 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; + +import dev.langchain4j.agent.tool.Tool; +import lombok.extern.slf4j.Slf4j; + /** * 执行 SQL 查询的 Tool * 执行指定的 SELECT SQL 查询并返回结果 */ @Slf4j @Component -@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") public class ExecuteSqlQueryTool { @Autowired(required = false) - private DataSource agentDataSource; + private DataSource dataSource; /** * 执行 SELECT SQL 查询 @@ -37,6 +39,8 @@ public class ExecuteSqlQueryTool { */ @Tool("Execute a SELECT SQL query and return the results. Example: SELECT * FROM sys_user") public String executeSql(String sql) { + // 2. 手动推入数据源上下文 + DynamicDataSourceContextHolder.push("agent"); if (sql == null || sql.trim().isEmpty()) { return "Error: SQL query cannot be empty"; } @@ -48,11 +52,11 @@ public class ExecuteSqlQueryTool { } try { - if (agentDataSource == null) { + if (dataSource == null) { return "Error: Database datasource not configured"; } - try (Connection connection = agentDataSource.getConnection()) { + try (Connection connection = dataSource.getConnection()) { try (PreparedStatement preparedStatement = connection.prepareStatement(sql); ResultSet resultSet = preparedStatement.executeQuery()) { @@ -82,7 +86,12 @@ public class ExecuteSqlQueryTool { } } catch (Exception e) { log.error("Error executing SQL: {}", sql, e); + // 3. 必须在 finally 中清除上下文,防止污染其他请求 + DynamicDataSourceContextHolder.clear(); return "Error: " + e.getMessage(); + } finally { + // 3. 必须在 finally 中清除上下文,防止污染其他请求 + DynamicDataSourceContextHolder.clear(); } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java index 81623dca..d9ed0666 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryAllTablesTool.java @@ -1,17 +1,14 @@ package org.ruoyi.agent.tool; +import java.util.List; + +import org.ruoyi.agent.domain.TableStructure; +import org.ruoyi.agent.manager.TableSchemaManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + import dev.langchain4j.agent.tool.Tool; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -import javax.sql.DataSource; -import java.sql.Connection; -import java.sql.DatabaseMetaData; -import java.sql.ResultSet; -import java.util.ArrayList; -import java.util.List; /** * 查询数据库所有表的 Tool @@ -19,12 +16,11 @@ import java.util.List; */ @Slf4j @Component -@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") public class QueryAllTablesTool { - @Autowired(required = false) - private DataSource agentDataSource; - + + @Autowired + private TableSchemaManager tableSchemaManager; // 注入管理器 /** * 查询数据库中所有表 * 返回数据库中存在的所有表的列表 @@ -34,44 +30,36 @@ public class QueryAllTablesTool { @Tool("Query all tables in the database and return table names and basic information") public String queryAllTables() { try { - if (agentDataSource == null) { - return "Error: Database datasource not configured"; - } - - try (Connection connection = agentDataSource.getConnection()) { - DatabaseMetaData databaseMetaData = connection.getMetaData(); - ResultSet resultSet = databaseMetaData.getTables(null, null, null, new String[]{"TABLE"}); - - List tableNames = new ArrayList<>(); - List tableDetails = new ArrayList<>(); - - while (resultSet.next()) { - String tableName = resultSet.getString("TABLE_NAME"); - String tableComment = resultSet.getString("REMARKS"); - String tableType = resultSet.getString("TABLE_TYPE"); - - tableNames.add(tableName); - tableDetails.add(String.format("- %s (%s) - %s", - tableName, tableType, tableComment != null ? tableComment : "No comment")); + // 1. 从管理器获取所有允许的表结构信息(内部已包含初始化/缓存逻辑) + List tableSchemas = tableSchemaManager.getAllowedTableSchemas(); + + if (tableSchemas == null || tableSchemas.isEmpty()) { + return "No tables found in database or cache is empty."; } - resultSet.close(); - - if (tableNames.isEmpty()) { - return "No tables found in database"; - } - + + // 2. 格式化结果 StringBuilder result = new StringBuilder(); - result.append("Found ").append(tableNames.size()).append(" tables:\n"); - for (String detail : tableDetails) { - result.append(detail).append("\n"); + result.append("Found ").append(tableSchemas.size()).append(" tables in cache:\n"); + + for (TableStructure schema : tableSchemas) { + String tableName = schema.getTableName(); + String tableType = schema.getTableType() != null ? schema.getTableType() : "TABLE"; + String tableComment = schema.getTableComment(); + + result.append(String.format("- %s (%s) - %s\n", + tableName, + tableType, + tableComment != null ? tableComment : "No comment")); } - - log.info("Successfully queried {} tables", tableNames.size()); + + log.info("Successfully retrieved {} tables from schema cache", tableSchemas.size()); return result.toString(); + + } catch (Exception e) { + log.error("Error retrieving tables from cache", e); + return "Error: " + e.getMessage(); } - } catch (Exception e) { - log.error("Error querying all tables", e); - return "Error: " + e.getMessage(); - } + + } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java index b7872322..33d06b27 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/agent/tool/QueryTableSchemaTool.java @@ -1,83 +1,57 @@ package org.ruoyi.agent.tool; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; -import dev.langchain4j.agent.tool.Tool; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.stereotype.Component; - -import javax.sql.DataSource; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; -/** - * 查询表建表详情的 Tool - * 根据表名查询该表的建表 SQL 语句 - */ -@Slf4j +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder; + +import dev.langchain4j.agent.tool.Tool; +import lombok.extern.slf4j.Slf4j; + @Component -@ConditionalOnProperty(name = "agent.mysql.enabled", havingValue = "true") +@Slf4j public class QueryTableSchemaTool { @Autowired(required = false) - private DataSource agentDataSource; + private DataSource dataSource; - /** - * 根据表名查询建表详情 - * 返回指定表的 CREATE TABLE 语句 - * - * @param tableName 表名 - * @return 包含建表 SQL 的结果 - */ @Tool("Query the CREATE TABLE statement (DDL) for a specific table by table name") public String queryTableSchema(String tableName) { + // 2. 手动推入数据源上下文 + DynamicDataSourceContextHolder.push("agent"); if (tableName == null || tableName.trim().isEmpty()) { return "Error: Table name cannot be empty"; } - // 验证表名有效性,防止 SQL 注入 - if (!isValidIdentifier(tableName)) { + if (!tableName.matches("^[a-zA-Z0-9_]+$")) { return "Error: Invalid table name format"; } - try { - if (agentDataSource == null) { - return "Error: Database datasource not configured"; + String sql = "SHOW CREATE TABLE `" + tableName + "`"; + + try (Connection connection = dataSource.getConnection(); + PreparedStatement ps = connection.prepareStatement(sql); + ResultSet rs = ps.executeQuery()) { + + if (rs.next()) { + return rs.getString("Create Table"); } - try (Connection connection = agentDataSource.getConnection()) { - String sql = "SHOW CREATE TABLE " + tableName; - PreparedStatement preparedStatement = connection.prepareStatement(sql); - ResultSet resultSet = preparedStatement.executeQuery(); + return "Table not found: " + tableName; - if (resultSet.next()) { - String createTableSql = resultSet.getString("Create Table"); - resultSet.close(); - preparedStatement.close(); - - log.info("Successfully queried schema for table: {}", tableName); - return "CREATE TABLE DDL for " + tableName + ":\n\n" + createTableSql; - } - - resultSet.close(); - preparedStatement.close(); - return "Error: Table not found or not accessible: " + tableName; - } } catch (Exception e) { - log.error("Error querying table schema for table: {}", tableName, e); + // 3. 必须在 finally 中清除上下文,防止污染其他请求 + DynamicDataSourceContextHolder.clear(); + log.error("Error querying table schema: {}", tableName, e); return "Error: " + e.getMessage(); + } finally { + // 3. 必须在 finally 中清除上下文,防止污染其他请求 + DynamicDataSourceContextHolder.clear(); } } - - /** - * 验证是否为有效的 SQL 标识符 - */ - private boolean isValidIdentifier(String identifier) { - if (identifier == null || identifier.isEmpty()) { - return false; - } - return identifier.matches("^[a-zA-Z0-9_\\.]+$"); - } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java index c24de577..7d85cdb1 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatMessageController.java @@ -6,7 +6,9 @@ import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; -import org.ruoyi.service.chat.IChatMessageService; +import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; +import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo; +import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; import org.springframework.web.bind.annotation.*; import org.springframework.validation.annotation.Validated; import org.ruoyi.common.idempotent.annotation.RepeatSubmit; @@ -18,8 +20,6 @@ import org.ruoyi.common.core.validate.AddGroup; import org.ruoyi.common.core.validate.EditGroup; import org.ruoyi.common.log.enums.BusinessType; import org.ruoyi.common.excel.utils.ExcelUtil; -import org.ruoyi.domain.vo.chat.ChatMessageVo; -import org.ruoyi.domain.bo.chat.ChatMessageBo; import org.ruoyi.common.mybatis.core.page.TableDataInfo; /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java index 06212c1d..15884158 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/controller/chat/ChatModelController.java @@ -6,7 +6,7 @@ import lombok.RequiredArgsConstructor; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.*; import cn.dev33.satoken.annotation.SaCheckPermission; -import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.enums.ModelType; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java index f358b6f5..25045920 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphInstanceVo.java @@ -10,6 +10,8 @@ import org.ruoyi.domain.entity.knowledge.KnowledgeGraphInstance; import java.io.Serial; import java.io.Serializable; +import java.util.Date; + /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java index 1d534e38..78973e84 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/domain/vo/knowledge/KnowledgeGraphSegmentVo.java @@ -10,6 +10,8 @@ import org.ruoyi.domain.entity.knowledge.KnowledgeGraphSegment; import java.io.Serial; import java.io.Serializable; +import java.util.Date; + /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java index a36438fe..3312388b 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/enums/ChatModeType.java @@ -14,7 +14,6 @@ public enum ChatModeType { ZHI_PU("zhipu", "智谱清言"), DEEP_SEEK("deepseek", "深度求索"), QIAN_WEN("qianwen", "通义千问"), - PPIO("ppio", "PPIO派欧云"), OPEN_AI("openai", "openai"); private final String code; private final String description; 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/factory/EmbeddingModelFactory.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/EmbeddingModelFactory.java index 0aecf556..41ae8160 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/EmbeddingModelFactory.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/factory/EmbeddingModelFactory.java @@ -2,7 +2,7 @@ package org.ruoyi.factory; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.service.embed.BaseEmbedModelService; import org.ruoyi.service.embed.MultiModalEmbedModelService; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatMessageMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatMessageMapper.java index 63793d19..51ad8651 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatMessageMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatMessageMapper.java @@ -1,7 +1,7 @@ package org.ruoyi.mapper.chat; -import org.ruoyi.domain.entity.chat.ChatMessage; -import org.ruoyi.domain.vo.chat.ChatMessageVo; +import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo; +import org.ruoyi.common.chat.entity.chat.ChatMessage; import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; /** diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java index 50c296a4..ee93917a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mapper/chat/ChatModelMapper.java @@ -1,6 +1,6 @@ package org.ruoyi.mapper.chat; -import org.ruoyi.common.chat.domain.entity.chat.ChatModel; +import org.ruoyi.common.chat.entity.chat.ChatModel; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java index 75894c9a..2e81a9a1 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/AbstractStreamingChatService.java @@ -3,6 +3,7 @@ package org.ruoyi.service.chat.impl; import dev.langchain4j.agentic.AgenticServices; import dev.langchain4j.agentic.supervisor.SupervisorAgent; import dev.langchain4j.agentic.supervisor.SupervisorResponseStrategy; +import dev.langchain4j.community.model.dashscope.QwenChatModel; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.mcp.McpToolProvider; @@ -24,17 +25,20 @@ import org.ruoyi.agent.WebSearchAgent; import org.ruoyi.agent.tool.ExecuteSqlQueryTool; import org.ruoyi.agent.tool.QueryAllTablesTool; import org.ruoyi.agent.tool.QueryTableSchemaTool; -import org.ruoyi.common.chat.Service.IChatService; +import org.ruoyi.common.chat.base.ThreadContext; +import org.ruoyi.common.chat.domain.dto.request.ReSumeRunner; +import org.ruoyi.common.chat.domain.dto.request.WorkFlowRunner; +import org.ruoyi.common.chat.enums.RoleType; +import org.ruoyi.common.chat.service.chat.IChatService; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.domain.entity.chat.ChatContext; +import org.ruoyi.common.chat.entity.chat.ChatContext; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.service.chatMessage.AbstractChatMessageService; +import org.ruoyi.common.chat.service.workFlow.IWorkFlowStarterService; import org.ruoyi.common.core.utils.ObjectUtils; import org.ruoyi.common.core.utils.SpringUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.sse.utils.SseMessageUtils; -import org.ruoyi.domain.bo.chat.ChatMessageBo; -import org.ruoyi.enums.RoleType; -import org.ruoyi.service.chat.IChatMessageService; import org.ruoyi.service.chat.impl.memory.PersistentChatMemoryStore; import org.springframework.util.CollectionUtils; import org.springframework.validation.annotation.Validated; @@ -58,7 +62,7 @@ import java.util.concurrent.ConcurrentHashMap; */ @Slf4j @Validated -public abstract class AbstractStreamingChatService implements IChatService { +public abstract class AbstractStreamingChatService extends AbstractChatMessageService implements IChatService { /** * 默认保留的消息窗口大小(用于长期记忆) @@ -76,6 +80,11 @@ public abstract class AbstractStreamingChatService implements IChatService { */ private static final Map memoryCache = new ConcurrentHashMap<>(); + /** + * 获取工作流启用Bean对象 + */ + private static final IWorkFlowStarterService starterService = SpringUtils.getBean(IWorkFlowStarterService.class); + /** * 定义聊天流程骨架 */ @@ -108,9 +117,28 @@ public abstract class AbstractStreamingChatService implements IChatService { // 保存用户消息 saveChatMessage(chatRequest, userId, content, RoleType.USER.getName(), chatModelVo); + + // 判断用户是否重新输入 + boolean isResume = chatRequest.getIsResume() != null && chatRequest.getIsResume(); + if (isResume){ + ReSumeRunner reSumeRunner = chatRequest.getReSumeRunner(); + if (ObjectUtils.isNotEmpty(reSumeRunner)){ + starterService.resumeFlow(reSumeRunner.getRuntimeUuid(), reSumeRunner.getFeedbackContent(), emitter); + return emitter; + } + } + + // 判断用户是否开启工作流 + boolean enableWorkFlow = chatRequest.getEnableWorkFlow() != null && chatRequest.getEnableWorkFlow(); + if (enableWorkFlow) { + WorkFlowRunner runner = chatRequest.getWorkFlowRunner(); + if (ObjectUtils.isNotEmpty(runner)){ + return starterService.streaming(ThreadContext.getCurrentUser(), runner.getUuid(), runner.getInputs(), chatRequest.getSessionId()); + } + } + // 使用长期记忆增强的消息列表 List messagesWithMemory = buildMessagesWithMemory(chatRequest); - if (chatRequest.getEnableThinking()) { String msg = doAgent(content, chatModelVo); SseMessageUtils.sendMessage(userId, msg); @@ -119,13 +147,10 @@ public abstract class AbstractStreamingChatService implements IChatService { saveChatMessage(chatRequest, userId, msg, RoleType.ASSISTANT.getName(), chatModelVo); } else { // 创建包含内存管理的响应处理器 - if (ObjectUtils.isEmpty(handler)) { - handler = createResponseHandler(chatRequest, userId, tokenValue, chatModelVo); - } + handler = ObjectUtils.isEmpty(handler) ? createResponseHandler(chatRequest, userId, tokenValue, chatModelVo) : handler; // 调用具体实现的聊天方法 doChat(chatModelVo, chatRequest, messagesWithMemory, handler); } - } catch (Exception e) { SseMessageUtils.sendMessage(userId, "对话出错:" + e.getMessage()); SseMessageUtils.completeConnection(userId, tokenValue); @@ -144,6 +169,12 @@ public abstract class AbstractStreamingChatService implements IChatService { */ protected List buildMessagesWithMemory(ChatRequest chatRequest) { List messages = new ArrayList<>(); + // 工作流对话消息 + List chatMessages = chatRequest.getChatMessages(); + if (!CollectionUtils.isEmpty(chatMessages)){ + messages.addAll(chatMessages); + } + // 开启长期记忆 if (enablePersistentMemory && chatRequest.getSessionId() != null) { MessageWindowChatMemory memory = createChatMemory(chatRequest.getSessionId()); if (memory != null) { @@ -155,11 +186,6 @@ public abstract class AbstractStreamingChatService implements IChatService { } return messages; } - // 工作流方式 - List chatMessages = chatRequest.getChatMessages(); - if (!CollectionUtils.isEmpty(chatMessages)){ - messages.addAll(chatMessages); - } return messages; } @@ -276,40 +302,6 @@ public abstract class AbstractStreamingChatService implements IChatService { }; } - /** - * 保存聊天消息到数据库 - * - * @param chatRequest 聊天请求 - * @param userId 用户ID - * @param content 消息内容 - * @param role 消息角色 - * @param chatModelVo 模型配置 - */ - private void saveChatMessage(ChatRequest chatRequest, Long userId, String content, String role, ChatModelVo chatModelVo) { - try { - // 验证必要的上下文信息 - if (chatRequest == null || userId == null) { - log.warn("缺少必要的聊天上下文信息,无法保存消息"); - return; - } - - // 创建ChatMessageBo对象 - ChatMessageBo messageBO = new ChatMessageBo(); - messageBO.setUserId(userId); - messageBO.setSessionId(chatRequest.getSessionId()); - messageBO.setContent(content); - messageBO.setRole(role); - messageBO.setModelName(chatRequest.getModel()); - messageBO.setBillingType(chatModelVo.getModelType()); - messageBO.setRemark(null); - - IChatMessageService chatMessageService = SpringUtils.getBean(IChatMessageService.class); - chatMessageService.insertByBo(messageBO); - } catch (Exception e) { - log.error("保存{}聊天消息时出错: {}", getProviderName(), e.getMessage(), e); - } - } - /** * 构建具体厂商的 StreamingChatModel * 子类必须实现此方法,返回对应厂商的模型实例 @@ -325,26 +317,26 @@ public abstract class AbstractStreamingChatService implements IChatService { protected String doAgent(String userMessage, ChatModelVo chatModelVo) { // 步骤1: 配置MCP传输层 - 连接到bing-cn-mcp服务器 // 该服务提供两个工具: bing_search (必应搜索) 和 crawl_webpage (网页抓取) - McpTransport transport = new StdioMcpTransport.Builder() - .command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", - "bing-cn-mcp" - )) - .logEvents(true) - .build(); + // McpTransport transport = new StdioMcpTransport.Builder() + // .command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", + // "bing-cn-mcp" + // )) + // .logEvents(true) + // .build(); - // 步骤2: 创建MCP客户端 - McpClient mcpClient = new DefaultMcpClient.Builder() - .transport(transport) - .build(); + // // 步骤2: 创建MCP客户端 + // McpClient mcpClient = new DefaultMcpClient.Builder() + // .transport(transport) + // .build(); - // 步骤3: 配置工具提供者 - ToolProvider toolProvider = McpToolProvider.builder() - .mcpClients(List.of(mcpClient)) - .build(); + // // 步骤3: 配置工具提供者 + // ToolProvider toolProvider = McpToolProvider.builder() + // .mcpClients(List.of(mcpClient)) + // .build(); McpTransport transport1 = new StdioMcpTransport.Builder() - .command(List.of("C:\\Program Files\\nodejs\\npx.cmd", "-y", + .command(List.of("npx", "-y", "mcp-echarts" )) .logEvents(true) @@ -361,40 +353,52 @@ public abstract class AbstractStreamingChatService implements IChatService { .build(); // 步骤4: 配置OpenAI模型 - OpenAiChatModel PLANNER_MODEL = OpenAiChatModel.builder() - .baseUrl(chatModelVo.getApiHost()) + // OpenAiChatModel PLANNER_MODEL = OpenAiChatModel.builder() + // .baseUrl(chatModelVo.getApiHost()) + // .apiKey(chatModelVo.getApiKey()) + // .modelName(chatModelVo.getModelName()) + // .build(); + + + QwenChatModel qwenChatModel = QwenChatModel.builder() + // .baseUrl(chatModelVo.getApiHost()) .apiKey(chatModelVo.getApiKey()) .modelName(chatModelVo.getModelName()) - .build(); - + .build(); + SqlAgent sqlAgent = AgenticServices.agentBuilder(SqlAgent.class) - .chatModel(PLANNER_MODEL) + .chatModel( + qwenChatModel) .tools( - new QueryAllTablesTool(), - new QueryTableSchemaTool(), - new ExecuteSqlQueryTool() + SpringUtils.getBean(QueryAllTablesTool.class), // 必须通过 getBean 获取 + SpringUtils.getBean(QueryTableSchemaTool.class), + SpringUtils.getBean(ExecuteSqlQueryTool.class) ) .build(); - WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class) - .chatModel(PLANNER_MODEL) - .toolProvider(toolProvider) - .build(); + // WebSearchAgent searchAgent = AgenticServices.agentBuilder(WebSearchAgent.class) + // .chatModel(PLANNER_MODEL) + // .toolProvider(toolProvider) + // .build(); ChartGenerationAgent chartGenerationAgent = AgenticServices.agentBuilder(ChartGenerationAgent.class) - .chatModel(PLANNER_MODEL) + .chatModel( + qwenChatModel) .toolProvider(toolProvider1) .build(); - + String res = sqlAgent.getData(userMessage); + String res1 = chartGenerationAgent.generateChart(res); + System.out.println(res1); + System.out.println(res); SupervisorAgent supervisor = AgenticServices .supervisorBuilder() - .chatModel(PLANNER_MODEL) + .chatModel(qwenChatModel) .subAgents(sqlAgent, chartGenerationAgent) .responseStrategy(SupervisorResponseStrategy.LAST) .build(); String invoke = supervisor.invoke(userMessage); System.out.println(invoke); - return invoke; + return res1; } } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java index 0804f010..95f681fd 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatMessageServiceImpl.java @@ -1,6 +1,10 @@ package org.ruoyi.service.chat.impl; +import org.ruoyi.common.chat.domain.bo.chat.ChatMessageBo; import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; +import org.ruoyi.common.chat.domain.vo.chat.ChatMessageVo; +import org.ruoyi.common.chat.entity.chat.ChatMessage; +import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.StringUtils; import org.ruoyi.common.mybatis.core.page.TableDataInfo; @@ -10,11 +14,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.service.chat.IChatMessageService; import org.springframework.stereotype.Service; -import org.ruoyi.domain.bo.chat.ChatMessageBo; -import org.ruoyi.domain.vo.chat.ChatMessageVo; -import org.ruoyi.domain.entity.chat.ChatMessage; import org.ruoyi.mapper.chat.ChatMessageMapper; import java.util.List; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java index 32c8f85a..127d66a5 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatModelServiceImpl.java @@ -1,8 +1,8 @@ package org.ruoyi.service.chat.impl; -import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; -import org.ruoyi.common.chat.domain.entity.chat.ChatModel; +import org.ruoyi.common.chat.entity.chat.ChatModel; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.StringUtils; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java index 6b63e254..af71f54a 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/ChatServiceFacade.java @@ -1,12 +1,14 @@ package org.ruoyi.service.chat.impl; -import java.util.List; - -import org.ruoyi.common.chat.Service.IChatModelService; -import org.ruoyi.common.chat.Service.IChatService; +import cn.dev33.satoken.stp.StpUtil; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.chat.service.chat.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatService; import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; -import org.ruoyi.common.chat.domain.entity.chat.ChatContext; +import org.ruoyi.common.chat.entity.chat.ChatContext; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.chat.factory.ChatServiceFactory; import org.ruoyi.common.satoken.utils.LoginHelper; @@ -18,10 +20,7 @@ import org.ruoyi.service.vector.VectorStoreService; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import cn.dev33.satoken.stp.StpUtil; -import jakarta.servlet.http.HttpServletRequest; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import java.util.List; /** * 聊天服务业务实现 @@ -52,7 +51,6 @@ public class ChatServiceFacade { * @return SseEmitter */ public SseEmitter sseChat(ChatRequest chatRequest, HttpServletRequest request) { - // 1. 根据模型名称查询完整配置 ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); if (chatModelVo == null) { diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java index 2b9a7da4..ca719572 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/ChatMemoryUsageExample.java @@ -108,7 +108,7 @@ public class ChatMemoryUsageExample { log.info("=== 示例4:清理过期消息 ==="); /* // 假设已有IChatMessageService实例 - IChatMessageService chatMessageService = getBean(IChatMessageService.class); + AbstractChatMessageService chatMessageService = getBean(AbstractChatMessageService.class); // 场景:用户要求"忘记我们之前的对话" Long sessionId = 789L; diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java index 63d9e151..a10ccfb6 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/memory/PersistentChatMemoryStore.java @@ -4,8 +4,8 @@ import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.store.memory.chat.ChatMemoryStore; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.ChatMessageDTO; +import org.ruoyi.common.chat.service.chatMessage.IChatMessageService; import org.ruoyi.common.core.utils.SpringUtils; -import org.ruoyi.service.chat.IChatMessageService; import java.util.ArrayList; import java.util.List; 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 26c1858c..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 @@ -8,15 +8,20 @@ import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel; import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.mcp.McpToolProvider; +import dev.langchain4j.mcp.client.DefaultMcpClient; +import dev.langchain4j.mcp.client.McpClient; +import dev.langchain4j.mcp.client.transport.McpTransport; +import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.service.tool.ToolProvider; import lombok.extern.slf4j.Slf4j; import org.ruoyi.agent.McpAgent; +import org.ruoyi.config.McpSseConfig; import org.ruoyi.enums.ChatModeType; -import org.ruoyi.mcp.service.core.ToolProviderFactory; import org.ruoyi.service.chat.impl.AbstractStreamingChatService; -import org.ruoyi.common.core.utils.SpringUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; @@ -24,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服务调用 @@ -35,14 +42,23 @@ import java.util.List; @Slf4j public class QianWenChatServiceImpl extends AbstractStreamingChatService { - /** - * 千问开发者默认地址 - */ - private static final String QWEN_API_HOST = "https://dashscope.aliyuncs.com/api/v1"; + @Autowired + private McpSseConfig mcpSseConfig; // 添加文档解析的前缀字段 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() @@ -92,64 +108,92 @@ public class QianWenChatServiceImpl extends AbstractStreamingChatService { }).orElse(messagesWithMemory); } + /** + * 获取缓存键 + */ + private String getCacheKey(ChatModelVo chatModelVo) { + return chatModelVo.getApiKey() + ":" + chatModelVo.getModelName(); + } + + /** + * 初始化MCP客户端连接 + */ + private McpClient initializeMcpClient() { + // 步骤1:根据SSE对外暴露端点连接 + McpTransport httpMcpTransport = new StreamableHttpMcpTransport.Builder(). + url(mcpSseConfig.getUrl()). + logRequests(true). + build(); + + // 步骤2:开启客户端连接 + return new DefaultMcpClient.Builder() + .transport(httpMcpTransport) + .build(); + } + /** * 调用MCP服务(智能体) - * 使用统一的ToolProviderFactory获取所有已配置的工具(BUILTIN + MCP) - * * @param userMessage 用户信息 * @param chatModelVo 模型信息 * @return 返回LLM信息 */ protected String doAgent(String userMessage, ChatModelVo chatModelVo) { - // 步骤1: 获取统一工具提供工厂 - ToolProviderFactory toolProviderFactory = SpringUtils.getBean(ToolProviderFactory.class); - - // 步骤2: 获取 BUILTIN 工具对象 - List builtinTools = toolProviderFactory.getAllBuiltinToolObjects(); - - // 步骤3: 获取 MCP 工具提供者 - ToolProvider mcpToolProvider = toolProviderFactory.getAllEnabledMcpToolsProvider(); - - log.info("doAgent: BUILTIN tools count = {}, MCP tools enabled = {}", - builtinTools.size(), mcpToolProvider != null); - - // 步骤4: 加载LLM模型 - QwenChatModel qwenChatModel = QwenChatModel.builder() - .baseUrl(QWEN_API_HOST) - .apiKey(chatModelVo.getApiKey()) - .modelName(chatModelVo.getModelName()) - .build(); - - // 步骤5: 创建MCP Agent,使用所有已配置的工具 - // 使用 .tools() 传入 BUILTIN 工具对象(Java 对象,带 @Tool 注解的方法) - // 使用 .toolProvider() 传入 MCP 工具提供者(MCP 协议工具) - var agentBuilder = AgenticServices.agentBuilder(McpAgent.class) - .chatModel(qwenChatModel); - - // 添加 BUILTIN 工具(如果有) - if (!builtinTools.isEmpty()) { - agentBuilder.tools(builtinTools.toArray(new Object[0])); - log.debug("Added {} BUILTIN tools to agent", builtinTools.size()); + // 判断是否开启MCP服务 + if (!mcpSseConfig.isEnabled()) { + return ""; } - - // 添加 MCP 工具(如果有) - if (mcpToolProvider != null) { - agentBuilder.toolProvider(mcpToolProvider); - log.debug("Added MCP tool provider to agent"); + // 生成缓存键 + 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); + } - McpAgent mcpAgent = agentBuilder.build(); + // 获取或初始化MCP客户端 + McpClient mcpClient = mcpClientCache.computeIfAbsent(cacheKey, k -> initializeMcpClient()); - // 步骤6: 创建超级智能体协调MCP Agent - SupervisorAgent supervisor = AgenticServices - .supervisorBuilder() - .chatModel(qwenChatModel) - .subAgents(mcpAgent) - .responseStrategy(SupervisorResponseStrategy.LAST) - .build(); + // 步骤3:将mcp对象包装 + ToolProvider toolProvider = toolProviderCache.computeIfAbsent(cacheKey, k -> McpToolProvider.builder() + .mcpClients(List.of(mcpClient)) + .build()); - // 步骤7: 调用大模型LLM - return supervisor.invoke(userMessage); + // 步骤4:加载LLM模型对话 + QwenChatModel qwenChatModel = QwenChatModel.builder() + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .build(); + + // 步骤5:将MCP对象由智能体Agent管控 + McpAgent mcpAgent = AgenticServices.agentBuilder(McpAgent.class) + .chatModel(qwenChatModel) + .toolProvider(toolProvider) + .build(); + + // 步骤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/chat/impl/provider/PPIOServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java similarity index 64% rename from ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java rename to ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java index d6c0053b..222ee3cc 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/PPIOServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/chat/impl/provider/ZhiPuChatServiceImpl.java @@ -1,10 +1,9 @@ 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 dev.langchain4j.model.openai.OpenAiStreamingChatModel; import lombok.extern.slf4j.Slf4j; import org.ruoyi.common.chat.domain.dto.request.ChatRequest; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; @@ -14,37 +13,31 @@ import org.springframework.stereotype.Service; import java.util.List; - /** - * PPIO服务调用 + * 智谱AI服务调用 * - * @author ageerle@163.com - * @date 2025/12/13 + * @author zengxb + * @date 2026/02/26 */ @Service @Slf4j -public class PPIOServiceImpl extends AbstractStreamingChatService { - - @Override - protected StreamingChatModel buildStreamingChatModel(ChatModelVo chatModelVo, ChatRequest chatRequest) { - return OpenAiStreamingChatModel.builder() - .baseUrl(chatModelVo.getApiHost()) - .apiKey(chatModelVo.getApiKey()) - .modelName(chatModelVo.getModelName()) - .returnThinking(chatRequest.getEnableThinking()) - .build(); - } - +public class ZhiPuChatServiceImpl extends AbstractStreamingChatService { @Override protected void doChat(ChatModelVo chatModelVo, ChatRequest chatRequest, List messagesWithMemory, StreamingChatResponseHandler handler) { - StreamingChatModel streamingChatModel = buildStreamingChatModel(chatModelVo, chatRequest); + 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.PPIO.getCode(); + return ChatModeType.ZHI_PU.getCode(); } - } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java index 890bdc12..bdceb35f 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/graph/impl/GraphExtractionServiceImpl.java @@ -3,7 +3,7 @@ package org.ruoyi.service.graph.impl; import cn.hutool.core.util.StrUtil; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.domain.bo.chat.ChatModelBo; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.config.GraphExtractPrompt; 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..4a9065a7 --- /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.image.IImageGenerationService; +import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; +import org.ruoyi.common.chat.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(); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java index b7106120..ce02785e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/service/knowledge/impl/KnowledgeAttachServiceImpl.java @@ -2,7 +2,7 @@ package org.ruoyi.service.knowledge.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.RandomUtil; -import org.ruoyi.common.chat.Service.IChatModelService; +import org.ruoyi.common.chat.service.chat.IChatModelService; import org.ruoyi.common.chat.domain.vo.chat.ChatModelVo; import org.ruoyi.common.core.domain.dto.OssDTO; import org.ruoyi.common.core.service.OssService; diff --git a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java index cb78b33d..9b591bfc 100644 --- a/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java +++ b/ruoyi-modules/ruoyi-generator/src/main/java/org/ruoyi/generator/util/VelocityUtils.java @@ -12,6 +12,8 @@ import org.ruoyi.common.json.utils.JsonUtils; import org.ruoyi.common.mybatis.helper.DataBaseHelper; import org.ruoyi.generator.domain.GenTable; import org.ruoyi.generator.domain.GenTableColumn; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.apache.velocity.VelocityContext; import java.util.*; diff --git a/ruoyi-modules/ruoyi-mcp/pom.xml b/ruoyi-modules/ruoyi-mcp/pom.xml deleted file mode 100644 index e5dfa27a..00000000 --- a/ruoyi-modules/ruoyi-mcp/pom.xml +++ /dev/null @@ -1,77 +0,0 @@ - - - 4.0.0 - - - org.ruoyi - ruoyi-modules - ${revision} - - - ruoyi-mcp - - - MCP模块 - 管理MCP工具连接、市场集成和内置工具 - - - - - - org.ruoyi - ruoyi-common-core - - - - org.ruoyi - ruoyi-common-web - - - - org.ruoyi - ruoyi-common-mybatis - - - - org.ruoyi - ruoyi-common-log - - - - org.ruoyi - ruoyi-common-tenant - - - - org.ruoyi - ruoyi-common-security - - - - org.ruoyi - ruoyi-common-excel - - - - org.ruoyi - ruoyi-common-idempotent - - - - - dev.langchain4j - langchain4j-mcp - ${langchain4j.community.version} - - - - - com.fasterxml.jackson.core - jackson-databind - - - - - diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java deleted file mode 100644 index e686c9a2..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/McpProperties.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.ruoyi.mcp.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * MCP 配置属性 - * - * @author ruoyi team - */ -@Data -@Component -@ConfigurationProperties(prefix = "app.mcp") -public class McpProperties { - - /** - * 客户端配置 - */ - private ClientConfig client = new ClientConfig(); - - - @Data - public static class ClientConfig { - /** - * 请求超时时间(秒) - */ - private int requestTimeout = 30; - - /** - * 连接超时时间(秒) - */ - private int connectionTimeout = 10; - - /** - * 最大重试次数 - */ - private int maxRetries = 3; - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java deleted file mode 100644 index 30647c65..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/config/SystemToolInitializer.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.ruoyi.mcp.config; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.mcp.domain.entity.McpTool; -import org.ruoyi.mcp.enums.McpToolStatus; -import org.ruoyi.mcp.mapper.McpToolMapper; -import org.ruoyi.mcp.service.core.BuiltinToolDefinition; -import org.ruoyi.mcp.service.core.BuiltinToolRegistry; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; -import org.springframework.transaction.annotation.Transactional; - -/** - * 系统工具初始化器 - * 在应用启动时,将系统内置工具同步到数据库 - * 这样可以统一管理所有工具,支持动态启用/禁用 - * - * @author ruoyi team - */ -@Slf4j -@Component -@Order(999) // 确保在其他初始化器之后执行 -@RequiredArgsConstructor -public class SystemToolInitializer implements ApplicationRunner { - - private final McpToolMapper mcpToolMapper; - private final BuiltinToolRegistry builtinToolRegistry; - - @Override - @Transactional - public void run(ApplicationArguments args) { - log.info("开始同步系统内置工具到数据库..."); - - int addedCount = 0; - int existingCount = 0; - - for (BuiltinToolDefinition tool : builtinToolRegistry.getAllBuiltinTools()) { - try { - boolean added = syncBuiltinTool(tool); - if (added) { - addedCount++; - } else { - existingCount++; - } - } catch (Exception e) { - log.error("同步内置工具失败: {}", tool.name(), e); - } - } - - log.info("系统内置工具同步完成: 新增 {} 个, 已存在 {} 个", addedCount, existingCount); - } - - /** - * 同步单个内置工具到数据库 - * - * @param tool 工具定义 - * @return 是否新增(true=新增, false=已存在) - */ - private boolean syncBuiltinTool(BuiltinToolDefinition tool) { - // 检查是否已存在 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(McpTool::getName, tool.name()) - .eq(McpTool::getType, BuiltinToolRegistry.TYPE_BUILTIN); - - McpTool existing = mcpToolMapper.selectOne(wrapper); - - if (existing != null) { - // 已存在,更新描述信息(保留状态不变) - if (!tool.description().equals(existing.getDescription())) { - existing.setDescription(tool.description()); - mcpToolMapper.updateById(existing); - log.debug("更新内置工具描述: {}", tool.name()); - } - return false; - } - - // 新增 - McpTool newTool = new McpTool(); - newTool.setName(tool.name()); - newTool.setDescription(tool.description()); - newTool.setType(BuiltinToolRegistry.TYPE_BUILTIN); - newTool.setStatus(McpToolStatus.ENABLED.getValue()); // 默认启用 - newTool.setConfigJson(null); // 内置工具不需要配置 - mcpToolMapper.insert(newTool); - - log.info("新增内置工具: {} ({})", tool.name(), tool.displayName()); - return true; - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java deleted file mode 100644 index 52a0eacc..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpMarketController.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.ruoyi.mcp.controller; - -import cn.dev33.satoken.annotation.SaCheckPermission; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.ruoyi.common.core.domain.R; -import org.ruoyi.common.excel.utils.ExcelUtil; -import org.ruoyi.common.idempotent.annotation.RepeatSubmit; -import org.ruoyi.common.log.annotation.Log; -import org.ruoyi.common.log.enums.BusinessType; -import org.ruoyi.common.mybatis.core.page.PageQuery; -import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.common.web.core.BaseController; -import org.ruoyi.mcp.domain.bo.McpMarketBo; -import org.ruoyi.mcp.domain.dto.McpMarketListResult; -import org.ruoyi.mcp.domain.dto.McpMarketToolListResult; -import org.ruoyi.mcp.domain.vo.McpMarketVo; -import org.ruoyi.mcp.service.IMcpMarketService; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.List; -import java.util.Map; - -/** - * MCP 市场管理 Controller - * - * @author ruoyi team - */ -@Validated -@RequiredArgsConstructor -@RestController -@RequestMapping("/mcp/market") -public class McpMarketController extends BaseController { - - private final IMcpMarketService mcpMarketService; - - /** - * 查询市场列表 - */ - @SaCheckPermission("mcp:market:list") - @GetMapping("/list") - public TableDataInfo list(McpMarketBo bo, PageQuery pageQuery) { - return mcpMarketService.selectPageList(bo, pageQuery); - } - - /** - * 查询市场列表(不分页) - */ - @SaCheckPermission("mcp:market:list") - @GetMapping("/all") - public McpMarketListResult listAll( - @RequestParam(required = false) String keyword, - @RequestParam(required = false) String status) { - return mcpMarketService.listMarkets(keyword, status); - } - - /** - * 导出 MCP 市场列表 - */ - @SaCheckPermission("mcp:market:export") - @Log(title = "MCP市场管理", businessType = BusinessType.EXPORT) - @PostMapping("/export") - public void export(McpMarketBo bo, HttpServletResponse response) { - List list = mcpMarketService.queryList(bo); - ExcelUtil.exportExcel(list, "MCP市场", McpMarketVo.class, response); - } - - /** - * 根据市场ID获取详细信息 - * - * @param id 市场ID - */ - @SaCheckPermission("mcp:market:query") - @GetMapping("/{id}") - public R getInfo(@PathVariable Long id) { - return R.ok(mcpMarketService.selectById(id)); - } - - /** - * 新增市场 - */ - @SaCheckPermission("mcp:market:add") - @Log(title = "MCP市场管理", businessType = BusinessType.INSERT) - @RepeatSubmit() - @PostMapping - public R add(@Validated @RequestBody McpMarketBo bo) { - mcpMarketService.insert(bo); - return R.ok(); - } - - /** - * 修改市场 - */ - @SaCheckPermission("mcp:market:edit") - @Log(title = "MCP市场管理", businessType = BusinessType.UPDATE) - @RepeatSubmit() - @PutMapping - public R edit(@Validated @RequestBody McpMarketBo bo) { - mcpMarketService.update(bo); - return R.ok(); - } - - /** - * 删除市场 - * - * @param ids 市场ID串 - */ - @SaCheckPermission("mcp:market:remove") - @Log(title = "MCP市场管理", businessType = BusinessType.DELETE) - @DeleteMapping("/{ids}") - public R remove(@PathVariable Long[] ids) { - mcpMarketService.deleteByIds(List.of(ids)); - return R.ok(); - } - - /** - * 更新市场状态 - */ - @SaCheckPermission("mcp:market:edit") - @Log(title = "MCP市场管理", businessType = BusinessType.UPDATE) - @PutMapping("/{id}/status") - public R updateStatus(@PathVariable Long id, @RequestParam String status) { - mcpMarketService.updateStatus(id, status); - return R.ok(); - } - - /** - * 获取市场工具列表(分页) - */ - @SaCheckPermission("mcp:market:query") - @GetMapping("/{marketId}/tools") - public McpMarketToolListResult getMarketTools( - @PathVariable Long marketId, - @RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size) { - return mcpMarketService.getMarketTools(marketId, page, size); - } - - /** - * 刷新市场工具列表 - */ - @SaCheckPermission("mcp:market:edit") - @Log(title = "MCP市场管理", businessType = BusinessType.UPDATE) - @PostMapping("/{marketId}/refresh") - public R refreshMarketTools(@PathVariable Long marketId) { - return R.ok(mcpMarketService.refreshMarketTools(marketId)); - } - - /** - * 加载单个工具到本地 - */ - @SaCheckPermission("mcp:market:add") - @Log(title = "MCP市场管理", businessType = BusinessType.INSERT) - @PostMapping("/tools/{toolId}/load") - public R loadToolToLocal(@PathVariable Long toolId) { - mcpMarketService.loadToolToLocal(toolId); - return R.ok(); - } - - /** - * 批量加载工具到本地 - */ - @SaCheckPermission("mcp:market:add") - @Log(title = "MCP市场管理", businessType = BusinessType.INSERT) - @PostMapping("/tools/batch-load") - public R> batchLoadTools(@RequestBody List toolIds) { - int successCount = mcpMarketService.batchLoadTools(toolIds); - return R.ok(Map.of("successCount", successCount)); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java deleted file mode 100644 index eae82f72..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/controller/McpToolController.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.ruoyi.mcp.controller; - -import cn.dev33.satoken.annotation.SaCheckPermission; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.ruoyi.common.core.domain.R; -import org.ruoyi.common.excel.utils.ExcelUtil; -import org.ruoyi.common.idempotent.annotation.RepeatSubmit; -import org.ruoyi.common.log.annotation.Log; -import org.ruoyi.common.log.enums.BusinessType; -import org.ruoyi.common.mybatis.core.page.PageQuery; -import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.common.web.core.BaseController; -import org.ruoyi.mcp.domain.bo.McpToolBo; -import org.ruoyi.mcp.domain.dto.McpToolListResult; -import org.ruoyi.mcp.domain.dto.McpToolTestResult; -import org.ruoyi.mcp.domain.vo.McpToolVo; -import org.ruoyi.mcp.service.IMcpToolService; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * MCP 工具管理 Controller - * - * @author ruoyi team - */ -@Validated -@RequiredArgsConstructor -@RestController -@RequestMapping("/mcp/tool") -public class McpToolController extends BaseController { - - private final IMcpToolService mcpToolService; - - /** - * 查询 MCP 工具列表 - */ - @SaCheckPermission("mcp:tool:list") - @GetMapping("/list") - public TableDataInfo list(McpToolBo bo, PageQuery pageQuery) { - return mcpToolService.selectPageList(bo, pageQuery); - } - - /** - * 查询 MCP 工具列表(不分页) - */ - @SaCheckPermission("mcp:tool:list") - @GetMapping("/all") - public McpToolListResult listAll( - @RequestParam(required = false) String keyword, - @RequestParam(required = false) String type, - @RequestParam(required = false) String status) { - return mcpToolService.listTools(keyword, type, status); - } - - /** - * 导出 MCP 工具列表 - */ - @SaCheckPermission("mcp:tool:export") - @Log(title = "MCP工具管理", businessType = BusinessType.EXPORT) - @PostMapping("/export") - public void export(McpToolBo bo, HttpServletResponse response) { - List list = mcpToolService.queryList(bo); - ExcelUtil.exportExcel(list, "MCP工具", McpToolVo.class, response); - } - - /** - * 根据工具ID获取详细信息 - * - * @param id 工具ID - */ - @SaCheckPermission("mcp:tool:query") - @GetMapping("/{id}") - public R getInfo(@PathVariable Long id) { - return R.ok(mcpToolService.selectById(id)); - } - - /** - * 新增 MCP 工具 - */ - @SaCheckPermission("mcp:tool:add") - @Log(title = "MCP工具管理", businessType = BusinessType.INSERT) - @RepeatSubmit() - @PostMapping - public R add(@Validated @RequestBody McpToolBo bo) { - mcpToolService.insert(bo); - return R.ok(); - } - - /** - * 修改 MCP 工具 - */ - @SaCheckPermission("mcp:tool:edit") - @Log(title = "MCP工具管理", businessType = BusinessType.UPDATE) - @RepeatSubmit() - @PutMapping - public R edit(@Validated @RequestBody McpToolBo bo) { - mcpToolService.update(bo); - return R.ok(); - } - - /** - * 删除 MCP 工具 - * - * @param ids 工具ID串 - */ - @SaCheckPermission("mcp:tool:remove") - @Log(title = "MCP工具管理", businessType = BusinessType.DELETE) - @DeleteMapping("/{ids}") - public R remove(@PathVariable Long[] ids) { - mcpToolService.deleteByIds(List.of(ids)); - return R.ok(); - } - - /** - * 更新工具状态 - */ - @SaCheckPermission("mcp:tool:edit") - @Log(title = "MCP工具管理", businessType = BusinessType.UPDATE) - @PutMapping("/{id}/status") - public R updateStatus(@PathVariable Long id, @RequestParam String status) { - mcpToolService.updateStatus(id, status); - return R.ok(); - } - - /** - * 测试工具连接 - */ - @SaCheckPermission("mcp:tool:query") - @PostMapping("/{id}/test") - public R testTool(@PathVariable Long id) { - return R.ok(mcpToolService.testTool(id)); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java deleted file mode 100644 index 00493b8d..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpMarketBo.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.ruoyi.mcp.domain.bo; - -import io.github.linpeilie.annotations.AutoMapper; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.common.mybatis.core.domain.BaseEntity; -import org.ruoyi.mcp.domain.entity.McpMarket; - -/** - * MCP 市场业务对象 - * - * @author ruoyi team - */ -@Data -@EqualsAndHashCode(callSuper = true) -@AutoMapper(target = McpMarket.class, reverseConvertGenerate = false) -public class McpMarketBo extends BaseEntity { - - /** - * 市场ID - */ - private Long id; - - /** - * 市场名称 - */ - @NotBlank(message = "市场名称不能为空") - @Size(min = 0, max = 200, message = "市场名称不能超过{max}个字符") - private String name; - - /** - * 市场 URL - */ - @NotBlank(message = "市场URL不能为空") - @Size(min = 0, max = 500, message = "市场URL不能超过{max}个字符") - private String url; - - /** - * 市场描述 - */ - private String description; - - /** - * 认证配置(JSON格式) - */ - private String authConfig; - - /** - * 状态:ENABLED-启用, DISABLED-禁用 - */ - private String status; - -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java deleted file mode 100644 index 7bb2005a..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/bo/McpToolBo.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.ruoyi.mcp.domain.bo; - -import io.github.linpeilie.annotations.AutoMapper; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.Size; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.common.mybatis.core.domain.BaseEntity; -import org.ruoyi.mcp.domain.entity.McpTool; - -import java.io.Serial; - -/** - * MCP 工具业务对象 - * - * @author ruoyi team - */ -@Data -@EqualsAndHashCode(callSuper = true) -@AutoMapper(target = McpTool.class, reverseConvertGenerate = false) -public class McpToolBo extends BaseEntity { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * 工具ID - */ - private Long id; - - /** - * 工具名称 - */ - @NotBlank(message = "工具名称不能为空") - @Size(min = 0, max = 200, message = "工具名称不能超过{max}个字符") - private String name; - - /** - * 工具描述 - */ - private String description; - - /** - * 工具类型:LOCAL-本地, REMOTE-远程, BUILTIN-内置 - */ - @NotBlank(message = "工具类型不能为空") - private String type; - - /** - * 状态:ENABLED-启用, DISABLED-禁用 - */ - private String status; - - /** - * 配置信息(JSON格式) - */ - private String configJson; - -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java deleted file mode 100644 index 69f0a5d9..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketListResult.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.ruoyi.mcp.domain.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.ruoyi.mcp.domain.entity.McpMarket; - -import java.util.List; - -/** - * MCP 市场列表返回结果 - * - * @author ruoyi team - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class McpMarketListResult { - - /** - * 是否成功 - */ - private boolean success; - - /** - * 市场列表 - */ - private List data; - - /** - * 总数 - */ - private int total; - - public static McpMarketListResult of(List data) { - return McpMarketListResult.builder() - .success(true) - .data(data) - .total(data != null ? data.size() : 0) - .build(); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java deleted file mode 100644 index d52e91c7..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketRefreshResult.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.ruoyi.mcp.domain.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * MCP 市场工具刷新结果 - * - * @author ruoyi team - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class McpMarketRefreshResult { - - /** - * 是否成功 - */ - private boolean success; - - /** - * 消息 - */ - private String message; - - /** - * 新增工具数量 - */ - private int addedCount; - - /** - * 更新工具数量 - */ - private int updatedCount; -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java deleted file mode 100644 index 8f2e6a81..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpMarketToolListResult.java +++ /dev/null @@ -1,63 +0,0 @@ -package org.ruoyi.mcp.domain.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.ruoyi.mcp.domain.entity.McpMarketTool; - -import java.util.List; - -/** - * MCP 市场工具列表返回结果(分页) - * - * @author ruoyi team - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class McpMarketToolListResult { - - /** - * 是否成功 - */ - private boolean success; - - /** - * 工具列表 - */ - private List data; - - /** - * 总数 - */ - private long total; - - /** - * 当前页 - */ - private int page; - - /** - * 每页大小 - */ - private int size; - - /** - * 总页数 - */ - private long pages; - - public static McpMarketToolListResult of(List data, long total, int page, int size) { - long pages = (total + size - 1) / size; - return McpMarketToolListResult.builder() - .success(true) - .data(data) - .total(total) - .page(page) - .size(size) - .pages(pages) - .build(); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java deleted file mode 100644 index e330abc2..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolListResult.java +++ /dev/null @@ -1,44 +0,0 @@ -package org.ruoyi.mcp.domain.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.ruoyi.mcp.domain.entity.McpTool; - -import java.util.List; - -/** - * MCP 工具列表返回结果 - * - * @author ruoyi team - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class McpToolListResult { - - /** - * 是否成功 - */ - private boolean success; - - /** - * 工具列表 - */ - private List data; - - /** - * 总数 - */ - private int total; - - public static McpToolListResult of(List data) { - return McpToolListResult.builder() - .success(true) - .data(data) - .total(data != null ? data.size() : 0) - .build(); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java deleted file mode 100644 index eeb6f3dd..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/dto/McpToolTestResult.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.ruoyi.mcp.domain.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * MCP 工具测试结果 - * - * @author ruoyi team - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class McpToolTestResult { - - /** - * 是否成功 - */ - private boolean success; - - /** - * 消息 - */ - private String message; - - /** - * 发现的工具数量 - */ - private Integer toolCount; - - /** - * 工具名称列表 - */ - private List tools; - - public static McpToolTestResult success(String message, int toolCount, List tools) { - return McpToolTestResult.builder() - .success(true) - .message(message) - .toolCount(toolCount) - .tools(tools) - .build(); - } - - public static McpToolTestResult fail(String message) { - return McpToolTestResult.builder() - .success(false) - .message(message) - .build(); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java deleted file mode 100644 index 8d2cf21e..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarket.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.ruoyi.mcp.domain.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.common.tenant.core.TenantEntity; - -/** - * MCP 市场信息实体 - * - * @author ruoyi team - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("mcp_market_info") -public class McpMarket extends TenantEntity { - - /** - * 市场ID - */ - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - /** - * 市场名称 - */ - private String name; - - /** - * 市场 URL - */ - private String url; - - /** - * 市场描述 - */ - private String description; - - /** - * 认证配置(JSON格式) - */ - private String authConfig; - - /** - * 状态:ENABLED-启用, DISABLED-禁用 - */ - private String status; - -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java deleted file mode 100644 index 69b942ab..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpMarketTool.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.ruoyi.mcp.domain.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.common.mybatis.core.domain.BaseEntity; - -/** - * MCP 市场工具关联实体 - * - * @author ruoyi team - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("mcp_market_tool") -public class McpMarketTool extends BaseEntity { - - /** - * ID - */ - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - /** - * 市场 ID - */ - private Long marketId; - - /** - * 工具名称 - */ - private String toolName; - - /** - * 工具描述 - */ - private String toolDescription; - - /** - * 工具版本 - */ - private String toolVersion; - - /** - * 工具元数据(JSON格式) - */ - private String toolMetadata; - - /** - * 是否已加载到本地 - */ - private Boolean isLoaded; - - /** - * 关联的本地工具 ID - */ - private Long localToolId; - -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java deleted file mode 100644 index 0a5b3088..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/entity/McpTool.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.ruoyi.mcp.domain.entity; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; -import lombok.EqualsAndHashCode; -import org.ruoyi.common.tenant.core.TenantEntity; - - -/** - * MCP 工具信息实体 - * - * @author ruoyi team - */ -@Data -@EqualsAndHashCode(callSuper = true) -@TableName("mcp_tool_info") -public class McpTool extends TenantEntity { - - /** - * 工具ID - */ - @TableId(value = "id", type = IdType.AUTO) - private Long id; - - /** - * 工具名称 - */ - private String name; - - /** - * 工具描述 - */ - private String description; - - /** - * 工具类型:LOCAL-本地, REMOTE-远程, BUILTIN-内置 - */ - private String type; - - /** - * 状态:ENABLED-启用, DISABLED-禁用 - */ - private String status; - - /** - * 配置信息(JSON格式) - * LOCAL: {"command": "npx", "args": ["-y", "@example/mcp-server"], "env": {...}} - * REMOTE: {"baseUrl": "http://localhost:8080/mcp"} - * BUILTIN: null - */ - private String configJson; - -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java deleted file mode 100644 index 243604cf..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpMarketVo.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.ruoyi.mcp.domain.vo; - -import cn.idev.excel.annotation.ExcelIgnoreUnannotated; -import cn.idev.excel.annotation.ExcelProperty; -import io.github.linpeilie.annotations.AutoMapper; -import lombok.Data; -import org.ruoyi.mcp.domain.entity.McpMarket; - -import java.io.Serial; -import java.io.Serializable; -import java.util.Date; - -/** - * MCP 市场视图对象 - * - * @author ruoyi team - */ -@Data -@ExcelIgnoreUnannotated -@AutoMapper(target = McpMarket.class) -public class McpMarketVo implements Serializable { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * 市场ID - */ - @ExcelProperty(value = "市场ID") - private Long id; - - /** - * 市场名称 - */ - @ExcelProperty(value = "市场名称") - private String name; - - /** - * 市场 URL - */ - @ExcelProperty(value = "市场URL") - private String url; - - /** - * 市场描述 - */ - @ExcelProperty(value = "市场描述") - private String description; - - /** - * 认证配置 - */ - @ExcelProperty(value = "认证配置") - private String authConfig; - - /** - * 状态 - */ - @ExcelProperty(value = "状态") - private String status; - - /** - * 创建时间 - */ - @ExcelProperty(value = "创建时间") - private Date createTime; - - /** - * 更新时间 - */ - @ExcelProperty(value = "更新时间") - private Date updateTime; - -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java deleted file mode 100644 index 88cd2494..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/domain/vo/McpToolVo.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.ruoyi.mcp.domain.vo; - -import cn.idev.excel.annotation.ExcelIgnoreUnannotated; -import cn.idev.excel.annotation.ExcelProperty; -import io.github.linpeilie.annotations.AutoMapper; -import lombok.Data; -import org.ruoyi.mcp.domain.entity.McpTool; - -import java.io.Serial; -import java.io.Serializable; -import java.util.Date; - -/** - * MCP 工具视图对象 - * - * @author ruoyi team - */ -@Data -@ExcelIgnoreUnannotated -@AutoMapper(target = McpTool.class) -public class McpToolVo implements Serializable { - - @Serial - private static final long serialVersionUID = 1L; - - /** - * 工具ID - */ - @ExcelProperty(value = "工具ID") - private Long id; - - /** - * 工具名称 - */ - @ExcelProperty(value = "工具名称") - private String name; - - /** - * 工具描述 - */ - @ExcelProperty(value = "工具描述") - private String description; - - /** - * 工具类型 - */ - @ExcelProperty(value = "工具类型") - private String type; - - /** - * 状态 - */ - @ExcelProperty(value = "状态") - private String status; - - /** - * 配置信息 - */ - @ExcelProperty(value = "配置信息") - private String configJson; - - /** - * 创建时间 - */ - @ExcelProperty(value = "创建时间") - private Date createTime; - - /** - * 更新时间 - */ - @ExcelProperty(value = "更新时间") - private Date updateTime; - -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java deleted file mode 100644 index caf8c49b..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/enums/McpToolStatus.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.ruoyi.mcp.enums; - -import lombok.Getter; - -/** - * MCP 工具状态枚举 - * - * @author ruoyi team - */ -@Getter -public enum McpToolStatus { - - /** - * 启用状态 - */ - ENABLED("ENABLED", "启用"), - - /** - * 禁用状态 - */ - DISABLED("DISABLED", "禁用"); - - /** - * 状态值(存储到数据库) - */ - private final String value; - - /** - * 状态描述 - */ - private final String description; - - McpToolStatus(String value, String description) { - this.value = value; - this.description = description; - } - - /** - * 判断是否为启用状态 - * - * @param value 状态值 - * @return 是否启用 - */ - public static boolean isEnabled(String value) { - return ENABLED.value.equals(value); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java deleted file mode 100644 index 7d9ed31d..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.ruoyi.mcp.mapper; - -import org.apache.ibatis.annotations.Mapper; -import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; -import org.ruoyi.mcp.domain.entity.McpMarket; -import org.ruoyi.mcp.domain.vo.McpMarketVo; - -/** - * MCP 市场信息 Mapper - * - * @author ruoyi team - */ -@Mapper -public interface McpMarketMapper extends BaseMapperPlus { -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java deleted file mode 100644 index 2211906d..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpMarketToolMapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.ruoyi.mcp.mapper; - -import org.apache.ibatis.annotations.Mapper; -import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; -import org.ruoyi.mcp.domain.entity.McpMarketTool; - -/** - * MCP 市场工具关联 Mapper - * - * @author ruoyi team - */ -@Mapper -public interface McpMarketToolMapper extends BaseMapperPlus { -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java deleted file mode 100644 index 5e46f399..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/mapper/McpToolMapper.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.ruoyi.mcp.mapper; - -import org.apache.ibatis.annotations.Mapper; -import org.ruoyi.common.mybatis.core.mapper.BaseMapperPlus; -import org.ruoyi.mcp.domain.entity.McpTool; -import org.ruoyi.mcp.domain.vo.McpToolVo; - -/** - * MCP 工具信息 Mapper - * - * @author ruoyi team - */ -@Mapper -public interface McpToolMapper extends BaseMapperPlus { -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java deleted file mode 100644 index b7a68b1f..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpMarketService.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.ruoyi.mcp.service; - -import org.ruoyi.common.mybatis.core.page.PageQuery; -import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.mcp.domain.bo.McpMarketBo; -import org.ruoyi.mcp.domain.dto.McpMarketListResult; -import org.ruoyi.mcp.domain.dto.McpMarketRefreshResult; -import org.ruoyi.mcp.domain.dto.McpMarketToolListResult; -import org.ruoyi.mcp.domain.vo.McpMarketVo; - -import java.util.List; - -/** - * MCP 市场服务接口 - * - * @author ruoyi team - */ -public interface IMcpMarketService { - - /** - * 分页查询市场列表 - * - * @param bo 查询条件 - * @param pageQuery 分页参数 - * @return 市场分页列表 - */ - TableDataInfo selectPageList(McpMarketBo bo, PageQuery pageQuery); - - /** - * 查询市场列表(不分页) - * - * @param keyword 关键词 - * @param status 状态 - * @return 市场列表结果 - */ - McpMarketListResult listMarkets(String keyword, String status); - - /** - * 查询市场列表(用于导出) - * - * @param bo 查询条件 - * @return 市场列表 - */ - List queryList(McpMarketBo bo); - - /** - * 根据ID查询市场 - * - * @param id 市场ID - * @return 市场信息 - */ - McpMarketVo selectById(Long id); - - /** - * 新增市场 - * - * @param bo 市场信息 - * @return 新增后的市场ID - */ - String insert(McpMarketBo bo); - - /** - * 更新市场 - * - * @param bo 市场信息 - * @return 结果 - */ - String update(McpMarketBo bo); - - /** - * 删除市场 - * - * @param ids 市场 ID 列表 - */ - void deleteByIds(List ids); - - /** - * 更新市场状态 - * - * @param id 市场 ID - * @param status 状态 - */ - void updateStatus(Long id, String status); - - /** - * 获取市场工具列表 - * - * @param marketId 市场 ID - * @param page 页码 - * @param size 每页大小 - * @return 工具列表结果 - */ - McpMarketToolListResult getMarketTools(Long marketId, int page, int size); - - /** - * 刷新市场工具列表 - * - * @param marketId 市场 ID - * @return 刷新结果 - */ - McpMarketRefreshResult refreshMarketTools(Long marketId); - - /** - * 加载工具到本地 - * - * @param toolId 市场工具 ID - */ - void loadToolToLocal(Long toolId); - - /** - * 批量加载工具到本地 - * - * @param toolIds 工具 ID 列表 - * @return 成功加载的数量 - */ - int batchLoadTools(List toolIds); -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java deleted file mode 100644 index d7cba323..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/IMcpToolService.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.ruoyi.mcp.service; - -import org.ruoyi.common.mybatis.core.page.PageQuery; -import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.mcp.domain.bo.McpToolBo; -import org.ruoyi.mcp.domain.dto.McpToolListResult; -import org.ruoyi.mcp.domain.dto.McpToolTestResult; -import org.ruoyi.mcp.domain.vo.McpToolVo; - -import java.util.List; - -/** - * MCP 工具服务接口 - * - * @author ruoyi team - */ -public interface IMcpToolService { - - /** - * 分页查询工具列表 - * - * @param bo 查询条件 - * @param pageQuery 分页参数 - * @return 工具分页列表 - */ - TableDataInfo selectPageList(McpToolBo bo, PageQuery pageQuery); - - /** - * 查询工具列表(不分页) - * - * @param keyword 关键词 - * @param type 类型 - * @param status 状态 - * @return 工具列表结果 - */ - McpToolListResult listTools(String keyword, String type, String status); - - /** - * 查询工具列表(用于导出) - * - * @param bo 查询条件 - * @return 工具列表 - */ - List queryList(McpToolBo bo); - - /** - * 根据ID查询工具 - * - * @param id 工具ID - * @return 工具信息 - */ - McpToolVo selectById(Long id); - - /** - * 新增工具 - * - * @param bo 工具信息 - * @return 新增后的工具ID - */ - String insert(McpToolBo bo); - - /** - * 更新工具 - * - * @param bo 工具信息 - * @return 结果 - */ - String update(McpToolBo bo); - - /** - * 删除工具 - * - * @param ids 工具 ID 列表 - */ - void deleteByIds(List ids); - - /** - * 更新工具状态 - * - * @param id 工具 ID - * @param status 状态 - */ - void updateStatus(Long id, String status); - - /** - * 测试工具连接 - * - * @param id 工具 ID - * @return 测试结果 - */ - McpToolTestResult testTool(Long id); -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java deleted file mode 100644 index 96890034..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolDefinition.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.ruoyi.mcp.service.core; - -/** - * 内置工具定义 - * 用于描述系统内置的工具信息 - * - * @param name 工具名称(唯一标识) - * @param displayName 显示名称 - * @param description 工具描述 - * @author ruoyi team - */ -public record BuiltinToolDefinition(String name, String displayName, String description) { -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java deleted file mode 100644 index d72922b0..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolProvider.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.ruoyi.mcp.service.core; - -/** - * 内置工具提供者接口 - * 所有系统内置工具都应实现此接口,以便自动注册到 BuiltinToolRegistry - * - * @author ruoyi team - * - *

使用方式: - *

- * {@code
- * @Component
- * public class MyTool implements BuiltinToolProvider {
- *     @Override
- *     public String getToolName() {
- *         return "my_tool";
- *     }
- *
- *     @Override
- *     public String getDisplayName() {
- *         return "我的工具";
- *     }
- *
- *     @Override
- *     public String getDescription() {
- *         return "工具描述...";
- *     }
- * }
- * }
- * 
- */ -public interface BuiltinToolProvider { - - /** - * 获取工具名称(唯一标识,用于数据库存储) - * 建议使用 snake_case 格式,如:list_directory, edit_file - * - * @return 工具名称 - */ - String getToolName(); - - /** - * 获取工具显示名称(用于 UI 展示) - * - * @return 显示名称 - */ - String getDisplayName(); - - /** - * 获取工具描述(用于 AI 理解工具用途) - * - * @return 工具描述 - */ - String getDescription(); -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java deleted file mode 100644 index b3953b7f..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/BuiltinToolRegistry.java +++ /dev/null @@ -1,129 +0,0 @@ -package org.ruoyi.mcp.service.core; - -import jakarta.annotation.PostConstruct; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * 内置工具注册表 - * 自动发现并注册所有实现 {@link BuiltinToolProvider} 接口的工具 - * - *

工具注册流程: - *

    - *
  1. Spring 自动注入所有 {@link BuiltinToolProvider} 实现
  2. - *
  3. {@link #init()} 方法在 Bean 初始化后自动调用
  4. - *
  5. 将所有工具注册到内部 Map
  6. - *
- * - *

添加新工具只需: - *

    - *
  1. 创建一个类实现 {@link BuiltinToolProvider} 接口
  2. - *
  3. 添加 {@code @Component} 注解
  4. - *
  5. 工具会自动被发现和注册
  6. - *
- * - * @author ruoyi team - */ -@Slf4j -@Component -@RequiredArgsConstructor -public class BuiltinToolRegistry { - - /** - * 工具类型常量 - */ - public static final String TYPE_BUILTIN = "BUILTIN"; - - /** - * Spring 自动注入所有实现 BuiltinToolProvider 接口的 Bean - */ - private final List toolProviders; - - /** - * 内置工具定义映射表 (工具名称 -> 工具提供者) - */ - private final Map registeredTools = new ConcurrentHashMap<>(); - - /** - * 初始化方法,在 Bean 创建后自动调用 - * 将所有 BuiltinToolProvider 注册到内部 Map - */ - @PostConstruct - public void init() { - log.info("开始注册内置工具,发现 {} 个工具提供者", toolProviders.size()); - - for (BuiltinToolProvider provider : toolProviders) { - String toolName = provider.getToolName(); - - if (registeredTools.containsKey(toolName)) { - log.warn("工具名称重复: {},将覆盖原有注册", toolName); - } - - registeredTools.put(toolName, provider); - log.info("注册内置工具: {} ({})", toolName, provider.getDisplayName()); - } - - log.info("内置工具注册完成,共 {} 个工具", registeredTools.size()); - } - - /** - * 获取工具提供者 - * - * @param toolName 工具名称 - * @return 工具提供者,如果不存在则返回 null - */ - public BuiltinToolProvider getToolProvider(String toolName) { - return registeredTools.get(toolName); - } - - /** - * 检查工具是否已注册 - * - * @param toolName 工具名称 - * @return 是否已注册 - */ - public boolean hasTool(String toolName) { - return registeredTools.containsKey(toolName); - } - - /** - * 获取所有内置工具定义 - * - * @return 内置工具定义集合 - */ - public Collection getAllBuiltinTools() { - return registeredTools.values().stream() - .map(provider -> new BuiltinToolDefinition( - provider.getToolName(), - provider.getDisplayName(), - provider.getDescription() - )) - .toList(); - } - - /** - * 获取所有内置工具对象 - * 这些对象包含 @Tool 注解的方法,可直接用于 AgenticServices - * - * @return 内置工具对象列表 - */ - public List getAllBuiltinToolObjects() { - return List.copyOf(registeredTools.values()); - } - - /** - * 根据工具名称获取工具对象 - * - * @param toolName 工具名称 - * @return 工具对象,如果不存在则返回 null - */ - public Object getBuiltinToolObject(String toolName) { - return registeredTools.get(toolName); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java deleted file mode 100644 index 71a853f3..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/LangChain4jMcpToolProviderService.java +++ /dev/null @@ -1,448 +0,0 @@ -package org.ruoyi.mcp.service.core; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import dev.langchain4j.mcp.McpToolProvider; -import dev.langchain4j.mcp.client.DefaultMcpClient; -import dev.langchain4j.mcp.client.McpClient; -import dev.langchain4j.mcp.client.transport.McpTransport; -import dev.langchain4j.mcp.client.transport.http.StreamableHttpMcpTransport; -import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport; -import dev.langchain4j.service.tool.ToolProvider; -import jakarta.annotation.PreDestroy; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.mcp.domain.entity.McpTool; -import org.ruoyi.mcp.enums.McpToolStatus; -import org.ruoyi.mcp.mapper.McpToolMapper; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * LangChain4j MCP 工具提供者服务 - * 从数据库读取 MCP 工具配置,创建 LangChain4j 的 McpToolProvider 供 Agent 使用 - * - * @author ruoyi team - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class LangChain4jMcpToolProviderService { - - /** - * 最大失败次数,超过此次数将暂时禁用工具 - */ - private static final int MAX_FAILURE_COUNT = 3; - /** - * 工具禁用时长(毫秒),默认 5 分钟 - */ - private static final long DISABLE_DURATION = 5 * 60 * 1000; - private final McpToolMapper mcpToolMapper; - private final ObjectMapper objectMapper; - /** - * 缓存活跃的 MCP Client - */ - private final Map activeClients = new ConcurrentHashMap<>(); - /** - * 工具健康状态缓存(工具ID -> 是否健康) - */ - private final Map toolHealthStatus = new ConcurrentHashMap<>(); - /** - * 工具失败次数(工具ID -> 失败次数) - */ - private final Map toolFailureCount = new ConcurrentHashMap<>(); - /** - * 工具禁用时间(工具ID -> 禁用截止时间戳) - */ - private final Map toolDisabledUntil = new ConcurrentHashMap<>(); - - /** - * 根据工具 ID 列表获取 ToolProvider - * - * @param toolIds 工具 ID 列表 - * @return ToolProvider 实例 - */ - public ToolProvider getToolProvider(List toolIds) { - if (toolIds == null || toolIds.isEmpty()) { - return McpToolProvider.builder().build(); - } - - List clients = new ArrayList<>(); - for (Long toolId : toolIds) { - try { - McpClient client = getOrCreateClient(toolId); - if (client != null) { - clients.add(client); - } - } catch (Exception e) { - log.error("Failed to create MCP client for tool {}: {}", toolId, e.getMessage()); - } - } - - if (clients.isEmpty()) { - return McpToolProvider.builder().build(); - } - - return McpToolProvider.builder() - .mcpClients(clients) - .build(); - } - - /** - * 获取所有启用的 MCP 工具的 ToolProvider - * - * @return ToolProvider 实例 - */ - public ToolProvider getAllEnabledToolsProvider() { - List enabledTools = mcpToolMapper.selectList( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .ne(McpTool::getType,"BUILTIN") - .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) - ); - - if (enabledTools.isEmpty()) { - return McpToolProvider.builder().build(); - } - - List toolIds = enabledTools.stream() - .map(McpTool::getId) - .toList(); - - return getToolProvider(toolIds); - } - - /** - * 获取指定名称的 MCP 工具的 ToolProvider - * - * @param toolNames 工具名称列表 - * @return ToolProvider 实例 - */ - public ToolProvider getToolProviderByNames(List toolNames) { - if (toolNames == null || toolNames.isEmpty()) { - return McpToolProvider.builder().build(); - } - - List tools = mcpToolMapper.selectList( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .in(McpTool::getName, toolNames) - .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) - ); - - if (tools.isEmpty()) { - return McpToolProvider.builder().build(); - } - - List toolIds = tools.stream() - .map(McpTool::getId) - .toList(); - - return getToolProvider(toolIds); - } - - /** - * 获取或创建 MCP Client - */ - private McpClient getOrCreateClient(Long toolId) { - // 检查工具是否被禁用 - if (isToolDisabled(toolId)) { - log.warn("Tool {} is temporarily disabled", toolId); - return null; - } - - // 返回缓存的客户端 - McpClient cachedClient = activeClients.get(toolId); - if (cachedClient != null && isToolHealthy(toolId)) { - return cachedClient; - } - - // 查询工具配置 - McpTool tool = mcpToolMapper.selectById(toolId); - if (tool == null || !McpToolStatus.isEnabled(tool.getStatus())) { - return null; - } - - String toolType = tool.getType(); - // 只支持 LOCAL 和 REMOTE 类型 - if (!ToolProviderFactory.TYPE_LOCAL.equals(toolType) && !ToolProviderFactory.TYPE_REMOTE.equals(toolType)) { - log.warn("Unsupported tool type: {} for tool: {}", toolType, tool.getName()); - return null; - } - - // 创建并缓存新客户端 - try { - McpClient client = createMcpClient(tool); - if (client == null) { - log.warn("Failed to create MCP client for tool: {}", tool.getName()); - return null; - } - activeClients.put(toolId, client); - markToolHealthy(toolId); - log.info("Created MCP client for tool: {}", tool.getName()); - return client; - } catch (Exception e) { - log.error("Failed to create MCP client for tool {}: {}", tool.getName(), e.getMessage(), e); - handleToolFailure(toolId); - return null; - } - } - - /** - * 检查工具是否被暂时禁用 - */ - private boolean isToolDisabled(Long toolId) { - Long disabledUntil = toolDisabledUntil.get(toolId); - if (disabledUntil == null) { - return false; - } - if (System.currentTimeMillis() > disabledUntil) { - // 禁用时间已过,重新启用 - toolDisabledUntil.remove(toolId); - toolFailureCount.put(toolId, 0); - log.info("Tool {} is re-enabled after disable period", toolId); - return false; - } - return true; - } - - /** - * 检查工具是否健康 - */ - private boolean isToolHealthy(Long toolId) { - return toolHealthStatus.getOrDefault(toolId, true); - } - - /** - * 标记工具为健康状态 - */ - private void markToolHealthy(Long toolId) { - toolHealthStatus.put(toolId, true); - toolFailureCount.put(toolId, 0); - } - - /** - * 处理工具失败 - */ - private void handleToolFailure(Long toolId) { - int failures = toolFailureCount.getOrDefault(toolId, 0) + 1; - toolFailureCount.put(toolId, failures); - toolHealthStatus.put(toolId, false); - - if (failures >= MAX_FAILURE_COUNT) { - // 禁用工具一段时间 - long disableUntil = System.currentTimeMillis() + DISABLE_DURATION; - toolDisabledUntil.put(toolId, disableUntil); - log.warn("Tool {} has failed {} times, disabling until {}", - toolId, failures, new java.util.Date(disableUntil)); - } else { - log.warn("Tool {} has failed {} times (max: {})", - toolId, failures, MAX_FAILURE_COUNT); - } - } - - /** - * 手动检查工具健康状态 - * - * @param toolId 工具 ID - * @return 工具是否健康 - */ - public boolean checkToolHealth(Long toolId) { - McpTool tool = mcpToolMapper.selectById(toolId); - if (tool == null || !McpToolStatus.isEnabled(tool.getStatus())) { - return false; - } - - try { - // 尝试创建客户端来验证连接 - McpClient client = createMcpClient(tool); - if (client != null) { - markToolHealthy(toolId); - return true; - } - return false; - } catch (Exception e) { - log.error("Health check failed for tool {}: {}", tool.getName(), e.getMessage()); - handleToolFailure(toolId); - return false; - } - } - - /** - * 获取所有工具的健康状态 - * - * @return 工具 ID -> 健康状态的映射 - */ - public Map getAllToolsHealthStatus() { - List allTools = mcpToolMapper.selectList( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) - ); - - Map statusMap = new ConcurrentHashMap<>(); - for (McpTool tool : allTools) { - boolean isHealthy = isToolHealthy(tool.getId()) && !isToolDisabled(tool.getId()); - statusMap.put(tool.getId(), isHealthy); - } - return statusMap; - } - - /** - * 根据工具配置创建 MCP Client - */ - private McpClient createMcpClient(McpTool tool) throws Exception { - if ("LOCAL".equals(tool.getType())) { - return createStdioClient(tool); - } else if ("REMOTE".equals(tool.getType())) { - return createRemoteClient(tool); - } - return null; - } - - /** - * 创建 STDIO Client (本地命令行工具) - */ - private McpClient createStdioClient(McpTool tool) throws Exception { - String configJson = tool.getConfigJson(); - if (configJson == null || configJson.isBlank()) { - throw new IllegalArgumentException("Config JSON is required for LOCAL type tool"); - } - - JsonNode configNode = objectMapper.readTree(configJson); - - // 解析命令 - String command = null; - List args = new ArrayList<>(); - - if (configNode.has("command")) { - command = configNode.get("command").asText(); - } - - if (configNode.has("args") && configNode.get("args").isArray()) { - for (JsonNode arg : configNode.get("args")) { - args.add(arg.asText()); - } - } - - if (command == null || command.isBlank()) { - throw new IllegalArgumentException("Command is required in config JSON"); - } - - // 处理 Windows 系统的命令 - command = resolveCommand(command); - - // 构建完整命令列表 - List fullCommand = new ArrayList<>(); - fullCommand.add(command); - fullCommand.addAll(args); - - log.info("Creating STDIO MCP client for tool: {}, command: {}", tool.getName(), fullCommand); - - // 创建传输层 - McpTransport transport = StdioMcpTransport.builder() - .command(fullCommand) - .logEvents(true) - .build(); - - // 创建客户端 - return new DefaultMcpClient.Builder() - .transport(transport) - .build(); - } - - /** - * 创建远程 HTTP/SSE Client - */ - private McpClient createRemoteClient(McpTool tool) throws Exception { - String configJson = tool.getConfigJson(); - if (configJson == null || configJson.isBlank()) { - throw new IllegalArgumentException("Config JSON is required for REMOTE type tool"); - } - - JsonNode configNode = objectMapper.readTree(configJson); - - if (!configNode.has("url")) { - throw new IllegalArgumentException("baseUrl is required in config JSON for REMOTE type tool"); - } - - String baseUrl = configNode.get("url").asText(); - log.info("Creating HTTP/SSE MCP client for tool: {}, baseUrl: {}", tool.getName(), baseUrl); - - // 创建 HTTP/SSE 传输层 - McpTransport transport = StreamableHttpMcpTransport.builder() - .url(baseUrl) - .logRequests(true) - .build(); - - // 创建客户端 - return new DefaultMcpClient.Builder() - .transport(transport) - .build(); - } - - /** - * 解析命令,处理 Windows 系统的兼容性问题 - */ - private String resolveCommand(String command) { - if (command == null || command.isBlank()) { - return command; - } - - boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - - if (isWindows) { - String lowerCommand = command.toLowerCase(); - if (lowerCommand.equals("npx") || lowerCommand.equals("npm") || - lowerCommand.equals("node") || lowerCommand.equals("pnpm") || - lowerCommand.equals("yarn") || lowerCommand.equals("uvx") || - lowerCommand.equals("uv")) { - String resolvedCommand = command + ".cmd"; - log.debug("Windows detected, resolved command: {} -> {}", command, resolvedCommand); - return resolvedCommand; - } - } - - return command; - } - - /** - * 刷新指定工具的客户端连接 - */ - public void refreshClient(Long toolId) { - closeClient(toolId); - log.info("Refreshed MCP client for tool: {}", toolId); - } - - /** - * 关闭指定工具的客户端连接 - */ - private void closeClient(Long toolId) { - McpClient client = activeClients.remove(toolId); - if (client != null) { - try { - // LangChain4j McpClient 没有 close 方法,直接移除即可 - log.info("Removed MCP client for tool: {}", toolId); - } catch (Exception e) { - log.warn("Error closing MCP client for tool {}: {}", toolId, e.getMessage()); - } - } - } - - /** - * 应用关闭时清理所有连接 - */ - @PreDestroy - public void cleanup() { - log.info("Cleaning up {} MCP clients...", activeClients.size()); - activeClients.keySet().forEach(this::closeClient); - } - - /** - * 获取当前活跃的客户端数量 - */ - public int getActiveClientCount() { - return activeClients.size(); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java deleted file mode 100644 index edf21bb3..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/core/ToolProviderFactory.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.ruoyi.mcp.service.core; - -import dev.langchain4j.mcp.McpToolProvider; -import dev.langchain4j.service.tool.ToolProvider; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.mcp.domain.entity.McpTool; -import org.ruoyi.mcp.enums.McpToolStatus; -import org.ruoyi.mcp.mapper.McpToolMapper; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * 统一工具提供工厂 - * 整合所有类型的MCP工具提供者,为Agent和Chat服务提供统一的工具获取入口 - * - *

支持的工具类型: - *

    - *
  • BUILTIN - 内置工具(如文件操作工具)
  • - *
  • LOCAL - 本地STDIO工具(通过命令行启动的MCP服务器)
  • - *
  • REMOTE - 远程HTTP/SSE工具(通过网络连接的MCP服务器)
  • - *
- * - * @author ruoyi team - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class ToolProviderFactory { - - /** - * 工具类型常量 - */ - public static final String TYPE_BUILTIN = "BUILTIN"; - public static final String TYPE_LOCAL = "LOCAL"; - public static final String TYPE_REMOTE = "REMOTE"; - private final BuiltinToolRegistry builtinToolRegistry; - private final LangChain4jMcpToolProviderService langChain4jMcpToolProviderService; - private final McpToolMapper mcpToolMapper; - - /** - * 根据工具ID列表获取LangChain4j的ToolProvider - * 用于LangChain4j Agent框架使用工具 - * - * @param toolIds 工具ID列表 - * @return ToolProvider实例 - */ - public ToolProvider getToolProvider(List toolIds) { - if (toolIds == null || toolIds.isEmpty()) { - return McpToolProvider.builder().build(); - } - - // 只获取非内置工具(LangChain4j的MCP工具) - List mcpToolIds = new ArrayList<>(); - - for (Long toolId : toolIds) { - McpTool tool = mcpToolMapper.selectById(toolId); - if (tool != null && McpToolStatus.isEnabled(tool.getStatus())) { - if (!TYPE_BUILTIN.equals(tool.getType())) { - mcpToolIds.add(toolId); - } - } - } - - // 使用LangChain4j服务获取MCP工具的ToolProvider - return langChain4jMcpToolProviderService.getToolProvider(mcpToolIds); - } - - /** - * 根据工具名称列表获取LangChain4j的ToolProvider - * - * @param toolNames 工具名称列表 - * @return ToolProvider实例 - */ - public ToolProvider getToolProviderByNames(List toolNames) { - if (toolNames == null || toolNames.isEmpty()) { - return McpToolProvider.builder().build(); - } - - // 直接使用LangChain4j服务,它已经实现了按名称查询 - return langChain4jMcpToolProviderService.getToolProviderByNames(toolNames); - } - - /** - * 获取所有已启用的MCP工具的ToolProvider - * - * @return ToolProvider实例 - */ - public ToolProvider getAllEnabledMcpToolsProvider() { - return langChain4jMcpToolProviderService.getAllEnabledToolsProvider(); - } - - /** - * 检查工具是否为内置工具 - * - * @param toolName 工具名称 - * @return 是否为内置工具 - */ - public boolean isBuiltinTool(String toolName) { - return builtinToolRegistry.hasTool(toolName); - } - - /** - * 根据工具名称获取工具ID - * - * @param toolName 工具名称 - * @return 工具ID,未找到返回null - */ - public Long getToolIdByName(String toolName) { - McpTool tool = mcpToolMapper.selectOne( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .eq(McpTool::getName, toolName) - .last("LIMIT 1") - ); - return tool != null ? tool.getId() : null; - } - - /** - * 根据工具名称列表获取工具ID列表 - * - * @param toolNames 工具名称列表 - * @return 工具ID列表 - */ - public List getToolIdsByNames(List toolNames) { - if (toolNames == null || toolNames.isEmpty()) { - return List.of(); - } - - List tools = mcpToolMapper.selectList( - new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() - .in(McpTool::getName, toolNames) - .eq(McpTool::getStatus, McpToolStatus.ENABLED.getValue()) - ); - - return tools.stream() - .map(McpTool::getId) - .toList(); - } - - /** - * 刷新工具连接 - * - * @param toolId 工具ID - */ - public void refreshTool(Long toolId) { - langChain4jMcpToolProviderService.refreshClient(toolId); - log.info("已刷新工具连接: toolId={}", toolId); - } - - /** - * 获取工具健康状态 - * - * @return 工具ID -> 健康状态的映射 - */ - public Map getToolsHealthStatus() { - return langChain4jMcpToolProviderService.getAllToolsHealthStatus(); - } - - /** - * 获取所有 BUILTIN 工具对象 - * 这些对象包含 @Tool 注解的方法,可直接用于 AgenticServices - * - * @return BUILTIN 工具对象列表 - */ - public List getAllBuiltinToolObjects() { - return builtinToolRegistry.getAllBuiltinToolObjects(); - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java deleted file mode 100644 index b3bed3ab..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpMarketServiceImpl.java +++ /dev/null @@ -1,328 +0,0 @@ -package org.ruoyi.mcp.service.impl; - -import cn.hutool.http.HttpRequest; -import cn.hutool.http.HttpResponse; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.core.exception.ServiceException; -import org.ruoyi.common.core.utils.MapstructUtils; -import org.ruoyi.common.mybatis.core.page.PageQuery; -import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.mcp.domain.bo.McpMarketBo; -import org.ruoyi.mcp.domain.dto.McpMarketListResult; -import org.ruoyi.mcp.domain.dto.McpMarketRefreshResult; -import org.ruoyi.mcp.domain.dto.McpMarketToolListResult; -import org.ruoyi.mcp.domain.entity.McpMarket; -import org.ruoyi.mcp.domain.entity.McpMarketTool; -import org.ruoyi.mcp.domain.entity.McpTool; -import org.ruoyi.mcp.domain.vo.McpMarketVo; -import org.ruoyi.mcp.enums.McpToolStatus; -import org.ruoyi.mcp.mapper.McpMarketMapper; -import org.ruoyi.mcp.mapper.McpMarketToolMapper; -import org.ruoyi.mcp.mapper.McpToolMapper; -import org.ruoyi.mcp.service.IMcpMarketService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * MCP 市场服务实现 - * - * @author ruoyi team - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class McpMarketServiceImpl implements IMcpMarketService { - - private final McpMarketMapper baseMapper; - private final McpMarketToolMapper mcpMarketToolMapper; - private final McpToolMapper mcpToolMapper; - private final ObjectMapper objectMapper; - - @Override - public TableDataInfo selectPageList(McpMarketBo bo, PageQuery pageQuery) { - LambdaQueryWrapper wrapper = buildQueryWrapper(bo); - Page page = baseMapper.selectVoPage(pageQuery.build(), wrapper); - return TableDataInfo.build(page); - } - - @Override - public McpMarketListResult listMarkets(String keyword, String status) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - - if (StringUtils.hasText(keyword)) { - wrapper.and(w -> w.like(McpMarket::getName, keyword) - .or() - .like(McpMarket::getDescription, keyword)); - } - if (StringUtils.hasText(status)) { - wrapper.eq(McpMarket::getStatus, status); - } - - wrapper.orderByDesc(McpMarket::getUpdateTime); - - List list = baseMapper.selectList(wrapper); - - return McpMarketListResult.of(list); - } - - @Override - public List queryList(McpMarketBo bo) { - LambdaQueryWrapper wrapper = buildQueryWrapper(bo); - return baseMapper.selectVoList(wrapper); - } - - @Override - public McpMarketVo selectById(Long id) { - return baseMapper.selectVoById(id); - } - - @Override - @Transactional - public String insert(McpMarketBo bo) { - McpMarket market = MapstructUtils.convert(bo, McpMarket.class); - if (market.getStatus() == null) { - market.setStatus(McpToolStatus.ENABLED.getValue()); - } - baseMapper.insert(market); - return String.valueOf(market.getId()); - } - - @Override - @Transactional - public String update(McpMarketBo bo) { - McpMarket market = MapstructUtils.convert(bo, McpMarket.class); - baseMapper.updateById(market); - return String.valueOf(market.getId()); - } - - @Override - @Transactional - public void deleteByIds(List ids) { - for (Long id : ids) { - // 先删除关联的市场工具 - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(McpMarketTool::getMarketId, id); - mcpMarketToolMapper.delete(wrapper); - } - - // 删除市场 - baseMapper.deleteBatchIds(ids); - } - - @Override - @Transactional - public void updateStatus(Long id, String status) { - McpMarket market = new McpMarket(); - market.setId(id); - market.setStatus(status); - baseMapper.updateById(market); - } - - @Override - public McpMarketToolListResult getMarketTools(Long marketId, int page, int size) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - wrapper.eq(McpMarketTool::getMarketId, marketId); - wrapper.orderByDesc(McpMarketTool::getCreateTime); - - Page pageResult = mcpMarketToolMapper.selectPage(new Page<>(page, size), wrapper); - - return McpMarketToolListResult.of( - pageResult.getRecords(), - pageResult.getTotal(), - (int) pageResult.getCurrent(), - (int) pageResult.getSize() - ); - } - - @Override - @Transactional - public McpMarketRefreshResult refreshMarketTools(Long marketId) { - McpMarket market = baseMapper.selectById(marketId); - if (market == null) { - throw new ServiceException("市场不存在"); - } - - int addedCount = 0; - int updatedCount = 0; - - try { - // 从市场 URL 获取工具列表(使用hutool的HttpUtil) - HttpResponse response = HttpRequest.get(market.getUrl()) - .timeout(30000) // 30秒超时 - .execute(); - String responseBody = response.body(); - JsonNode rootNode = objectMapper.readTree(responseBody); - - // 假设响应格式为 { "data": [...] } 或直接是数组 - JsonNode toolsNode = rootNode.has("data") ? rootNode.get("data") : rootNode; - - if (toolsNode.isArray()) { - // 获取现有工具 - LambdaQueryWrapper existingWrapper = new LambdaQueryWrapper<>(); - existingWrapper.eq(McpMarketTool::getMarketId, marketId); - List existingTools = mcpMarketToolMapper.selectList(existingWrapper); - - // 创建现有工具的名称到ID映射 - Map existingToolMap = existingTools.stream() - .collect(Collectors.toMap(McpMarketTool::getToolName, t -> t)); - - // 处理新工具 - for (JsonNode toolNode : toolsNode) { - String toolName = getTextValue(toolNode, "name", "title"); - McpMarketTool existingTool = existingToolMap.get(toolName); - - if (existingTool != null) { - // 更新现有工具 - existingTool.setToolDescription(getTextValue(toolNode, "description", "desc")); - existingTool.setToolVersion(getTextValue(toolNode, "version")); - existingTool.setToolMetadata(toolNode.toString()); - mcpMarketToolMapper.updateById(existingTool); - updatedCount++; - } else { - // 插入新工具 - McpMarketTool tool = new McpMarketTool(); - tool.setMarketId(marketId); - tool.setToolName(toolName); - tool.setToolDescription(getTextValue(toolNode, "description", "desc")); - tool.setToolVersion(getTextValue(toolNode, "version")); - tool.setToolMetadata(toolNode.toString()); - tool.setIsLoaded(false); - mcpMarketToolMapper.insert(tool); - addedCount++; - } - } - } - - log.info("Successfully refreshed market tools for market: {}, added: {}, updated: {}", - market.getName(), addedCount, updatedCount); - - return McpMarketRefreshResult.builder() - .success(true) - .message("刷新成功") - .addedCount(addedCount) - .updatedCount(updatedCount) - .build(); - } catch (Exception e) { - log.error("Failed to refresh market tools for market {}: {}", marketId, e.getMessage()); - return McpMarketRefreshResult.builder() - .success(false) - .message("刷新市场工具列表失败: " + e.getMessage()) - .addedCount(0) - .updatedCount(0) - .build(); - } - } - - /** - * 从 JSON 节点获取文本值,尝试多个字段名 - */ - private String getTextValue(JsonNode node, String... fieldNames) { - for (String fieldName : fieldNames) { - if (node.has(fieldName) && !node.get(fieldName).isNull()) { - return node.get(fieldName).asText(); - } - } - return null; - } - - @Override - @Transactional - public void loadToolToLocal(Long toolId) { - McpMarketTool marketTool = mcpMarketToolMapper.selectById(toolId); - if (marketTool == null) { - throw new ServiceException("市场工具不存在"); - } - - if (marketTool.getIsLoaded()) { - throw new ServiceException("工具已加载到本地"); - } - - try { - // 解析工具元数据 - JsonNode metadata = objectMapper.readTree(marketTool.getToolMetadata()); - - // 创建本地工具 - McpTool localTool = new McpTool(); - localTool.setName(marketTool.getToolName()); - localTool.setDescription(marketTool.getToolDescription()); - - // 根据元数据判断类型 - if (metadata.has("baseUrl") || metadata.has("url")) { - localTool.setType("REMOTE"); - String baseUrl = metadata.has("baseUrl") ? metadata.get("baseUrl").asText() : - metadata.has("url") ? metadata.get("url").asText() : null; - localTool.setConfigJson(objectMapper.writeValueAsString(Map.of("baseUrl", baseUrl != null ? baseUrl : ""))); - } else { - localTool.setType("LOCAL"); - // 构建本地工具配置 - Map config = new HashMap<>(); - if (metadata.has("command")) { - config.put("command", metadata.get("command").asText()); - } - if (metadata.has("args") && metadata.get("args").isArray()) { - config.put("args", objectMapper.convertValue(metadata.get("args"), List.class)); - } - if (metadata.has("env") && metadata.get("env").isObject()) { - config.put("env", objectMapper.convertValue(metadata.get("env"), Map.class)); - } - // 如果有 npm 包名,使用 npx 启动 - if (metadata.has("package") || metadata.has("npmPackage")) { - String packageName = metadata.has("package") ? metadata.get("package").asText() : - metadata.get("npmPackage").asText(); - config.put("command", "npx"); - config.put("args", List.of("-y", packageName)); - } - localTool.setConfigJson(objectMapper.writeValueAsString(config)); - } - - localTool.setStatus(McpToolStatus.ENABLED.getValue()); - mcpToolMapper.insert(localTool); - - // 更新市场工具状态 - marketTool.setIsLoaded(true); - marketTool.setLocalToolId(localTool.getId()); - mcpMarketToolMapper.updateById(marketTool); - - log.info("Successfully loaded tool {} to local", marketTool.getToolName()); - } catch (Exception e) { - log.error("Failed to load tool to local: {}", e.getMessage()); - throw new ServiceException("加载工具到本地失败: " + e.getMessage()); - } - } - - @Override - @Transactional - public int batchLoadTools(List toolIds) { - int successCount = 0; - for (Long toolId : toolIds) { - try { - loadToolToLocal(toolId); - successCount++; - } catch (Exception e) { - log.warn("Failed to load tool {}: {}", toolId, e.getMessage()); - } - } - return successCount; - } - - private LambdaQueryWrapper buildQueryWrapper(McpMarketBo bo) { - Map params = bo.getParams(); - LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); - wrapper.eq(StringUtils.hasText(bo.getStatus()), McpMarket::getStatus, bo.getStatus()) - .like(StringUtils.hasText(bo.getName()), McpMarket::getName, bo.getName()) - .like(StringUtils.hasText(bo.getDescription()), McpMarket::getDescription, bo.getDescription()); - return wrapper; - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java deleted file mode 100644 index 26c2d247..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/service/impl/McpToolServiceImpl.java +++ /dev/null @@ -1,226 +0,0 @@ -package org.ruoyi.mcp.service.impl; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.core.exception.ServiceException; -import org.ruoyi.common.core.utils.MapstructUtils; -import org.ruoyi.common.mybatis.core.page.PageQuery; -import org.ruoyi.common.mybatis.core.page.TableDataInfo; -import org.ruoyi.mcp.domain.bo.McpToolBo; -import org.ruoyi.mcp.domain.dto.McpToolListResult; -import org.ruoyi.mcp.domain.dto.McpToolTestResult; -import org.ruoyi.mcp.domain.entity.McpTool; -import org.ruoyi.mcp.domain.vo.McpToolVo; -import org.ruoyi.mcp.enums.McpToolStatus; -import org.ruoyi.mcp.mapper.McpToolMapper; -import org.ruoyi.mcp.service.IMcpToolService; -import org.ruoyi.mcp.service.core.BuiltinToolRegistry; -import org.ruoyi.mcp.service.core.LangChain4jMcpToolProviderService; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; - -import java.util.List; -import java.util.Map; - -/** - * MCP 工具服务实现 - * - * @author ruoyi team - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class McpToolServiceImpl implements IMcpToolService { - - private final McpToolMapper baseMapper; - private final LangChain4jMcpToolProviderService langChain4jMcpToolProviderService; - private final BuiltinToolRegistry builtinToolRegistry; - - @Override - public TableDataInfo selectPageList(McpToolBo bo, PageQuery pageQuery) { - LambdaQueryWrapper wrapper = buildQueryWrapper(bo); - Page page = baseMapper.selectVoPage(pageQuery.build(), wrapper); - return TableDataInfo.build(page); - } - - @Override - public McpToolListResult listTools(String keyword, String type, String status) { - LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); - - if (StringUtils.hasText(keyword)) { - wrapper.and(w -> w.like(McpTool::getName, keyword) - .or() - .like(McpTool::getDescription, keyword)); - } - if (StringUtils.hasText(type)) { - wrapper.eq(McpTool::getType, type); - } - if (StringUtils.hasText(status)) { - wrapper.eq(McpTool::getStatus, status); - } - - wrapper.orderByDesc(McpTool::getUpdateTime); - - List list = baseMapper.selectList(wrapper); - - return McpToolListResult.of(list); - } - - @Override - public List queryList(McpToolBo bo) { - LambdaQueryWrapper wrapper = buildQueryWrapper(bo); - return baseMapper.selectVoList(wrapper); - } - - @Override - public McpToolVo selectById(Long id) { - return baseMapper.selectVoById(id); - } - - @Override - @Transactional - public String insert(McpToolBo bo) { - McpTool tool = MapstructUtils.convert(bo, McpTool.class); - if (tool.getStatus() == null) { - tool.setStatus(McpToolStatus.ENABLED.getValue()); - } - if (tool.getType() == null) { - tool.setType("LOCAL"); - } - baseMapper.insert(tool); - return String.valueOf(tool.getId()); - } - - @Override - @Transactional - public String update(McpToolBo bo) { - McpTool existingTool = baseMapper.selectById(bo.getId()); - if (existingTool != null && BuiltinToolRegistry.TYPE_BUILTIN.equals(existingTool.getType())) { - throw new ServiceException("内置工具不允许编辑"); - } - - McpTool tool = MapstructUtils.convert(bo, McpTool.class); - baseMapper.updateById(tool); - - // 如果工具正在使用中,需要刷新连接 - langChain4jMcpToolProviderService.refreshClient(bo.getId()); - - return String.valueOf(tool.getId()); - } - - @Override - @Transactional - public void deleteByIds(List ids) { - // 过滤掉内置工具 - List deletableIds = ids.stream() - .filter(id -> { - McpTool tool = baseMapper.selectById(id); - return tool == null || !BuiltinToolRegistry.TYPE_BUILTIN.equals(tool.getType()); - }) - .toList(); - - if (deletableIds.isEmpty()) { - throw new ServiceException("所选工具均为内置工具,不允许删除"); - } - - // 刷新连接(LangChain4j会自动处理) - deletableIds.forEach(id -> langChain4jMcpToolProviderService.refreshClient(id)); - baseMapper.deleteBatchIds(deletableIds); - } - - @Override - @Transactional - public void updateStatus(Long id, String status) { - McpTool tool = new McpTool(); - tool.setId(id); - tool.setStatus(status); - baseMapper.updateById(tool); - - // 刷新连接 - langChain4jMcpToolProviderService.refreshClient(id); - } - - @Override - public McpToolTestResult testTool(Long id) { - McpTool tool = baseMapper.selectById(id); - if (tool == null) { - return McpToolTestResult.fail("工具不存在"); - } - - // 根据工具类型选择不同的测试逻辑 - if (BuiltinToolRegistry.TYPE_BUILTIN.equals(tool.getType())) { - // 内置工具 - 直接验证是否在注册表中 - return testBuiltinTool(tool); - } else { - // MCP 工具 (LOCAL/REMOTE) - 测试连接 - return testMcpTool(tool); - } - } - - /** - * 测试内置工具 - * 内置工具不需要网络连接,只需验证是否在注册表中 - * - * @param tool 工具信息 - * @return 测试结果 - */ - private McpToolTestResult testBuiltinTool(McpTool tool) { - try { - boolean isRegistered = builtinToolRegistry.hasTool(tool.getName()); - if (isRegistered) { - return McpToolTestResult.success( - String.format("内置工具 [%s] 已注册,可正常使用", tool.getName()), - 1, - List.of(tool.getName()) - ); - } else { - return McpToolTestResult.fail( - String.format("内置工具 [%s] 未在注册表中找到,请检查工具名称是否正确", tool.getName()) - ); - } - } catch (Exception e) { - log.error("测试内置工具失败: {} - {}", tool.getName(), e.getMessage()); - return McpToolTestResult.fail("测试失败: " + e.getMessage()); - } - } - - /** - * 测试MCP工具连接 - * - * @param tool 工具信息 - * @return 测试结果 - */ - private McpToolTestResult testMcpTool(McpTool tool) { - try { - boolean isHealthy = langChain4jMcpToolProviderService.checkToolHealth(tool.getId()); - if (isHealthy) { - return McpToolTestResult.success( - String.format("MCP工具 [%s] 连接测试成功", tool.getName()), - 1, - List.of(tool.getName()) - ); - } else { - return McpToolTestResult.fail( - String.format("MCP工具 [%s] 连接测试失败", tool.getName()) - ); - } - } catch (Exception e) { - log.error("测试MCP工具失败: {} - {}", tool.getName(), e.getMessage()); - return McpToolTestResult.fail("测试失败: " + e.getMessage()); - } - } - - private LambdaQueryWrapper buildQueryWrapper(McpToolBo bo) { - Map params = bo.getParams(); - LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); - wrapper.eq(StringUtils.hasText(bo.getType()), McpTool::getType, bo.getType()) - .eq(StringUtils.hasText(bo.getStatus()), McpTool::getStatus, bo.getStatus()) - .like(StringUtils.hasText(bo.getName()), McpTool::getName, bo.getName()) - .like(StringUtils.hasText(bo.getDescription()), McpTool::getDescription, bo.getDescription()); - return wrapper; - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java deleted file mode 100644 index 26473da2..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/EditFileTool.java +++ /dev/null @@ -1,151 +0,0 @@ -package org.ruoyi.mcp.tools; - -import dev.langchain4j.agent.tool.Tool; -import org.ruoyi.mcp.service.core.BuiltinToolProvider; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.util.Arrays; -import java.util.List; - -/** - * 编辑文件工具 - * 支持基于diff的文件编辑 - */ -@Component -public class EditFileTool implements BuiltinToolProvider { - - public static final String DESCRIPTION = "Edits a file by applying a diff. " + - "Use this tool when you need to make specific changes to a file. " + - "The tool will show the diff before applying changes. " + - "Use absolute paths within the workspace directory."; - - private final String rootDirectory; - private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); - - public EditFileTool() { - this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString(); - } - - /** - * 编辑文件 - * - * @param filePath 文件绝对路径 - * @param diff 要应用的diff内容 - * @return 操作结果 - */ - @Tool(DESCRIPTION) - public String editFile(String filePath, String diff) { - try { - // 验证参数 - if (filePath == null || filePath.trim().isEmpty()) { - return "Error: File path cannot be empty"; - } - - if (diff == null || diff.trim().isEmpty()) { - return "Error: Diff cannot be empty"; - } - - Path path = Paths.get(filePath); - - // 验证是否为绝对路径 - if (!path.isAbsolute()) { - return "Error: File path must be absolute: " + filePath; - } - - // 验证是否在工作目录内 - if (!isWithinWorkspace(path)) { - return "Error: File path must be within the workspace directory (" + rootDirectory + "): " + filePath; - } - - // 检查文件是否存在 - if (!Files.exists(path)) { - return "Error: File not found: " + filePath; - } - - // 检查是否为目录 - if (Files.isDirectory(path)) { - return "Error: Path is a directory, not a file: " + filePath; - } - - // 读取原始内容 - String originalContent = Files.readString(path, StandardCharsets.UTF_8); - List originalLines = Arrays.asList(originalContent.split("\n")); - - // 应用diff - try { - // 这里简化处理,直接用新内容替换 - // 在实际应用中,可能需要更复杂的diff解析 - String newContent = applyDiff(originalContent, diff); - - // 写入文件 - Files.writeString(path, newContent, StandardCharsets.UTF_8, - StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); - - String relativePath = getRelativePath(path); - return String.format("Successfully edited file: %s", relativePath); - - } catch (Exception e) { - return "Error: Failed to apply diff: " + e.getMessage(); - } - - } catch (IOException e) { - logger.error("Error editing file: {}", filePath, e); - return "Error: " + e.getMessage(); - } catch (Exception e) { - logger.error("Unexpected error editing file: {}", filePath, e); - return "Error: Unexpected error: " + e.getMessage(); - } - } - - /** - * 简化的diff应用逻辑 - * 实际应用中可能需要使用更复杂的diff解析器 - */ - private String applyDiff(String originalContent, String diff) { - // 这里简化处理,实际应用中需要解析diff格式 - // 目前将diff作为新内容直接替换 - // 可以考虑使用jgit等库来解析 unified diff 格式 - return diff; - } - - private boolean isWithinWorkspace(Path filePath) { - try { - Path workspaceRoot = Paths.get(rootDirectory).toRealPath(); - Path normalizedPath = filePath.normalize(); - return normalizedPath.startsWith(workspaceRoot.normalize()); - } catch (IOException e) { - logger.warn("Could not resolve workspace path", e); - return false; - } - } - - private String getRelativePath(Path filePath) { - try { - Path workspaceRoot = Paths.get(rootDirectory); - return workspaceRoot.relativize(filePath).toString(); - } catch (Exception e) { - return filePath.toString(); - } - } - - @Override - public String getToolName() { - return "edit_file"; - } - - @Override - public String getDisplayName() { - return "编辑文件"; - } - - @Override - public String getDescription() { - return DESCRIPTION; - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java deleted file mode 100644 index 8f6c0cdd..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ListDirectoryTool.java +++ /dev/null @@ -1,285 +0,0 @@ -package org.ruoyi.mcp.tools; - -import dev.langchain4j.agent.tool.Tool; -import org.ruoyi.mcp.service.core.BuiltinToolProvider; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Stream; - -/** - * 目录列表工具 - * 列出指定目录的文件和子目录,支持递归列表 - */ -@Component -public class ListDirectoryTool implements BuiltinToolProvider { - - public static final String DESCRIPTION = "Lists files and directories in the specified path. " + - "Supports recursive listing and filtering. " + - "Shows file sizes, modification times, and types. " + - "Use absolute paths within the workspace directory."; - - private final String rootDirectory; - private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); - - public ListDirectoryTool() { - this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString(); - } - - /** - * 列出目录内容 - * - * @param filePath 目录绝对路径 - * @param recursive 是否递归列出子目录(可选,默认 false) - * @param maxDepth 最大递归深度(可选,默认 3,范围 1-10) - * @return 目录列表结果 - */ - @Tool(DESCRIPTION) - public String listDirectory(String filePath, Boolean recursive, Integer maxDepth) { - // 创建参数对象 - ListDirectoryParams params = new ListDirectoryParams(); - params.filePath = filePath; - params.recursive = recursive != null ? recursive : false; - params.maxDepth = maxDepth != null ? maxDepth : 3; - - return execute(params); - } - - public String execute(ListDirectoryParams params) { - try { - // 验证参数 - String validationError = validateParams(params); - if (validationError != null) { - return "Error: " + validationError; - } - - Path dirPath = Paths.get(params.filePath); - - // 检查目录是否存在 - if (!Files.exists(dirPath)) { - return "Error: Directory not found: " + params.filePath; - } - - // 检查是否为目录 - if (!Files.isDirectory(dirPath)) { - return "Error: Path is not a directory: " + params.filePath; - } - - // 列出文件和目录 - List fileInfos = listFiles(dirPath, params); - - // 生成输出 - return formatFileList(fileInfos, params); - - } catch (IOException e) { - logger.error("Error listing directory: {}", params.filePath, e); - return "Error: " + e.getMessage(); - } catch (Exception e) { - logger.error("Unexpected error listing directory: {}", params.filePath, e); - return "Error: Unexpected error: " + e.getMessage(); - } - } - - private String validateParams(ListDirectoryParams params) { - // 验证路径 - if (params.filePath == null || params.filePath.trim().isEmpty()) { - return "Directory path cannot be empty"; - } - - Path dirPath = Paths.get(params.filePath); - - // 验证是否为绝对路径 - if (!dirPath.isAbsolute()) { - return "Directory path must be absolute: " + params.filePath; - } - - // 验证是否在工作目录内 - if (!isWithinWorkspace(dirPath)) { - return "Directory path must be within the workspace directory (" + rootDirectory + "): " + params.filePath; - } - - // 验证最大深度 - if (params.maxDepth != null && (params.maxDepth < 1 || params.maxDepth > 10)) { - return "Max depth must be between 1 and 10"; - } - - return null; - } - - private List listFiles(Path dirPath, ListDirectoryParams params) throws IOException { - List fileInfos = new ArrayList<>(); - - if (params.recursive != null && params.recursive) { - int maxDepth = params.maxDepth != null ? params.maxDepth : 3; - listFilesRecursive(dirPath, fileInfos, 0, maxDepth, params); - } else { - listFilesInDirectory(dirPath, fileInfos, params); - } - - // 排序:目录在前,然后按名称排序 - fileInfos.sort(Comparator - .comparing((FileInfo f) -> !f.isDirectory()) - .thenComparing(FileInfo::name)); - - return fileInfos; - } - - private void listFilesInDirectory(Path dirPath, List fileInfos, ListDirectoryParams params) throws IOException { - try (Stream stream = Files.list(dirPath)) { - stream.forEach(path -> { - try { - FileInfo fileInfo = createFileInfo(path, dirPath); - fileInfos.add(fileInfo); - } catch (IOException e) { - logger.warn("Could not get info for file: " + path, e); - } - }); - } - } - - private void listFilesRecursive(Path dirPath, List fileInfos, int currentDepth, int maxDepth, ListDirectoryParams params) throws IOException { - if (currentDepth >= maxDepth) { - return; - } - - try (Stream stream = Files.list(dirPath)) { - List paths = stream.toList(); - - for (Path path : paths) { - try { - FileInfo fileInfo = createFileInfo(path, Paths.get(params.filePath)); - fileInfos.add(fileInfo); - - // 如果是目录,递归列出 - if (Files.isDirectory(path)) { - listFilesRecursive(path, fileInfos, currentDepth + 1, maxDepth, params); - } - } catch (IOException e) { - logger.warn("Could not get info for file: " + path, e); - } - } - } - } - - private FileInfo createFileInfo(Path path, Path basePath) throws IOException { - String name = path.getFileName().toString(); - boolean isDirectory = Files.isDirectory(path); - long size = isDirectory ? 0 : Files.size(path); - - LocalDateTime lastModified = LocalDateTime.ofInstant( - Files.getLastModifiedTime(path).toInstant(), - ZoneId.systemDefault() - ); - - String relativePath = basePath.relativize(path).toString(); - - return new FileInfo(name, relativePath, isDirectory, size, lastModified); - } - - private String formatFileList(List fileInfos, ListDirectoryParams params) { - if (fileInfos.isEmpty()) { - return "Directory is empty."; - } - - StringBuilder sb = new StringBuilder(); - sb.append(String.format("Directory listing for: %s\n", getRelativePath(Paths.get(params.filePath)))); - sb.append(String.format("Total items: %d\n\n", fileInfos.size())); - - // 表头 - sb.append(String.format("%-4s %-40s %-12s %-20s %s\n", - "Type", "Name", "Size", "Modified", "Path")); - sb.append("-".repeat(80)).append("\n"); - - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - for (FileInfo fileInfo : fileInfos) { - String type = fileInfo.isDirectory() ? "DIR" : "FILE"; - String sizeStr = fileInfo.isDirectory() ? "-" : formatFileSize(fileInfo.size()); - String modifiedStr = fileInfo.lastModified().format(formatter); - - sb.append(String.format("%-4s %-40s %-12s %-20s %s\n", - type, - truncate(fileInfo.name()), - sizeStr, - modifiedStr, - fileInfo.relativePath() - )); - } - - return sb.toString(); - } - - private String formatFileSize(long bytes) { - if (bytes < 1024) return bytes + " B"; - if (bytes < 1024 * 1024) return String.format("%.1f KB", bytes / 1024.0); - if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024)); - return String.format("%.1f GB", bytes / (1024.0 * 1024 * 1024)); - } - - private String truncate(String str) { - if (str.length() <= 40) { - return str; - } - return str.substring(0, 40 - 3) + "..."; - } - - private boolean isWithinWorkspace(Path dirPath) { - try { - Path workspaceRoot = Paths.get(rootDirectory).toRealPath(); - Path normalizedPath = dirPath.normalize(); - return normalizedPath.startsWith(workspaceRoot.normalize()); - } catch (IOException e) { - logger.warn("Could not resolve workspace path", e); - return false; - } - } - - private String getRelativePath(Path dirPath) { - try { - Path workspaceRoot = Paths.get(rootDirectory); - return workspaceRoot.relativize(dirPath).toString(); - } catch (Exception e) { - return dirPath.toString(); - } - } - - @Override - public String getToolName() { - return "list_directory"; - } - - @Override - public String getDisplayName() { - return "列出目录"; - } - - @Override - public String getDescription() { - return DESCRIPTION; - } - - /** - * 文件信息 - */ - public record FileInfo(String name, String relativePath, boolean isDirectory, long size, - LocalDateTime lastModified) { - } - - /** - * 列表目录参数 - */ - public static class ListDirectoryParams { - public String filePath; - public Boolean recursive; - public Integer maxDepth; - } -} diff --git a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java b/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java deleted file mode 100644 index 7b4886e7..00000000 --- a/ruoyi-modules/ruoyi-mcp/src/main/java/org/ruoyi/mcp/tools/ReadFileTool.java +++ /dev/null @@ -1,121 +0,0 @@ -package org.ruoyi.mcp.tools; - -import dev.langchain4j.agent.tool.Tool; -import org.ruoyi.mcp.service.core.BuiltinToolProvider; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -/** - * 读取文件工具 - * 读取指定路径的文件内容 - */ -@Component -public class ReadFileTool implements BuiltinToolProvider { - - public static final String DESCRIPTION = "Reads the contents of a file. " + - "Use absolute paths within the workspace directory. " + - "Returns the complete file content as a string."; - - private final String rootDirectory; - private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass()); - - public ReadFileTool() { - this.rootDirectory = Paths.get(System.getProperty("user.dir"), "workspace").toString(); - } - - /** - * 读取文件内容 - * - * @param filePath 文件绝对路径 - * @return 文件内容 - */ - @Tool(DESCRIPTION) - public String readFile(String filePath) { - try { - // 验证参数 - if (filePath == null || filePath.trim().isEmpty()) { - return "Error: File path cannot be empty"; - } - - Path path = Paths.get(filePath); - - // 验证是否为绝对路径 - if (!path.isAbsolute()) { - return "Error: File path must be absolute: " + filePath; - } - - // 验证是否在工作目录内 - if (!isWithinWorkspace(path)) { - return "Error: File path must be within the workspace directory (" + rootDirectory + "): " + filePath; - } - - // 检查文件是否存在 - if (!Files.exists(path)) { - return "Error: File not found: " + filePath; - } - - // 检查是否为目录 - if (Files.isDirectory(path)) { - return "Error: Path is a directory, not a file: " + filePath; - } - - // 读取文件内容 - String content = Files.readString(path, StandardCharsets.UTF_8); - - // 获取相对路径 - String relativePath = getRelativePath(path); - long sizeBytes = content.getBytes(StandardCharsets.UTF_8).length; - long lineCount = content.lines().count(); - - return String.format("File: %s (%d lines, %d bytes)\n\n%s", - relativePath, lineCount, sizeBytes, content); - - } catch (IOException e) { - logger.error("Error reading file: {}", filePath, e); - return "Error: " + e.getMessage(); - } catch (Exception e) { - logger.error("Unexpected error reading file: {}", filePath, e); - return "Error: Unexpected error: " + e.getMessage(); - } - } - - private boolean isWithinWorkspace(Path filePath) { - try { - Path workspaceRoot = Paths.get(rootDirectory).toRealPath(); - Path normalizedPath = filePath.normalize(); - return normalizedPath.startsWith(workspaceRoot.normalize()); - } catch (IOException e) { - logger.warn("Could not resolve workspace path", e); - return false; - } - } - - private String getRelativePath(Path filePath) { - try { - Path workspaceRoot = Paths.get(rootDirectory); - return workspaceRoot.relativize(filePath).toString(); - } catch (Exception e) { - return filePath.toString(); - } - } - - @Override - public String getToolName() { - return "read_file"; - } - - @Override - public String getDisplayName() { - return "读取文件"; - } - - @Override - public String getDescription() { - return DESCRIPTION; - } -}