diff --git a/README.md b/README.md index f8ce58b7..78ea6497 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,13 @@ QQ技术交流群
技术讨论 + + +95e8b1b3baeadbd24650bfb974ca5a58
+微信技术交流群
+技术讨论 + + diff --git a/docs/工作流模块说明.md b/docs/工作流模块说明.md new file mode 100644 index 00000000..8d2f4396 --- /dev/null +++ b/docs/工作流模块说明.md @@ -0,0 +1,432 @@ +# Ruoyi-AI 工作流模块详细说明文档 + +## 概述 + +Ruoyi-AI 工作流模块是一个基于 LangGraph4j 的智能工作流引擎,支持可视化工作流设计、AI 模型集成、条件分支、人机交互等高级功能。该模块采用微服务架构,提供完整的 RESTful API 和流式响应支持。 + +## 模块架构 + +### 1. 模块结构 + +``` +ruoyi-ai/ +├── ruoyi-modules/ +│ └── ruoyi-workflow/ # 工作流核心模块 +│ ├── pom.xml +│ └── src/main/java/org/ruoyi/workflow/ +│ └── controller/ # 控制器层 +│ ├── WorkflowController.java +│ ├── WorkflowRuntimeController.java +│ └── admin/ # 管理端控制器 +│ ├── AdminWorkflowController.java +│ └── AdminWorkflowComponentController.java +└── ruoyi-modules-api/ + └── ruoyi-workflow-api/ # 工作流API模块 + ├── pom.xml + └── src/main/java/org/ruoyi/workflow/ + ├── entity/ # 实体类 + ├── dto/ # 数据传输对象 + ├── service/ # 服务接口 + ├── mapper/ # 数据访问层 + ├── workflow/ # 工作流核心逻辑 + ├── enums/ # 枚举类 + ├── util/ # 工具类 + └── exception/ # 异常处理 +``` + +### 2. 核心依赖 + +- **LangGraph4j**: 1.5.3 - 工作流图执行引擎 +- **LangChain4j**: 1.2.0 - AI 模型集成框架 +- **Spring Boot**: 3.x - 应用框架 +- **MyBatis Plus**: 数据访问层 +- **Redis**: 缓存和状态管理 +- **Swagger/OpenAPI**: API 文档 + +## 核心功能 + +### 1. 工作流管理 + +#### 1.1 工作流定义 +- **创建工作流**: 支持自定义标题、描述、公开性设置 +- **编辑工作流**: 可视化节点编辑、连接线配置 +- **版本控制**: 支持工作流的版本管理和回滚 +- **权限管理**: 支持公开/私有工作流设置 + +#### 1.2 工作流执行 +- **流式执行**: 基于 SSE 的实时流式响应 +- **状态管理**: 完整的执行状态跟踪 +- **错误处理**: 详细的错误信息和异常处理 +- **中断恢复**: 支持工作流中断和恢复执行 + +### 2. 节点类型 + +#### 2.1 基础节点 +- **Start**: 开始节点,定义工作流入口 +- **End**: 结束节点,定义工作流出口 + +#### 2.2 AI 模型节点 +- **Answer**: 大语言模型问答节点 +- **Dalle3**: DALL-E 3 图像生成 +- **Tongyiwanx**: 通义万相图像生成 +- **Classifier**: 内容分类节点 + +#### 2.3 数据处理节点 +- **DocumentExtractor**: 文档信息提取 +- **KeywordExtractor**: 关键词提取 +- **FaqExtractor**: 常见问题提取 +- **KnowledgeRetrieval**: 知识库检索 + +#### 2.4 控制流节点 +- **Switcher**: 条件分支节点 +- **HumanFeedback**: 人机交互节点 + +#### 2.5 外部集成节点 +- **Google**: Google 搜索集成 +- **MailSend**: 邮件发送 +- **HttpRequest**: HTTP 请求 +- **Template**: 模板转换 + +### 3. 数据流管理 + +#### 3.1 输入输出定义 +```java +// 节点输入输出数据结构 +public class NodeIOData { + private String name; // 参数名称 + private NodeIODataContent content; // 参数内容 +} + +// 支持的数据类型 +public enum WfIODataTypeEnum { + TEXT, // 文本 + NUMBER, // 数字 + BOOLEAN, // 布尔值 + FILES, // 文件 + OPTIONS // 选项 +} +``` + +#### 3.2 参数引用 +- **节点间引用**: 支持上游节点输出作为下游节点输入 +- **参数映射**: 自动处理参数名称映射 +- **类型转换**: 自动进行数据类型转换 + +## 数据库设计 + +### 1. 核心表结构 + +#### 1.1 工作流定义表 (t_workflow) +```sql +CREATE TABLE t_workflow ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + title VARCHAR(100) NOT NULL DEFAULT '', + remark TEXT NOT NULL DEFAULT '', + user_id BIGINT NOT NULL DEFAULT 0, + is_public TINYINT(1) NOT NULL DEFAULT 0, + is_enable TINYINT(1) NOT NULL DEFAULT 1, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0 +); +``` + +#### 1.2 工作流节点表 (t_workflow_node) +```sql +CREATE TABLE t_workflow_node ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + workflow_id BIGINT NOT NULL DEFAULT 0, + workflow_component_id BIGINT NOT NULL DEFAULT 0, + user_id BIGINT NOT NULL DEFAULT 0, + title VARCHAR(100) NOT NULL DEFAULT '', + remark VARCHAR(500) NOT NULL DEFAULT '', + input_config JSON NOT NULL DEFAULT ('{}'), + node_config JSON NOT NULL DEFAULT ('{}'), + position_x DOUBLE NOT NULL DEFAULT 0, + position_y DOUBLE NOT NULL DEFAULT 0, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0 +); +``` + +#### 1.3 工作流边表 (t_workflow_edge) +```sql +CREATE TABLE t_workflow_edge ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + workflow_id BIGINT NOT NULL DEFAULT 0, + source_node_uuid VARCHAR(32) NOT NULL DEFAULT '', + source_handle VARCHAR(32) NOT NULL DEFAULT '', + target_node_uuid VARCHAR(32) NOT NULL DEFAULT '', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0 +); +``` + +#### 1.4 工作流运行时表 (t_workflow_runtime) +```sql +CREATE TABLE t_workflow_runtime ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + user_id BIGINT NOT NULL DEFAULT 0, + workflow_id BIGINT NOT NULL DEFAULT 0, + input JSON NOT NULL DEFAULT ('{}'), + output JSON NOT NULL DEFAULT ('{}'), + status SMALLINT NOT NULL DEFAULT 1, + status_remark VARCHAR(250) NOT NULL DEFAULT '', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0 +); +``` + +#### 1.5 工作流组件表 (t_workflow_component) +```sql +CREATE TABLE t_workflow_component ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) DEFAULT '' NOT NULL, + name VARCHAR(32) DEFAULT '' NOT NULL, + title VARCHAR(100) DEFAULT '' NOT NULL, + remark TEXT NOT NULL, + display_order INT DEFAULT 0 NOT NULL, + is_enable TINYINT(1) DEFAULT 0 NOT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted TINYINT(1) DEFAULT 0 NOT NULL +); +``` + +## API 接口 + +### 1. 工作流管理接口 + +#### 1.1 基础操作 +```http +# 创建工作流 +POST /workflow/add +Content-Type: application/json +{ + "title": "工作流标题", + "remark": "工作流描述", + "isPublic": false +} + +# 更新工作流 +POST /workflow/update +Content-Type: application/json +{ + "uuid": "工作流UUID", + "title": "新标题", + "remark": "新描述" +} + +# 删除工作流 +POST /workflow/del/{uuid} + +# 启用/禁用工作流 +POST /workflow/enable/{uuid}?enable=true +``` + +#### 1.2 搜索和查询 +```http +# 搜索我的工作流 +GET /workflow/mine/search?keyword=关键词&isPublic=true¤tPage=1&pageSize=10 + +# 搜索公开工作流 +GET /workflow/public/search?keyword=关键词¤tPage=1&pageSize=10 + +# 获取工作流组件列表 +GET /workflow/public/component/list +``` + +### 2. 工作流执行接口 + +#### 2.1 流式执行 +```http +# 流式执行工作流 +POST /workflow/run +Content-Type: application/json +Accept: text/event-stream +{ + "uuid": "工作流UUID", + "inputs": [ + { + "name": "input", + "content": { + "type": 1, + "textContent": "用户输入内容" + } + } + ] +} +``` + +#### 2.2 运行时管理 +```http +# 恢复中断的工作流 +POST /workflow/runtime/resume/{runtimeUuid} +Content-Type: application/json +{ + "feedbackContent": "用户反馈内容" +} + +# 查询工作流执行历史 +GET /workflow/runtime/page?wfUuid=工作流UUID¤tPage=1&pageSize=10 + +# 查询运行时节点详情 +GET /workflow/runtime/nodes/{runtimeUuid} + +# 清理运行时数据 +POST /workflow/runtime/clear?wfUuid=工作流UUID +``` + +### 3. 管理端接口 + +#### 3.1 工作流管理 +```http +# 搜索所有工作流 +POST /admin/workflow/search +Content-Type: application/json +{ + "title": "搜索关键词", + "isPublic": true, + "isEnable": true +} + +# 启用/禁用工作流 +POST /admin/workflow/enable?uuid=工作流UUID&isEnable=true +``` + +## 核心实现 + +### 1. 工作流引擎 (WorkflowEngine) + +工作流引擎是整个模块的核心,负责: +- 工作流图的构建和编译 +- 节点执行调度 +- 状态管理和持久化 +- 流式输出处理 + +```java +public class WorkflowEngine { + // 核心执行方法 + public void run(User user, List userInputs, SseEmitter sseEmitter) { + // 1. 验证工作流状态 + // 2. 创建运行时实例 + // 3. 构建状态图 + // 4. 执行工作流 + // 5. 处理流式输出 + } + + // 恢复执行方法 + public void resume(String userInput) { + // 1. 更新状态 + // 2. 继续执行 + } +} +``` + +### 2. 节点工厂 (WfNodeFactory) + +节点工厂负责根据组件类型创建对应的节点实例: + +```java +public class WfNodeFactory { + public static AbstractWfNode create(WorkflowComponent component, + WorkflowNode node, + WfState wfState, + WfNodeState nodeState) { + // 根据组件类型创建对应的节点实例 + switch (component.getName()) { + case "Answer": + return new LLMAnswerNode(component, node, wfState, nodeState); + case "Switcher": + return new SwitcherNode(component, node, wfState, nodeState); + // ... 其他节点类型 + } + } +} +``` + +### 3. 图构建器 (WorkflowGraphBuilder) + +图构建器负责将工作流定义转换为可执行的状态图: + +```java +public class WorkflowGraphBuilder { + public StateGraph build(WorkflowNode startNode) { + // 1. 构建编译节点树 + // 2. 转换为状态图 + // 3. 添加节点和边 + // 4. 处理条件分支 + // 5. 处理并行执行 + } +} +``` + +## 流式响应机制 + +### 1. SSE 事件类型 + +工作流执行过程中会发送多种类型的 SSE 事件: + +```javascript +// 节点开始执行 +[NODE_RUN_节点UUID] - 节点执行开始事件 + +// 节点输入数据 +[NODE_INPUT_节点UUID] - 节点输入数据事件 + +// 节点输出数据 +[NODE_OUTPUT_节点UUID] - 节点输出数据事件 + +// 流式内容块 +[NODE_CHUNK_节点UUID] - 流式内容块事件 + +// 等待用户输入 +[NODE_WAIT_FEEDBACK_BY_节点UUID] - 等待用户输入事件 +``` + +### 2. 流式处理流程 + +1. **初始化**: 创建工作流运行时实例 +2. **节点执行**: 逐个执行工作流节点 +3. **实时输出**: 通过 SSE 实时推送执行结果 +4. **状态更新**: 实时更新节点和工作流状态 +5. **错误处理**: 捕获并处理执行过程中的错误 + + +## 扩展开发 + +### 1. 自定义节点开发 + +要开发自定义工作流节点,需要: + +1. **创建节点类**:继承 `AbstractWfNode` +2. **实现处理逻辑**:重写 `onProcess()` 方法 +3. **定义配置类**:创建节点配置类 +4. **注册组件**:在组件表中注册新组件 + +```java +public class CustomNode extends AbstractWfNode { + @Override + protected NodeProcessResult onProcess() { + // 实现自定义处理逻辑 + List outputs = new ArrayList<>(); + // ... 处理逻辑 + return NodeProcessResult.success(outputs); + } +} +``` + +### 2. 自定义组件注册 + +```sql +-- 在 t_workflow_component 表中添加新组件 +INSERT INTO t_workflow_component (uuid, name, title, remark, is_enable) +VALUES (REPLACE(UUID(), '-', ''), 'CustomNode', '自定义节点', '自定义节点描述', true); +``` diff --git a/pom.xml b/pom.xml index b164fbfb..6f282a46 100644 --- a/pom.xml +++ b/pom.xml @@ -270,13 +270,6 @@ ${lock4j.version} - - - com.xuxueli - xxl-job-core - ${xxl-job.version} - - com.alibaba transmittable-thread-local @@ -373,6 +366,23 @@ langchain4j-community-neo4j ${langchain4j-neo4j.version} + ruoyi-aihuman + ${revision} + + + + + org.ruoyi + ruoyi-workflow + ${revision} + + + + org.ruoyi + ruoyi-workflow-api + ${revision} + + diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 3cf17cc7..5aa1fba8 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -61,6 +61,14 @@ org.ruoyi ruoyi-graph + + org.ruoyi + ruoyi-workflow + + + + org.ruoyi + ruoyi-aihuman diff --git a/ruoyi-admin/src/main/java/org/ruoyi/RuoYiAIApplication.java b/ruoyi-admin/src/main/java/org/ruoyi/RuoYiAIApplication.java index cc65b7b4..ea5d664f 100644 --- a/ruoyi-admin/src/main/java/org/ruoyi/RuoYiAIApplication.java +++ b/ruoyi-admin/src/main/java/org/ruoyi/RuoYiAIApplication.java @@ -11,7 +11,7 @@ import org.springframework.scheduling.annotation.EnableScheduling; * * @author Lion Li */ -@SpringBootApplication +@SpringBootApplication(scanBasePackages = {"org.ruoyi", "org.ruoyi.aihuman"}) @EnableScheduling @EnableAsync public class RuoYiAIApplication { @@ -22,4 +22,4 @@ public class RuoYiAIApplication { application.run(args); System.out.println("(♥◠‿◠)ノ゙ RuoYiAI启动成功 ლ(´ڡ`ლ)゙"); } -} +} \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application-dev.yml b/ruoyi-admin/src/main/resources/application-dev.yml index a722f88b..cb082f50 100644 --- a/ruoyi-admin/src/main/resources/application-dev.yml +++ b/ruoyi-admin/src/main/resources/application-dev.yml @@ -37,6 +37,8 @@ spring: connectionTestQuery: SELECT 1 # 多久检查一次连接的活性 keepaliveTime: 30000 + mail: + username: xx --- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉) spring.data: @@ -102,7 +104,15 @@ pdf: #百炼模型配置 dashscope: key: sk-xxxx - model: qvq-max + +local: + images: xx + + + + files: xx + + --- # Neo4j 知识图谱配置 neo4j: diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 6d5e6d2f..4c6d38a0 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -156,6 +156,8 @@ security: # actuator 监控配置 - /actuator - /actuator/** + - /workflow/** + - /admin/workflow/** # 多租户配置 tenant: # 是否开启 @@ -328,3 +330,19 @@ spring: servers-configuration: classpath:mcp-server.json request-timeout: 300s +# 向量库配置 +vector-store: + # 向量存储类型 可选(weaviate/milvus) + # 如需修改向量库类型,请修改此配置值! + type: weaviate + + # Weaviate配置 + weaviate: + protocol: http + host: 127.0.0.1:6038 + classname: LocalKnowledge + # Milvus配置 + milvus: + url: http://localhost:19530 + collectionname: LocalKnowledge + diff --git a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java index f0eacdfc..ba351cf7 100644 --- a/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java +++ b/ruoyi-common/ruoyi-common-chat/src/main/java/org/ruoyi/common/chat/request/ChatRequest.java @@ -26,11 +26,18 @@ public class ChatRequest { */ private String prompt; + /** * 系统提示词 */ private String sysPrompt; + + /** + * 消息id + */ + private Long messageId; + /** * 是否开启流式对话 */ @@ -72,6 +79,11 @@ public class ChatRequest { */ private Boolean hasAttachment; + /** + * 是否启用深度思考 + */ + private Boolean enableThinking; + /** * 是否自动切换模型 */ @@ -82,9 +94,4 @@ public class ChatRequest { */ private String token; - /** - * 消息ID(保存消息成功后设置,用于后续扣费更新) - */ - private Long messageId; - } diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/ruoyi/common/core/config/VectorStoreProperties.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/ruoyi/common/core/config/VectorStoreProperties.java new file mode 100644 index 00000000..b4bb135d --- /dev/null +++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/ruoyi/common/core/config/VectorStoreProperties.java @@ -0,0 +1,62 @@ +package org.ruoyi.common.core.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 向量库配置属性 + * + * @author ageer + */ +@Data +@Component +@ConfigurationProperties(prefix = "vector-store") +public class VectorStoreProperties { + + /** + * 向量库类型 + */ + private String type; + + /** + * Weaviate配置 + */ + private Weaviate weaviate = new Weaviate(); + + /** + * Milvus配置 + */ + private Milvus milvus = new Milvus(); + + @Data + public static class Weaviate { + /** + * 协议 + */ + private String protocol; + + /** + * 主机地址 + */ + private String host; + + /** + * 类名 + */ + private String classname; + } + + @Data + public static class Milvus { + /** + * 连接URL + */ + private String url; + + /** + * 集合名称 + */ + private String collectionname; + } +} \ No newline at end of file diff --git a/ruoyi-modules-api/pom.xml b/ruoyi-modules-api/pom.xml index a3f0ef00..2cf63600 100644 --- a/ruoyi-modules-api/pom.xml +++ b/ruoyi-modules-api/pom.xml @@ -17,6 +17,7 @@ ruoyi-chat-api ruoyi-knowledge-api ruoyi-system-api + ruoyi-workflow-api diff --git a/ruoyi-modules-api/ruoyi-chat-api/pom.xml b/ruoyi-modules-api/ruoyi-chat-api/pom.xml index c1af20a5..7e08c9af 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/pom.xml +++ b/ruoyi-modules-api/ruoyi-chat-api/pom.xml @@ -16,7 +16,7 @@ 17 17 UTF-8 - 1.0.0 + 1.0.0-M7 diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java index e8f0e308..34465613 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/ChatModel.java @@ -1,6 +1,7 @@ package org.ruoyi.domain; +import com.alibaba.excel.annotation.ExcelProperty; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @@ -81,6 +82,11 @@ public class ChatModel extends BaseEntity { */ private Integer priority; + /** + * 模型供应商 + */ + private String ProviderName; + /** * 备注 */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/McpInfo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/McpInfo.java new file mode 100644 index 00000000..f8392ad6 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/McpInfo.java @@ -0,0 +1,67 @@ +package org.ruoyi.domain; + +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.annotation.DataColumn; +import org.ruoyi.core.domain.BaseEntity; + +import java.util.Date; + +/** + * MCP对象 mcp_info + * + * @author ageerle + * @date Sat Aug 09 16:50:58 CST 2025 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("mcp_info") +public class McpInfo extends BaseEntity { + + + /** + * id + */ + @TableId(value = "mcp_id", type = IdType.AUTO) + private Integer mcpId; + + /** + * 服务器名称 + */ + private String serverName; + + /** + * 链接方式 + */ + + private String transportType; + + /** + * Command + */ + private String command; + + /** + * Args + */ + private String arguments; + + private String description; + + /** + * Env + */ + private String env; + + /** + * 是否启用 + */ + private Boolean status; + + + + +} diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java index b828515b..34ee975e 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/ChatModelBo.java @@ -1,5 +1,6 @@ package org.ruoyi.domain.bo; +import com.alibaba.excel.annotation.ExcelProperty; import io.github.linpeilie.annotations.AutoMapper; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; @@ -85,6 +86,10 @@ public class ChatModelBo extends BaseEntity { @NotBlank(message = "密钥不能为空", groups = { AddGroup.class, EditGroup.class }) private String apiKey; + /** + * 模型供应商 + */ + private String ProviderName; /** * 备注 diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/McpInfoBo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/McpInfoBo.java new file mode 100644 index 00000000..2228fb99 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/bo/McpInfoBo.java @@ -0,0 +1,59 @@ +package org.ruoyi.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.ruoyi.domain.McpInfo; + + +import java.io.Serializable; + +/** + * MCP业务对象 mcp_info + * + * @author ageerle + * @date Sat Aug 09 16:50:58 CST 2025 + */ +@Data + +@AutoMapper(target = McpInfo.class, reverseConvertGenerate = false) +public class McpInfoBo implements Serializable { + + /** + * id + */ + @NotNull(message = "id不能为空" ) + private Integer mcpId; + + /** + * 服务器名称 + */ + private String serverName; + + /** + * 链接方式 + */ + private String transportType; + + /** + * Command + */ + private String command; + + /** + * Args + */ + private String arguments; + private String description; + /** + * Env + */ + private String env; + + /** + * 是否启用 + */ + private Boolean status; + + +} diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java index 0638c13a..062a378a 100644 --- a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/ChatModelVo.java @@ -70,6 +70,11 @@ public class ChatModelVo implements Serializable { @ExcelProperty(value = "是否显示") private String modelShow; + /** + * 模型维度 + */ + private Integer dimension; + /** * 系统提示词 */ @@ -95,6 +100,12 @@ public class ChatModelVo implements Serializable { @ExcelProperty(value = "优先级") private Integer priority; + /** + * 模型供应商 + */ + @ExcelProperty(value = "模型供应商") + private String ProviderName; + /** * 备注 */ diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/McpInfoVo.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/McpInfoVo.java new file mode 100644 index 00000000..b5cc27c1 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/domain/vo/McpInfoVo.java @@ -0,0 +1,65 @@ +package org.ruoyi.domain.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.ruoyi.common.excel.annotation.ExcelDictFormat; +import org.ruoyi.common.excel.convert.ExcelDictConvert; +import org.ruoyi.domain.McpInfo; + +import java.io.Serializable; + + +/** + * MCP视图对象 mcp_info + * + * @author jiyi + * @date Sat Aug 09 16:50:58 CST 2025 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = McpInfo.class) +public class McpInfoVo implements Serializable { + private Integer mcpId; + + /** + * 服务器名称 + */ + @ExcelProperty(value = "服务器名称") + private String serverName; + + /** + * 链接方式 + */ + @ExcelProperty(value = "链接方式", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "mcp_transport_type") + private String transportType; + + /** + * Command + */ + @ExcelProperty(value = "Command") + private String command; + + /** + * Args + */ + @ExcelProperty(value = "Args") + private String arguments; + @ExcelProperty(value = "Description") + private String description; + /** + * Env + */ + @ExcelProperty(value = "Env") + private String env; + + /** + * 是否启用 + */ + @ExcelProperty(value = "是否启用") + private Boolean status; + + +} diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/mapper/McpInfoMapper.java b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/mapper/McpInfoMapper.java new file mode 100644 index 00000000..fb26aaac --- /dev/null +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/java/org/ruoyi/mapper/McpInfoMapper.java @@ -0,0 +1,33 @@ +package org.ruoyi.mapper; + + +import org.apache.ibatis.annotations.*; +import org.ruoyi.core.mapper.BaseMapperPlus; +import org.ruoyi.domain.McpInfo; +import org.ruoyi.domain.vo.McpInfoVo; + +import java.util.List; + +/** + * MCPMapper接口 + * + * @author jiuyi + * @date Sat Aug 09 16:50:58 CST 2025 + */ +@Mapper +public interface McpInfoMapper extends BaseMapperPlus { + @Select("SELECT * FROM mcp_info WHERE server_name = #{serverName}") + McpInfo selectByServerName(@Param("serverName") String serverName); + + @Select("SELECT * FROM mcp_info WHERE status = 1") + List selectActiveServers(); + + @Select("SELECT server_name FROM mcp_info WHERE status = 1") + List selectActiveServerNames(); + + @Update("UPDATE mcp_info SET status = #{status} WHERE server_name = #{serverName}") + int updateActiveStatus(@Param("serverName") String serverName, @Param("status") Boolean status); + + @Delete("DELETE FROM mcp_info WHERE server_name = #{serverName}") + int deleteByServerName(@Param("serverName") String serverName); +} diff --git a/ruoyi-modules-api/ruoyi-chat-api/src/main/resources/mapper/McpInfoMapper.xml b/ruoyi-modules-api/ruoyi-chat-api/src/main/resources/mapper/McpInfoMapper.xml new file mode 100644 index 00000000..f2a28e2a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-chat-api/src/main/resources/mapper/McpInfoMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/pom.xml b/ruoyi-modules-api/ruoyi-knowledge-api/pom.xml index f8082a67..fc7e01bc 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/pom.xml +++ b/ruoyi-modules-api/ruoyi-knowledge-api/pom.xml @@ -74,6 +74,18 @@ 1.19.6 + + io.milvus + milvus-sdk-java + 2.6.4 + + + + + dev.langchain4j + langchain4j-milvus + + dev.langchain4j langchain4j-open-ai @@ -101,11 +113,10 @@ commons-compress - - com.alibaba - dashscope-sdk-java - 2.19.0 - + + org.ruoyi + ruoyi-chat-api + diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/KnowledgeInfo.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/KnowledgeInfo.java index 627cd3c6..f892b3c1 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/KnowledgeInfo.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/KnowledgeInfo.java @@ -83,6 +83,11 @@ public class KnowledgeInfo extends BaseEntity { */ private String vectorModelName; + /** + * 向量化模型id + */ + private Long embeddingModelId; + /** * 向量化模型名称 */ diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/KnowledgeInfoBo.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/KnowledgeInfoBo.java index 39083871..af453492 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/KnowledgeInfoBo.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/KnowledgeInfoBo.java @@ -92,7 +92,11 @@ public class KnowledgeInfoBo extends BaseEntity { /** * 向量化模型名称 */ - @NotBlank(message = "向量模型不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long embeddingModelId; + + /** + * 向量化模型名称 + */ private String embeddingModelName; diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/QueryVectorBo.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/QueryVectorBo.java index ff3a26e5..0d5b2e6e 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/QueryVectorBo.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/QueryVectorBo.java @@ -31,7 +31,12 @@ public class QueryVectorBo { private String vectorModelName; /** - * 向量化模型名称 + * 向量化模型ID + */ + private Long embeddingModelId; + + /** + * 向量化模型ID */ private String embeddingModelName; diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/StoreEmbeddingBo.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/StoreEmbeddingBo.java index e4d8c381..eedfe4d6 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/StoreEmbeddingBo.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/bo/StoreEmbeddingBo.java @@ -32,9 +32,14 @@ public class StoreEmbeddingBo { private List fids; /** - * 向量库模型名称 + * 向量库名称 */ - private String vectorModelName; + private String vectorStoreName; + + /** + * 向量化模型id + */ + private Long embeddingModelId; /** * 向量化模型名称 diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/vo/KnowledgeInfoVo.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/vo/KnowledgeInfoVo.java index 2dc73d45..c907e940 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/vo/KnowledgeInfoVo.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/domain/vo/KnowledgeInfoVo.java @@ -101,6 +101,11 @@ public class KnowledgeInfoVo implements Serializable { */ private String vectorModelName; + /** + * 向量化模型id + */ + private Long embeddingModelId; + /** * 向量化模型名称 */ diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/BaseEmbedModelService.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/BaseEmbedModelService.java new file mode 100644 index 00000000..9b3d0021 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/BaseEmbedModelService.java @@ -0,0 +1,26 @@ +package org.ruoyi.embedding; + +import dev.langchain4j.model.embedding.EmbeddingModel; +import org.ruoyi.domain.vo.ChatModelVo; +import org.ruoyi.embedding.model.ModalityType; + +import java.util.Set; + +/** + * BaseEmbedModelService 接口,扩展了 EmbeddingModel 接口 + * 该接口定义了嵌入模型服务的基本配置和功能方法 + */ +public interface BaseEmbedModelService extends EmbeddingModel { + /** + * 根据配置信息配置嵌入模型 + * @param config 包含模型配置信息的 ChatModelVo 对象 + */ + void configure(ChatModelVo config); + + /** + * 获取当前嵌入模型支持的所有模态类型 + * @return 返回支持的模态类型集合 + */ + Set getSupportedModalities(); + +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/EmbeddingModelFactory.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/EmbeddingModelFactory.java new file mode 100644 index 00000000..3acf8a91 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/EmbeddingModelFactory.java @@ -0,0 +1,120 @@ +package org.ruoyi.embedding; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.domain.vo.ChatModelVo; +import org.ruoyi.service.IChatModelService; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 嵌入模型工厂服务类 + * 负责创建和管理各种嵌入模型实例 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class EmbeddingModelFactory { + + private final ApplicationContext applicationContext; + + private final IChatModelService chatModelService; + + // 模型缓存,使用ConcurrentHashMap保证线程安全 + private final Map modelCache = new ConcurrentHashMap<>(); + + /** + * 创建嵌入模型实例 + * 如果模型已存在于缓存中,则直接返回;否则创建新的实例 + * + * @param embeddingModelName 嵌入模型名称 + * @param dimension 模型维度大小 + */ + public BaseEmbedModelService createModel(String embeddingModelName, Integer dimension) { + return modelCache.computeIfAbsent(embeddingModelName, name -> { + ChatModelVo modelConfig = chatModelService.selectModelByName(embeddingModelName); + if (modelConfig == null) { + throw new IllegalArgumentException("未找到模型配置,name=" + name); + } + if (modelConfig.getDimension() != null) { + modelConfig.setDimension(dimension); + } + return createModelInstance(modelConfig.getProviderName(), modelConfig); + }); + } + + /** + * 检查模型是否支持多模态 + * + * @param embeddingModelName 嵌入模型名称 + * @return boolean 如果模型支持多模态则返回true,否则返回false + */ + public boolean isMultimodalModel(String embeddingModelName) { + return createModel(embeddingModelName, null) instanceof MultiModalEmbedModelService; + } + + /** + * 创建多模态嵌入模型实例 + * + * @param embeddingModelName 嵌入模型名称 + * @return MultiModalEmbedModelService 多模态嵌入模型服务实例 + * @throws IllegalArgumentException 当模型不支持多模态时抛出 + */ + public MultiModalEmbedModelService createMultimodalModel(String embeddingModelName) { + BaseEmbedModelService model = createModel(embeddingModelName, null); + if (model instanceof MultiModalEmbedModelService) { + return (MultiModalEmbedModelService) model; + } + throw new IllegalArgumentException("该模型不支持多模态"); + } + + /** + * 刷新模型缓存 + * 根据给定的嵌入模型ID从缓存中移除对应的模型 + * + * @param embeddingModelId 嵌入模型的唯一标识ID + */ + public void refreshModel(Long embeddingModelId) { + // 从模型缓存中移除指定ID的模型 + modelCache.remove(embeddingModelId); + } + + /** + * 获取所有支持模型工厂的列表 + * + * @return List 支持的模型工厂名称列表 + */ + public List getSupportedFactories() { + return new ArrayList<>(applicationContext.getBeansOfType(BaseEmbedModelService.class) + .keySet()); + } + + /** + * 创建具体的模型实例 + * 根据提供的工厂名称和配置信息创建并配置模型实例 + * + * @param factory 工厂名称,用于标识模型类型 + * @param config 模型配置信息 + * @return BaseEmbedModelService 配置好的模型实例 + * @throws IllegalArgumentException 当无法获取指定的模型实例时抛出 + */ + private BaseEmbedModelService createModelInstance(String factory, ChatModelVo config) { + try { + // 从Spring上下文中获取模型实例 + BaseEmbedModelService model = applicationContext.getBean(factory, BaseEmbedModelService.class); + // 配置模型参数 + model.configure(config); + log.info("成功创建嵌入模型: factory={}, modelId={}", config.getProviderName(), config.getId()); + + return model; + } catch (NoSuchBeanDefinitionException e) { + throw new IllegalArgumentException("获取不到嵌入模型: " + factory, e); + } + } +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/MultiModalEmbedModelService.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/MultiModalEmbedModelService.java new file mode 100644 index 00000000..062ff00f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/MultiModalEmbedModelService.java @@ -0,0 +1,35 @@ +package org.ruoyi.embedding; + +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.model.output.Response; +import org.ruoyi.embedding.model.MultiModalInput; + + +/** + * 多模态嵌入模型服务接口,继承自基础嵌入模型服务 + * 该接口提供了处理图像、视频以及多模态数据并转换为嵌入向量的功能 + */ +public interface MultiModalEmbedModelService extends BaseEmbedModelService { + /** + * 将图像数据转换为嵌入向量 + * @param imageDataUrl 图像的地址,必须是公开可访问的URL + * @return 包含嵌入向量的响应对象,可能包含状态信息和嵌入结果 + */ + Response embedImage(String imageDataUrl); + + /** + * 将视频数据转换为嵌入向量 + * @param videoDataUrl 视频的地址,必须是公开可访问的URL + * @return 包含嵌入向量的响应对象,可能包含状态信息和嵌入结果 + */ + Response embedVideo(String videoDataUrl); + + + /** + * 处理多模态输入并返回嵌入向量的方法 + * + * @param input 包含多种模态信息(如图像、文本等)的输入对象 + * @return Response 包含嵌入向量的响应对象,Embedding通常表示输入数据的向量表示 + */ + Response embedMultiModal(MultiModalInput input); +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/AliBaiLianBaseEmbedProvider.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/AliBaiLianBaseEmbedProvider.java new file mode 100644 index 00000000..1511a0fe --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/AliBaiLianBaseEmbedProvider.java @@ -0,0 +1,14 @@ +package org.ruoyi.embedding.impl; + + +import org.springframework.stereotype.Component; + +/** + * @Author: Robust_H + * @Date: 2025-09-30-下午3:00 + * @Description: 阿里百炼基础嵌入模型(兼容openai) + */ +@Component("alibailian") +public class AliBaiLianBaseEmbedProvider extends OpenAiEmbeddingProvider{ + +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/AliBaiLianMultiEmbeddingProvider.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/AliBaiLianMultiEmbeddingProvider.java new file mode 100644 index 00000000..ad3e8374 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/AliBaiLianMultiEmbeddingProvider.java @@ -0,0 +1,281 @@ +package org.ruoyi.embedding.impl; + +import com.fasterxml.jackson.databind.ObjectMapper; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.output.Response; +import dev.langchain4j.model.output.TokenUsage; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.ruoyi.domain.vo.ChatModelVo; +import org.ruoyi.embedding.MultiModalEmbedModelService; +import org.ruoyi.embedding.model.AliyunMultiModalEmbedRequest; +import org.ruoyi.embedding.model.AliyunMultiModalEmbedResponse; +import org.ruoyi.embedding.model.ModalityType; +import org.ruoyi.embedding.model.MultiModalInput; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.TimeUnit; + +/** + * 阿里云百炼多模态嵌入模型服务实现类 + * 实现了MultiModalEmbedModelService接口,提供文本、图像和视频的嵌入向量生成服务 + */ +@Component("bailianMultiModel") +@Slf4j +public class AliBaiLianMultiEmbeddingProvider implements MultiModalEmbedModelService { + private ChatModelVo chatModelVo; + + private final OkHttpClient okHttpClient; + + /** + * 构造函数,初始化HTTP客户端 + * 设置连接超时、读取超时和写入超时时间 + */ + public AliBaiLianMultiEmbeddingProvider() { + this.okHttpClient = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build(); + } + + /** + * 图像嵌入向量生成 + * @param imageDataUrl 图像数据的URL + * @return 包含图像嵌入向量的Response对象 + */ + @Override + public Response embedImage(String imageDataUrl) { + return embedSingleModality("image", imageDataUrl); + } + + /** + * 视频嵌入向量生成 + * @param videoDataUrl 视频数据的URL + * @return 包含视频嵌入向量的Response对象 + */ + @Override + public Response embedVideo(String videoDataUrl) { + return embedSingleModality("video", videoDataUrl); + } + + /** + * 多模态嵌入向量生成 + * 支持同时处理文本、图像和视频等多种模态的数据 + * @param input 包含多种模态输入的对象 + * @return 包含多模态嵌入向量的Response对象 + */ + @Override + public Response embedMultiModal(MultiModalInput input) { + try { + // 构建请求内容 + List> contents = buildContentMap(input); + if (contents.isEmpty()) { + throw new IllegalArgumentException("至少提供一种模态的内容"); + } + + // 构建请求 + AliyunMultiModalEmbedRequest request = buildRequest(contents, chatModelVo); + AliyunMultiModalEmbedResponse resp = executeRequest(request, chatModelVo); + + // 转换为 embeddings + Response> response = toEmbeddings(resp); + List embeddings = response.content(); + + if (embeddings.isEmpty()) { + log.warn("阿里云混合模态嵌入返回为空"); + return Response.from(Embedding.from(new float[0]), response.tokenUsage()); + } + + // 多模态通常取第一个向量作为代表,也可以根据业务场景返回多个 + return Response.from(embeddings.get(0), response.tokenUsage()); + + } catch (Exception e) { + log.error("阿里云混合模态嵌入失败", e); + throw new IllegalArgumentException("阿里云混合模态嵌入失败", e); + } + } + + /** + * 配置模型参数 + * @param config 模型配置信息 + */ + @Override + public void configure(ChatModelVo config) { + this.chatModelVo = config; + } + + /** + * 获取支持的模态类型 + * @return 支持的模态类型集合 + */ + @Override + public Set getSupportedModalities() { + return Set.of(ModalityType.TEXT, ModalityType.VIDEO, ModalityType.IMAGE); + } + + /** + * 批量文本嵌入向量生成 + * @param textSegments 文本段列表 + * @return 包含所有文本嵌入向量的Response对象 + */ + @Override + public Response> embedAll(List textSegments) { + if (textSegments.isEmpty()) return Response.from(Collections.emptyList()); + + try { + List> contents = new ArrayList<>(); + for (TextSegment segment : textSegments) { + contents.add(Map.of("text", segment.text())); + } + + AliyunMultiModalEmbedRequest request = buildRequest(contents, chatModelVo); + AliyunMultiModalEmbedResponse resp = executeRequest(request, chatModelVo); + + return toEmbeddings(resp); + } catch (Exception e) { + log.error("阿里云文本嵌入失败", e); + throw new IllegalArgumentException("阿里云文本嵌入失败", e); + } + } + + /** + * 单模态嵌入(图片/视频/单条文本)复用方法 + * @param key 模态类型(image/video/text) + * @param dataUrl 数据URL + * @return 包含嵌入向量的Response对象 + */ + + public Response embedSingleModality(String key, String dataUrl) { + try { + AliyunMultiModalEmbedRequest request = buildRequest(List.of(Map.of(key, dataUrl)), chatModelVo); + AliyunMultiModalEmbedResponse resp = executeRequest(request, chatModelVo); + + Response> response = toEmbeddings(resp); + List embeddings = response.content(); + + if (embeddings.isEmpty()) { + log.warn("阿里云 {} 嵌入返回为空", key); + return Response.from(Embedding.from(new float[0]), response.tokenUsage()); + } + + return Response.from(embeddings.get(0), response.tokenUsage()); + } catch (Exception e) { + log.error("阿里云 {} 嵌入失败", key, e); + throw new IllegalArgumentException("阿里云 " + key + " 嵌入失败", e); + } + } + + /** + * 构建请求对象 + * @param contents 请求内容列表 + * @param chatModelVo 模型配置信息 + * @return 构建好的请求对象 + */ + private AliyunMultiModalEmbedRequest buildRequest(List> contents, ChatModelVo chatModelVo) { + if (contents.isEmpty()) throw new IllegalArgumentException("请求内容不能为空"); + return AliyunMultiModalEmbedRequest.create(chatModelVo.getModelName(), contents); + } + + /** + * 执行 HTTP 请求并解析响应 + * @param request 请求对象 + * @param chatModelVo 模型配置信息 + * @return API响应对象 + * @throws IOException IO异常 + */ + private AliyunMultiModalEmbedResponse executeRequest(AliyunMultiModalEmbedRequest request, ChatModelVo chatModelVo) throws IOException { + String jsonBody = request.toJson(); + RequestBody body = RequestBody.create(jsonBody, MediaType.get("application/json")); + + Request httpRequest = new Request.Builder() + .url(chatModelVo.getApiHost()) + .addHeader("Authorization", "Bearer " + chatModelVo.getApiKey()) + .post(body) + .build(); + + try (okhttp3.Response response = okHttpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + String err = response.body() != null ? response.body().string() : "无错误信息"; + throw new IllegalArgumentException("API调用失败: " + response.code() + " - " + err, null); + } + + ResponseBody responseBody = response.body(); + if (responseBody == null) throw new IllegalArgumentException("响应体为空", null); + + return parseEmbeddingsFromResponse(responseBody.string()); + } + } + + /** + * 解析嵌入向量列表 + * @param responseBody API响应的JSON字符串 + * @return 嵌入向量响应对象 + * @throws IOException IO异常 + */ + private AliyunMultiModalEmbedResponse parseEmbeddingsFromResponse(String responseBody) throws IOException { + ObjectMapper objectMapper1 = new ObjectMapper(); + return objectMapper1.readValue(responseBody, AliyunMultiModalEmbedResponse.class); + } + + /** + * 构建 API 请求内容 Map + * @param input 多模态输入对象 + * @return 包含各种模态内容的Map列表 + */ + private List> buildContentMap(MultiModalInput input) { + List> contents = new ArrayList<>(); + + if (input.getText() != null && !input.getText().isBlank()) { + contents.add(Map.of("text", input.getText())); + } + if (input.getImageUrl() != null && !input.getImageUrl().isBlank()) { + contents.add(Map.of("image", input.getImageUrl())); + } + if (input.getVideoUrl() != null && !input.getVideoUrl().isBlank()) { + contents.add(Map.of("video", input.getVideoUrl())); + } + if (input.getMultiImageUrls() != null && input.getMultiImageUrls().length > 0) { + contents.add(Map.of("multi_images", Arrays.asList(input.getMultiImageUrls()))); + } + + return contents; + } + + /** + * 将 API 原始响应解析为 LangChain4j 的 Response + * @param resp API原始响应对象 + * @return 包含嵌入向量和token使用情况的Response对象 + */ + private Response> toEmbeddings(AliyunMultiModalEmbedResponse resp) { + if (resp == null || resp.output() == null || resp.output().embeddings() == null) { + return Response.from(Collections.emptyList()); + } + + // 转换 double -> float + List embeddings = resp.output().embeddings().stream() + .map(item -> { + float[] vector = new float[item.embedding().size()]; + for (int i = 0; i < item.embedding().size(); i++) { + vector[i] = item.embedding().get(i).floatValue(); + } + return Embedding.from(vector); + }) + .toList(); + + // 构建 TokenUsage + TokenUsage tokenUsage = null; + if (resp.usage() != null) { + tokenUsage = new TokenUsage( + resp.usage().input_tokens(), + resp.usage().image_tokens(), + resp.usage().input_tokens() +resp.usage().image_tokens() + ); + } + + return Response.from(embeddings, tokenUsage); + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/OllamaEmbeddingProvider.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/OllamaEmbeddingProvider.java new file mode 100644 index 00000000..1a179be7 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/OllamaEmbeddingProvider.java @@ -0,0 +1,42 @@ +package org.ruoyi.embedding.impl; + +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.ollama.OllamaEmbeddingModel; +import dev.langchain4j.model.output.Response; +import org.ruoyi.domain.vo.ChatModelVo; +import org.ruoyi.embedding.BaseEmbedModelService; +import org.ruoyi.embedding.model.ModalityType; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * @Author: Robust_H + * @Date: 2025-09-30-下午3:00 + * @Description: Ollama嵌入模型 + */ +@Component("ollama") +public class OllamaEmbeddingProvider implements BaseEmbedModelService { + private ChatModelVo chatModelVo; + + @Override + public void configure(ChatModelVo config) { + this.chatModelVo = config; + } + @Override + public Set getSupportedModalities() { + return Set.of(ModalityType.TEXT); + } + + // ollama不能设置embedding维度,使用milvus时请注意!!创建向量表时需要先设定维度大小 + @Override + public Response> embedAll(List textSegments) { + return OllamaEmbeddingModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .modelName(chatModelVo.getModelName()) + .build() + .embedAll(textSegments); + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/OpenAiEmbeddingProvider.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/OpenAiEmbeddingProvider.java new file mode 100644 index 00000000..8a0c9f62 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/OpenAiEmbeddingProvider.java @@ -0,0 +1,44 @@ +package org.ruoyi.embedding.impl; + +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.openai.OpenAiEmbeddingModel; +import dev.langchain4j.model.output.Response; +import org.ruoyi.domain.vo.ChatModelVo; +import org.ruoyi.embedding.BaseEmbedModelService; +import org.ruoyi.embedding.model.ModalityType; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * @Author: Robust_H + * @Date: 2025-09-30-下午3:59 + * @Description: OpenAi嵌入模型 + */ +@Component("openai") +public class OpenAiEmbeddingProvider implements BaseEmbedModelService { + protected ChatModelVo chatModelVo; + + @Override + public void configure(ChatModelVo config) { + this.chatModelVo = config; + } + + @Override + public Set getSupportedModalities() { + return Set.of(ModalityType.TEXT); + } + + @Override + public Response> embedAll(List textSegments) { + return OpenAiEmbeddingModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .dimensions(chatModelVo.getDimension()) + .build() + .embedAll(textSegments); + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/SiliconFlowEmbeddingProvider.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/SiliconFlowEmbeddingProvider.java new file mode 100644 index 00000000..e0ccd2b9 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/SiliconFlowEmbeddingProvider.java @@ -0,0 +1,18 @@ +package org.ruoyi.embedding.impl; + + +import org.ruoyi.embedding.BaseEmbedModelService; +import org.ruoyi.embedding.model.ModalityType; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * @Author: Robust_H + * @Date: 2025-09-30-下午3:59 + * @Description: 硅基流动(兼容 OpenAi) + */ +@Component("siliconflow") +public class SiliconFlowEmbeddingProvider extends OpenAiEmbeddingProvider { + +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/ZhiPuAiEmbeddingProvider.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/ZhiPuAiEmbeddingProvider.java new file mode 100644 index 00000000..621e47c6 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/impl/ZhiPuAiEmbeddingProvider.java @@ -0,0 +1,44 @@ +package org.ruoyi.embedding.impl; + +import dev.langchain4j.community.model.zhipu.ZhipuAiEmbeddingModel; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.output.Response; +import org.ruoyi.domain.vo.ChatModelVo; +import org.ruoyi.embedding.BaseEmbedModelService; +import org.ruoyi.embedding.model.ModalityType; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * @Author: Robust_H + * @Date: 2025-09-30-下午4:02 + * @Description: 智谱AI + */ +@Component("zhipu") +public class ZhiPuAiEmbeddingProvider implements BaseEmbedModelService { + private ChatModelVo chatModelVo; + + @Override + public void configure(ChatModelVo config) { + this.chatModelVo = config; + } + + @Override + public Set getSupportedModalities() { + return Set.of(); + } + + @Override + public Response> embedAll(List textSegments) { + return ZhipuAiEmbeddingModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .apiKey(chatModelVo.getApiKey()) + .model(chatModelVo.getModelName()) + .dimensions(chatModelVo.getDimension()) + .build() + .embedAll(textSegments); + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/AliyunMultiModalEmbedRequest.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/AliyunMultiModalEmbedRequest.java new file mode 100644 index 00000000..eb880588 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/AliyunMultiModalEmbedRequest.java @@ -0,0 +1,44 @@ +package org.ruoyi.embedding.model; + +import org.ruoyi.common.json.utils.JsonUtils; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * @Author: Robust_H + * @Date: 2025-10-1-上午10:00 + * @Description: 阿里云多模态嵌入请求 + */ +@Data +public class AliyunMultiModalEmbedRequest { + private String model; + private Input input; + + /** + * 表示输入数据的记录类(Record) + * 该类用于封装一个包含多个映射关系列表的输入数据结构 + * + * @param contents 包含多个Map的列表,每个Map中存储String类型的键和Object类型的值 + */ + public record Input(List> contents) { } + + /** + * 创建请求对象 + */ + public static AliyunMultiModalEmbedRequest create(String modelName, List> contents) { + AliyunMultiModalEmbedRequest request = new AliyunMultiModalEmbedRequest(); + request.setModel(modelName); + Input input = new Input(contents); + request.setInput(input); + return request; + } + + /** + * 转换为JSON字符串 + */ + public String toJson() { + return JsonUtils.toJsonString(this); + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/AliyunMultiModalEmbedResponse.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/AliyunMultiModalEmbedResponse.java new file mode 100644 index 00000000..03446d47 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/AliyunMultiModalEmbedResponse.java @@ -0,0 +1,44 @@ +package org.ruoyi.embedding.model; + +import java.util.List; + +/** + * 阿里云多模态嵌入 API 响应数据模型 + */ +public record AliyunMultiModalEmbedResponse( + Output output, // 输出结果对象 + String request_id, // 请求唯一标识 + String code, // 错误码 + String message, // 错误消息 + Usage usage // 用量信息 +) { + + /** + * 输出对象,包含嵌入向量结果 + */ + public record Output( + List embeddings // 嵌入向量列表 + ) { + } + + /** + * 单个嵌入向量条目 + */ + public record EmbeddingItem( + int index, // 输入内容的索引 + List embedding, // 生成的 1024 维向量 + String type // 输入的类型 (text/image/video/multi_images) + ) { + } + + /** + * 用量统计信息 + */ + public record Usage( + int input_tokens, // 本次请求输入的 Token 数量 + int image_tokens, // 本次请求输入的图像 Token 数量 + int image_count, // 本次请求输入的图像数量 + int duration // 本次请求输入的视频时长(秒) + ) { + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/ModalityType.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/ModalityType.java new file mode 100644 index 00000000..782aac62 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/ModalityType.java @@ -0,0 +1,8 @@ +package org.ruoyi.embedding.model; + +/** + * 模态类型 + */ +public enum ModalityType { + TEXT, IMAGE, AUDIO, VIDEO, MULTI +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/MultiModalInput.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/MultiModalInput.java new file mode 100644 index 00000000..f2a31e98 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/embedding/model/MultiModalInput.java @@ -0,0 +1,71 @@ +package org.ruoyi.embedding.model; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import lombok.Builder; +import lombok.Data; + +/** + * @Author: Robust_H + * @Date: 2025-09-30-下午2:13 + * @Description: 多模态输入 + */ +@Data +@Builder +public class MultiModalInput { + private String text; + private byte[] imageData; + private byte[] videoData; + private String imageMimeType; + private String videoMimeType; + private String[] multiImageUrls; + private String imageUrl; + private String videoUrl; + + /** + * 检查是否有文本内容 + */ + public boolean hasText() { + return StrUtil.isNotBlank(text); + } + + /** + * 检查是否有图片内容 + */ + public boolean hasImage() { + return ArrayUtil.isNotEmpty(imageData) || StrUtil.isNotBlank(imageUrl); + } + + /** + * 检查是否有视频内容 + */ + public boolean hasVideo() { + return ArrayUtil.isNotEmpty(videoData) || StrUtil.isNotBlank(videoUrl); + } + + /** + * 检查是否有多图片 + */ + public boolean hasMultiImages() { + return ArrayUtil.isNotEmpty(multiImageUrls); + } + + /** + * 检查是否有任何内容 + */ + public boolean hasAnyContent() { + return hasText() || hasImage() || hasVideo() || hasMultiImages(); + } + + /** + * 获取内容的数量 + */ + public int getContentCount() { + int count = 0; + if (hasText()) count++; + if (hasImage()) count++; + if (hasVideo()) count++; + if (hasMultiImages()) count++; + return count; + } +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/VectorStoreService.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/VectorStoreService.java index 29bfecc3..2937fc44 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/VectorStoreService.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/VectorStoreService.java @@ -1,5 +1,6 @@ package org.ruoyi.service; +import org.ruoyi.common.core.exception.ServiceException; import org.ruoyi.domain.bo.QueryVectorBo; import org.ruoyi.domain.bo.StoreEmbeddingBo; @@ -11,15 +12,15 @@ import java.util.List; */ public interface VectorStoreService { - void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo); + void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) throws ServiceException; List getQueryVector(QueryVectorBo queryVectorBo); - void createSchema(String kid,String modelName); + void createSchema(String kid, String embeddingModelName); - void removeById(String id,String modelName); + void removeById(String id,String modelName) throws ServiceException; - void removeByDocId(String docId, String kid); + void removeByDocId(String docId, String kid) throws ServiceException; - void removeByFid(String fid, String kid); + void removeByFid(String fid, String kid) throws ServiceException; } diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java index 799ce729..c9aeeab4 100644 --- a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/impl/VectorStoreServiceImpl.java @@ -1,37 +1,14 @@ package org.ruoyi.service.impl; -import cn.hutool.json.JSONObject; -import com.google.protobuf.ServiceException; -import dev.langchain4j.data.embedding.Embedding; -import dev.langchain4j.data.segment.TextSegment; -import dev.langchain4j.model.embedding.EmbeddingModel; -import dev.langchain4j.model.ollama.OllamaEmbeddingModel; -import dev.langchain4j.model.openai.OpenAiEmbeddingModel; -import dev.langchain4j.store.embedding.EmbeddingMatch; -import dev.langchain4j.store.embedding.EmbeddingSearchRequest; -import dev.langchain4j.store.embedding.EmbeddingStore; -import dev.langchain4j.store.embedding.weaviate.WeaviateEmbeddingStore; -import io.weaviate.client.Config; -import io.weaviate.client.WeaviateClient; -import io.weaviate.client.base.Result; -import io.weaviate.client.v1.batch.api.ObjectsBatchDeleter; -import io.weaviate.client.v1.batch.model.BatchDeleteResponse; -import io.weaviate.client.v1.filters.Operator; -import io.weaviate.client.v1.filters.WhereFilter; -import io.weaviate.client.v1.graphql.model.GraphQLResponse; -import io.weaviate.client.v1.schema.model.Property; -import io.weaviate.client.v1.schema.model.Schema; -import io.weaviate.client.v1.schema.model.WeaviateClass; import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.ruoyi.common.core.service.ConfigService; import org.ruoyi.domain.bo.QueryVectorBo; import org.ruoyi.domain.bo.StoreEmbeddingBo; import org.ruoyi.service.VectorStoreService; +import org.ruoyi.service.strategy.VectorStoreStrategyFactory; +import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Service; import java.util.*; -import java.util.stream.Collectors; /** * 向量库管理 @@ -39,235 +16,61 @@ import java.util.stream.Collectors; * @author ageer */ @Service +@Primary @Slf4j @RequiredArgsConstructor public class VectorStoreServiceImpl implements VectorStoreService { - private final ConfigService configService; + private final VectorStoreStrategyFactory strategyFactory; -// private EmbeddingStore embeddingStore; - private WeaviateClient client; + /** + * 获取当前配置的向量库策略 + */ + private VectorStoreService getCurrentStrategy() { + return strategyFactory.getStrategy(); + } @Override public void createSchema(String kid, String modelName) { - String protocol = configService.getConfigValue("weaviate", "protocol"); - String host = configService.getConfigValue("weaviate", "host"); - String className = configService.getConfigValue("weaviate", "classname")+kid; - // 创建 Weaviate 客户端 - client= new WeaviateClient(new Config(protocol, host)); - // 检查类是否存在,如果不存在就创建 schema - Result schemaResult = client.schema().getter().run(); - Schema schema = schemaResult.getResult(); - boolean classExists = false; - for (WeaviateClass weaviateClass : schema.getClasses()) { - if (weaviateClass.getClassName().equals(className)) { - classExists = true; - break; - } - } - if (!classExists) { - // 类不存在,创建 schema - WeaviateClass build = WeaviateClass.builder() - .className(className) - .vectorizer("none") - .properties( - List.of(Property.builder().name("text").dataType(Collections.singletonList("text")).build(), - Property.builder().name("fid").dataType(Collections.singletonList("text")).build(), - Property.builder().name("kid").dataType(Collections.singletonList("text")).build(), - Property.builder().name("docId").dataType(Collections.singletonList("text")).build()) - ) - .build(); - Result createResult = client.schema().classCreator().withClass(build).run(); - if (createResult.hasErrors()) { - log.error("Schema 创建失败: {}", createResult.getError()); - } else { - log.info("Schema 创建成功: {}", className); - } - } -// embeddingStore = WeaviateEmbeddingStore.builder() -// .scheme(protocol) -// .host(host) -// .objectClass(className) -// .scheme(protocol) -// .avoidDups(true) -// .consistencyLevel("ALL") -// .build(); + VectorStoreService strategy = getCurrentStrategy(); + strategy.createSchema(kid, modelName); } @Override public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) { - createSchema(storeEmbeddingBo.getKid(), storeEmbeddingBo.getVectorModelName()); - EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName(), - storeEmbeddingBo.getApiKey(), storeEmbeddingBo.getBaseUrl()); - List chunkList = storeEmbeddingBo.getChunkList(); - List fidList = storeEmbeddingBo.getFids(); - String kid = storeEmbeddingBo.getKid(); - String docId = storeEmbeddingBo.getDocId(); - log.info("向量存储条数记录: " + chunkList.size()); - long startTime = System.currentTimeMillis(); - for (int i = 0; i < chunkList.size(); i++) { - String text = chunkList.get(i); - String fid = fidList.get(i); - Embedding embedding = embeddingModel.embed(text).content(); - Map properties = Map.of( - "text", text, - "fid",fid, - "kid", kid, - "docId", docId - ); - Float[] vector = toObjectArray(embedding.vector()); - client.data().creator() - .withClassName("LocalKnowledge" + kid) // 注意替换成实际类名 - .withProperties(properties) - .withVector(vector) - .run(); - } - long endTime = System.currentTimeMillis(); - log.info("向量存储完成消耗时间:"+ (endTime-startTime)/1000+"秒"); + log.info("存储向量数据: kid={}, docId={}, 数据条数={}", + storeEmbeddingBo.getKid(), storeEmbeddingBo.getDocId(), storeEmbeddingBo.getChunkList().size()); + VectorStoreService strategy = getCurrentStrategy(); + strategy.storeEmbeddings(storeEmbeddingBo); } - private static Float[] toObjectArray(float[] primitive) { - Float[] result = new Float[primitive.length]; - for (int i = 0; i < primitive.length; i++) { - result[i] = primitive[i]; // 自动装箱 - } - return result; - } @Override public List getQueryVector(QueryVectorBo queryVectorBo) { - createSchema(queryVectorBo.getKid(), queryVectorBo.getVectorModelName()); - EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName(), - queryVectorBo.getApiKey(), queryVectorBo.getBaseUrl()); - Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content(); - float[] vector = queryEmbedding.vector(); - List vectorStrings = new ArrayList<>(); - for (float v : vector) { - vectorStrings.add(String.valueOf(v)); - } - String vectorStr = String.join(",", vectorStrings); - String className = configService.getConfigValue("weaviate", "classname") ; - // 构建 GraphQL 查询 - String graphQLQuery = String.format( - "{\n" + - " Get {\n" + - " %s(nearVector: {vector: [%s]} limit: %d) {\n" + - " text\n" + - " fid\n" + - " kid\n" + - " docId\n" + - " _additional {\n" + - " distance\n" + - " id\n" + - " }\n" + - " }\n" + - " }\n" + - "}", - className+ queryVectorBo.getKid(), - vectorStr, - queryVectorBo.getMaxResults() - ); - - Result result = client.graphQL().raw().withQuery(graphQLQuery).run(); - List resultList = new ArrayList<>(); - if (result != null && !result.hasErrors()) { - Object data = result.getResult().getData(); - JSONObject entries = new JSONObject(data); - Map entriesMap = entries.get("Get", Map.class); - cn.hutool.json.JSONArray objects = entriesMap.get(className + queryVectorBo.getKid()); - if(objects.isEmpty()){ - return resultList; - } - for (Object object : objects) { - Map map = (Map) object; - String content = map.get("text"); - resultList.add( content); - } - return resultList; - } else { - log.error("GraphQL 查询失败: {}", result.getError()); - return resultList; - } + log.info("查询向量数据: kid={}, query={}, maxResults={}", + queryVectorBo.getKid(), queryVectorBo.getQuery(), queryVectorBo.getMaxResults()); + VectorStoreService strategy = getCurrentStrategy(); + return strategy.getQueryVector(queryVectorBo); } @Override - @SneakyThrows public void removeById(String id, String modelName) { - String protocol = configService.getConfigValue("weaviate", "protocol"); - String host = configService.getConfigValue("weaviate", "host"); - String className = configService.getConfigValue("weaviate", "classname"); - String finalClassName = className + id; - WeaviateClient client = new WeaviateClient(new Config(protocol, host)); - Result result = client.schema().classDeleter().withClassName(finalClassName).run(); - if (result.hasErrors()) { - log.error("失败删除向量: " + result.getError()); - throw new ServiceException("失败删除向量数据!"); - } else { - log.info("成功删除向量数据: " + result.getResult()); - } + log.info("根据ID删除向量数据: id={}, modelName={}", id, modelName); + VectorStoreService strategy = getCurrentStrategy(); + strategy.removeById(id, modelName); } @Override public void removeByDocId(String docId, String kid) { - String className = configService.getConfigValue("weaviate", "classname") + kid; - // 构建 Where 条件 - WhereFilter whereFilter = WhereFilter.builder() - .path("docId") - .operator(Operator.Equal) - .valueText(docId) - .build(); - ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter(); - Result result = deleter.withClassName(className) - .withWhere(whereFilter) - .run(); - if (result != null && !result.hasErrors()) { - log.info("成功删除 docId={} 的所有向量数据", docId); - } else { - log.error("删除失败: {}", result.getError()); - } + log.info("根据docId删除向量数据: docId={}, kid={}", docId, kid); + VectorStoreService strategy = getCurrentStrategy(); + strategy.removeByDocId(docId, kid); } @Override public void removeByFid(String fid, String kid) { - String className = configService.getConfigValue("weaviate", "classname") + kid; - // 构建 Where 条件 - WhereFilter whereFilter = WhereFilter.builder() - .path("fid") - .operator(Operator.Equal) - .valueText(fid) - .build(); - ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter(); - Result result = deleter.withClassName(className) - .withWhere(whereFilter) - .run(); - if (result != null && !result.hasErrors()) { - log.info("成功删除 fid={} 的所有向量数据", fid); - } else { - log.error("删除失败: {}", result.getError()); - } + log.info("根据fid删除向量数据: fid={}, kid={}", fid, kid); + VectorStoreService strategy = getCurrentStrategy(); + strategy.removeByFid(fid, kid); } - - /** - * 获取向量模型 - */ - @SneakyThrows - public EmbeddingModel getEmbeddingModel(String modelName, String apiKey, String baseUrl) { - EmbeddingModel embeddingModel; - if ("quentinz/bge-large-zh-v1.5".equals(modelName)) { - embeddingModel = OllamaEmbeddingModel.builder() - .baseUrl(baseUrl) - .modelName(modelName) - .build(); - } else if ("baai/bge-m3".equals(modelName)) { - embeddingModel = OpenAiEmbeddingModel.builder() - .apiKey(apiKey) - .baseUrl(baseUrl) - .modelName(modelName) - .build(); - } else { - throw new ServiceException("未找到对应向量化模型!"); - } - return embeddingModel; - } - } diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/AbstractVectorStoreStrategy.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/AbstractVectorStoreStrategy.java new file mode 100644 index 00000000..d35d9739 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/AbstractVectorStoreStrategy.java @@ -0,0 +1,52 @@ +package org.ruoyi.service.strategy; + +import org.ruoyi.common.core.exception.ServiceException; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.model.ollama.OllamaEmbeddingModel; +import dev.langchain4j.model.openai.OpenAiEmbeddingModel; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.config.VectorStoreProperties; +import org.ruoyi.common.core.utils.StringUtils; +import org.ruoyi.service.VectorStoreService; +import org.ruoyi.embedding.EmbeddingModelFactory; + +/** + * 向量库策略抽象基类 + * 提供公共的方法实现,如embedding模型获取等 + * + * @author Yzm + */ +@Slf4j +@RequiredArgsConstructor +public abstract class AbstractVectorStoreStrategy implements VectorStoreService { + + protected final VectorStoreProperties vectorStoreProperties; + + private final EmbeddingModelFactory embeddingModelFactory; + + /** + * 获取向量模型 + */ + @SneakyThrows + protected EmbeddingModel getEmbeddingModel(String modelName, Integer dimension) { + return embeddingModelFactory.createModel(modelName, dimension); + } + + /** + * 将float数组转换为Float对象数组 + */ + protected static Float[] toObjectArray(float[] primitive) { + Float[] result = new Float[primitive.length]; + for (int i = 0; i < primitive.length; i++) { + result[i] = primitive[i]; // 自动装箱 + } + return result; + } + + /** + * 获取向量库类型标识 + */ + public abstract String getVectorStoreType(); +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/VectorStoreStrategyFactory.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/VectorStoreStrategyFactory.java new file mode 100644 index 00000000..0bab68cc --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/VectorStoreStrategyFactory.java @@ -0,0 +1,57 @@ +package org.ruoyi.service.strategy; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.config.VectorStoreProperties; +import org.ruoyi.service.strategy.impl.MilvusVectorStoreStrategy; +import org.ruoyi.service.strategy.impl.WeaviateVectorStoreStrategy; +import org.ruoyi.service.VectorStoreService; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 向量库策略工厂 + * 根据配置动态选择向量库实现 + * + * @author Yzm + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class VectorStoreStrategyFactory { + + private final VectorStoreProperties vectorStoreProperties; + private final WeaviateVectorStoreStrategy weaviateStrategy; + private final MilvusVectorStoreStrategy milvusStrategy; + + private Map strategies; + + @PostConstruct + public void init() { + strategies = new HashMap<>(); + strategies.put("weaviate", weaviateStrategy); + strategies.put("milvus", milvusStrategy); + log.info("向量库策略工厂初始化完成,支持的策略: {}", strategies.keySet()); + } + + /** + * 获取当前配置的向量库策略 + */ + public VectorStoreService getStrategy() { + String vectorStoreType = vectorStoreProperties.getType(); + if (vectorStoreType == null || vectorStoreType.trim().isEmpty()) { + vectorStoreType = "weaviate"; // 默认使用weaviate + } + VectorStoreService strategy = strategies.get(vectorStoreType.toLowerCase()); + if (strategy == null) { + log.warn("未找到向量库策略: {}, 使用默认策略: weaviate", vectorStoreType); + strategy = strategies.get("weaviate"); + } + log.debug("使用向量库策略: {}", vectorStoreType); + return strategy; + } + +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/impl/MilvusVectorStoreStrategy.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/impl/MilvusVectorStoreStrategy.java new file mode 100644 index 00000000..8d3d50fa --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/impl/MilvusVectorStoreStrategy.java @@ -0,0 +1,157 @@ +package org.ruoyi.service.strategy.impl; + +import dev.langchain4j.data.document.Metadata; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.data.segment.TextSegment; +import dev.langchain4j.model.embedding.EmbeddingModel; +import dev.langchain4j.store.embedding.EmbeddingMatch; +import dev.langchain4j.store.embedding.EmbeddingSearchRequest; +import dev.langchain4j.store.embedding.EmbeddingStore; +import dev.langchain4j.store.embedding.filter.Filter; +import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder; +import dev.langchain4j.store.embedding.milvus.MilvusEmbeddingStore; +import io.milvus.param.IndexType; +import io.milvus.param.MetricType; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.config.VectorStoreProperties; +import org.ruoyi.domain.bo.QueryVectorBo; +import org.ruoyi.domain.bo.StoreEmbeddingBo; +import org.ruoyi.embedding.EmbeddingModelFactory; +import org.ruoyi.service.strategy.AbstractVectorStoreStrategy; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.IntStream; + +@Slf4j +@Component +public class MilvusVectorStoreStrategy extends AbstractVectorStoreStrategy { + + + private final Integer DIMENSION = 2048; + + public MilvusVectorStoreStrategy(VectorStoreProperties vectorStoreProperties, EmbeddingModelFactory embeddingModelFactory) { + super(vectorStoreProperties, embeddingModelFactory); + } + + // 缓存不同集合与 autoFlush 配置的 Milvus 连接 + private final Map> storeCache = new ConcurrentHashMap<>(); + + private EmbeddingStore getMilvusStore(String collectionName, boolean autoFlushOnInsert) { + String key = collectionName + "|" + autoFlushOnInsert; + return storeCache.computeIfAbsent(key, k -> + MilvusEmbeddingStore.builder() + .uri(vectorStoreProperties.getMilvus().getUrl()) + .collectionName(collectionName) + .dimension(DIMENSION) + .indexType(IndexType.IVF_FLAT) + .metricType(MetricType.L2) + .autoFlushOnInsert(autoFlushOnInsert) + .idFieldName("id") + .textFieldName("text") + .metadataFieldName("metadata") + .vectorFieldName("vector") + .build() + ); + } + + @Override + public void createSchema(String kid, String modelName) { + String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid; + // 使用缓存获取连接以确保只初始化一次 + EmbeddingStore store = getMilvusStore(collectionName, true); + log.info("Milvus集合初始化完成: {}", collectionName); + } + + @Override + public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) { + EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName(), DIMENSION); + + List chunkList = storeEmbeddingBo.getChunkList(); + List fidList = storeEmbeddingBo.getFids(); + String kid = storeEmbeddingBo.getKid(); + String docId = storeEmbeddingBo.getDocId(); + String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid; + + log.info("Milvus向量存储条数记录: {}", chunkList.size()); + long startTime = System.currentTimeMillis(); + + // 复用连接,写入场景使用 autoFlush=false 以提升批量插入性能 + EmbeddingStore embeddingStore = getMilvusStore(collectionName, false); + + IntStream.range(0, chunkList.size()).forEach(i -> { + String text = chunkList.get(i); + String fid = fidList.get(i); + Metadata metadata = new Metadata(); + metadata.put("fid", fid); + metadata.put("kid", kid); + metadata.put("docId", docId); + + TextSegment textSegment = TextSegment.from(text, metadata); + Embedding embedding = embeddingModel.embed(text).content(); + embeddingStore.add(embedding, textSegment); + }); + long endTime = System.currentTimeMillis(); + log.info("Milvus向量存储完成消耗时间:{}秒", (endTime - startTime) / 1000); + } + + @Override + public List getQueryVector(QueryVectorBo queryVectorBo) { + EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName(), DIMENSION); + + Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content(); + String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + queryVectorBo.getKid(); + + // 查询复用连接,autoFlush 对查询无影响,此处保持 true + EmbeddingStore embeddingStore = getMilvusStore(collectionName, true); + + List resultList = new ArrayList<>(); + EmbeddingSearchRequest request = EmbeddingSearchRequest.builder() + .queryEmbedding(queryEmbedding) + .maxResults(queryVectorBo.getMaxResults()) + .build(); + List> matches = embeddingStore.search(request).matches(); + for (EmbeddingMatch match : matches) { + TextSegment segment = match.embedded(); + if (segment != null) { + resultList.add(segment.text()); + } + } + return resultList; + } + + @Override + @SneakyThrows + public void removeById(String id, String modelName) { + // 注意:此处原逻辑使用 collectionname + id,保持现状 + EmbeddingStore embeddingStore = getMilvusStore(vectorStoreProperties.getMilvus().getCollectionname() + id, false); + embeddingStore.remove(id); + } + + @Override + public void removeByDocId(String docId, String kid) { + String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid; + EmbeddingStore embeddingStore = getMilvusStore(collectionName, false); + Filter filter = MetadataFilterBuilder.metadataKey("docId").isEqualTo(docId); + embeddingStore.removeAll(filter); + log.info("Milvus成功删除 docId={} 的所有向量数据", docId); + } + + @Override + public void removeByFid(String fid, String kid) { + String collectionName = vectorStoreProperties.getMilvus().getCollectionname() + kid; + EmbeddingStore embeddingStore = getMilvusStore(collectionName, false); + Filter filter = MetadataFilterBuilder.metadataKey("fid").isEqualTo(fid); + embeddingStore.removeAll(filter); + log.info("Milvus成功删除 fid={} 的所有向量数据", fid); + } + + @Override + public String getVectorStoreType() { + return "milvus"; + } +} diff --git a/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/impl/WeaviateVectorStoreStrategy.java b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/impl/WeaviateVectorStoreStrategy.java new file mode 100644 index 00000000..3d61f8ac --- /dev/null +++ b/ruoyi-modules-api/ruoyi-knowledge-api/src/main/java/org/ruoyi/service/strategy/impl/WeaviateVectorStoreStrategy.java @@ -0,0 +1,232 @@ +package org.ruoyi.service.strategy.impl; + +import cn.hutool.json.JSONObject; +import org.ruoyi.common.core.exception.ServiceException; +import dev.langchain4j.data.embedding.Embedding; +import dev.langchain4j.model.embedding.EmbeddingModel; +import io.weaviate.client.Config; +import io.weaviate.client.WeaviateClient; +import io.weaviate.client.base.Result; +import io.weaviate.client.v1.batch.api.ObjectsBatchDeleter; +import io.weaviate.client.v1.batch.model.BatchDeleteResponse; +import io.weaviate.client.v1.filters.Operator; +import io.weaviate.client.v1.filters.WhereFilter; +import io.weaviate.client.v1.graphql.model.GraphQLResponse; +import io.weaviate.client.v1.schema.model.Property; +import io.weaviate.client.v1.schema.model.Schema; +import io.weaviate.client.v1.schema.model.WeaviateClass; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.config.VectorStoreProperties; +import org.ruoyi.domain.bo.QueryVectorBo; +import org.ruoyi.domain.bo.StoreEmbeddingBo; +import org.ruoyi.embedding.EmbeddingModelFactory; +import org.ruoyi.service.strategy.AbstractVectorStoreStrategy; +import org.springframework.stereotype.Component; +import java.util.*; + +/** + * Weaviate向量库策略实现 + * + * @author Yzm + */ +@Slf4j +@Component +public class WeaviateVectorStoreStrategy extends AbstractVectorStoreStrategy { + + private WeaviateClient client; + + public WeaviateVectorStoreStrategy(VectorStoreProperties vectorStoreProperties, EmbeddingModelFactory embeddingModelFactory) { + super(vectorStoreProperties, embeddingModelFactory); + } + + @Override + public String getVectorStoreType() { + return "weaviate"; + } + + @Override + public void createSchema(String kid, String embeddingModelName) { + String protocol = vectorStoreProperties.getWeaviate().getProtocol(); + String host = vectorStoreProperties.getWeaviate().getHost(); + String className = vectorStoreProperties.getWeaviate().getClassname() + kid; + // 创建 Weaviate 客户端 + client = new WeaviateClient(new Config(protocol, host)); + // 检查类是否存在,如果不存在就创建 schema + Result schemaResult = client.schema().getter().run(); + Schema schema = schemaResult.getResult(); + boolean classExists = false; + for (WeaviateClass weaviateClass : schema.getClasses()) { + if (weaviateClass.getClassName().equals(className)) { + classExists = true; + break; + } + } + if (!classExists) { + // 类不存在,创建 schema + WeaviateClass build = WeaviateClass.builder() + .className(className) + .vectorizer("none") + .properties( + List.of(Property.builder().name("text").dataType(Collections.singletonList("text")).build(), + Property.builder().name("fid").dataType(Collections.singletonList("text")).build(), + Property.builder().name("kid").dataType(Collections.singletonList("text")).build(), + Property.builder().name("docId").dataType(Collections.singletonList("text")).build()) + ) + .build(); + Result createResult = client.schema().classCreator().withClass(build).run(); + if (createResult.hasErrors()) { + log.error("Schema 创建失败: {}", createResult.getError()); + } else { + log.info("Schema 创建成功: {}", className); + } + } + } + + @Override + public void storeEmbeddings(StoreEmbeddingBo storeEmbeddingBo) { + createSchema(storeEmbeddingBo.getKid(),storeEmbeddingBo.getEmbeddingModelName()); + EmbeddingModel embeddingModel = getEmbeddingModel(storeEmbeddingBo.getEmbeddingModelName(), null); + List chunkList = storeEmbeddingBo.getChunkList(); + List fidList = storeEmbeddingBo.getFids(); + String kid = storeEmbeddingBo.getKid(); + String docId = storeEmbeddingBo.getDocId(); + log.info("向量存储条数记录: " + chunkList.size()); + long startTime = System.currentTimeMillis(); + for (int i = 0; i < chunkList.size(); i++) { + String text = chunkList.get(i); + String fid = fidList.get(i); + Embedding embedding = embeddingModel.embed(text).content(); + Map properties = Map.of( + "text", text, + "fid", fid, + "kid", kid, + "docId", docId + ); + Float[] vector = toObjectArray(embedding.vector()); + client.data().creator() + .withClassName("LocalKnowledge" + kid) + .withProperties(properties) + .withVector(vector) + .run(); + } + long endTime = System.currentTimeMillis(); + log.info("向量存储完成消耗时间:" + (endTime - startTime) / 1000 + "秒"); + } + + + + @Override + public List getQueryVector(QueryVectorBo queryVectorBo) { + createSchema(queryVectorBo.getKid(),queryVectorBo.getEmbeddingModelName()); + EmbeddingModel embeddingModel = getEmbeddingModel(queryVectorBo.getEmbeddingModelName(),null); + Embedding queryEmbedding = embeddingModel.embed(queryVectorBo.getQuery()).content(); + float[] vector = queryEmbedding.vector(); + List vectorStrings = new ArrayList<>(); + for (float v : vector) { + vectorStrings.add(String.valueOf(v)); + } + String vectorStr = String.join(",", vectorStrings); + String className = vectorStoreProperties.getWeaviate().getClassname(); + + // 构建 GraphQL 查询 + String graphQLQuery = String.format( + "{\n" + + " Get {\n" + + " %s(nearVector: {vector: [%s]} limit: %d) {\n" + + " text\n" + + " fid\n" + + " kid\n" + + " docId\n" + + " _additional {\n" + + " distance\n" + + " id\n" + + " }\n" + + " }\n" + + " }\n" + + "}", + className + queryVectorBo.getKid(), + vectorStr, + queryVectorBo.getMaxResults() + ); + + Result result = client.graphQL().raw().withQuery(graphQLQuery).run(); + List resultList = new ArrayList<>(); + if (result != null && !result.hasErrors()) { + Object data = result.getResult().getData(); + JSONObject entries = new JSONObject(data); + Map entriesMap = entries.get("Get", Map.class); + cn.hutool.json.JSONArray objects = entriesMap.get(className + queryVectorBo.getKid()); + if (objects.isEmpty()) { + return resultList; + } + for (Object object : objects) { + Map map = (Map) object; + String content = map.get("text"); + resultList.add(content); + } + return resultList; + } else { + log.error("GraphQL 查询失败: {}", result.getError()); + return resultList; + } + } + + @Override + @SneakyThrows + public void removeById(String id, String modelName) { + String protocol = vectorStoreProperties.getWeaviate().getProtocol(); + String host = vectorStoreProperties.getWeaviate().getHost(); + String className = vectorStoreProperties.getWeaviate().getClassname(); + String finalClassName = className + id; + WeaviateClient client = new WeaviateClient(new Config(protocol, host)); + Result result = client.schema().classDeleter().withClassName(finalClassName).run(); + if (result.hasErrors()) { + log.error("失败删除向量: " + result.getError()); + throw new ServiceException("失败删除向量数据!"); + } else { + log.info("成功删除向量数据: " + result.getResult()); + } + } + + @Override + public void removeByDocId(String docId, String kid) { + String className = vectorStoreProperties.getWeaviate().getClassname() + kid; + // 构建 Where 条件 + WhereFilter whereFilter = WhereFilter.builder() + .path("docId") + .operator(Operator.Equal) + .valueText(docId) + .build(); + ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter(); + Result result = deleter.withClassName(className) + .withWhere(whereFilter) + .run(); + if (result != null && !result.hasErrors()) { + log.info("成功删除 docId={} 的所有向量数据", docId); + } else { + log.error("删除失败: {}", result.getError()); + } + } + + @Override + public void removeByFid(String fid, String kid) { + String className = vectorStoreProperties.getWeaviate().getClassname() + kid; + // 构建 Where 条件 + WhereFilter whereFilter = WhereFilter.builder() + .path("fid") + .operator(Operator.Equal) + .valueText(fid) + .build(); + ObjectsBatchDeleter deleter = client.batch().objectsBatchDeleter(); + Result result = deleter.withClassName(className) + .withWhere(whereFilter) + .run(); + if (result != null && !result.hasErrors()) { + log.info("成功删除 fid={} 的所有向量数据", fid); + } else { + log.error("删除失败: {}", result.getError()); + } + } + +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/domain/vo/ChatConfigVo.java b/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/domain/vo/ChatConfigVo.java index 148d2dc8..410e4a2d 100644 --- a/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/domain/vo/ChatConfigVo.java +++ b/ruoyi-modules-api/ruoyi-system-api/src/main/java/org/ruoyi/system/domain/vo/ChatConfigVo.java @@ -18,7 +18,7 @@ import java.io.Serializable; */ @Data @ExcelIgnoreUnannotated -@AutoMapper(target = ChatConfig.class) + @AutoMapper(target = ChatConfig.class) public class ChatConfigVo implements Serializable { @Serial diff --git a/ruoyi-modules-api/ruoyi-workflow-api/pom.xml b/ruoyi-modules-api/ruoyi-workflow-api/pom.xml new file mode 100644 index 00000000..9049508f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/pom.xml @@ -0,0 +1,133 @@ + + + 4.0.0 + + org.ruoyi + ruoyi-modules-api + ${revision} + ../pom.xml + + + ruoyi-workflow-api + + + 工作流API模块 + + + + 17 + 17 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter + + + + com.fasterxml.jackson.core + jackson-databind + + + + org.springframework + spring-web + + + + org.ruoyi + ruoyi-system-api + + + + org.ruoyi + ruoyi-common-satoken + + + + org.ruoyi + ruoyi-common-mail + + + + org.ruoyi + ruoyi-chat + + + + dev.langchain4j + langchain4j-core + 1.2.0 + + + + cn.hutool + hutool-all + 5.8.12 + compile + + + + org.bsc.langgraph4j + langgraph4j-core + 1.5.3 + + + + org.bsc.langgraph4j + langgraph4j-langchain4j + 1.5.3 + + + + io.swagger.core.v3 + swagger-annotations + 2.2.8 + + + + dev.langchain4j + langchain4j-open-ai + 1.2.0 + compile + + + + dev.langchain4j + langchain4j-community-dashscope + 1.2.0-beta8 + + + + com.baomidou + mybatis-plus-generator + 3.5.3.1 + + + + dev.langchain4j + langchain4j-http-client-jdk + 1.2.0 + + + + dev.langchain4j + langchain4j-document-parser-apache-poi + 1.2.0-beta8 + + + + + com.google.api-client + google-api-client + 2.6.0 + + + + + + diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/CodeGenerator.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/CodeGenerator.java new file mode 100644 index 00000000..6b77b030 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/CodeGenerator.java @@ -0,0 +1,43 @@ +package org.ruoyi.workflow; + +import com.baomidou.mybatisplus.generator.FastAutoGenerator; +import com.baomidou.mybatisplus.generator.config.OutputFile; +import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; + +import java.sql.Types; +import java.util.Collections; + +public class CodeGenerator { + public static void main(String[] args) { + FastAutoGenerator.create("jdbc:postgres://172.17.30.40:5432/aideepin?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&tinyInt1isBit=false&allowMultiQueries=true", "postgres", "postgres") + .globalConfig(builder -> { + builder.author("moyz") // 设置作者 + .enableSwagger() // 开启 swagger 模式 + .fileOverride() // 覆盖已生成文件 + .outputDir("D://"); // 指定输出目录 + }) + .dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> { + int typeCode = metaInfo.getJdbcType().TYPE_CODE; + if (typeCode == Types.SMALLINT) { + // 自定义类型转换 + return DbColumnType.INTEGER; + } + return typeRegistry.getColumnType(metaInfo); + + })) + .packageConfig(builder -> { + builder.mapper("com.adi.common.mapper") + .parent("") + .moduleName("") + .entity("po") + .serviceImpl("service.impl") + .pathInfo(Collections.singletonMap(OutputFile.xml, "D://mybatisplus-generatorcode")); // 设置mapperXml生成路径 + }) + .strategyConfig(builder -> { + builder.addInclude("adi_knowledge_base_qa_record") // 设置需要生成的表名 + .addTablePrefix("adi_"); + builder.mapperBuilder().enableBaseResultMap().enableMapperAnnotation().build(); + }) + .execute(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/BaseResponse.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/BaseResponse.java new file mode 100644 index 00000000..d6220abd --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/BaseResponse.java @@ -0,0 +1,51 @@ +package org.ruoyi.workflow.base; + +import lombok.Data; +import org.ruoyi.workflow.enums.ErrorEnum; + +import java.io.Serializable; + +@Data +public class BaseResponse implements Serializable { + + private static final long serialVersionUID = 1L; + /** + * 是否成功 + */ + private boolean success; + /** + * 状态码 + */ + private String code; + /** + * 提示 + */ + private String message; + /** + * 数据 + */ + private T data; + + public BaseResponse() { + } + + public BaseResponse(boolean success) { + this.success = success; + } + + public BaseResponse(boolean success, T data) { + this.data = data; + this.success = success; + } + + public BaseResponse(String code, String message, T data) { + this.code = code; + this.success = false; + this.message = message; + this.data = data; + } + + public static BaseResponse success(String message) { + return new BaseResponse(ErrorEnum.SUCCESS.getCode(), message, ""); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/NodeInputConfigTypeHandler.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/NodeInputConfigTypeHandler.java new file mode 100644 index 00000000..4bd44fbb --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/NodeInputConfigTypeHandler.java @@ -0,0 +1,118 @@ +package org.ruoyi.workflow.base; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.util.JsonUtil; +import org.ruoyi.workflow.workflow.WfNodeInputConfig; +import org.ruoyi.workflow.workflow.def.WfNodeIO; +import org.ruoyi.workflow.workflow.def.WfNodeParamRef; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import static org.ruoyi.workflow.workflow.WfNodeIODataUtil.INPUT_TYPE_TO_NODE_IO_DEF; + +@Slf4j +@MappedJdbcTypes({JdbcType.JAVA_OBJECT}) +@MappedTypes({WfNodeInputConfig.class}) +public class NodeInputConfigTypeHandler extends BaseTypeHandler { + + public static WfNodeInputConfig fillNodeInputConfig(String jsonSource) { + ObjectNode jsonNode = (ObjectNode) JsonUtil.toJsonNode(jsonSource); + return createNodeInputConfig(jsonNode); + } + + public static WfNodeInputConfig createNodeInputConfig(ObjectNode jsonNode) { + List userInputs = new ArrayList<>(); + WfNodeInputConfig result = new WfNodeInputConfig(); + result.setUserInputs(userInputs); + result.setRefInputs(new ArrayList<>()); + if (null == jsonNode) { + return result; + } + ArrayNode userInputsJson = jsonNode.withArray("user_inputs"); + ArrayNode refInputs = jsonNode.withArray("ref_inputs"); + if (!userInputsJson.isEmpty()) { + for (JsonNode userInput : userInputsJson) { + if (userInput instanceof ObjectNode objectNode) { + int type = objectNode.get("type").asInt(); + Class nodeIOClass = INPUT_TYPE_TO_NODE_IO_DEF.get(WfIODataTypeEnum.getByValue(type)); + WfNodeIO wfNodeIO = JsonUtil.fromJson(objectNode, nodeIOClass); + if (null != wfNodeIO) { + userInputs.add(wfNodeIO); + } else { + log.warn("用户输入格式不正确:{}", userInput); + } + } + } + } + if (!refInputs.isEmpty()) { + List list = JsonUtil.fromArrayNode(refInputs, WfNodeParamRef.class); + if (CollectionUtils.isNotEmpty(list)) { + result.setRefInputs(list); + } else { + log.warn("引用输入格式不正确:{}", refInputs); + } + } + return result; + } + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, WfNodeInputConfig parameter, JdbcType jdbcType) { +// PGobject jsonObject = new PGobject(); +// jsonObject.setType("jsonb"); +// try { +// jsonObject.setValue(JsonUtil.toJson(parameter)); +// ps.setObject(i, jsonObject); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } + } + + @Override + public WfNodeInputConfig getNullableResult(ResultSet rs, String columnName) throws SQLException { + String jsonSource = rs.getString(columnName); + if (jsonSource != null) { + try { + return fillNodeInputConfig(jsonSource); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + } + + @Override + public WfNodeInputConfig getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String jsonSource = rs.getString(columnIndex); + if (jsonSource != null) { + return fillNodeInputConfig(jsonSource); + } + return null; + } + + @Override + public WfNodeInputConfig getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String jsonSource = cs.getString(columnIndex); + if (jsonSource != null) { + try { + return fillNodeInputConfig(jsonSource); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + return null; + } +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/ThreadContext.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/ThreadContext.java new file mode 100644 index 00000000..cb7e992a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/base/ThreadContext.java @@ -0,0 +1,122 @@ +package org.ruoyi.workflow.base; + +import cn.dev33.satoken.stp.StpUtil; +import org.apache.commons.lang3.StringUtils; +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; + +/** + * 线程上下文适配器,统一接入 Sa-Token 登录态。 + */ +public class ThreadContext { + + private static final ThreadLocal CURRENT_USER = new ThreadLocal<>(); + private static final ThreadLocal CURRENT_TOKEN = new ThreadLocal<>(); + + private ThreadContext() { + } + + /** + * 获取当前登录的工作流用户。 + */ + public static User getCurrentUser() { + User cached = CURRENT_USER.get(); + if (cached != null) { + return cached; + } + LoginUser loginUser = LoginHelper.getLoginUser(); + if (loginUser == null) { + throw new BaseException(A_USER_NOT_FOUND.getInfo()); + } + User mapped = mapToWorkflowUser(loginUser); + CURRENT_USER.set(mapped); + return mapped; + } + + /** + * 允许在测试或特殊场景下显式设置当前用户。 + */ + public static void setCurrentUser(User user) { + if (user == null) { + CURRENT_USER.remove(); + } else { + CURRENT_USER.set(user); + } + } + + /** + * 获取当前登录用户 ID。 + */ + public static Long getCurrentUserId() { + Long userId = LoginHelper.getUserId(); + if (userId != null) { + return userId; + } + return getCurrentUser().getId(); + } + + /** + * 获取当前访问 token。 + */ + public static String getToken() { + String token = CURRENT_TOKEN.get(); + if (StringUtils.isNotBlank(token)) { + return token; + } + try { + token = StpUtil.getTokenValue(); + } catch (Exception ignore) { + token = null; + } + if (StringUtils.isNotBlank(token)) { + CURRENT_TOKEN.set(token); + } + return token; + } + + public static void setToken(String token) { + if (StringUtils.isBlank(token)) { + CURRENT_TOKEN.remove(); + } else { + CURRENT_TOKEN.set(token); + } + } + + public static boolean isLogin() { + return LoginHelper.isLogin(); + } + + public static User getExistCurrentUser() { + return getCurrentUser(); + } + + public static void unload() { + CURRENT_USER.remove(); + CURRENT_TOKEN.remove(); + } + + private static User mapToWorkflowUser(LoginUser loginUser) { + User user = new User(); + user.setId(loginUser.getUserId()); + String nickname = loginUser.getNickName(); + user.setName(StringUtils.defaultIfBlank(nickname, loginUser.getUsername())); + user.setEmail(loginUser.getUsername()); + user.setUuid(String.valueOf(loginUser.getUserId())); + user.setUserStatus(UserStatusEnum.NORMAL); + user.setIsAdmin(LoginHelper.isSuperAdmin(loginUser.getUserId())); + user.setUnderstandContextMsgPairNum(0); + user.setQuotaByTokenDaily(0); + user.setQuotaByTokenMonthly(0); + user.setQuotaByRequestDaily(0); + user.setQuotaByRequestMonthly(0); + user.setQuotaByImageDaily(0); + user.setQuotaByImageMonthly(0); + user.setIsDeleted(false); + return user; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/config/BeanConfig.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/config/BeanConfig.java new file mode 100644 index 00000000..5623208b --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/config/BeanConfig.java @@ -0,0 +1,76 @@ +package org.ruoyi.workflow.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.workflow.util.LocalDateTimeUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.client.RestTemplate; + +@Slf4j +@Configuration +public class BeanConfig { + + @Bean + public RestTemplate restTemplate() { + log.info("Configuration:create restTemplate"); + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + // 设置建立连接超时时间 毫秒 + requestFactory.setConnectTimeout(60000); + // 设置读取数据超时时间 毫秒 + requestFactory.setReadTimeout(60000); + RestTemplate restTemplate = new RestTemplate(); + // 注册LOG拦截器 +// restTemplate.setInterceptors(Lists.newArrayList(new LogClientHttpRequestInterceptor())); + restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(requestFactory)); + + return restTemplate; + } + + @Bean + @Primary + public ObjectMapper objectMapper() { + log.info("Configuration:create objectMapper"); + ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder().createXmlMapper(false).build(); + objectMapper.registerModules(LocalDateTimeUtil.getSimpleModule(), new JavaTimeModule(), new Jdk8Module()); + //设置null值不参与序列化(字段不被显示) + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return objectMapper; + } + + @Bean(name = "mainExecutor") + @Primary + public AsyncTaskExecutor mainExecutor() { + int processorsNum = Runtime.getRuntime().availableProcessors(); + log.info("mainExecutor,processorsNum:{}", processorsNum); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(processorsNum * 2); + executor.setMaxPoolSize(100); + return executor; + } + + @Bean(name = "imagesExecutor") + public AsyncTaskExecutor imagesExecutor() { + int processorsNum = Runtime.getRuntime().availableProcessors(); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + log.info("imagesExecutor corePoolSize:{},maxPoolSize:{}", processorsNum, processorsNum * 2); + executor.setCorePoolSize(processorsNum); + executor.setMaxPoolSize(processorsNum * 2); + return executor; + } + + @Bean(name = "beanValidator") + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/cosntant/AdiConstant.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/cosntant/AdiConstant.java new file mode 100644 index 00000000..d9b4bac7 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/cosntant/AdiConstant.java @@ -0,0 +1,407 @@ +package org.ruoyi.workflow.cosntant; + +import dev.langchain4j.model.input.PromptTemplate; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class AdiConstant { + public static final int DEFAULT_PAGE_SIZE = 10; + /** + * 验证码id过期时间:1小时 + */ + public static final int AUTH_CAPTCHA_ID_EXPIRE = 1; + /** + * 验证码过期时间,5分钟 + */ + public static final int AUTH_CAPTCHA_EXPIRE = 5; + /** + * 注册激活码有效时长,8小时 + */ + public static final int AUTH_ACTIVE_CODE_EXPIRE = 8; + /** + * token存活时间(8小时) + */ + public static final int USER_TOKEN_EXPIRE = 8; + public static final String DEFAULT_PASSWORD = "123456"; + public static final int LOGIN_MAX_FAIL_TIMES = 3; + public static final String[] WEB_RESOURCES = { + "/swagger-ui/index.html", + "/swagger-ui", + "/swagger-resources", + "/v3/api-docs", + "/favicon.ico", + ".css", + ".js", + "/doc.html" + }; + public static final int SECRET_KEY_TYPE_SYSTEM = 1; + public static final int SECRET_KEY_TYPE_CUSTOM = 2; + public static final String OPENAI_MESSAGE_DONE_FLAG = "[DONE]"; + public static final String DEFAULT_MODEL = "gpt-3.5-turbo"; + public static final String CREATE_IMAGE_RESP_FORMATS_B64JSON = "b64_json"; + public static final String OPENAI_CREATE_IMAGE_RESP_FORMATS_URL = "url"; + public static final List DALLE2_CREATE_IMAGE_SIZES = List.of("256x256", "512x512", "1024x1024"); + public static final List DALLE3_CREATE_IMAGE_SIZES = List.of("1024x1024", "1024x1792", "1792x1024"); + public static final PromptTemplate PROMPT_EXTRA_TEMPLATE = PromptTemplate.from(""" + ## 要求 + 尽可能准确地回答用户的问题 + + ## 用户的问题 + {{question}} + + ## 注意 + {{extraInfo}} + """); + public static final PromptTemplate PROMPT_INFO_TEMPLATE = PromptTemplate.from(""" + ## 要求 + 根据已知信息,尽可能准确地回答用户的问题 + + ## 用户的问题 + {{question}} + + ## 已知信息 + {{information}} + + ## 注意 + 回答的内容不能让用户感知到已知信息的存在 + """); + /** + * 可能的 extraInfo 如适用转音频的要求: 2. 回答的内容要尽量口语化,以方便将内容转成语音 + */ + public static final PromptTemplate PROMPT_INFO_EXTRA_TEMPLATE = PromptTemplate.from(""" + ## 要求 + 根据已知信息,尽可能准确地回答用户的问题 + + ## 用户的问题 + {{question}} + + ## 已知信息 + {{information}} + + ## 注意 + 1. 回答的内容不能让用户感知到已知信息的存在 + {{extraInfo}} + """); + public static final String PROMPT_EXTRA_AUDIO = "2. 回答的内容要尽量口语化,以方便将内容转成语音"; + public static final Double LLM_TEMPERATURE_DEFAULT = 0.7D; + public static final Double RAG_RETRIEVE_MIN_SCORE_DEFAULT = 0.6D; + public static final int tts_ = 1; + public static final String[] POI_DOC_TYPES = {"doc", "docx", "ppt", "pptx", "xls", "xlsx"}; + public static final long SSE_TIMEOUT = (2 * 60 + 30) * 1000L; // 2.5分钟 + public static final int RAG_TYPE_KB = 1; + public static final int RAG_TYPE_SEARCH = 2; + /** + * 每块文档长度(按token算) + */ + public static final int RAG_MAX_SEGMENT_SIZE_IN_TOKENS = 1000; + /** + * 文档召回默认数量 + */ + public static final int RAG_RETRIEVE_NUMBER_DEFAULT = 3; + /** + * 文档召回最大数量 + */ + public static final int RAG_RETRIEVE_NUMBER_MAX = 5; + /** + * 向量搜索时命中所需的最低分数 + */ + public static final double RAG_MIN_SCORE = 0.6; + /** + * 默认的最大输入token数 + */ + public static final int LLM_MAX_INPUT_TOKENS_DEFAULT = 4096; + public static final String LLM_INPUT_TYPE_TEXT = "text"; + public static final String LLM_INPUT_TYPE_IMAGE = "image"; + public static final String LLM_INPUT_TYPE_AUDIO = "audio"; + public static final String LLM_INPUT_TYPE_VIDEO = "video"; + public static final String[] GRAPH_ENTITY_EXTRACTION_ENTITY_TYPES = {"organization", "person", "geo", "event"}; + public static final String GRAPH_TUPLE_DELIMITER = "<|>"; + public static final String GRAPH_RECORD_DELIMITER = "##"; + public static final String GRAPH_COMPLETION_DELIMITER = "<|COMPLETE|>"; + public static final List GRAPH_STORE_MAIN_FIELDS = List.of("name", "label", "text_segment_id", "description"); + /** + * 唯一标识字段,如果该字段有指定,则根据该配置判断Vertex或Edge是否唯一,如知识库中根据 name、metadata->>kb_uuid 来做判断 + */ + public static final String GRAPH_METADATA_IDENTIFY_COLUMNS = "graph_metadata_identify_columns"; + /** + * 内容追加字段 + * 更新数据时,如遇到该标识中的字段,追加内容而不是替换 + */ + public static final String GRAPH_METADATA_APPEND_COLUMNS = "graph_metadata_append_columns_if_exist"; + public static final int AI_IMAGE_TYPE_REGULAR = 1; + public static final int AI_IMAGE_TYPE_THUMBNAIL = 2; + public static final int AI_IMAGE_TYPE_REGULAR_MARK = 3; + public static final int AI_IMAGE_TYPE_THUMBNAIL_MARK = 4; + public static final String DOC_INDEX_TYPE_EMBEDDING = "embedding"; + public static final String DOC_INDEX_TYPE_GRAPHICAL = "graphical"; + public static final String DRAW_TYPE_PUBLIC = "public"; + public static final String DRAW_TYPE_STARRED = "starred"; + public static final String DRAW_TYPE_MINE = "mine"; + public static final String MP_LIMIT_1 = "limit 1"; + /** + * 文件存储在本地 + */ + public static final int STORAGE_LOCATION_LOCAL = 1; + /** + * 文件存储到阿里云OSS + */ + public static final int STORAGE_LOCATION_ALI_OSS = 2; + public static final String URL_PREFIX_FILE = "/file/"; + public static final String URL_PREFIX_IMAGE = "/image/"; + public static final String URL_PREFIX_MY_IMAGE = "/my-image/"; + public static final String URL_PREFIX_MY_THUMBNAIL = "/my-thumbnail/"; + public static final List IMAGE_EXTENSIONS = List.of("jpg", "jpeg", "png", "gif", "bmp", "webp"); + public static final String W_FAILED = "FAILED"; + public static final String COLUMN_NAME_IS_DELETE = "is_deleted"; + public static final String COLUMN_NAME_USER_ID = "user_id"; + public static final String COLUMN_NAME_ID = "id"; + public static final String COLUMN_NAME_UUID = "uuid"; + public static final String FORM_DATA_BOUNDARY_PRE = "----WebKitFormBoundary"; + + private AdiConstant() { + } + + public static class ConversationConstant { + public static final String DEFAULT_NAME = "通用智能助手"; + public static final int ANSWER_CONTENT_TYPE_AUTO = 1; + public static final int ANSWER_CONTENT_TYPE_TEXT = 2; + public static final int ANSWER_CONTENT_TYPE_AUDIO = 3; + public static final String AUDIO_CONFIG_FIELD_ANSWER_VOICE = "answer_voice"; + public static final String AUDIO_CONFIG_FIELD_VOICE_PLATFORM = "platform"; + + private ConversationConstant() { + } + } + + public static class GenerateImage { + public static final int INTERACTING_METHOD_GENERATE_IMAGE = 1; + public static final int INTERACTING_METHOD_EDIT_IMAGE = 2; + public static final int INTERACTING_METHOD_VARIATION = 3; + public static final int INTERACTING_METHOD_BACKGROUND_GENERATION = 4; + public static final int STATUS_DOING = 1; + public static final int STATUS_FAIL = 2; + public static final int STATUS_SUCCESS = 3; + + private GenerateImage() { + } + } + + public static class MetadataKey { + public static final String KB_UUID = "kb_uuid"; + public static final String KB_ITEM_UUID = "kb_item_uuid"; + public static final String ENGINE_NAME = "engine_name"; + public static final String SEARCH_UUID = "search_uuid"; + + private MetadataKey() { + } + } + + public static class SysConfigKey { + public static final String DEEPSEEK_SETTING = "deepseek_setting"; + public static final String OPENAI_SETTING = "openai_setting"; + public static final String DASHSCOPE_SETTING = "dashscope_setting"; + public static final String QIANFAN_SETTING = "qianfan_setting"; + public static final String OLLAMA_SETTING = "ollama_setting"; + public static final String SILICONFLOW_SETTING = "siliconflow_setting"; + public static final String GOOGLE_SETTING = "google_setting"; + public static final String BING_SETTING = "bing_setting"; + public static final String BAIDU_SETTING = "baidu_setting"; + public static final String REQUEST_TEXT_RATE_LIMIT = "request_text_rate_limit"; + public static final String REQUEST_IMAGE_RATE_LIMIT = "request_image_rate_limit"; + public static final String CONVERSATION_MAX_NUM = "conversation_max_num"; + public static final String QUOTA_BY_TOKEN_DAILY = "quota_by_token_daily"; + public static final String QUOTA_BY_TOKEN_MONTHLY = "quota_by_token_monthly"; + public static final String QUOTA_BY_REQUEST_DAILY = "quota_by_request_daily"; + public static final String QUOTA_BY_REQUEST_MONTHLY = "quota_by_request_monthly"; + public static final String QUOTA_BY_IMAGE_DAILY = "quota_by_image_daily"; + public static final String QUOTA_BY_IMAGE_MONTHLY = "quota_by_image_monthly"; + public static final String QUOTA_BY_QA_ASK_DAILY = "quota_by_qa_ask_daily"; + public static final String STORAGE_LOCATION = "storage_location"; + public static final String STORAGE_LOCATION_ALI_OSS = "storage_location_ali_oss"; + public static final String ASR_SETTING = "asr_setting"; + public static final String TTS_SETTING = "tts_setting"; + + private SysConfigKey() { + } + } + + public static class ModelPlatform { + public static final String DEEPSEEK = "deepseek"; + public static final String OPENAI = "openai"; + public static final String DASHSCOPE = "dashscope"; + public static final String QIANFAN = "qianfan"; + public static final String OLLAMA = "ollama"; + public static final String SILICONFLOW = "siliconflow"; + + private ModelPlatform() { + } + + // 获取所有公共静态常量(String类型的值)的列表 + public static List getModelConstants() { + List list = new ArrayList<>(); + Class clazz = ModelPlatform.class; + for (Field field : clazz.getDeclaredFields()) { + try { + String value = (String) field.get(null); + list.add(value); + } catch (ReflectiveOperationException e) { + log.error("error", e); + } + + } + return list; + } + } + + public static class ModelType { + public static final String TEXT = "text"; + public static final String IMAGE = "image"; + public static final String EMBEDDING = "embedding"; + public static final String RERANK = "rerank"; + public static final String ASR = "asr"; + public static final String TTS = "tts"; + + private ModelType() { + } + + public static List getModelType() { + List list = new ArrayList<>(); + Class clazz = ModelType.class; + for (Field field : clazz.getDeclaredFields()) { + try { + String value = (String) field.get(null); + list.add(value); + } catch (ReflectiveOperationException e) { + log.error("error", e); + } + + } + return list; + } + } + + public static class SearchEngineName { + public static final String GOOGLE = "google"; + public static final String BING = "bing"; + public static final String BAIDU = "baidu"; + public static final String[] GOOGLE_COUNTRIES = {"cn", "af", "al", "dz", "as", "ad", "ao", "ai", "aq", "ag", "ar", "am", "aw", "au", "at", "az", "bs", "bh", "bd", "bb", "by", "be", "bz", "bj", "bm", "bt", "bo", "ba", "bw", "bv", "br", "io", "bn", "bg", "bf", "bi", "kh", "cm", "ca", "cv", "ky", "cf", "td", "cl", "cx", "cc", "co", "km", "cg", "cd", "ck", "cr", "ci", "hr", "cu", "cy", "cz", "dk", "dj", "dm", "do", "ec", "eg", "sv", "gq", "er", "ee", "et", "fk", "fo", "fj", "fi", "fr", "gf", "pf", "tf", "ga", "gm", "ge", "de", "gh", "gi", "gr", "gl", "gd", "gp", "gu", "gt", "gn", "gw", "gy", "ht", "hm", "va", "hn", "hk", "hu", "is", "in", "id", "ir", "iq", "ie", "il", "it", "jm", "jp", "jo", "kz", "ke", "ki", "kp", "kr", "kw", "kg", "la", "lv", "lb", "ls", "lr", "ly", "li", "lt", "lu", "mo", "mk", "mg", "mw", "my", "mv", "ml", "mt", "mh", "mq", "mr", "mu", "yt", "mx", "fm", "md", "mc", "mn", "ms", "ma", "mz", "mm", "na", "nr", "np", "nl", "an", "nc", "nz", "ni", "ne", "ng", "nu", "nf", "mp", "no", "om", "pk", "pw", "ps", "pa", "pg", "py", "pe", "ph", "pn", "pl", "pt", "pr", "qa", "re", "ro", "ru", "rw", "sh", "kn", "lc", "pm", "vc", "ws", "sm", "st", "sa", "sn", "rs", "sc", "sl", "sg", "sk", "si", "sb", "so", "za", "gs", "es", "lk", "sd", "sr", "sj", "sz", "se", "ch", "sy", "tw", "tj", "tz", "th", "tl", "tg", "tk", "to", "tt", "tn", "tr", "tm", "tc", "tv", "ug", "ua", "ae", "uk", "gb", "us", "um", "uy", "uz", "vu", "ve", "vn", "vg", "vi", "wf", "eh", "ye", "zm", "zw"}; + public static final String[] GOOGLE_LANGUAGES = {"zh-cn", "zh-tw", "af", "ak", "sq", "ws", "am", "ar", "hy", "az", "eu", "be", "bem", "bn", "bh", "xx-bork", "bs", "br", "bg", "bt", "km", "ca", "chr", "ny", "co", "hr", "cs", "da", "nl", "xx-elmer", "en", "eo", "et", "ee", "fo", "tl", "fi", "fr", "fy", "gaa", "gl", "ka", "de", "el", "kl", "gn", "gu", "xx-hacker", "ht", "ha", "haw", "iw", "hi", "hu", "is", "ig", "id", "ia", "ga", "it", "ja", "jw", "kn", "kk", "rw", "rn", "xx-klingon", "kg", "ko", "kri", "ku", "ckb", "ky", "lo", "la", "lv", "ln", "lt", "loz", "lg", "ach", "mk", "mg", "ms", "ml", "mt", "mv", "mi", "mr", "mfe", "mo", "mn", "sr-me", "my", "ne", "pcm", "nso", "no", "nn", "oc", "or", "om", "ps", "fa", "xx-pirate", "pl", "pt", "pt-br", "pt-pt", "pa", "qu", "ro", "rm", "nyn", "ru", "gd", "sr", "sh", "st", "tn", "crs", "sn", "sd", "si", "sk", "sl", "so", "es", "es-419", "su", "sw", "sv", "tg", "ta", "tt", "te", "th", "ti", "to", "lua", "tum", "tr", "tk", "tw", "ug", "uk", "ur", "uz", "vu", "vi", "cy", "wo", "xh", "yi", "yo", "zu"}; + + private SearchEngineName() { + } + } + + public static class SSEEventName { + public static final String START = "[START]"; + public static final String DONE = "[DONE]"; + public static final String ERROR = "[ERROR]"; + public static final String META = "[META]"; + public static final String AUDIO = "[AUDIO]"; + public static final String THINKING = "[THINKING]"; + public static final String AI_SEARCH_SOURCE_LINKS = "[SOURCE_LINKS]"; + public static final String WF_NODE_CHUNK = "[WF_NODE_CHUNK]"; + public static final String WF_NODE_OUTPUT = "[WF_NODE_OUTPUT]"; + public static final String STATE_CHANGED = "[STATE_CHANGED]"; + + private SSEEventName() { + } + } + + public static class SSEEventData { + + /** + * 状态:问题分析中 + * 如敏感词校验等 + */ + public static final String STATE_QUESTION_ANALYSING = """ + {"state":"question_analysing","remark":"问题分析中"} + """; + + public static final String STATE_KNOWLEDGE_SEARCHING = """ + {"state":"knowledge_searching","remark":"知识库搜索中"} + """; + //使用 THINKING 事件代替 + public static final String STATE_THINKING = """ + {"state":"thinking","remark":"推理中"} + """; + public static final String STATE_RESPONDING = """ + {"state":"responding","remark":"回答中"} + """; + } + + public static class WorkflowConstant { + public static final String DEFAULT_INPUT_PARAM_NAME = "input"; + public static final String DEFAULT_OUTPUT_PARAM_NAME = "output"; + public static final String DEFAULT_ERROR_OUTPUT_PARAM_NAME = "error_msg"; + public static final String HUMAN_FEEDBACK_KEY = "human_feedback"; + public static final int NODE_PROCESS_STATUS_READY = 1; + public static final int NODE_PROCESS_STATUS_DOING = 2; + public static final int NODE_PROCESS_STATUS_SUCCESS = 3; + public static final int NODE_PROCESS_STATUS_FAIL = 4; + + public static final int WORKFLOW_PROCESS_STATUS_READY = 1; + public static final int WORKFLOW_PROCESS_STATUS_DOING = 2; + public static final int WORKFLOW_PROCESS_STATUS_SUCCESS = 3; + public static final int WORKFLOW_PROCESS_STATUS_FAIL = 4; + public static final int WORKFLOW_PROCESS_STATUS_WAITING_INPUT = 5; + + public static final int WORKFLOW_NODE_PROCESS_TYPE_NORMAL = 1; + public static final int WORKFLOW_NODE_PROCESS_TYPE_CONDITIONAL = 2; + public static final int WORKFLOW_NODE_PROCESS_TYPE_PARALLEL = 3; + + public static final int MAIL_SENDER_TYPE_SYS = 1; + public static final int MAIL_SENDER_TYPE_CUSTOM = 2; + } + + public static class TokenEstimator { + public static String OPENAI = "openai"; + public static String HUGGING_FACE = "huggingface"; + public static String QWEN = "qwen"; + + public static List ALL = List.of(OPENAI, HUGGING_FACE, QWEN); + } + + public static class EmbeddingModel { + public static String ALL_MINILM_L6 = "local:all-minilm-l6-v2"; + } + + public static class McpConstant { + public static final String TRANSPORT_TYPE_SSE = "sse"; + public static final String TRANSPORT_TYPE_STDIO = "stdio"; + public static final String INSTALL_TYPE_REMOTE = "remote"; + public static final String INSTALL_TYPE_WASM = "wasm"; + public static final String INSTALL_TYPE_LOCAL = "local"; + public static final String INSTALL_TYPE_DOCKER = "docker"; + } + + public static class TtsConstant { + + /** + * 语音合成器位置-客户端 + */ + public static final String SYNTHESIZER_CLIENT = "client"; + /** + * 语音合成器位置-服务端 + */ + public static final String SYNTHESIZER_SERVER = "server"; + + /** + * 通义默认语音音色-龙应严(义正严辞女声) + */ + public static final String DASHSCOPE_DEFAULT_VOICE = "longyingyan"; + } + + public static class CustomChatRequestParameterKeys { + /** + * 是否开启思考模式,默认不开启 + */ + public static final String ENABLE_THINKING = "enable_thinking"; + + private CustomChatRequestParameterKeys() { + } + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/cosntant/RedisKeyConstant.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/cosntant/RedisKeyConstant.java new file mode 100644 index 00000000..77e86997 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/cosntant/RedisKeyConstant.java @@ -0,0 +1,116 @@ +package org.ruoyi.workflow.cosntant; + +public class RedisKeyConstant { + + /** + * 账号激活码的key + */ + public static final String AUTH_ACTIVE_CODE = "auth:activeCode:{0}"; + /** + * 注册时使用的验证码 + * 参数:验证码id + * 值:验证码 + */ + public static final String AUTH_REGISTER_CAPTCHA_ID = "auth:register:captcha:{0}"; + /** + * 登录时使用的验证码id缓存 + * 参数:验证码id + * 值:验证码 + */ + public static final String AUTH_LOGIN_CAPTCHA_ID = "auth:login:captcha:{0}"; + /** + * 注册验证码缓存 + * 参数:验证码 + * 值:1 + */ + public static final String AUTH_CAPTCHA = "auth:register:captcha:{0}"; + /** + * 登录token + * {0}:用户token + * 值:json.format(user) + */ + public static final String USER_TOKEN = "user:token:{0}"; + /** + * 参数:游客的uuid + * 值:json.format(guest) + */ + public static final String GUEST_UUID = "guest:uuid:{0}"; + /** + * 登录失败次数 + * 参数:用户邮箱 + * 值: 失效次数 + */ + public static final String LOGIN_FAIL_COUNT = "user:login:fail:{0}"; + /** + * 用户是否请求ai中 + * 参数:用户id + * 值: 1或者0 + */ + public static final String USER_ASKING = "user:asking:{0}"; + /** + * 用户是否画画中 + * 参数:用户id + * 值: 1或者0 + */ + public static final String USER_DRAWING = "user:drawing:{0}"; + /** + * 用户提问限流计数 + * 参数:用户id + * 值: 当前时间窗口访问量 + */ + public static final String USER_REQUEST_TEXT_TIMES = "user:request-text:times:{0}"; + public static final String USER_REQUEST_IMAGE_TIMES = "user:request-image:times:{0}"; + /** + * 用户信息缓存 + * 参数:用户id + * 值: user object + */ + public static final String USER_INFO = "user:info:"; + /** + * 找回密码的请求绑在 + * 参数:随机数 + * 值: 用户id,用于校验后续流程中的重置密码使用 + */ + public static final String FIND_MY_PASSWORD = "user:find:password:{0}"; + /** + * qa提问次数(每天) + * 参数:用户id:日期yyyyMMdd + * 值:提问数量 + */ + public static final String AQ_ASK_TIMES = "qa:ask:limit:{0}:{1}"; + /** + * 知识库知识点生成数量 + * 值: 用户id + */ + public static final String QA_ITEM_CREATE_LIMIT = "aq:item:create:{0}"; + /** + * 信号(重新生成知识库统计数据) + * 值:知识库uuid + */ + public static final String KB_STATISTIC_RECALCULATE_SIGNAL = "kb:statistic:recalculate:signal"; + public static final String STATISTIC = "statistic"; + public static final String STATISTIC_USER = "user"; + public static final String STATISTIC_KNOWLEDGE_BASE = "kb"; + public static final String STATISTIC_TOKEN_COST = "token-cost"; + public static final String STATISTIC_CONVERSATION = "conversation"; + public static final String STATISTIC_IMAGE_COST = "image-cost"; + public static final String TOKEN_USAGE_KEY = "token:usage:{0}"; + /** + * 用户正在对文档进行索引 + * 值:用户id + */ + public static final String USER_INDEXING = "user:indexing:{0}"; + /** + * 用户评论并发限制 + * 值:用户id + */ + public static final String DRAW_COMMENT_LIMIT_KEY = "user:draw:comment-submitting:{0}"; + public static final String WORKFLOW_KEY = "workflow"; + public static final String WORKFLOW_COMPONENTS = "workflow:components"; + public static final String WORKFLOW_COMPONENT_START_KEY = "workflow:component:start"; + public static final String WORKFLOW_COMPONENT_KEY = "workflow:component"; + public static final String WORKFLOW_COPY_DOING = "workflow:copy:doing:{0}"; + + private RedisKeyConstant() { + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfAddReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfAddReq.java new file mode 100644 index 00000000..f201919d --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfAddReq.java @@ -0,0 +1,17 @@ +package org.ruoyi.workflow.dto.workflow; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Data +@Validated +public class WfAddReq { + + @NotBlank + private String title; + + private String remark; + + private Boolean isPublic; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfBaseInfoUpdateReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfBaseInfoUpdateReq.java new file mode 100644 index 00000000..7b12335b --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfBaseInfoUpdateReq.java @@ -0,0 +1,15 @@ +package org.ruoyi.workflow.dto.workflow; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Validated +@Data +public class WfBaseInfoUpdateReq { + @NotBlank + private String uuid; + private String title; + private String remark; + private Boolean isPublic; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfComponentReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfComponentReq.java new file mode 100644 index 00000000..84da556f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfComponentReq.java @@ -0,0 +1,18 @@ +package org.ruoyi.workflow.dto.workflow; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Data +@Validated +public class WfComponentReq { + private String uuid; + @NotBlank(message = "标题不能为空") + private String name; + @NotBlank(message = "标题不能为空") + private String title; + private String remark; + private Boolean isEnable; + private Integer displayOrder; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfComponentSearchReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfComponentSearchReq.java new file mode 100644 index 00000000..5ebc9dff --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfComponentSearchReq.java @@ -0,0 +1,9 @@ +package org.ruoyi.workflow.dto.workflow; + +import lombok.Data; + +@Data +public class WfComponentSearchReq { + private String title; + private Boolean isEnable; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfEdgeReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfEdgeReq.java new file mode 100644 index 00000000..40eb9a19 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfEdgeReq.java @@ -0,0 +1,25 @@ +package org.ruoyi.workflow.dto.workflow; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Validated +@Data +public class WfEdgeReq { + private Long id; + @NotBlank + private String uuid; + @Min(1) + private Long workflowId; + @NotBlank + private String sourceNodeUuid; + private String sourceHandle; + @NotBlank + private String targetNodeUuid; + /** + * 是否新增 + */ + private Boolean isNew; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfNodeDto.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfNodeDto.java new file mode 100644 index 00000000..b5c5baa5 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfNodeDto.java @@ -0,0 +1,32 @@ +package org.ruoyi.workflow.dto.workflow; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Validated +@Data +public class WfNodeDto { + private Long id; + @NotBlank + @Size(min = 32, max = 32) + private String uuid; + private Long workflowId; + @Min(1) + private Long workflowComponentId; + @NotBlank + private String title; + private String remark; + @NotNull + private ObjectNode inputConfig; + @NotNull + private ObjectNode nodeConfig; + @NotNull + private Double positionX; + @NotNull + private Double positionY; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfRuntimeNodeDto.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfRuntimeNodeDto.java new file mode 100644 index 00000000..4ca93525 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfRuntimeNodeDto.java @@ -0,0 +1,17 @@ +package org.ruoyi.workflow.dto.workflow; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +@Validated +@Data +public class WfRuntimeNodeDto { + private Long id; + private String uuid; + private Long workflowRuntimeId; + private Long nodeId; + private ObjectNode input; + private ObjectNode output; + private Integer status; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfRuntimeResp.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfRuntimeResp.java new file mode 100644 index 00000000..b5f5fd8a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfRuntimeResp.java @@ -0,0 +1,22 @@ +package org.ruoyi.workflow.dto.workflow; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class WfRuntimeResp { + private Long id; + private String uuid; + private Long workflowId; + private ObjectNode input; + private ObjectNode output; + private Integer status; + private String statusRemark; + + private String workflowUuid; + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfSearchReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfSearchReq.java new file mode 100644 index 00000000..939c3970 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WfSearchReq.java @@ -0,0 +1,10 @@ +package org.ruoyi.workflow.dto.workflow; + +import lombok.Data; + +@Data +public class WfSearchReq { + private String title; + private Boolean isEnable; + private Boolean isPublic; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResp.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResp.java new file mode 100644 index 00000000..1e14b116 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResp.java @@ -0,0 +1,22 @@ +package org.ruoyi.workflow.dto.workflow; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +public class WorkflowResp { + private Long id; + private String uuid; + private String title; + private String remark; + private Boolean isPublic; + private Long userId; + private String userUuid; + private String userName; + private List nodes; + private List edges; + private LocalDateTime createTime; + private LocalDateTime updateTime; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResumeReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResumeReq.java new file mode 100644 index 00000000..27680932 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowResumeReq.java @@ -0,0 +1,8 @@ +package org.ruoyi.workflow.dto.workflow; + +import lombok.Data; + +@Data +public class WorkflowResumeReq { + private String feedbackContent; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowRunReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowRunReq.java new file mode 100644 index 00000000..54a32a03 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowRunReq.java @@ -0,0 +1,14 @@ +package org.ruoyi.workflow.dto.workflow; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Data; + +import java.util.List; + +@Data +public class WorkflowRunReq { + private List inputs; + private String uuid; + + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowUpdateReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowUpdateReq.java new file mode 100644 index 00000000..3e26c490 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/dto/workflow/WorkflowUpdateReq.java @@ -0,0 +1,24 @@ +package org.ruoyi.workflow.dto.workflow; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +@Validated +@Data +public class WorkflowUpdateReq { + @NotBlank + private String uuid; + @Size(min = 1) + private List nodes; + @NotNull + private List edges; + + private List deleteNodes; + + private List deleteEdges; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/BaseEntity.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/BaseEntity.java new file mode 100644 index 00000000..b657eb16 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/BaseEntity.java @@ -0,0 +1,29 @@ +package org.ruoyi.workflow.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +public class BaseEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.AUTO) + private Long id; + + @TableField(value = "create_time") + private LocalDateTime createTime; + + @TableField(value = "update_time") + private LocalDateTime updateTime; + + @Schema(title = "是否删除(0:未删除,1:已删除)") + @TableField(value = "is_deleted") + private Boolean isDeleted; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/User.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/User.java new file mode 100644 index 00000000..20ac4dc0 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/User.java @@ -0,0 +1,66 @@ +package org.ruoyi.workflow.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 java.time.LocalDateTime; + +@EqualsAndHashCode(callSuper = true) +@Data +@TableName("adi_user") +@Schema(title = "User对象") +public class User extends BaseEntity { + + @Schema(name = "用户名称") + @TableField("name") + private String name; + + @TableField("email") + private String email; + + @TableField("password") + private String password; + + @TableField("uuid") + private String uuid; + + @Schema(name = "上下文理解中需要携带的消息对数量(提示词及回复)") + @TableField("understand_context_msg_pair_num") + private Integer understandContextMsgPairNum; + + @Schema(name = "token quota in one day") + @TableField("quota_by_token_daily") + private Integer quotaByTokenDaily; + + @Schema(name = "token quota in one month") + @TableField("quota_by_token_monthly") + private Integer quotaByTokenMonthly; + + @Schema(name = "request quota in one day") + @TableField("quota_by_request_daily") + private Integer quotaByRequestDaily; + + @Schema(name = "request quota in one month") + @TableField("quota_by_request_monthly") + private Integer quotaByRequestMonthly; + + @TableField("quota_by_image_daily") + private Integer quotaByImageDaily; + + @TableField("quota_by_image_monthly") + private Integer quotaByImageMonthly; + + @TableField("user_status") + private UserStatusEnum userStatus; + + @TableField("active_time") + private LocalDateTime activeTime; + + @Schema(title = "是否管理员(0:否,1:是)") + @TableField(value = "is_admin") + private Boolean isAdmin; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/Workflow.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/Workflow.java new file mode 100644 index 00000000..4334b069 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/Workflow.java @@ -0,0 +1,37 @@ +package org.ruoyi.workflow.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 java.io.Serial; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("t_workflow") +@Schema(title = "工作流定义 | workflow definition") +public class Workflow extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField("uuid") + private String uuid; + + @TableField("title") + private String title; + + @TableField("remark") + private String remark; + + @TableField("user_id") + private Long userId; + + @TableField("is_public") + private Boolean isPublic; + + @TableField("is_enable") + private Boolean isEnable; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowComponent.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowComponent.java new file mode 100644 index 00000000..d75e92c8 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowComponent.java @@ -0,0 +1,37 @@ +package org.ruoyi.workflow.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 java.io.Serial; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "t_workflow_component", autoResultMap = true) +@Schema(title = "工作流组件") +public class WorkflowComponent extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField("uuid") + private String uuid; + + @TableField("name") + private String name; + + @TableField("title") + private String title; + + @TableField("remark") + private String remark; + + @TableField("display_order") + private Integer displayOrder; + + @TableField("is_enable") + private Boolean isEnable; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowEdge.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowEdge.java new file mode 100644 index 00000000..0a9bffda --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowEdge.java @@ -0,0 +1,34 @@ +package org.ruoyi.workflow.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 java.io.Serial; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("t_workflow_edge") +@Schema(title = "工作流定义-边 | workflow definition edge") +public class WorkflowEdge extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField("uuid") + private String uuid; + + @TableField("workflow_id") + private Long workflowId; + + @TableField("source_node_uuid") + private String sourceNodeUuid; + + @TableField("source_handle") + private String sourceHandle; + + @TableField("target_node_uuid") + private String targetNodeUuid; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowNode.java new file mode 100644 index 00000000..cc15504a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowNode.java @@ -0,0 +1,46 @@ +package org.ruoyi.workflow.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 java.io.Serial; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "t_workflow_node", autoResultMap = true) +@Schema(title = "工作流定义-节点 | workflow definition node") +public class WorkflowNode extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField("uuid") + private String uuid; + + @TableField("workflow_id") + private Long workflowId; + + @TableField("workflow_component_id") + private Long workflowComponentId; + + @TableField("title") + private String title; + + @TableField("remark") + private String remark; + + @TableField(value = "input_config") + private String inputConfig; + + @TableField(value = "node_config") + private String nodeConfig; + + @TableField("position_x") + private Double positionX; + + @TableField("position_y") + private Double positionY; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntime.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntime.java new file mode 100644 index 00000000..557de3f4 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntime.java @@ -0,0 +1,40 @@ +package org.ruoyi.workflow.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 java.io.Serial; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "t_workflow_runtime", autoResultMap = true) +@Schema(title = "工作流运行时 | Workflow runtime") +public class WorkflowRuntime extends BaseEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @TableField("uuid") + private String uuid; + + @TableField("user_id") + private Long userId; + + @TableField("workflow_id") + private Long workflowId; + + @TableField(value = "input") + private String input; + + @TableField(value = "output") + private String output; + + @TableField("status") + private Integer status; + + @TableField("status_remark") + private String statusRemark; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntimeNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntimeNode.java new file mode 100644 index 00000000..271d9eb7 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/entity/WorkflowRuntimeNode.java @@ -0,0 +1,42 @@ +package org.ruoyi.workflow.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 java.io.Serial; + +@Data +@EqualsAndHashCode(callSuper = true) +@TableName(value = "t_workflow_runtime_node", autoResultMap = true) +@Schema(title = "工作流实例-节点 | Workflow runtime - node") +public class WorkflowRuntimeNode extends BaseEntity { + @Serial + private static final long serialVersionUID = 1L; + + @TableField("uuid") + private String uuid; + + @TableField("user_id") + private Long userId; + + @TableField("workflow_runtime_id") + private Long workflowRuntimeId; + + @TableField("node_id") + private Long nodeId; + + @TableField(value = "input") + private String input; + + @TableField(value = "output") + private String output; + + @TableField("status") + private Integer status; + + @TableField("status_remark") + private String statusRemark; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/AiModelStatus.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/AiModelStatus.java new file mode 100644 index 00000000..0dec4a94 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/AiModelStatus.java @@ -0,0 +1,14 @@ +package org.ruoyi.workflow.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum AiModelStatus implements BaseEnum { + ACTIVE(1, "启用"), + INACTIVE(2, "停用"); + + private final Integer value; + private final String desc; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/BaseEnum.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/BaseEnum.java new file mode 100644 index 00000000..66a2fb63 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/BaseEnum.java @@ -0,0 +1,12 @@ +package org.ruoyi.workflow.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +public interface BaseEnum extends IEnum { + /** + * 获取对应名称 + * + * @return String + */ + String getDesc(); +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/ErrorEnum.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/ErrorEnum.java new file mode 100644 index 00000000..f9de801e --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/ErrorEnum.java @@ -0,0 +1,127 @@ +package org.ruoyi.workflow.enums; + +import lombok.Getter; + +@Getter +public enum ErrorEnum { + SUCCESS("00000", "成功"), + A_URL_NOT_FOUND("A0001", "地址不存在"), + A_PARAMS_ERROR("A0002", "参数校验不通过"), + A_REQUEST_TOO_MUCH("A0003", "访问次数太多"), + A_LOGIN_ERROR("A0004", "登陆失败,账号或密码错误"), + A_LOGIN_ERROR_MAX("A0005", "失败次数太多,请输入验证码重试"), + A_LOGIN_CAPTCHA_ERROR("A0006", "验证码不正确"), + A_USER_NOT_EXIST("A0007", "用户不存在"), + A_CONVERSATION_NOT_EXIST("A0008", "对话不存在"), + A_IMAGE_NUMBER_ERROR("A0009", "图片数量不对"), + A_IMAGE_SIZE_ERROR("A0010", "图片尺寸不对"), + A_FILE_NOT_EXIST("A0011", "文件不存在"), + A_DRAWING("A0012", "作图还未完成"), + A_USER_EXIST("A0013", "账号已经存在,请使用账号密码登录"), + A_FIND_PASSWORD_CODE_ERROR("A0014", "重置码已过期或不存在"), + A_USER_WAIT_CONFIRM("A0015", "用户未激活"), + A_USER_NOT_AUTH("A0016", "用户无权限"), + A_DATA_NOT_FOUND("A0017", "数据不存在"), + A_UPLOAD_FAIL("A0018", "上传失败"), + A_QA_ASK_LIMIT("A0019", "请求次数太多"), + A_QA_ITEM_LIMIT("A0020", "知识点生成已超额度"), + A_CONVERSATION_EXIST("A0021", "会话(角色)已存在"), + A_MODEL_NOT_FOUND("A0022", "模型不存在"), + A_MODEL_ALREADY_EXIST("A0023", "模型已存在"), + A_CONVERSATION_NOT_FOUND("A0024", "会话(角色)找不到"), + A_AI_IMAGE_NOT_FOUND("A0024", "图片找不到"), + A_ENABLE_MODEL_NOT_FOUND("A0025", "没有可用的模型"), + A_DOC_INDEX_DOING("A0026", "文档索引正在进行中,请稍后重试"), + A_PRESET_CONVERSATION_NOT_EXIST("A0027", "预设会话或角色不存在"), + A_CONVERSATION_TITLE_EXIST("A0028", "会话(角色)标题已存在"), + A_AI_IMAGE_NO_AUTH("A0029", "无权限查看该图片"), + A_USER_NOT_FOUND("A0030", "用户不存在"), + A_ACTIVE_CODE_INVALID("A0031", "激活码已失效"), + A_OLD_PASSWORD_INVALID("A0032", "原密码不正确"), + A_OPT_TOO_FREQUENTLY("A0032", "操作太频繁"), + A_DRAW_NOT_FOUND("A00033", "绘图记录找不到"), + A_WF_NOT_FOUND("A00034", "工作流找不到"), + A_WF_DISABLED("A0035", "工作流已停用"), + A_WF_NODE_NOT_FOUND("A0036", "工作流节点找不到"), + A_WF_NODE_CONFIG_NOT_FOUND("A0037", "工作流节点配置找不到"), + A_WF_NODE_CONFIG_ERROR("A0038", "工作流节点配置异常"), + A_WF_INPUT_INVALID("A0039", "工作流输入参数错误"), + A_WF_INPUT_MISSING("A0040", "工作流输入缺少参数"), + A_WF_MULTIPLE_START_NODE("A0041", "多个开始节点"), + A_WF_START_NODE_NOT_FOUND("A0042", "没有开始节点"), + A_WF_END_NODE_NOT_FOUND("A0043", "没有结束节点"), + A_WF_EDGE_NOT_FOUND("A0044", "工作流的边找不到"), + A_WF_RUNTIME_NOT_FOUND("A00045", "工作流运行时数据找不到"), + A_SEARCH_QUERY_IS_EMPTY("A00046", "搜索内容不能为空"), + A_WF_COMPONENT_NOT_FOUND("A00047", "工作流基础组件找不到"), + A_WF_RESUME_FAIL("A00048", "工作流恢复执行时失败"), + A_MAIL_SENDER_EMPTY("A00049", "邮件发送人不能为空"), + A_MAIL_SENDER_CONFIG_ERROR("A00050", "邮件发送人配置错误"), + A_MAIL_RECEIVER_EMPTY("A00051", "邮件接收人不能为空"), + A_MCP_SERVER_NOT_FOUND("A00052", "MCP服务找不到"), + A_USER_MCP_SERVER_NOT_FOUND("A00053", "用户的MCP服务找不到"), + A_PARAMS_INVALID_BY_("A00054", "参数校验异常:{0}"), + A_AI_MESSAGE_NOT_FOUND("A00055", "找不到AI的消息"), + A_USER_QUESTION_NOT_FOUND("A00056", "用户问题不存在"), + A_PLATFORM_NOT_MATCH("A0057", "平台不匹配"), + B_UNCAUGHT_ERROR("B0001", "未捕捉异常"), + B_COMMON_ERROR("B0002", "业务出错"), + B_GLOBAL_ERROR("B0003", "全局异常"), + B_SAVE_IMAGE_ERROR("B0004", "保存图片异常"), + B_FIND_IMAGE_404("B0005", "无法找到图片"), + B_DAILY_QUOTA_USED("B0006", "今天额度已经用完"), + B_MONTHLY_QUOTA_USED("B0007", "当月额度已经用完"), + B_LLM_NOT_SUPPORT("B0008", "LLM不支持该功能"), + B_LLM_SECRET_KEY_NOT_SET("B0009", "LLM的secret key没设置"), + B_MESSAGE_NOT_FOUND("B0008", "消息不存在"), + B_LLM_SERVICE_DISABLED("B0009", "LLM服务不可用"), + B_KNOWLEDGE_BASE_IS_EMPTY("B0010", "知识库内容为空"), + B_NO_ANSWER("B0011", "[无答案]"), + B_SAVE_FILE_ERROR("B0012", "保存文件异常"), + B_BREAK_SEARCH("B0013", "中断搜索"), + B_GRAPH_FILTER_NOT_FOUND("B0014", "图过滤器未定义"), + B_DB_ERROR("B0015", "数据库查询异常"), + B_ACTIVE_USER_ERROR("B0016", "激活用户失败"), + B_RESET_PASSWORD_ERROR("B0017", "重置密码失败"), + B_IMAGE_LOAD_ERROR("B0018", "加载图片失败"), + B_IO_EXCEPTION("B0019", "IO异常"), + B_SERVER_EXCEPTION("B0020", "服务端异常"), + B_DELETE_FILE_ERROR("B0021", "删除文件异常"), + B_WF_RUN_ERROR("B0022", "工作流运行异常"), + B_WF_NODE_DEFINITION_NOT_FOUND("B0023", "工作流节点定义找不到"), + B_DIR_CREATE_FAIL("B0024", "创建目录失败"), + B_LLM_TEMPERATURE_ERROR("B0025", "采样温度应该在 0.1-1之间"), + B_ASR_SETTING_NOT_FOUND("B0026", "语音识别设置未找到"), + B_URL_INVALID("B0027", "不是有效的网络地址"), + B_ASR_MODEL_NOT_FOUND("B0028", "语音识别模型未找到"), + B_TTS_SETTING_NOT_FOUND("B0029", "语音合成设置未找到"), + B_TTS_MODEL_NOT_FOUND("B0030", "语音合成模型未找到"), + B_VOICE_NOT_FOUND("B0031", "声音不存在"), + C_DRAW_FAIL("C0001", "大模型生成图片失败,原因:{0}"), + C_ALI_OSS_CONFIG_ERROR("C0002", "阿里云OSS初始化失败,原因:{0}"), + C_LLM_RESPONSE_INVALID("C0003", "大模型生成结果内容无效"), + C_WF_COMPONENT_DELETED_FAIL_BY_USED("C0004", "工作流组件已经被使用,无法被删除,可先停用"); + + private final String code; + private final String info; + + ErrorEnum(String code, String info) { + this.code = code; + this.info = info; + } + + public static ErrorEnum getErrorEnum(String code) { + ErrorEnum result = null; + for (ErrorEnum c : ErrorEnum.values()) { + if (c.getCode().equals(code)) { + result = c; + break; + } + } + if (null == result) { + result = B_COMMON_ERROR; + } + return result; + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/UserStatusEnum.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/UserStatusEnum.java new file mode 100644 index 00000000..2be9a950 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/UserStatusEnum.java @@ -0,0 +1,23 @@ +package org.ruoyi.workflow.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum UserStatusEnum implements BaseEnum { + + WAIT_CONFIRM(1, "待验证"), + NORMAL(2, "正常"), + FREEZE(3, "冻结"); + + private final Integer value; + private final String desc; + + public static UserStatusEnum getByValue(Integer val) { + return Arrays.stream(UserStatusEnum.values()).filter(item -> item.value.equals(val)).findFirst().orElse(null); + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/WfIODataTypeEnum.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/WfIODataTypeEnum.java new file mode 100644 index 00000000..8f8989c6 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/enums/WfIODataTypeEnum.java @@ -0,0 +1,25 @@ +package org.ruoyi.workflow.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +@Getter +@AllArgsConstructor +public enum WfIODataTypeEnum implements BaseEnum { + TEXT(1, "文本"), + NUMBER(2, "数字"), + OPTIONS(3, "下拉选项"), + FILES(4, "文件列表"), + BOOL(5, "布尔值"), + REF_INPUT(6, "引用节点的输入参数"), + REF_OUTPUT(7, "引用节点的输出参数"); + + private final Integer value; + private final String desc; + + public static WfIODataTypeEnum getByValue(Integer val) { + return Arrays.stream(WfIODataTypeEnum.values()).filter(item -> item.value.equals(val)).findFirst().orElse(null); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/helper/SSEEmitterHelper.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/helper/SSEEmitterHelper.java new file mode 100644 index 00000000..4d432c07 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/helper/SSEEmitterHelper.java @@ -0,0 +1,138 @@ +package org.ruoyi.workflow.helper; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +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; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class SSEEmitterHelper { + + private static final Cache COMPLETED_SSE = CacheBuilder.newBuilder() + .expireAfterWrite(10, TimeUnit.MINUTES).build(); + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public static void parseAndSendPartialMsg(SseEmitter sseEmitter, String name, String content) { + if (Boolean.TRUE.equals(COMPLETED_SSE.getIfPresent(sseEmitter))) { + log.warn("sseEmitter already completed,name:{}", name); + return; + } + String[] lines = content.split("[\\r\\n]", -1); + if (lines.length > 1) { + sendPartial(sseEmitter, name, " " + lines[0]); + for (int i = 1; i < lines.length; i++) { + sendPartial(sseEmitter, name, "-_wrap_-"); + sendPartial(sseEmitter, name, " " + lines[i]); + } + } else { + sendPartial(sseEmitter, name, " " + content); + } + } + + public static void sendPartial(SseEmitter sseEmitter, String name, String msg) { + if (Boolean.TRUE.equals(COMPLETED_SSE.getIfPresent(sseEmitter))) { + log.warn("sseEmitter already completed,name:{}", name); + return; + } + try { + if (StringUtils.isNotBlank(name)) { + sseEmitter.send(SseEmitter.event().name(name).data(msg)); + } else { + sseEmitter.send(msg); + } + } catch (IOException ioException) { + log.error("stream onNext error", ioException); + } + } + + + public boolean checkOrComplete(User user, SseEmitter sseEmitter) { + //Check: If still waiting response + String askingKey = MessageFormat.format(RedisKeyConstant.USER_ASKING, user.getId()); + String askingVal = stringRedisTemplate.opsForValue().get(askingKey); + if (StringUtils.isNotBlank(askingVal)) { + sendErrorAndComplete(user.getId(), sseEmitter, "正在回复中..."); + return false; + } + return true; + } + + + public void startSse(User user, SseEmitter sseEmitter, String data) { + + String askingKey = MessageFormat.format(RedisKeyConstant.USER_ASKING, user.getId()); + stringRedisTemplate.opsForValue().set(askingKey, "1", 15, TimeUnit.SECONDS); + + try { + SseEmitter.SseEventBuilder builder = SseEmitter.event().name(AdiConstant.SSEEventName.START); + if (StringUtils.isNotBlank(data)) { + builder.data(data); + } + sseEmitter.send(builder); + } catch (IOException e) { + log.error("startSse error", e); + sseEmitter.completeWithError(e); + COMPLETED_SSE.put(sseEmitter, Boolean.TRUE); + stringRedisTemplate.delete(askingKey); + } + } + + public void sendComplete(long userId, SseEmitter sseEmitter, String msg) { + if (Boolean.TRUE.equals(COMPLETED_SSE.getIfPresent(sseEmitter))) { + log.warn("sseEmitter already completed,userId:{}", userId); + delSseRequesting(userId); + return; + } + try { + sseEmitter.send(SseEmitter.event().name(AdiConstant.SSEEventName.DONE).data(msg)); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + COMPLETED_SSE.put(sseEmitter, Boolean.TRUE); + delSseRequesting(userId); + sseEmitter.complete(); + } + } + + + public void sendErrorAndComplete(long userId, SseEmitter sseEmitter, String errorMsg) { + if (Boolean.TRUE.equals(COMPLETED_SSE.getIfPresent(sseEmitter))) { + log.warn("sseEmitter already completed,ignore error:{}", errorMsg); + delSseRequesting(userId); + return; + } + try { + SseEmitter.SseEventBuilder event = SseEmitter.event(); + event.name(AdiConstant.SSEEventName.ERROR); + event.data(Objects.toString(errorMsg, "")); + sseEmitter.send(event); + } catch (IOException e) { + log.warn("sendErrorAndComplete userId:{},errorMsg:{}", userId, errorMsg); + throw new RuntimeException(e); + } finally { + COMPLETED_SSE.put(sseEmitter, Boolean.TRUE); + delSseRequesting(userId); + sseEmitter.complete(); + } + } + + private void delSseRequesting(long userId) { + String askingKey = MessageFormat.format(RedisKeyConstant.USER_ASKING, userId); + stringRedisTemplate.delete(askingKey); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowComponentMapper.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowComponentMapper.java new file mode 100644 index 00000000..0935eb58 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowComponentMapper.java @@ -0,0 +1,11 @@ +package org.ruoyi.workflow.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.ruoyi.workflow.entity.WorkflowComponent; + +@Mapper +public interface WorkflowComponentMapper extends BaseMapper { + Integer countRefNodes(@Param("uuid") String uuid); +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowEdgeMapper.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowEdgeMapper.java new file mode 100644 index 00000000..28d21dec --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowEdgeMapper.java @@ -0,0 +1,9 @@ +package org.ruoyi.workflow.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.workflow.entity.WorkflowEdge; + +@Mapper +public interface WorkflowEdgeMapper extends BaseMapper { +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowMapper.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowMapper.java new file mode 100644 index 00000000..b8bb1988 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowMapper.java @@ -0,0 +1,9 @@ +package org.ruoyi.workflow.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.workflow.entity.Workflow; + +@Mapper +public interface WorkflowMapper extends BaseMapper { +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowNodeMapper.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowNodeMapper.java new file mode 100644 index 00000000..380eec9d --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowNodeMapper.java @@ -0,0 +1,10 @@ +package org.ruoyi.workflow.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.workflow.entity.WorkflowNode; + +@Mapper +public interface WorkflowNodeMapper extends BaseMapper { + WorkflowNode getStartNode(long workflowId); +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowRunMapper.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowRunMapper.java new file mode 100644 index 00000000..3e73dd6a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowRunMapper.java @@ -0,0 +1,13 @@ +package org.ruoyi.workflow.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.ruoyi.workflow.entity.WorkflowRuntime; + +@Mapper +public interface WorkflowRunMapper extends BaseMapper { + + Page pageByWfUuid(Page page, @Param("wfUuid") String wfUuid, @Param("userId") Long userId); +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowRuntimeNodeMapper.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowRuntimeNodeMapper.java new file mode 100644 index 00000000..91345f1c --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/mapper/WorkflowRuntimeNodeMapper.java @@ -0,0 +1,9 @@ +package org.ruoyi.workflow.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.workflow.entity.WorkflowRuntimeNode; + +@Mapper +public interface WorkflowRuntimeNodeMapper extends BaseMapper { +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowComponentService.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowComponentService.java new file mode 100644 index 00000000..04e3b03c --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowComponentService.java @@ -0,0 +1,126 @@ +package org.ruoyi.workflow.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.apache.commons.lang3.StringUtils; +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; +import org.ruoyi.workflow.workflow.WfComponentNameEnum; +import org.springframework.beans.BeanUtils; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +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; + +@Slf4j +@Service +public class WorkflowComponentService extends ServiceImpl { + + @Lazy + @Resource + private WorkflowComponentService self; + + @CacheEvict(cacheNames = {WORKFLOW_COMPONENTS, WORKFLOW_COMPONENT_START_KEY}) + public WorkflowComponent addOrUpdate(WfComponentReq req) { + WorkflowComponent wfComponent; + if (StringUtils.isNotBlank(req.getUuid())) { + wfComponent = PrivilegeUtil.checkAndGetByUuid(req.getUuid(), this.query(), ErrorEnum.A_WF_COMPONENT_NOT_FOUND); + + WorkflowComponent update = new WorkflowComponent(); + BeanUtils.copyProperties(req, update, "id", "uuid"); + update.setId(wfComponent.getId()); + update.setName(req.getName()); + update.setTitle(req.getTitle()); + update.setRemark(req.getRemark()); + update.setIsEnable(req.getIsEnable()); + update.setDisplayOrder(req.getDisplayOrder()); + this.baseMapper.updateById(update); + + return update; + } else { + wfComponent = new WorkflowComponent(); + BeanUtils.copyProperties(req, wfComponent, "id", "uuid"); + wfComponent.setUuid(UuidUtil.createShort()); + this.baseMapper.insert(wfComponent); + + return wfComponent; + } + } + + @CacheEvict(cacheNames = {WORKFLOW_COMPONENTS, WORKFLOW_COMPONENT_START_KEY}) + public void enable(String uuid, Boolean isEnable) { + WorkflowComponent wfComponent = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_COMPONENT_NOT_FOUND); + WorkflowComponent update = new WorkflowComponent(); + update.setIsEnable(isEnable); + update.setId(wfComponent.getId()); + this.baseMapper.updateById(update); + } + + @CacheEvict(cacheNames = {WORKFLOW_COMPONENTS, WORKFLOW_COMPONENT_START_KEY}) + public void deleteByUuid(String uuid) { + WorkflowComponent component = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_COMPONENT_NOT_FOUND); + Integer refNodeCount = baseMapper.countRefNodes(uuid); + if (refNodeCount != null && refNodeCount > 0) { + throw new BaseException(C_WF_COMPONENT_DELETED_FAIL_BY_USED.getInfo()); + } + boolean updated = ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(WorkflowComponent::getId, component.getId()) + .set(WorkflowComponent::getIsDeleted, true) + .set(WorkflowComponent::getIsEnable, false) + .update(); + if (!updated) { + throw new BaseException(ErrorEnum.A_WF_COMPONENT_NOT_FOUND.getInfo()); + } + } + + public Page search(WfComponentSearchReq searchReq, Integer currentPage, Integer pageSize) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(WorkflowComponent::getIsDeleted, false); + wrapper.eq(null != searchReq.getIsEnable(), WorkflowComponent::getIsEnable, searchReq.getIsEnable()); + wrapper.like(StringUtils.isNotBlank(searchReq.getTitle()), WorkflowComponent::getTitle, searchReq.getTitle()); + wrapper.orderByAsc(List.of(WorkflowComponent::getDisplayOrder, WorkflowComponent::getId)); + return baseMapper.selectPage(new Page<>(currentPage, pageSize), wrapper); + } + + @Cacheable(cacheNames = WORKFLOW_COMPONENTS) + public List getAllEnable() { + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowComponent::getIsEnable, true) + .eq(WorkflowComponent::getIsDeleted, false) + .orderByAsc(List.of(WorkflowComponent::getDisplayOrder, WorkflowComponent::getId)) + .list(); + } + + @Cacheable(cacheNames = WORKFLOW_COMPONENT_START_KEY) + public WorkflowComponent getStartComponent() { + List components = self.getAllEnable(); + return components.stream() + .filter(component -> WfComponentNameEnum.START.getName().equals(component.getName())) + .findFirst() + .orElseThrow(() -> new BaseException(ErrorEnum.B_WF_NODE_DEFINITION_NOT_FOUND.getInfo())); + } + + public WorkflowComponent getComponent(Long id) { + List components = self.getAllEnable(); + return components.stream() + .filter(component -> component.getId().equals(id)) + .findFirst() + .orElseThrow(() -> new BaseException(ErrorEnum.B_WF_NODE_DEFINITION_NOT_FOUND.getInfo())); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowEdgeService.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowEdgeService.java new file mode 100644 index 00000000..ba545e2d --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowEdgeService.java @@ -0,0 +1,118 @@ +package org.ruoyi.workflow.service; + +import cn.hutool.core.collection.CollUtil; +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.apache.commons.collections4.CollectionUtils; +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; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class WorkflowEdgeService extends ServiceImpl { + + @Lazy + @Resource + private WorkflowEdgeService self; + + public List listDtoByWfId(long workflowId) { + List edges = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowEdge::getWorkflowId, workflowId) + .eq(WorkflowEdge::getIsDeleted, false) + .list(); + return MPPageUtil.convertToList(edges, WfEdgeReq.class); + } + + @Transactional + public void createOrUpdateEdges(Long workflowId, List edges) { + List uuidList = new ArrayList<>(); + for (WfEdgeReq edge : edges) { + WorkflowEdge newOne = new WorkflowEdge(); + BeanUtils.copyProperties(edge, newOne); + newOne.setWorkflowId(workflowId); + + WorkflowEdge old = self.getByUuid(edge.getUuid()); + if (null != old) { + log.info("更新边,id:{},uuid:{},source:{},sourceHandle:{},target:{}", + edge.getId(), edge.getUuid(), edge.getSourceNodeUuid(), edge.getSourceHandle(), edge.getTargetNodeUuid()); + newOne.setId(old.getId()); + } else { + newOne.setId(null); + log.info("新增边,uuid:{},source:{},sourceHandle:{},target:{}", + edge.getUuid(), edge.getSourceNodeUuid(), edge.getSourceHandle(), edge.getTargetNodeUuid()); + } + uuidList.add(edge.getUuid()); + self.saveOrUpdate(newOne); + } + ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(WorkflowEdge::getWorkflowId, workflowId) + .notIn(CollUtil.isNotEmpty(uuidList), WorkflowEdge::getUuid, uuidList) + .set(WorkflowEdge::getIsDeleted, true) + .update(); + } + + public List listByWorkflowId(Long workflowId) { + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowEdge::getWorkflowId, workflowId) + .eq(WorkflowEdge::getIsDeleted, false) + .list(); + } + + public List copyByWorkflowId(long workflowId, long targetWorkflow) { + List result = new ArrayList<>(); + self.listByWorkflowId(workflowId).forEach(edge -> { + result.add(self.copyEdge(targetWorkflow, edge)); + }); + return result; + } + + public WorkflowEdge copyEdge(long targetWorkflow, WorkflowEdge sourceEdge) { + WorkflowEdge newEdge = new WorkflowEdge(); + BeanUtils.copyProperties(sourceEdge, newEdge, "id", "uuid", "createTime", "updateTime"); + newEdge.setUuid(UuidUtil.createShort()); + newEdge.setWorkflowId(targetWorkflow); + baseMapper.insert(newEdge); + return getById(newEdge.getId()); + } + + @Transactional + public void deleteEdges(Long workflowId, List uuids) { + if (CollectionUtils.isEmpty(uuids)) { + return; + } + for (String uuid : uuids) { + WorkflowEdge old = self.getByUuid(uuid); + if (null != old && !old.getWorkflowId().equals(workflowId)) { + log.error("该边不属于指定的工作流,删除失败,workflowId:{},node workflowId:{}", workflowId, workflowId); + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(WorkflowEdge::getWorkflowId, workflowId) + .eq(WorkflowEdge::getUuid, uuid) + .set(WorkflowEdge::getIsDeleted, true) + .update(); + } + } + + public WorkflowEdge getByUuid(String uuid) { + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowEdge::getUuid, uuid) + .eq(WorkflowEdge::getIsDeleted, false) + .last("limit 1") + .one(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowNodeService.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowNodeService.java new file mode 100644 index 00000000..cfd1d19a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowNodeService.java @@ -0,0 +1,245 @@ +package org.ruoyi.workflow.service; + +import cn.hutool.core.collection.CollUtil; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +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.apache.commons.collections4.CollectionUtils; +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; +import org.ruoyi.workflow.util.MPPageUtil; +import org.ruoyi.workflow.util.UuidUtil; +import org.ruoyi.workflow.workflow.WfComponentNameEnum; +import org.ruoyi.workflow.workflow.WfNodeInputConfig; +import org.ruoyi.workflow.workflow.def.WfNodeIOText; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class WorkflowNodeService extends ServiceImpl { + + @Lazy + @Resource + private WorkflowNodeService self; + + @Resource + private WorkflowComponentService workflowComponentService; + + public WorkflowNode getStartNode(long workflowId) { + return baseMapper.getStartNode(workflowId); + } + + public List listDtoByWfId(long workflowId) { + List workflowNodeList = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowNode::getWorkflowId, workflowId) + .eq(WorkflowNode::getIsDeleted, false) + .list(); + workflowNodeList.forEach(this::checkAndDecrypt); + return MPPageUtil.convertToList(workflowNodeList, WfNodeDto.class, (source, target) -> { + target.setInputConfig(JsonUtil.toBean(source.getInputConfig(), ObjectNode.class)); + target.setNodeConfig(JsonUtil.toBean(source.getNodeConfig(), ObjectNode.class)); + return target; + }); + } + + public WorkflowNode getByUuid(long workflowId, String uuid) { + WorkflowNode node = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowNode::getWorkflowId, workflowId) + .eq(WorkflowNode::getUuid, uuid) + .eq(WorkflowNode::getIsDeleted, false) + .last("limit 1") + .one(); + checkAndDecrypt(node); + return node; + } + + public List listByWorkflowId(Long workflowId) { + List list = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowNode::getWorkflowId, workflowId) + .eq(WorkflowNode::getIsDeleted, false) + .list(); + list.forEach(this::checkAndDecrypt); + return list; + } + + public List copyByWorkflowId(long workflowId, long targetWorkflowId) { + List result = new ArrayList<>(); + self.listByWorkflowId(workflowId).forEach(node -> { + result.add(self.copyNode(targetWorkflowId, node)); + }); + return result; + } + + public WorkflowNode copyNode(Long targetWorkflowId, WorkflowNode sourceNode) { + WorkflowNode newNode = new WorkflowNode(); + BeanUtils.copyProperties(sourceNode, newNode, "id", "createTime", "updateTime"); + newNode.setWorkflowId(targetWorkflowId); + baseMapper.insert(newNode); + + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowNode::getWorkflowId, targetWorkflowId) + .eq(WorkflowNode::getUuid, newNode.getUuid()) + .eq(WorkflowNode::getIsDeleted, false) + .last("limit 1") + .one(); + } + + @Transactional + public void createOrUpdateNodes(Long workflowId, List nodes) { + List uuidList = new ArrayList<>(); + for (WfNodeDto node : nodes) { + WorkflowNode newOrUpdate = new WorkflowNode(); + BeanUtils.copyProperties(node, newOrUpdate); + newOrUpdate.setInputConfig(JsonUtil.toJson(node.getInputConfig())); + newOrUpdate.setNodeConfig(JsonUtil.toJson(node.getNodeConfig())); + newOrUpdate.setWorkflowId(workflowId); + checkAndEncrypt(newOrUpdate); + WorkflowNode old = self.getByUuid(workflowId, node.getUuid()); + if (null != old) { + log.info("更新节点,uuid:{},title:{}", node.getUuid(), node.getTitle()); + newOrUpdate.setId(old.getId()); + } else { + log.info("新增节点,uuid:{},title:{}", node.getUuid(), node.getTitle()); + newOrUpdate.setId(null); + } + uuidList.add(node.getUuid()); + self.saveOrUpdate(newOrUpdate); + } + ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(WorkflowNode::getWorkflowId, workflowId) + .notIn(CollUtil.isNotEmpty(uuidList), WorkflowNode::getUuid, uuidList) + .set(WorkflowNode::getIsDeleted, true) + .update(); + } + + private void checkAndEncrypt(WorkflowNode workflowNode) { + WorkflowComponent component = workflowComponentService.getAllEnable() + .stream() + .filter(item -> item.getId().equals(workflowNode.getWorkflowComponentId())) + .findFirst() + .orElse(null); + if (null == component) { + log.error("节点不存在,uuid:{},title:{}", workflowNode.getUuid(), workflowNode.getTitle()); + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + if (component.getName().equals(WfComponentNameEnum.MAIL_SEND.getName())) { + + //加密(目前暂时只在数据库层做加密,前后端交互时数据加解密待定) +// MailSendNodeConfig mailSendNodeConfig = JsonUtil.fromJson(workflowNode.getNodeConfig(), MailSendNodeConfig.class); +// if (null != mailSendNodeConfig && null != mailSendNodeConfig.getSender() && null != mailSendNodeConfig.getSender().getPassword()) { +// String password = mailSendNodeConfig.getSender().getPassword(); +// String encrypt = AesUtil.encrypt(password); +// mailSendNodeConfig.getSender().setPassword(encrypt); +// workflowNode.setNodeConfig(JsonUtil.toJson(mailSendNodeConfig)); +// } + } + } + + private void checkAndDecrypt(WorkflowNode workflowNode) { + if (null == workflowNode) { + log.warn("节点不存在"); + return; + } + WorkflowComponent component = workflowComponentService.getAllEnable() + .stream() + .filter(item -> item.getId().equals(workflowNode.getWorkflowComponentId())) + .findFirst() + .orElse(null); + if (null == component) { + log.error("节点不存在,uuid:{},title:{}", workflowNode.getUuid(), workflowNode.getTitle()); + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + if (component.getName().equals(WfComponentNameEnum.MAIL_SEND.getName())) { +// MailSendNodeConfig mailSendNodeConfig = JsonUtil.fromJson(workflowNode.getNodeConfig(), MailSendNodeConfig.class); +// if (null != mailSendNodeConfig && null != mailSendNodeConfig.getSender() && null != mailSendNodeConfig.getSender().getPassword()) { +// String password = mailSendNodeConfig.getSender().getPassword(); +// if (StringUtils.isNotBlank(password)) { +// String decrypt = AesUtil.decrypt(password); +// mailSendNodeConfig.getSender().setPassword(decrypt); +// } +// workflowNode.setNodeConfig(JsonUtil.toJson(mailSendNodeConfig)); +// } + } + } + + @Transactional + public void deleteNodes(Long workflowId, List uuids) { + if (CollectionUtils.isEmpty(uuids)) { + return; + } + for (String uuid : uuids) { + WorkflowNode old = self.getByUuid(workflowId, uuid); + if (null == old) { + continue; + } + if (!old.getWorkflowId().equals(workflowId)) { + log.error("节点不属于指定的工作流,删除失败,workflowId:{},node workflowId:{}", workflowId, workflowId); + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + if (workflowComponentService.getStartComponent().getId().equals(old.getWorkflowComponentId())) { + log.warn("开始节点不能删除,uuid:{}", old.getUuid()); + continue; + } + ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(WorkflowNode::getWorkflowId, workflowId) + .eq(WorkflowNode::getUuid, uuid) + .set(WorkflowNode::getIsDeleted, true) + .update(); + } + + } + + /** + * user_inputs: + * [ + * { + * "uuid": "12bc919774aa4e779d97e3dd9c836e11", + * "name": "var_user_input", + * "title": "用户输入", + * "type": 1, + * "required": true, + * "max_length": 1000 + * } + * ] + * + * @param workflow 工作流定义 + */ + public WorkflowNode createStartNode(Workflow workflow) { + WfNodeIOText wfNodeIOText = new WfNodeIOText(); + wfNodeIOText.setUuid(UuidUtil.createShort()); + wfNodeIOText.setType(WfIODataTypeEnum.TEXT.getValue()); + wfNodeIOText.setName("var_user_input"); + wfNodeIOText.setTitle("用户输入"); + wfNodeIOText.setRequired(false); + wfNodeIOText.setMaxLength(1000); + WfNodeInputConfig nodeInputConfig = new WfNodeInputConfig(); + nodeInputConfig.setUserInputs(List.of(wfNodeIOText)); + nodeInputConfig.setRefInputs(new ArrayList<>()); + WorkflowComponent startComponent = workflowComponentService.getStartComponent(); + WorkflowNode node = new WorkflowNode(); + node.setWorkflowComponentId(startComponent.getId()); + node.setWorkflowId(workflow.getId()); + node.setRemark("用户输入"); + node.setUuid(UuidUtil.createShort()); + node.setTitle("开始"); + node.setInputConfig(JsonUtil.toJson(nodeInputConfig)); + node.setNodeConfig("{}"); + baseMapper.insert(node); + return node; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeNodeService.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeNodeService.java new file mode 100644 index 00000000..43c73207 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeNodeService.java @@ -0,0 +1,103 @@ +package org.ruoyi.workflow.service; + +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.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; +import org.ruoyi.workflow.util.MPPageUtil; +import org.ruoyi.workflow.workflow.WfNodeState; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +@Slf4j +@Service +public class WorkflowRuntimeNodeService extends ServiceImpl { + + + public List listByWfRuntimeId(long runtimeId) { + List workflowNodeList = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(!ThreadContext.getCurrentUser().getIsAdmin(), WorkflowRuntimeNode::getUserId, ThreadContext.getCurrentUser().getId()) + .eq(WorkflowRuntimeNode::getWorkflowRuntimeId, runtimeId) + .eq(WorkflowRuntimeNode::getIsDeleted, false) + .orderByAsc(WorkflowRuntimeNode::getId) + .list(); + List result = MPPageUtil.convertToList(workflowNodeList, WfRuntimeNodeDto.class); + for (WfRuntimeNodeDto dto : result) { + fillInputOutput(dto); + } + return result; + } + + public WfRuntimeNodeDto createByState(User user, long wfNodeId, long wfRuntimeId, WfNodeState state) { + WorkflowRuntimeNode runtimeNode = new WorkflowRuntimeNode(); + runtimeNode.setUuid(state.getUuid()); + runtimeNode.setWorkflowRuntimeId(wfRuntimeId); + runtimeNode.setStatus(state.getProcessStatus()); + runtimeNode.setUserId(user.getId()); + runtimeNode.setNodeId(wfNodeId); + baseMapper.insert(runtimeNode); + runtimeNode = baseMapper.selectById(runtimeNode.getId()); + + WfRuntimeNodeDto result = new WfRuntimeNodeDto(); + BeanUtils.copyProperties(runtimeNode, result); + fillInputOutput(result); + return result; + } + + public void updateInput(Long id, WfNodeState state) { + if (CollectionUtils.isEmpty(state.getInputs())) { + log.warn("没有输入数据,id:{}", id); + return; + } + WorkflowRuntimeNode node = baseMapper.selectById(id); + if (null == node) { + log.error("节点实例不存在,id:{}", id); + return; + } + WorkflowRuntimeNode updateOne = new WorkflowRuntimeNode(); + updateOne.setId(id); + ObjectNode ob = JsonUtil.createObjectNode(); + state.getInputs().forEach(data -> ob.set(data.getName(), JsonUtil.classToJsonNode(data.getContent()))); + updateOne.setInput(JsonUtil.toJson(ob)); + updateOne.setStatus(state.getProcessStatus()); + updateOne.setStatusRemark(state.getProcessStatusRemark()); + baseMapper.updateById(updateOne); + } + + public void updateOutput(Long id, WfNodeState state) { + WorkflowRuntimeNode node = baseMapper.selectById(id); + if (null == node) { + log.error("节点实例不存在,id:{}", id); + return; + } + WorkflowRuntimeNode updateOne = new WorkflowRuntimeNode(); + updateOne.setId(id); + if (!CollectionUtils.isEmpty(state.getOutputs())) { + ObjectNode ob = JsonUtil.createObjectNode(); + state.getOutputs().forEach(data -> ob.set(data.getName(), JsonUtil.classToJsonNode(data.getContent()))); + updateOne.setOutput(JsonUtil.toJson(ob)); + } + updateOne.setStatus(state.getProcessStatus()); + updateOne.setStatusRemark(state.getProcessStatusRemark()); + baseMapper.updateById(updateOne); + } + + private void fillInputOutput(WfRuntimeNodeDto dto) { + if (null == dto.getInput()) { + dto.setInput(JsonUtil.createObjectNode()); + } + if (null == dto.getOutput()) { + dto.setOutput(JsonUtil.createObjectNode()); + } + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeService.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeService.java new file mode 100644 index 00000000..330974a9 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowRuntimeService.java @@ -0,0 +1,174 @@ +package org.ruoyi.workflow.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +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.apache.commons.lang3.StringUtils; +import org.ruoyi.workflow.base.ThreadContext; +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; +import org.ruoyi.workflow.util.PrivilegeUtil; +import org.ruoyi.workflow.util.UuidUtil; +import org.ruoyi.workflow.workflow.WfState; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.springframework.beans.BeanUtils; +import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.WORKFLOW_PROCESS_STATUS_DOING; + +@Slf4j +@Service +public class WorkflowRuntimeService extends ServiceImpl { + + @Resource + private WorkflowService workflowService; + + @Resource + private WorkflowRuntimeNodeService workflowRuntimeNodeService; + + public WfRuntimeResp create(User user, Long workflowId) { + WorkflowRuntime one = new WorkflowRuntime(); + one.setUuid(UuidUtil.createShort()); + one.setUserId(user.getId()); + one.setWorkflowId(workflowId); + baseMapper.insert(one); + + one = baseMapper.selectById(one.getId()); + return changeToDTO(one); + } + + public void updateInput(long id, WfState wfState) { + if (CollectionUtils.isEmpty(wfState.getInput())) { + log.warn("没有输入数据,id:{}", id); + return; + } + WorkflowRuntime node = baseMapper.selectById(id); + if (null == node) { + log.error("工作流实例不存在,id:{}", id); + return; + } + WorkflowRuntime updateOne = new WorkflowRuntime(); + updateOne.setId(id); + ObjectNode ob = JsonUtil.createObjectNode(); + for (NodeIOData data : wfState.getInput()) { + ob.set(data.getName(), JsonUtil.classToJsonNode(data.getContent())); + } + updateOne.setInput(JsonUtil.toJson(ob)); + updateOne.setStatus(WORKFLOW_PROCESS_STATUS_DOING); + baseMapper.updateById(updateOne); + } + + public WorkflowRuntime updateOutput(long id, WfState wfState) { + WorkflowRuntime node = baseMapper.selectById(id); + if (null == node) { + log.error("工作流实例不存在,id:{}", id); + return null; + } + WorkflowRuntime updateOne = new WorkflowRuntime(); + updateOne.setId(id); + ObjectNode ob = JsonUtil.createObjectNode(); + for (NodeIOData data : wfState.getOutput()) { + ob.set(data.getName(), JsonUtil.classToJsonNode(data.getContent())); + } + updateOne.setOutput(JsonUtil.toJson(ob)); + updateOne.setStatus(wfState.getProcessStatus()); + baseMapper.updateById(updateOne); + return updateOne; + } + + public void updateStatus(long id, int processStatus, String statusRemark) { + WorkflowRuntime node = baseMapper.selectById(id); + if (null == node) { + log.error("工作流实例不存在,id:{}", id); + return; + } + WorkflowRuntime updateOne = new WorkflowRuntime(); + updateOne.setId(id); + updateOne.setStatus(processStatus); + updateOne.setStatusRemark(StringUtils.substring(statusRemark, 0, 250)); + baseMapper.updateById(updateOne); + } + + public WorkflowRuntime getByUuid(String uuid) { + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(!ThreadContext.getCurrentUser().getIsAdmin(), WorkflowRuntime::getUserId, ThreadContext.getCurrentUserId()) + .eq(WorkflowRuntime::getUuid, uuid) + .eq(WorkflowRuntime::getIsDeleted, false) + .last("limit 1") + .one(); + } + + public Page page(String wfUuid, Integer currentPage, Integer pageSize) { + Workflow workflow = workflowService.getOrThrow(wfUuid); + User user = ThreadContext.getCurrentUser(); + Page page = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(WorkflowRuntime::getWorkflowId, workflow.getId()) + .eq(WorkflowRuntime::getIsDeleted, false) + .eq(!user.getIsAdmin(), WorkflowRuntime::getUserId, user.getId()) + .orderByDesc(WorkflowRuntime::getUpdateTime) + .page(new Page<>(currentPage, pageSize)); + Page result = new Page<>(); + MPPageUtil.convertToPage(page, result, WfRuntimeResp.class, (source, target) -> { + fillInputOutput(target); + return target; + }); + return result; + } + + public List listByRuntimeUuid(String runtimeUuid) { + WorkflowRuntime runtime = PrivilegeUtil.checkAndGetByUuid(runtimeUuid, this.query(), ErrorEnum.A_WF_RUNTIME_NOT_FOUND); + return workflowRuntimeNodeService.listByWfRuntimeId(runtime.getId()); + } + + public boolean deleteAll(String wfUuid) { + Workflow workflow = workflowService.getOrThrow(wfUuid); + User user = ThreadContext.getCurrentUser(); + return ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(WorkflowRuntime::getWorkflowId, workflow.getId()) + .eq(!user.getIsAdmin(), WorkflowRuntime::getUserId, user.getId()) + .set(WorkflowRuntime::getIsDeleted, true) + .update(); + } + + private WfRuntimeResp changeToDTO(WorkflowRuntime runtime) { + WfRuntimeResp result = new WfRuntimeResp(); + BeanUtils.copyProperties(runtime, result); + fillInputOutput(result); + return result; + } + +// private void fillNodes(WfRuntimeResp runtimeResp) { +// List nodes = workflowRuntimeNodeService.listByWfRuntimeId(runtimeResp.getId()); +// runtimeResp.setNodes(nodes); +// } + + private void fillInputOutput(WfRuntimeResp target) { + if (null == target.getInput()) { + target.setInput(JsonUtil.createObjectNode()); + } + if (null == target.getOutput()) { + target.setOutput(JsonUtil.createObjectNode()); + } + } + + public boolean softDelete(String uuid) { + WorkflowRuntime workflowRuntime = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_NOT_FOUND); + return ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(WorkflowRuntime::getId, workflowRuntime.getId()) + .set(WorkflowRuntime::getIsDeleted, true) + .update(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowService.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowService.java new file mode 100644 index 00000000..4d682d96 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/service/WorkflowService.java @@ -0,0 +1,193 @@ +package org.ruoyi.workflow.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.apache.commons.lang3.StringUtils; +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; +import org.ruoyi.workflow.util.UuidUtil; +import org.springframework.beans.BeanUtils; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class WorkflowService extends ServiceImpl { + + @Lazy + @Resource + private WorkflowService self; + + @Resource + private WorkflowNodeService workflowNodeService; + + @Resource + private WorkflowEdgeService workflowEdgeService; + + @Resource + private WorkflowComponentService workflowComponentService; + + @Transactional + public WorkflowResp add(String title, String remark, Boolean isPublic) { + String uuid = UuidUtil.createShort(); + Workflow one = new Workflow(); + one.setUuid(uuid); + one.setTitle(title); + one.setUserId(ThreadContext.getCurrentUserId()); + one.setRemark(remark); + one.setIsEnable(true); + one.setIsPublic(isPublic); + baseMapper.insert(one); + + workflowNodeService.createStartNode(one); + return changeWorkflowToDTO(one); + } + + public void setPublic(String wfUuid, Boolean isPublic) { + Workflow workflow = PrivilegeUtil.checkAndGetByUuid(wfUuid, this.query(), ErrorEnum.A_WF_NOT_FOUND); + ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(Workflow::getId, workflow.getId()) + .set(Workflow::getIsPublic, isPublic) + .update(); + } + + public WorkflowResp updateBaseInfo(String wfUuid, String title, String remark, Boolean isPublic) { + if (StringUtils.isAnyBlank(wfUuid, title)) { + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(Workflow::getUuid, wfUuid) + .eq(!ThreadContext.getCurrentUser().getIsAdmin(), Workflow::getUserId, ThreadContext.getCurrentUserId()) + .set(Workflow::getTitle, title) + .set(Workflow::getRemark, remark) + .set(null != isPublic, Workflow::getIsPublic, isPublic) + .update(); + Workflow workflow = getOrThrow(wfUuid); + return changeWorkflowToDTO(workflow); + } + + @Transactional + public WorkflowResp update(WorkflowUpdateReq req) { + Workflow workflow = PrivilegeUtil.checkAndGetByUuid(req.getUuid(), this.query(), ErrorEnum.A_WF_NOT_FOUND); + long workflowId = workflow.getId(); + workflowNodeService.createOrUpdateNodes(workflowId, req.getNodes()); + workflowEdgeService.createOrUpdateEdges(workflowId, req.getEdges()); + Workflow workflow2 = getOrThrow(req.getUuid()); + return changeWorkflowToDTO(workflow2); + } + + public Workflow getByUuid(String uuid) { + return ChainWrappers.lambdaQueryChain(baseMapper) + .eq(Workflow::getUuid, uuid) + .eq(Workflow::getIsDeleted, false) + .last("limit 1") + .one(); + } + + public Workflow getOrThrow(String uuid) { + Workflow workflow = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(Workflow::getUuid, uuid) + .eq(Workflow::getIsDeleted, false) + .last("limit 1") + .one(); + if (null == workflow) { + throw new BaseException(ErrorEnum.A_WF_NOT_FOUND.getInfo()); + } + return workflow; + } + + public Page search(String keyword, Boolean isPublic, Boolean isEnable, Integer currentPage, Integer pageSize) { + User user = ThreadContext.getCurrentUser(); + Page page = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(Workflow::getIsDeleted, false) + .eq(null != isPublic, Workflow::getIsPublic, isPublic) + .eq(null != isEnable, Workflow::getIsEnable, isEnable) + .like(StringUtils.isNotBlank(keyword), Workflow::getTitle, keyword) + .eq(!user.getIsAdmin(), Workflow::getUserId, user.getId()) + .orderByDesc(Workflow::getUpdateTime) + .page(new Page<>(currentPage, pageSize)); + Page result = new Page<>(); + List userIds = new ArrayList<>(); + MPPageUtil.convertToPage(page, result, WorkflowResp.class, (source, target) -> { + fillNodesAndEdges(target); + userIds.add(source.getUserId()); + return target; + }); +// fillUserInfos(userIds, result.getRecords()); + return result; + } + + public Page searchPublic(String keyword, Integer currentPage, Integer pageSize) { + Page page = ChainWrappers.lambdaQueryChain(baseMapper) + .eq(Workflow::getIsDeleted, false) + .eq(Workflow::getIsPublic, true) + .eq(Workflow::getIsEnable, true) + .like(StringUtils.isNotBlank(keyword), Workflow::getTitle, keyword) + .orderByDesc(Workflow::getUpdateTime) + .page(new Page<>(currentPage, pageSize)); + Page result = new Page<>(); + List userIds = new ArrayList<>(); + MPPageUtil.convertToPage(page, result, WorkflowResp.class, (source, target) -> { + fillNodesAndEdges(target); + userIds.add(source.getUserId()); + return target; + }); + // fillUserInfos(userIds, result.getRecords()); + return result; + } + + public void softDelete(String uuid) { + ChainWrappers.lambdaUpdateChain(baseMapper).eq(Workflow::getUuid, uuid) + .set(Workflow::getIsDeleted, true).update(); + } + + public void enable(String uuid, Boolean enable) { + if (null == enable) { + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + Workflow workflow = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_NOT_FOUND); + ChainWrappers.lambdaUpdateChain(baseMapper) + .eq(Workflow::getId, workflow.getId()) + .eq(!ThreadContext.getCurrentUser().getIsAdmin(), Workflow::getUserId, ThreadContext.getCurrentUserId()) + .set(Workflow::getIsEnable, enable) + .update(); + } + + private WorkflowResp changeWorkflowToDTO(Workflow workflow) { + WorkflowResp workflowResp = new WorkflowResp(); + BeanUtils.copyProperties(workflow, workflowResp); + + fillNodesAndEdges(workflowResp); +// User user = userService.getById(workflow.getUserId()); +// if (null != user) { +// workflowResp.setUserUuid(user.getUuid()); +// workflowResp.setUserName(user.getName()); +// } + return workflowResp; + } + + private void fillNodesAndEdges(WorkflowResp workflowResp) { + List nodes = workflowNodeService.listDtoByWfId(workflowResp.getId()); + workflowResp.setNodes(nodes); + List edges = workflowEdgeService.listDtoByWfId(workflowResp.getId()); + workflowResp.setEdges(edges); + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/JsonUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/JsonUtil.java new file mode 100644 index 00000000..975d67bc --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/JsonUtil.java @@ -0,0 +1,152 @@ +package org.ruoyi.workflow.util; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +public class JsonUtil { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.configure(SerializationFeature.INDENT_OUTPUT, Boolean.FALSE); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.registerModules(LocalDateTimeUtil.getSimpleModule(), new JavaTimeModule(), new Jdk8Module()); + } + + public static String toJson(Object obj) { + String resp = null; + try { + resp = objectMapper.writeValueAsString(obj); + } catch (IOException e) { + log.error("JsonUtil error", e); + } + return resp; + } + + + /** + * 创建JSON处理器的静态方法 + * + * @param content JSON字符串 + * @return + */ + private static JsonParser getParser(String content) { + if (StringUtils.isNotBlank(content)) { + try { + return objectMapper.getFactory().createParser(content); + } catch (IOException ioe) { + log.error("JsonUtil getParser error", ioe); + } + } + return null; + } + + /** + * JSON对象反序列化 + */ + public static T fromJson(String json, Class clazz) { + if (StringUtils.isBlank(json)) { + return null; + } + try { + JsonParser jp = getParser(json); + if (null == jp) { + log.error("json parser is null"); + return null; + } + return jp.readValueAs(clazz); + } catch (IOException ioe) { + log.error("反序列化失败", ioe); + } + return null; + } + + public static T fromJson(JsonNode jsonNode, Class clazz) { + try { + return objectMapper.treeToValue(jsonNode, clazz); + } catch (JsonProcessingException e) { + log.error("反序列化失败", e); + } + return null; + } + + + public static List fromArrayNode(ArrayNode arrayNode, Class clazz) { + List result = new ArrayList<>(); + try { + for (JsonNode jsonNode : arrayNode) { + result.add(objectMapper.treeToValue(jsonNode, clazz)); + } + } catch (JsonProcessingException e) { + log.error("反序列化失败", e); + } + return result; + } + + public static JsonNode toJsonNode(String json) { + try { + return objectMapper.readTree(json); + } catch (JsonProcessingException e) { + log.error("反序列化失败", e); + } + return null; + } + + public static Map toMap(String json) { + Map result; + try { + result = objectMapper.readValue(json, Map.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return result; + } + + public static T toBean(String json, Class tClass) { + T result; + try { + result = objectMapper.readValue(json, tClass); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + return result; + } + + public static Map toMap(Object obj) { + try { + return objectMapper.convertValue(obj, new TypeReference>() { + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static JsonNode classToJsonNode(Object obj) { + return objectMapper.valueToTree(obj); + } + + public static ObjectNode createObjectNode() { + return objectMapper.createObjectNode(); + } + +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeDeserializer.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeDeserializer.java new file mode 100644 index 00000000..b6845fb2 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeDeserializer.java @@ -0,0 +1,18 @@ +package org.ruoyi.workflow.util; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeDeserializer extends JsonDeserializer { + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext deserializationContext) + throws IOException { + return LocalDateTime.parse(p.getValueAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeSerializer.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeSerializer.java new file mode 100644 index 00000000..605e5ec5 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeSerializer.java @@ -0,0 +1,18 @@ +package org.ruoyi.workflow.util; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeSerializer extends JsonSerializer { + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeUtil.java new file mode 100644 index 00000000..ade5bddc --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/LocalDateTimeUtil.java @@ -0,0 +1,68 @@ +package org.ruoyi.workflow.util; + +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import org.apache.commons.lang3.StringUtils; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class LocalDateTimeUtil { + + public static final DateTimeFormatter PATTERN_DEFAULT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + public static final DateTimeFormatter PATTERN_YYYYMMDDMMHHSS = DateTimeFormatter.ofPattern("yyyyMMddmmHHss"); + public static final DateTimeFormatter PATTERN_YYYY_MM_DD = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private LocalDateTimeUtil() { + } + + public static SimpleModule getSimpleModule() { + // jackson中自定义处理序列化和反序列化 + SimpleModule customModule = new SimpleModule(); + customModule.addSerializer(Long.class, ToStringSerializer.instance); + // 时间序列化 + customModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer()); + customModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer()); + return customModule; + } + + public static LocalDateTime parse(String localDateTime) { + return LocalDateTime.parse(localDateTime, PATTERN_DEFAULT); + } + + public static LocalDateTime parse(Long epochMilli) { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMilli), ZoneId.systemDefault()); + } + + public static String format(LocalDateTime localDateTime) { + if (null == localDateTime) { + return StringUtils.EMPTY; + } + return localDateTime.format(PATTERN_DEFAULT); + } + + public static String format(LocalDateTime localDateTime, String pattern) { + if (null == localDateTime) { + return StringUtils.EMPTY; + } + return localDateTime.format(DateTimeFormatter.ofPattern(pattern)); + } + + public static String format(LocalDateTime localDateTime, DateTimeFormatter pattern) { + if (null == localDateTime) { + return StringUtils.EMPTY; + } + return localDateTime.format(pattern); + } + + public static int getIntDay(LocalDateTime localDateTime) { + return localDateTime.getYear() * 10000 + localDateTime.getMonthValue() * 100 + localDateTime.getDayOfMonth(); + } + + public static int getToday() { + LocalDateTime now = LocalDateTime.now(); + return now.getYear() * 10000 + now.getMonthValue() * 100 + now.getDayOfMonth(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/MPPageUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/MPPageUtil.java new file mode 100644 index 00000000..fde8b0d3 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/MPPageUtil.java @@ -0,0 +1,86 @@ +package org.ruoyi.workflow.util; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.springframework.beans.BeanUtils; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; + +@Slf4j +public class MPPageUtil { + + private MPPageUtil() { + } + + public static Page convertToPage(Page source, Class targetRecordClass) { + return MPPageUtil.convertToPage(source, new Page<>(), targetRecordClass, null); + } + + public static Page convertToPage(Page source, Page target, Class targetRecordClass) { + return MPPageUtil.convertToPage(source, target, targetRecordClass, null); + } + + public static Page convertToPage(Page source, Page target, Class targetRecordClass, BiFunction biFunction) { + BeanUtils.copyProperties(source, target); + List records = new ArrayList<>(); + target.setRecords(records); + try { + for (T t : source.getRecords()) { + U u = targetRecordClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(t, u); + if (null != biFunction) { + biFunction.apply(t, u); + } + records.add(u); + } + } catch (NoSuchMethodException e1) { + log.error("convertTo error1", e1); + } catch (Exception e2) { + log.error("convertTo error2", e2); + } + + return target; + } + + public static List convertToList(List source, Class targetRecordClass) { + return convertToList(source, targetRecordClass, null); + } + + public static List convertToList(List source, Class targetRecordClass, BiFunction biFunction) { + if (CollectionUtils.isEmpty(source)) { + return Collections.emptyList(); + } + List result = new ArrayList<>(); + for (T t : source) { + try { + U u = targetRecordClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(t, u); + if (null != biFunction) { + biFunction.apply(t, u); + } + result.add(u); + } catch (NoSuchMethodException e1) { + log.error("convertTo error1", e1); + } catch (Exception e2) { + log.error("convertTo error2", e2); + } + } + return result; + } + + public static U convertTo(T source, Class targetClass) { + try { + U target = targetClass.getDeclaredConstructor().newInstance(); + BeanUtils.copyProperties(source, target); + return target; + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { + throw new RuntimeException(e); + } + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/PrivilegeUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/PrivilegeUtil.java new file mode 100644 index 00000000..7ad0dc7a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/PrivilegeUtil.java @@ -0,0 +1,32 @@ +package org.ruoyi.workflow.util; + +import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; +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.*; + +public class PrivilegeUtil { + + private PrivilegeUtil() { + } + + public static T checkAndGetByUuid(String uuid, QueryChainWrapper lambdaQueryChainWrapper, ErrorEnum exceptionMessage) { + return checkAndGet(null, uuid, lambdaQueryChainWrapper, exceptionMessage); + } + + public static T checkAndGet(Long id, String uuid, QueryChainWrapper lambdaQueryChainWrapper, ErrorEnum exceptionMessage) { + T target; + if (Boolean.TRUE.equals(ThreadContext.getCurrentUser().getIsAdmin())) { + target = lambdaQueryChainWrapper.eq(null != id, COLUMN_NAME_ID, id).eq(null != uuid, COLUMN_NAME_UUID, uuid).eq(COLUMN_NAME_IS_DELETE, false).oneOpt().orElse(null); + } else { + target = lambdaQueryChainWrapper.eq(null != id, COLUMN_NAME_ID, id).eq(null != uuid, COLUMN_NAME_UUID, uuid).eq(COLUMN_NAME_USER_ID, ThreadContext.getCurrentUserId()).eq(COLUMN_NAME_IS_DELETE, false).oneOpt().orElse(null); + } + if (null == target) { + throw new BaseException(exceptionMessage.getInfo()); + } + return target; + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/RedisTemplateUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/RedisTemplateUtil.java new file mode 100644 index 00000000..87239a6a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/RedisTemplateUtil.java @@ -0,0 +1,27 @@ +package org.ruoyi.workflow.util; + +import jakarta.annotation.Resource; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class RedisTemplateUtil { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public boolean lock(String key, String clientId, int lockExpireInSecond) { + return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, clientId, lockExpireInSecond, TimeUnit.SECONDS)); + } + + public boolean unlock(String key, String clientId) { + boolean result = false; + if (clientId.equals(stringRedisTemplate.opsForValue().get(key))) { + result = Boolean.TRUE.equals(stringRedisTemplate.delete(key)); + } + return result; + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/SpringUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/SpringUtil.java new file mode 100644 index 00000000..82b9dd6f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/SpringUtil.java @@ -0,0 +1,30 @@ +package org.ruoyi.workflow.util; + +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +@Component +public class SpringUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + public static T getBean(String name, Class clazz) { + return applicationContext.getBean(name, clazz); + } + + public static T getBean(Class clazz) { + return applicationContext.getBean(clazz); + } + + public static String getProperty(String key) { + return applicationContext.getEnvironment().getProperty(key); + } + + @Override + public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException { + SpringUtil.applicationContext = applicationContext; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/UuidUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/UuidUtil.java new file mode 100644 index 00000000..57952e68 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/util/UuidUtil.java @@ -0,0 +1,12 @@ +package org.ruoyi.workflow.util; + +import java.util.UUID; + +public class UuidUtil { + private UuidUtil() { + } + + public static String createShort() { + return UUID.randomUUID().toString().replace("-", ""); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/CompileNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/CompileNode.java new file mode 100644 index 00000000..20960113 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/CompileNode.java @@ -0,0 +1,28 @@ +package org.ruoyi.workflow.workflow; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Builder +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CompileNode { + protected String id; + protected Boolean conditional = false; + + /** + * 以下两种情况会导致多个nextNode出现: + * 1. 下游节点为并行节点,所有的下游节点同时运行 + * 2. 当前节点为条件分支节点,下游节点为多个节点,实际执行时只会执行一条 + * 两种节点根据是否GraphCompileNode来区分 + */ + protected List nextNodes = new ArrayList<>(); +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/GraphCompileNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/GraphCompileNode.java new file mode 100644 index 00000000..19497b89 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/GraphCompileNode.java @@ -0,0 +1,38 @@ +package org.ruoyi.workflow.workflow; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@EqualsAndHashCode(callSuper = true) +@Data +public class GraphCompileNode extends CompileNode { + private CompileNode root; + + public void appendToLeaf(CompileNode node) { + boolean exists = false; + CompileNode tail = root; + while (!tail.getNextNodes().isEmpty()) { + tail = tail.getNextNodes().get(0); + if (tail.getId().equals(node.getId())) { + exists = true; + break; + } + } + if (!exists) { + tail.getNextNodes().add(node); + } + } + + public CompileNode getTail() { + if (root.nextNodes.isEmpty()) { + return root; + } + CompileNode tail = root.nextNodes.get(0); + while (!tail.getNextNodes().isEmpty()) { + tail = tail.getNextNodes().get(0); + } + return tail; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/InterruptedFlow.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/InterruptedFlow.java new file mode 100644 index 00000000..34b0018f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/InterruptedFlow.java @@ -0,0 +1,17 @@ +package org.ruoyi.workflow.workflow; + +import org.apache.commons.collections4.map.PassiveExpiringMap; + +/** + * 已中断正在等待用户输入的流程
+ * TODO 需要考虑项目多节点部署的情况 + */ +public class InterruptedFlow { + + /** + * 10分钟超时 + */ + private static final PassiveExpiringMap.ExpirationPolicy ep = new PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(60 * 1000 * 10); + public static PassiveExpiringMap RUNTIME_TO_GRAPH = new PassiveExpiringMap<>(ep); + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/NodeProcessResult.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/NodeProcessResult.java new file mode 100644 index 00000000..d78e1c4b --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/NodeProcessResult.java @@ -0,0 +1,24 @@ +package org.ruoyi.workflow.workflow; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.workflow.workflow.data.NodeIOData; + +import java.util.ArrayList; +import java.util.List; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class NodeProcessResult { + + private List content = new ArrayList<>(); + + /** + * 条件执行时使用 + */ + private String nextNodeUuid; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfComponentNameEnum.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfComponentNameEnum.java new file mode 100644 index 00000000..5e40a6d4 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfComponentNameEnum.java @@ -0,0 +1,50 @@ +package org.ruoyi.workflow.workflow; + +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum WfComponentNameEnum { + START("Start"), + + END("End"), + + LLM_ANSWER("Answer"), + + DALLE3("Dalle3"), + + TONGYI_WANX("Tongyiwanx"), + + DOCUMENT_EXTRACTOR("DocumentExtractor"), + + KEYWORD_EXTRACTOR("KeywordExtractor"), + + FAQ_EXTRACTOR("FaqExtractor"), + + KNOWLEDGE_RETRIEVER("KnowledgeRetrieval"), + + SWITCHER("Switcher"), + + CLASSIFIER("Classifier"), + + TEMPLATE("Template"), + + GOOGLE_SEARCH("Google"), + + HUMAN_FEEDBACK("HumanFeedback"), + + MAIL_SEND("MailSend"), + + HTTP_REQUEST("HttpRequest"); + + private final String name; + + WfComponentNameEnum(String name) { + this.name = name; + } + + public static WfComponentNameEnum getByName(String name) { + return Arrays.stream(WfComponentNameEnum.values()).filter(item -> item.name.equals(name)).findFirst().orElse(null); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java new file mode 100644 index 00000000..92095f0e --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeFactory.java @@ -0,0 +1,23 @@ +package org.ruoyi.workflow.workflow; + +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.answer.LLMAnswerNode; +import org.ruoyi.workflow.workflow.node.start.StartNode; + +public class WfNodeFactory { + public static AbstractWfNode create(WorkflowComponent wfComponent, WorkflowNode nodeDefinition, + WfState wfState, WfNodeState nodeState) { + AbstractWfNode wfNode = null; + switch (WfComponentNameEnum.getByName(wfComponent.getName())) { + case START -> wfNode = new StartNode(wfComponent, nodeDefinition, wfState, nodeState); + case LLM_ANSWER -> wfNode = new LLMAnswerNode(wfComponent, nodeDefinition, wfState, nodeState); + case END -> wfNode = new EndNode(wfComponent, nodeDefinition, wfState, nodeState); + default -> { + } + } + return wfNode; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeIODataUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeIODataUtil.java new file mode 100644 index 00000000..61828568 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeIODataUtil.java @@ -0,0 +1,132 @@ +package org.ruoyi.workflow.workflow; + +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.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; +import org.ruoyi.workflow.workflow.data.NodeIODataFilesContent; +import org.ruoyi.workflow.workflow.def.*; + +import java.util.*; + +import static org.ruoyi.workflow.cosntant.AdiConstant.IMAGE_EXTENSIONS; +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_INPUT_PARAM_NAME; +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME; + +public class WfNodeIODataUtil { + + public static final Map> INPUT_TYPE_TO_NODE_IO_DEF = new HashMap<>(); + + static { + INPUT_TYPE_TO_NODE_IO_DEF.put(WfIODataTypeEnum.TEXT, WfNodeIOText.class); + INPUT_TYPE_TO_NODE_IO_DEF.put(WfIODataTypeEnum.BOOL, WfNodeIOBool.class); + INPUT_TYPE_TO_NODE_IO_DEF.put(WfIODataTypeEnum.NUMBER, WfNodeIONumber.class); + INPUT_TYPE_TO_NODE_IO_DEF.put(WfIODataTypeEnum.OPTIONS, WfNodeIOOptions.class); + INPUT_TYPE_TO_NODE_IO_DEF.put(WfIODataTypeEnum.FILES, WfNodeIOFiles.class); + + } + + public static NodeIOData createNodeIOData(ObjectNode data) { + JsonNode nameObj = data.get("name"); + JsonNode content = data.get("content"); + if (null == nameObj || null == content) { + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + String name = nameObj.asText(); + Integer type = content.get("type").asInt(); + String title = content.get("title").asText(); + JsonNode value = content.get("value"); + NodeIOData result = null; + if (WfIODataTypeEnum.TEXT.getValue().equals(type)) { + result = NodeIOData.createByText(name, title, value.asText()); + } else if (WfIODataTypeEnum.NUMBER.getValue().equals(type)) { + result = NodeIOData.createByNumber(name, title, value.asDouble()); + } else if (WfIODataTypeEnum.BOOL.getValue().equals(type)) { + result = NodeIOData.createByBool(name, title, value.asBoolean()); + } else if (WfIODataTypeEnum.FILES.getValue().equals(type)) { + if (value.isArray()) { + List fileUrls = new ArrayList<>(); + Iterator iterator = value.elements(); + while (iterator.hasNext()) { + fileUrls.add(iterator.next().asText()); + } + result = NodeIOData.createByFiles(name, title, fileUrls); + } + } else if (WfIODataTypeEnum.OPTIONS.getValue().equals(type)) { + if (value instanceof ObjectNode) { + result = NodeIOData.createByOptions(name, title, JsonUtil.toMap(value)); + } + } + return result; + } + + /** + * 1.如果没有名称为 output 的输出参数,则需要新增
+ * 2.判断是否已经有文本类型的输出参数,如果有,则复制该参数并将参数名改为 output
+ * 3.如果没有文本类型的参数,则复制第一个参数,并将参数名改为 output + * + * @param inputs 输入参数列表 + * @return 输出参数列表 + */ + public static List changeInputsToOutputs(List inputs) { + if (CollectionUtils.isEmpty(inputs)) { + return new ArrayList<>(); + } + List result = CollUtil.newCopyOnWriteArrayList(inputs); + + boolean outputExist = false; + NodeIOData defaultInputName = null, txtExist = null, first = null; + for (NodeIOData nodeIOData : result) { + if (null == first) { + first = nodeIOData; + } + if (DEFAULT_OUTPUT_PARAM_NAME.equals(nodeIOData.getName())) { + outputExist = true; + } else if (DEFAULT_INPUT_PARAM_NAME.equals(nodeIOData.getName())) { + defaultInputName = nodeIOData; + } else if (null == txtExist && WfIODataTypeEnum.TEXT.getValue().equals(nodeIOData.getContent().getType())) { + txtExist = nodeIOData; + } + } + + if (outputExist) { + return result; + } + if (null != defaultInputName) { + defaultInputName.setName(DEFAULT_OUTPUT_PARAM_NAME); + } else if (null != txtExist) { + txtExist.setName(DEFAULT_OUTPUT_PARAM_NAME); + } else if (null != first) { + first.setName(DEFAULT_OUTPUT_PARAM_NAME); + } + result.add(inputs.get(0)); + return result; + } + + /** + * 将输入输出中的文件url转成markdown格式的文件地址
+ * 将变量渲染到模板时使用该方法,其他情况交由前端处理 + * + * @param ioDataList 输入输出列表 + */ + public static void changeFilesContentToMarkdown(List ioDataList) { + ioDataList.forEach(input -> { + if (input.getContent() instanceof NodeIODataFilesContent filesContent) { + List newValues = new ArrayList<>(); + for (String s : filesContent.getValue()) { + if (IMAGE_EXTENSIONS.contains(s.substring(s.lastIndexOf(".") + 1))) { + newValues.add("![" + filesContent.getTitle() + "](" + s + ")"); + } else { + newValues.add("[" + filesContent.getTitle() + "](" + s + ")"); + } + } + filesContent.setValue(newValues); + } + }); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeInputConfig.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeInputConfig.java new file mode 100644 index 00000000..8e66a507 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeInputConfig.java @@ -0,0 +1,26 @@ +package org.ruoyi.workflow.workflow; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.ruoyi.workflow.workflow.def.WfNodeIO; +import org.ruoyi.workflow.workflow.def.WfNodeParamRef; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +/** + * 节点的输入参数配置 + */ +@Validated +@Data +public class WfNodeInputConfig { + + @NotNull + @JsonProperty("user_inputs") + private List userInputs; + + @NotNull + @JsonProperty("ref_inputs") + private List refInputs; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeState.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeState.java new file mode 100644 index 00000000..650a087c --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfNodeState.java @@ -0,0 +1,53 @@ +package org.ruoyi.workflow.workflow; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.bsc.langgraph4j.state.AgentState; +import org.ruoyi.workflow.util.UuidUtil; +import org.ruoyi.workflow.workflow.data.NodeIOData; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_INPUT_PARAM_NAME; +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.NODE_PROCESS_STATUS_READY; + +/** + * 工作流节点实例状态 | workflow node instance state + */ +@Setter +@Getter +@ToString(callSuper = true) +public class WfNodeState extends AgentState implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String uuid = UuidUtil.createShort(); + private int processStatus = NODE_PROCESS_STATUS_READY; + private String processStatusRemark = ""; + private List inputs = new ArrayList<>(); + private List outputs = new ArrayList<>(); + + /** + * Constructs an AgentState with the given initial data. + * + * @param initData the initial data for the agent state + */ + public WfNodeState(Map initData) { + super(initData); + } + + public WfNodeState() { + super(Map.of()); + } + + public Optional getDefaultInput() { + return inputs.stream().filter(item -> DEFAULT_INPUT_PARAM_NAME.equals(item.getName())).findFirst(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfState.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfState.java new file mode 100644 index 00000000..0988d36c --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WfState.java @@ -0,0 +1,131 @@ +package org.ruoyi.workflow.workflow; + +import lombok.Getter; +import lombok.Setter; +import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator; +import org.bsc.langgraph4j.state.AgentState; +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; + +import java.util.*; + +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.WORKFLOW_PROCESS_STATUS_READY; + +/** + * 工作流实例状态 | workflow instance state + */ +@Setter +@Getter +public class WfState { + + private String uuid; + private User user; + private String processingNodeUuid; + + //Source node uuid => target node uuid list + private Map> edges = new HashMap<>(); + private Map> conditionalEdges = new HashMap<>(); + + //Source node uuid => streaming chat generator + private Map> nodeToStreamingGenerator = new HashMap<>(); + + /** + * 已运行节点列表 + */ + private List completedNodes = new LinkedList<>(); + + private List runtimeNodes = new ArrayList<>(); + + /** + * 工作流接收到的输入(也是开始节点的输入参数) + */ + private List input; + + /** + * 工作流执行结束后的输出 + */ + private List output = new ArrayList<>(); + private Integer processStatus = WORKFLOW_PROCESS_STATUS_READY; + + /** + * 人机交互节点 + */ + private Set interruptNodes = new HashSet<>(); + + public WfState(User user, List input, String uuid) { + this.input = input; + this.user = user; + this.uuid = uuid; + } + + /** + * 获取最新的输出结果 + * + * @return 参数列表 + */ + public List getLatestOutputs() { + WfNodeState upstreamState = completedNodes.get(completedNodes.size() - 1).getState(); + return upstreamState.getOutputs(); + } + + public Optional getNodeStateByNodeUuid(String nodeUuid) { + return this.completedNodes.stream().filter(item -> item.getNode().getUuid().equals(nodeUuid)).map(AbstractWfNode::getState).findFirst(); + } + + /** + * 新增一条边 + * 并行执行分支的情况下会出现一个 source node 对应多个 target node + * + * @param sourceNodeUuid 开始节点 + * @param targetNodeUuid 目标节点 + */ + public void addEdge(String sourceNodeUuid, String targetNodeUuid) { + List targetNodeUuids = edges.computeIfAbsent(sourceNodeUuid, k -> new ArrayList<>()); + targetNodeUuids.add(targetNodeUuid); + } + + /** + * 新增一条边 + * 按条件执行的分支会出现一个 source node 对应多个 target node 的情况 + * + * @param sourceNodeUuid 开始节点 + * @param targetNodeUuid 目标节点 + */ + public void addConditionalEdge(String sourceNodeUuid, String targetNodeUuid) { + List targetNodeUuids = conditionalEdges.computeIfAbsent(sourceNodeUuid, k -> new ArrayList<>()); + targetNodeUuids.add(targetNodeUuid); + } + + public List getIOByNodeUuid(String nodeUuid) { + List result = new ArrayList<>(); + Optional optional = completedNodes.stream().filter(node -> nodeUuid.equals(node.getNode().getUuid())).findFirst(); + if (optional.isEmpty()) { + return result; + } + result.addAll(optional.get().getState().getInputs()); + result.addAll(optional.get().getState().getOutputs()); + return result; + } + + public WfRuntimeNodeDto getRuntimeNodeByNodeUuid(String wfNodeUuid) { + WorkflowNode wfNode = getCompletedNodes().stream() + .map(AbstractWfNode::getNode) + .filter(node -> node.getUuid().equals(wfNodeUuid)) + .findFirst() + .orElse(null); + if (null == wfNode) { + return null; + } + return getRuntimeNodes().stream() + .filter(item -> item.getNodeId().equals(wfNode.getId())) + .findFirst() + .orElse(null); + } + + public void addInterruptNode(String nodeUuid) { + this.interruptNodes.add(nodeUuid); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java new file mode 100644 index 00000000..55d10a17 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowEngine.java @@ -0,0 +1,356 @@ +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.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.bsc.async.AsyncGenerator; +import org.bsc.langgraph4j.*; +import org.bsc.langgraph4j.checkpoint.MemorySaver; +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.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.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.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.*; +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.*; + +@Slf4j +public class WorkflowEngine { + private final Workflow workflow; + private final List components; + private final List wfNodes; + private final List wfEdges; + private final SSEEmitterHelper sseEmitterHelper; + private final WorkflowRuntimeService workflowRuntimeService; + private final WorkflowRuntimeNodeService workflowRuntimeNodeService; + private CompiledGraph app; + private SseEmitter sseEmitter; + private User user; + private WfState wfState; + private WfRuntimeResp wfRuntimeResp; + + public WorkflowEngine( + Workflow workflow, + SSEEmitterHelper sseEmitterHelper, + List components, + List nodes, + List wfEdges, + WorkflowRuntimeService workflowRuntimeService, + WorkflowRuntimeNodeService workflowRuntimeNodeService) { + this.workflow = workflow; + this.sseEmitterHelper = sseEmitterHelper; + this.components = components; + this.wfNodes = nodes; + this.wfEdges = wfEdges; + this.workflowRuntimeService = workflowRuntimeService; + this.workflowRuntimeNodeService = workflowRuntimeNodeService; + } + + public void run(User user, List userInputs, SseEmitter sseEmitter) { + this.user = user; + this.sseEmitter = sseEmitter; + log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); + if (!this.workflow.getIsEnable()) { + sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, ErrorEnum.A_WF_DISABLED.getInfo()); + throw new BaseException(ErrorEnum.A_WF_DISABLED.getInfo()); + } + + Long workflowId = this.workflow.getId(); + this.wfRuntimeResp = workflowRuntimeService.create(user, workflowId); + this.sseEmitterHelper.startSse(user, sseEmitter, JsonUtil.toJson(wfRuntimeResp)); + + String runtimeUuid = this.wfRuntimeResp.getUuid(); + try { + Pair> startAndEnds = findStartAndEndNode(); + WorkflowNode startNode = startAndEnds.getLeft(); + List wfInputs = getAndCheckUserInput(userInputs, startNode); + this.wfState = new WfState(user, wfInputs, runtimeUuid); + workflowRuntimeService.updateInput(this.wfRuntimeResp.getId(), wfState); + + + WorkflowGraphBuilder graphBuilder = new WorkflowGraphBuilder( + components, + wfNodes, + wfEdges, + this::runNode, + this.wfState); + StateGraph mainStateGraph = graphBuilder.build(startNode); + + MemorySaver saver = new MemorySaver(); + CompileConfig compileConfig = CompileConfig.builder().checkpointSaver(saver) + .interruptBefore(wfState.getInterruptNodes().toArray(String[]::new)) + .build(); + app = mainStateGraph.compile(compileConfig); + RunnableConfig invokeConfig = RunnableConfig.builder().build(); + exe(invokeConfig, false); + } catch (Exception e) { + errorWhenExe(e); + } + } + + private void exe(RunnableConfig invokeConfig, boolean resume) { + //不使用langgraph4j state的update相关方法,无需传入input + AsyncGenerator> outputs = app.stream(resume ? null : Map.of(), invokeConfig); + streamingResult(wfState, outputs, sseEmitter); + + StateSnapshot stateSnapshot = app.getState(invokeConfig); + String nextNode = stateSnapshot.config().nextNode().orElse(""); + //还有下个节点,表示进入中断状态,等待用户输入后继续执�? + if (StringUtils.isNotBlank(nextNode) && !nextNode.equalsIgnoreCase(END)) { + String intTip = WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes); + //将等待输入信息[事件与提示词]发送到到客户端 + SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_WAIT_FEEDBACK_BY_" + nextNode + "]", 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); + sseEmitterHelper.sendComplete(user.getId(), sseEmitter, updatedRuntime.getOutput()); + InterruptedFlow.RUNTIME_TO_GRAPH.remove(wfState.getUuid()); + } + } + + /** + * 中断流程等待用户输入时,会进行暂停状态,用户输入后调用本方法执行流程剩余部分 + * + * @param userInput 用户输入 + */ + public void resume(String userInput) { + RunnableConfig invokeConfig = RunnableConfig.builder().build(); + try { + app.updateState(invokeConfig, Map.of(HUMAN_FEEDBACK_KEY, userInput), null); + exe(invokeConfig, true); + } catch (Exception e) { + errorWhenExe(e); + } finally { + //有可能多次接收人机交互,待整个流程完全执行后才能删除 + if (wfState.getProcessStatus() != WORKFLOW_PROCESS_STATUS_WAITING_INPUT) { + InterruptedFlow.RUNTIME_TO_GRAPH.remove(wfState.getUuid()); + } + } + } + + private void errorWhenExe(Exception e) { + log.error("error", e); + String errorMsg = e.getMessage(); + if (errorMsg.contains("parallel node doesn't support conditional branch")) { + errorMsg = "并行节点中不能包含条件分�?"; + } + sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg); + workflowRuntimeService.updateStatus(wfRuntimeResp.getId(), WORKFLOW_PROCESS_STATUS_FAIL, errorMsg); + } + + private Map runNode(WorkflowNode wfNode, WfNodeState nodeState) { + Map resultMap = new HashMap<>(); + try { + WorkflowComponent wfComponent = components.stream().filter(item -> item.getId().equals(wfNode.getWorkflowComponentId())).findFirst().orElseThrow(); + AbstractWfNode abstractWfNode = WfNodeFactory.create(wfComponent, wfNode, wfState, nodeState); + //节点实例 + WfRuntimeNodeDto runtimeNodeDto = workflowRuntimeNodeService.createByState(user, wfNode.getId(), wfRuntimeResp.getId(), nodeState); + wfState.getRuntimeNodes().add(runtimeNodeDto); + + SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_RUN_" + wfNode.getUuid() + "]", JsonUtil.toJson(runtimeNodeDto)); + + NodeProcessResult processResult = abstractWfNode.process((is) -> { + workflowRuntimeNodeService.updateInput(runtimeNodeDto.getId(), nodeState); + List nodeIODataList = nodeState.getInputs(); +// if (!wfNode.getWorkflowComponentId().equals(1L)) { +// String inputConfig = wfNode.getInputConfig(); +// WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(inputConfig); +// List refInputs = nodeInputConfig.getRefInputs(); +// Set nameSet = CollStreamUtil.toSet(refInputs, WfNodeParamRef::getNodeParamName); +// if (CollUtil.isNotEmpty(nameSet)) { +// nodeIODataList = nodeIODataList.stream().filter(item -> nameSet.contains(item.getName())) +// .collect(Collectors.toList()); +// } else { +// nodeIODataList = nodeIODataList.stream().filter(item -> item.getName().contains("input")) +// .collect(Collectors.toList()); +// } +// } + for (NodeIOData input : nodeIODataList) { + String inputConfig = wfNode.getInputConfig(); + WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(inputConfig); + List refInputs = nodeInputConfig.getRefInputs(); + if (CollUtil.isNotEmpty(refInputs) && "input".equals(input.getName())) { + continue; + } + SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_INPUT_" + wfNode.getUuid() + "]", JsonUtil.toJson(input)); + } + }, (is) -> { + workflowRuntimeNodeService.updateOutput(runtimeNodeDto.getId(), nodeState); + //并行节点内部的节点执行结束后,需要主动向客户端发送输出结�? + String nodeUuid = wfNode.getUuid(); + List nodeOutputs = nodeState.getOutputs(); + for (NodeIOData output : nodeOutputs) { + log.info("callback node:{},output:{}", nodeUuid, output.getContent()); + SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_OUTPUT_" + nodeUuid + "]", JsonUtil.toJson(output)); + } + }); + if (StringUtils.isNotBlank(processResult.getNextNodeUuid())) { + resultMap.put("next", processResult.getNextNodeUuid()); + } + } catch (Exception e) { + log.error("Node run error", e); + throw new BaseException(ErrorEnum.B_WF_RUN_ERROR.getInfo()); + } + resultMap.put("name", wfNode.getTitle()); + //langgraph4j state中的data不做数据存储,只存储元数�? + StreamingChatGenerator generator = wfState.getNodeToStreamingGenerator().get(wfNode.getUuid()); + if (null != generator) { + resultMap.put("_streaming_messages", generator); + return resultMap; + } + return resultMap; + } + + /** + * 流式输出结果 + * + * @param outputs 输出 + * @param sseEmitter sse emitter + */ + private void streamingResult(WfState wfState, AsyncGenerator> outputs, SseEmitter sseEmitter) { + for (NodeOutput out : outputs) { + if (out instanceof StreamingOutput streamingOutput) { + String node = streamingOutput.node(); + String chunk = streamingOutput.chunk(); + log.info("node:{},chunk:{}", node, chunk); + 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() + .filter(item -> item.getNode().getUuid().endsWith(out.node())).findFirst().orElse(null); + if (null != abstractWfNode) { + WfRuntimeNodeDto runtimeNodeDto = wfState.getRuntimeNodeByNodeUuid(out.node()); + if (null != runtimeNodeDto) { + workflowRuntimeNodeService.updateOutput(runtimeNodeDto.getId(), abstractWfNode.getState()); + wfState.setOutput(abstractWfNode.getState().getOutputs()); + } else { + log.warn("Can not find runtime node, node uuid:{}", out.node()); + } + } else { + log.warn("Can not find node state,node uuid:{}", out.node()); + } + } + } + } + + /** + * 校验用户输入并组装成工作流的输入 + * + * @param userInputs 用户输入 + * @param startNode 开始节点定�? + * @return 正确的用户输入列�? + */ + private List getAndCheckUserInput(List userInputs, WorkflowNode startNode) { + WfNodeInputConfig wfNodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(startNode.getInputConfig()); + List defList = wfNodeInputConfig.getUserInputs(); + defList = CollStreamUtil.toList(defList, Function.identity()); + List wfInputs = new ArrayList<>(); + for (WfNodeIO paramDefinition : defList) { + String paramNameFromDef = paramDefinition.getName(); + boolean requiredParamMissing = paramDefinition.getRequired(); + for (ObjectNode userInput : userInputs) { + NodeIOData nodeIOData = WfNodeIODataUtil.createNodeIOData(userInput); + if (!paramNameFromDef.equalsIgnoreCase(nodeIOData.getName())) { + continue; + } + Integer dataType = nodeIOData.getContent().getType(); + if (null == dataType) { + throw new BaseException(A_WF_INPUT_INVALID.getInfo()); + } + requiredParamMissing = false; + boolean valid = paramDefinition.checkValue(nodeIOData); + if (!valid) { + log.error("用户输入无效,workflowId:{}", startNode.getWorkflowId()); + throw new BaseException(ErrorEnum.A_WF_INPUT_INVALID.getInfo()); + } + wfInputs.add(nodeIOData); + } + if (requiredParamMissing) { + log.error("在流程定义中必填的参数没有传进来,name:{}", paramNameFromDef); + throw new BaseException(A_WF_INPUT_MISSING.getInfo()); + } + } + return wfInputs; + } + + /** + * 查找开始及结束节点
+ * 开始节点只能有一个,结束节点可能多个 + * + * @return 开始节点及结束节点列表 + */ + public Pair> findStartAndEndNode() { + WorkflowNode startNode = null; + Set endNodes = new HashSet<>(); + for (WorkflowNode node : wfNodes) { + Optional wfComponent = components.stream().filter(item -> item.getId().equals(node.getWorkflowComponentId())).findFirst(); + if (wfComponent.isPresent() && WfComponentNameEnum.START.getName().equals(wfComponent.get().getName())) { + if (null != startNode) { + throw new BaseException(ErrorEnum.A_WF_MULTIPLE_START_NODE.getInfo()); + } + startNode = node; + } else if (wfComponent.isPresent() && WfComponentNameEnum.END.getName().equals(wfComponent.get().getName())) { + endNodes.add(node); + } + } + if (null == startNode) { + log.error("没有开始节点, workflowId:{}", wfNodes.get(0).getWorkflowId()); + throw new BaseException(ErrorEnum.A_WF_START_NODE_NOT_FOUND.getInfo()); + } + //Find all end nodes + wfNodes.forEach(item -> { + String nodeUuid = item.getUuid(); + boolean source = false; + boolean target = false; + for (WorkflowEdge edgeDef : wfEdges) { + if (edgeDef.getSourceNodeUuid().equals(nodeUuid)) { + source = true; + } else if (edgeDef.getTargetNodeUuid().equals(nodeUuid)) { + target = true; + } + } + if (!source && target) { + endNodes.add(item); + } + }); + log.info("start node:{}", startNode); + log.info("end nodes:{}", endNodes); + if (endNodes.isEmpty()) { + log.error("没有结束节点,workflowId:{}", startNode.getWorkflowId()); + throw new BaseException(A_WF_END_NODE_NOT_FOUND.getInfo()); + } + return Pair.of(startNode, endNodes); + } + + + public CompiledGraph getApp() { + return app; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowGraphBuilder.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowGraphBuilder.java new file mode 100644 index 00000000..aa11b4b5 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowGraphBuilder.java @@ -0,0 +1,257 @@ +package org.ruoyi.workflow.workflow; + +import lombok.extern.slf4j.Slf4j; +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.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; +import java.util.stream.Collectors; + +import static org.bsc.langgraph4j.StateGraph.END; +import static org.bsc.langgraph4j.StateGraph.START; +import static org.bsc.langgraph4j.action.AsyncEdgeAction.edge_async; +import static org.bsc.langgraph4j.action.AsyncNodeAction.node_async; +import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK; + +/** + * 负责构建工作流运行所依赖的状态图�? + */ +@Slf4j +public class WorkflowGraphBuilder { + + private final Map componentIndex; + private final Map nodeIndex; + private final Map> edgesBySource; + private final Map> edgesByTarget; + private final WorkflowNodeRunner nodeRunner; + private final WfState wfState; + + private final ObjectStreamStateSerializer stateSerializer = new ObjectStreamStateSerializer<>(WfNodeState::new); + private final Map>> stateGraphNodes = new HashMap<>(); + private final Map>> stateGraphEdges = new HashMap<>(); + private final Map rootToSubGraph = new HashMap<>(); + private final Map nodeToParallelBranch = new HashMap<>(); + + public WorkflowGraphBuilder( + List components, + List nodes, + List edges, + WorkflowNodeRunner nodeRunner, + WfState wfState) { + this.componentIndex = components.stream() + .collect(Collectors.toMap(WorkflowComponent::getId, Function.identity(), (origin, ignore) -> origin)); + this.nodeIndex = nodes.stream() + .collect(Collectors.toMap(WorkflowNode::getUuid, Function.identity(), (origin, ignore) -> origin)); + this.edgesBySource = edges.stream().collect(Collectors.groupingBy(WorkflowEdge::getSourceNodeUuid)); + this.edgesByTarget = edges.stream().collect(Collectors.groupingBy(WorkflowEdge::getTargetNodeUuid)); + this.nodeRunner = nodeRunner; + this.wfState = wfState; + } + + public StateGraph build(WorkflowNode startNode) throws GraphStateException { + CompileNode rootCompileNode = new CompileNode(); + rootCompileNode.setId(startNode.getUuid()); + buildCompileNode(rootCompileNode, startNode); + + StateGraph mainStateGraph = new StateGraph<>(stateSerializer); + wfState.addEdge(START, startNode.getUuid()); + buildStateGraph(null, mainStateGraph, rootCompileNode); + return mainStateGraph; + } + + private void buildCompileNode(CompileNode parentNode, WorkflowNode node) { + log.info("buildCompileNode, parentNode:{}, node:{}, title:{}", parentNode.getId(), node.getUuid(), node.getTitle()); + CompileNode newNode; + List upstreamNodeUuids = getUpstreamNodeUuids(node.getUuid()); + if (upstreamNodeUuids.isEmpty()) { + log.error("节点{}没有上游节点", node.getUuid()); + newNode = parentNode; + } else if (upstreamNodeUuids.size() == 1) { + String upstreamUuid = upstreamNodeUuids.get(0); + boolean pointToParallel = pointToParallelBranch(upstreamUuid); + if (pointToParallel) { + String rootId = node.getUuid(); + GraphCompileNode graphCompileNode = getOrCreateGraphCompileNode(rootId); + appendToNextNodes(parentNode, graphCompileNode); + newNode = graphCompileNode; + } else if (parentNode instanceof GraphCompileNode graphCompileNode) { + newNode = CompileNode.builder().id(node.getUuid()).conditional(false).nextNodes(new ArrayList<>()).build(); + graphCompileNode.appendToLeaf(newNode); + } else { + newNode = CompileNode.builder().id(node.getUuid()).conditional(false).nextNodes(new ArrayList<>()).build(); + appendToNextNodes(parentNode, newNode); + } + } else { + newNode = CompileNode.builder().id(node.getUuid()).conditional(false).nextNodes(new ArrayList<>()).build(); + GraphCompileNode parallelBranch = nodeToParallelBranch.get(parentNode.getId()); + appendToNextNodes(Objects.requireNonNullElse(parallelBranch, parentNode), newNode); + } + + if (newNode == null) { + log.error("节点:{}不存�?", node.getUuid()); + return; + } + for (String downstream : getDownstreamNodeUuids(node.getUuid())) { + WorkflowNode downstreamNode = nodeIndex.get(downstream); + if (downstreamNode != null) { + buildCompileNode(newNode, downstreamNode); + } + } + } + + private void buildStateGraph(CompileNode upstreamCompileNode, + StateGraph stateGraph, + CompileNode compileNode) throws GraphStateException { + log.info("buildStateGraph, upstream:{}, node:{}", upstreamCompileNode, compileNode.getId()); + String stateGraphNodeUuid = compileNode.getId(); + if (upstreamCompileNode == null) { + addNodeToStateGraph(stateGraph, stateGraphNodeUuid); + addEdgeToStateGraph(stateGraph, START, compileNode.getId()); + } else { + if (compileNode instanceof GraphCompileNode graphCompileNode) { + String stateGraphId = graphCompileNode.getId(); + CompileNode root = graphCompileNode.getRoot(); + String rootId = root.getId(); + String existSubGraphId = rootToSubGraph.get(rootId); + + if (StringUtils.isBlank(existSubGraphId)) { + StateGraph subgraph = new StateGraph<>(stateSerializer); + addNodeToStateGraph(subgraph, rootId); + addEdgeToStateGraph(subgraph, START, rootId); + for (CompileNode child : root.getNextNodes()) { + buildStateGraph(root, subgraph, child); + } + addEdgeToStateGraph(subgraph, graphCompileNode.getTail().getId(), END); + stateGraph.addNode(stateGraphId, subgraph.compile()); + rootToSubGraph.put(rootId, stateGraphId); + stateGraphNodeUuid = stateGraphId; + } else { + stateGraphNodeUuid = existSubGraphId; + } + } else { + addNodeToStateGraph(stateGraph, stateGraphNodeUuid); + } + + if (Boolean.FALSE.equals(upstreamCompileNode.getConditional())) { + addEdgeToStateGraph(stateGraph, upstreamCompileNode.getId(), stateGraphNodeUuid); + } + } + + List nextNodes = compileNode.getNextNodes(); + if (nextNodes.size() > 1) { + boolean conditional = nextNodes.stream().noneMatch(item -> item instanceof GraphCompileNode); + compileNode.setConditional(conditional); + for (CompileNode nextNode : nextNodes) { + buildStateGraph(compileNode, stateGraph, nextNode); + } + if (conditional) { + List targets = nextNodes.stream().map(CompileNode::getId).toList(); + Map mappings = new HashMap<>(); + for (String target : targets) { + mappings.put(target, target); + } + stateGraph.addConditionalEdges( + stateGraphNodeUuid, + edge_async(state -> state.data().get("next").toString()), + mappings + ); + } + } else if (nextNodes.size() == 1) { + for (CompileNode nextNode : nextNodes) { + buildStateGraph(compileNode, stateGraph, nextNode); + } + } else { + addEdgeToStateGraph(stateGraph, stateGraphNodeUuid, END); + } + } + + private GraphCompileNode getOrCreateGraphCompileNode(String rootId) { + GraphCompileNode exist = nodeToParallelBranch.get(rootId); + if (exist == null) { + GraphCompileNode graphCompileNode = new GraphCompileNode(); + graphCompileNode.setId("parallel_" + rootId); + graphCompileNode.setRoot(CompileNode.builder().id(rootId).conditional(false).nextNodes(new ArrayList<>()).build()); + nodeToParallelBranch.put(rootId, graphCompileNode); + exist = graphCompileNode; + } + return exist; + } + + private List getUpstreamNodeUuids(String nodeUuid) { + return edgesByTarget.getOrDefault(nodeUuid, List.of()) + .stream() + .map(WorkflowEdge::getSourceNodeUuid) + .toList(); + } + + private List getDownstreamNodeUuids(String nodeUuid) { + return edgesBySource.getOrDefault(nodeUuid, List.of()) + .stream() + .map(WorkflowEdge::getTargetNodeUuid) + .toList(); + } + + private boolean pointToParallelBranch(String nodeUuid) { + return edgesBySource.getOrDefault(nodeUuid, List.of()) + .stream() + .filter(edge -> StringUtils.isBlank(edge.getSourceHandle())) + .count() > 1; + } + + private void addNodeToStateGraph(StateGraph stateGraph, String stateGraphNodeUuid) throws GraphStateException { + List> stateGraphList = stateGraphNodes.computeIfAbsent(stateGraphNodeUuid, k -> new ArrayList<>()); + boolean exist = stateGraphList.stream().anyMatch(item -> item == stateGraph); + if (exist) { + log.info("state graph node exist,stateGraphNodeUuid:{}", stateGraphNodeUuid); + return; + } + log.info("addNodeToStateGraph,node uuid:{}", stateGraphNodeUuid); + WorkflowNode wfNode = getNodeByUuid(stateGraphNodeUuid); + stateGraph.addNode(stateGraphNodeUuid, node_async(state -> nodeRunner.run(wfNode, state))); + stateGraphList.add(stateGraph); + + WorkflowComponent component = componentIndex.get(wfNode.getWorkflowComponentId()); + if (component == null) { + throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo()); + } + if (HUMAN_FEEDBACK.getName().equals(component.getName())) { + wfState.addInterruptNode(stateGraphNodeUuid); + } + } + + private void addEdgeToStateGraph(StateGraph stateGraph, String source, String target) throws GraphStateException { + String key = source + "_" + target; + List> stateGraphList = stateGraphEdges.computeIfAbsent(key, k -> new ArrayList<>()); + boolean exist = stateGraphList.stream().anyMatch(item -> item == stateGraph); + if (exist) { + log.info("state graph edge exist,source:{},target:{}", source, target); + return; + } + log.info("addEdgeToStateGraph,source:{},target:{}", source, target); + stateGraph.addEdge(source, target); + stateGraphList.add(stateGraph); + } + + private WorkflowNode getNodeByUuid(String nodeUuid) { + WorkflowNode workflowNode = nodeIndex.get(nodeUuid); + if (workflowNode == null) { + throw new BaseException(ErrorEnum.A_WF_NODE_NOT_FOUND.getInfo()); + } + return workflowNode; + } + + private void appendToNextNodes(CompileNode compileNode, CompileNode newNode) { + boolean exist = compileNode.getNextNodes().stream().anyMatch(item -> item.getId().equals(newNode.getId())); + if (!exist) { + compileNode.getNextNodes().add(newNode); + } + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowNodeRunner.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowNodeRunner.java new file mode 100644 index 00000000..324f344a --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowNodeRunner.java @@ -0,0 +1,14 @@ +package org.ruoyi.workflow.workflow; + +import org.ruoyi.workflow.entity.WorkflowNode; + +import java.util.Map; + +/** + * 回调接口,负责执行业务节点并返回下游编排所需的元数据。 + */ +@FunctionalInterface +public interface WorkflowNodeRunner { + + Map run(WorkflowNode node, WfNodeState nodeState); +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java new file mode 100644 index 00000000..69e036fb --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowStarter.java @@ -0,0 +1,95 @@ +package org.ruoyi.workflow.workflow; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.common.core.exception.base.BaseException; +import org.ruoyi.workflow.entity.*; +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.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.List; + +import static org.ruoyi.workflow.cosntant.AdiConstant.SSE_TIMEOUT; +import static org.ruoyi.workflow.enums.ErrorEnum.*; + +@Slf4j +@Component +public class WorkflowStarter { + + @Lazy + @Resource + private WorkflowStarter self; + + @Resource + private WorkflowService workflowService; + + @Resource + private WorkflowNodeService workflowNodeService; + + @Resource + private WorkflowEdgeService workflowEdgeService; + + @Resource + private WorkflowComponentService workflowComponentService; + + @Resource + private WorkflowRuntimeService workflowRuntimeService; + + @Resource + private WorkflowRuntimeNodeService workflowRuntimeNodeService; + + @Resource + private SSEEmitterHelper sseEmitterHelper; + + + public SseEmitter streaming(User user, String workflowUuid, List userInputs) { + SseEmitter sseEmitter = new SseEmitter(SSE_TIMEOUT); + if (!sseEmitterHelper.checkOrComplete(user, sseEmitter)) { + return sseEmitter; + } + Workflow workflow = workflowService.getByUuid(workflowUuid); + if (null == workflow) { + sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_NOT_FOUND.getInfo()); + return sseEmitter; + } else if (Boolean.FALSE.equals(workflow.getIsEnable())) { + sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, A_WF_DISABLED.getInfo()); + return sseEmitter; + } + self.asyncRun(user, workflow, userInputs, sseEmitter); + return sseEmitter; + } + + @Async + public void asyncRun(User user, Workflow workflow, List userInputs, SseEmitter sseEmitter) { + log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs); + List components = workflowComponentService.getAllEnable(); + List nodes = workflowNodeService.lambdaQuery() + .eq(WorkflowNode::getWorkflowId, workflow.getId()) + .eq(WorkflowNode::getIsDeleted, false) + .list(); + List edges = workflowEdgeService.lambdaQuery() + .eq(WorkflowEdge::getWorkflowId, workflow.getId()) + .eq(WorkflowEdge::getIsDeleted, false) + .list(); + WorkflowEngine workflowEngine = new WorkflowEngine(workflow, + sseEmitterHelper, components, nodes, edges, + workflowRuntimeService, workflowRuntimeNodeService); + workflowEngine.run(user, userInputs, sseEmitter); + } + + @Async + public void resumeFlow(String runtimeUuid, String userInput) { + WorkflowEngine workflowEngine = InterruptedFlow.RUNTIME_TO_GRAPH.get(runtimeUuid); + if (null == workflowEngine) { + log.error("工作流恢复执行时失败,runtime:{}", runtimeUuid); + throw new BaseException(A_WF_RESUME_FAIL.getInfo()); + } + workflowEngine.resume(userInput); + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java new file mode 100644 index 00000000..adbd581b --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/WorkflowUtil.java @@ -0,0 +1,162 @@ +package org.ruoyi.workflow.workflow; + +import cn.hutool.core.collection.CollStreamUtil; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import dev.langchain4j.data.message.UserMessage; +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.chat.factory.ChatServiceFactory; +import org.ruoyi.chat.service.chat.IChatService; +import org.ruoyi.common.chat.entity.chat.Message; +import org.ruoyi.common.chat.request.ChatRequest; +import org.ruoyi.workflow.base.NodeInputConfigTypeHandler; +import org.ruoyi.workflow.entity.WorkflowNode; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.util.JsonUtil; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.data.NodeIODataContent; +import org.ruoyi.workflow.workflow.def.WfNodeParamRef; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME; + +@Slf4j +@Component +public class WorkflowUtil { + + @Resource + private ChatServiceFactory chatServiceFactory; + + @SuppressWarnings("unchecked") + public static String renderTemplate(String template, List values) { + String result = template; + for (NodeIOData next : values) { + String name = next.getName(); + NodeIODataContent dataContent = next.getContent(); + if (dataContent.getType().equals(WfIODataTypeEnum.FILES.getValue())) { + List value = (List) dataContent.getValue(); + result = result.replace("{" + name + "}", String.join(",", value)); + } else if (dataContent.getType().equals(WfIODataTypeEnum.OPTIONS.getValue())) { + Map value = (Map) dataContent.getValue(); + result = result.replace("{" + name + "}", value.toString()); + } else { + result = result.replace("{" + name + "}", dataContent.getValue().toString()); + } + } + return result; + } + + public static String getHumanFeedbackTip(String nodeUuid, List wfNodes) { + WorkflowNode wfNode = wfNodes.stream() + .filter(item -> item.getUuid().equals(nodeUuid)) + .findFirst().orElse(null); + if (null == wfNode) { + return ""; + } + String wfNodeNodeConfig = wfNode.getNodeConfig(); + if (StrUtil.isBlank(wfNodeNodeConfig)) { + return ""; + } + Map map = JsonUtil.toMap(wfNodeNodeConfig); + Object tip = map.getOrDefault("tip", ""); + return String.valueOf(tip); + } + + public void streamingInvokeLLM(WfState wfState, WfNodeState state, WorkflowNode node, String category, + String modelName, List systemMessage) { + log.info("stream invoke, category: {}, modelName: {}", category, modelName); + + // 根据 category 获取对应的 ChatService(不使用计费代理,工作流场景单独计费) + IChatService chatService = chatServiceFactory.getOriginalService(category); + + StreamingChatGenerator streamingGenerator = StreamingChatGenerator.builder() + .mapResult(response -> { + String responseTxt = response.aiMessage().text(); + log.info("llm response:{}", responseTxt); + NodeIOData output = NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", responseTxt); + wfState.getNodeStateByNodeUuid(node.getUuid()).ifPresent(item -> item.getOutputs().add(output)); + return Map.of("completeResult", response.aiMessage().text()); + }) + .startingNode(node.getUuid()) + .startingState(state) + .build(); + + // 构建 ruoyi-ai 的 ChatRequest + List messages = new ArrayList<>(); + + addUserMessage(node, state.getInputs(), messages); + + addSystemMessage(systemMessage, messages); + + ChatRequest chatRequest = new ChatRequest(); + chatRequest.setModel(modelName); + chatRequest.setMessages(messages); + + // 使用工作流专用方法 + chatService.chat(chatRequest, streamingGenerator.handler()); + wfState.getNodeToStreamingGenerator().put(node.getUuid(), streamingGenerator); + } + + /** + * 添加用户信息 + * + * @param node + * @param messages + */ + private void addUserMessage(WorkflowNode node, List userMessage, List messages) { + if (CollUtil.isEmpty(userMessage)) { + return; + } + + WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(node.getInputConfig()); + + List refInputs = nodeInputConfig.getRefInputs(); + + Set nameSet = CollStreamUtil.toSet(refInputs, WfNodeParamRef::getName); + + userMessage.stream().filter(item -> nameSet.contains(item.getName())) + .map(item -> getMessage("user", item.getContent().getValue())).forEach(messages::add); + + if (CollUtil.isNotEmpty(messages)) { + return; + } + + userMessage.stream().filter(item -> "input".equals(item.getName())) + .map(item -> getMessage("user", item.getContent().getValue())).forEach(messages::add); + } + + /** + * 组装message对象 + * + * @param role + * @param value + * @return + */ + private Message getMessage(String role, Object value) { + Message message = new Message(); + message.setContent(String.valueOf(value)); + message.setRole(role); + return message; + } + + /** + * 添加系统信息 + * + * @param systemMessage + * @param messages + */ + private void addSystemMessage(List systemMessage, List messages) { + if (CollUtil.isEmpty(systemMessage)) { + return; + } + systemMessage.stream().map(userMsg -> getMessage("system", userMsg.singleText())).forEach(messages::add); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIOData.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIOData.java new file mode 100644 index 00000000..3721ffb7 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIOData.java @@ -0,0 +1,63 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Builder; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 工作流节点输入输出数据 + */ +@Builder +@Data +public class NodeIOData implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + protected String name; + + protected NodeIODataContent content; + + public static NodeIOData createByText(String name, String title, String value) { + NodeIODataTextContent datContent = new NodeIODataTextContent(); + datContent.setValue(value); + datContent.setTitle(title); + return NodeIOData.builder().name(name).content(datContent).build(); + } + + public static NodeIOData createByNumber(String name, String title, Double value) { + NodeIODataNumberContent datContent = new NodeIODataNumberContent(); + datContent.setValue(value); + datContent.setTitle(title); + return NodeIOData.builder().name(name).content(datContent).build(); + } + + public static NodeIOData createByBool(String name, String title, Boolean value) { + NodeIODataBoolContent datContent = new NodeIODataBoolContent(); + datContent.setValue(value); + datContent.setTitle(title); + return NodeIOData.builder().name(name).content(datContent).build(); + } + + public static NodeIOData createByFiles(String name, String title, List value) { + NodeIODataFilesContent datContent = new NodeIODataFilesContent(); + datContent.setValue(value); + datContent.setTitle(title); + return NodeIOData.builder().name(name).content(datContent).build(); + } + + public static NodeIOData createByOptions(String name, String title, Map value) { + NodeIODataOptionsContent datContent = new NodeIODataOptionsContent(); + datContent.setValue(value); + datContent.setTitle(title); + return NodeIOData.builder().name(name).content(datContent).build(); + } + + public String valueToString() { + return content.getValue().toString(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataBoolContent.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataBoolContent.java new file mode 100644 index 00000000..57730365 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataBoolContent.java @@ -0,0 +1,22 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; + +import java.io.Serial; +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@Data +public class NodeIODataBoolContent extends NodeIODataContent implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String title; + + private Integer type = WfIODataTypeEnum.BOOL.getValue(); + + private Boolean value; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataContent.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataContent.java new file mode 100644 index 00000000..d99d9f67 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataContent.java @@ -0,0 +1,13 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Data; + +@Data +public abstract class NodeIODataContent { + + private String title; + + private Integer type; + + private T value; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataFilesContent.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataFilesContent.java new file mode 100644 index 00000000..ff0ef6a8 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataFilesContent.java @@ -0,0 +1,23 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +@EqualsAndHashCode(callSuper = true) +@Data +public class NodeIODataFilesContent extends NodeIODataContent> implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String title; + + private Integer type = WfIODataTypeEnum.FILES.getValue(); + + private List value; +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataNumberContent.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataNumberContent.java new file mode 100644 index 00000000..f9cc3f88 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataNumberContent.java @@ -0,0 +1,22 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; + +import java.io.Serial; +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@Data +public class NodeIODataNumberContent extends NodeIODataContent implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String title; + + private Integer type = WfIODataTypeEnum.NUMBER.getValue(); + + private Double value; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataOptionsContent.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataOptionsContent.java new file mode 100644 index 00000000..7cc4ecc7 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataOptionsContent.java @@ -0,0 +1,23 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; + +import java.io.Serial; +import java.io.Serializable; +import java.util.Map; + +@EqualsAndHashCode(callSuper = true) +@Data +public class NodeIODataOptionsContent extends NodeIODataContent> implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String title; + + private Integer type = WfIODataTypeEnum.OPTIONS.getValue(); + + private Map value; +} \ No newline at end of file diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataTextContent.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataTextContent.java new file mode 100644 index 00000000..cb2cf920 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/NodeIODataTextContent.java @@ -0,0 +1,22 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; + +import java.io.Serial; +import java.io.Serializable; + +@EqualsAndHashCode(callSuper = true) +@Data +public class NodeIODataTextContent extends NodeIODataContent implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private String title; + + private Integer type = WfIODataTypeEnum.TEXT.getValue(); + + private String value; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/WfUserReq.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/WfUserReq.java new file mode 100644 index 00000000..950a949c --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/data/WfUserReq.java @@ -0,0 +1,10 @@ +package org.ruoyi.workflow.workflow.data; + +import lombok.Data; + +import java.util.List; + +@Data +public class WfUserReq { + private List inputs; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIO.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIO.java new file mode 100644 index 00000000..50872cac --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIO.java @@ -0,0 +1,29 @@ +package org.ruoyi.workflow.workflow.def; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.ruoyi.workflow.workflow.data.NodeIOData; + +import java.io.Serializable; + +/** + * 工作流节点输入输出参数定义 + */ +@Data +@NoArgsConstructor +public abstract class WfNodeIO implements Serializable { + + protected String uuid; + protected Integer type; + protected String name; + protected String title; + protected Boolean required; + + /** + * 检查数据是否合规 + * + * @param data 节点输入输出数据 + * @return 是否正确 + */ + public abstract boolean checkValue(NodeIOData data); +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOBool.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOBool.java new file mode 100644 index 00000000..d884684b --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOBool.java @@ -0,0 +1,27 @@ +package org.ruoyi.workflow.workflow.def; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.data.NodeIODataBoolContent; + +/** + * 用户输入参数-布尔类型 参数定义 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class WfNodeIOBool extends WfNodeIO { + + protected Integer type = WfIODataTypeEnum.BOOL.getValue(); + + @Override + public boolean checkValue(NodeIOData data) { + if (!(data.getContent() instanceof NodeIODataBoolContent)) { + return false; + } + return !required || null != data.getContent().getValue(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOFiles.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOFiles.java new file mode 100644 index 00000000..694e03df --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOFiles.java @@ -0,0 +1,28 @@ +package org.ruoyi.workflow.workflow.def; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.data.NodeIODataFilesContent; + +/** + * 用户输入参数-文件列表类型 参数定义 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class WfNodeIOFiles extends WfNodeIO { + protected Integer type = WfIODataTypeEnum.FILES.getValue(); + private Integer limit; + + @Override + public boolean checkValue(NodeIOData data) { + if (!(data.getContent() instanceof NodeIODataFilesContent wfNodeIOFiles)) { + return false; + } + return !required || !CollectionUtils.isEmpty(wfNodeIOFiles.getValue()); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIONumber.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIONumber.java new file mode 100644 index 00000000..b7de4485 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIONumber.java @@ -0,0 +1,26 @@ +package org.ruoyi.workflow.workflow.def; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.data.NodeIODataNumberContent; + +/** + * 用户输入参数-数字类型 参数定义 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class WfNodeIONumber extends WfNodeIO { + protected Integer type = WfIODataTypeEnum.NUMBER.getValue(); + + @Override + public boolean checkValue(NodeIOData data) { + if (!(data.getContent() instanceof NodeIODataNumberContent nodeIONumber)) { + return false; + } + return !required || null != nodeIONumber.getValue(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOOptions.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOOptions.java new file mode 100644 index 00000000..5e7aefe1 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOOptions.java @@ -0,0 +1,34 @@ +package org.ruoyi.workflow.workflow.def; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.data.NodeIODataOptionsContent; + +import java.util.Map; + +/** + * 用户输入参数-下拉选项类型 参数定义 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class WfNodeIOOptions extends WfNodeIO { + protected Integer type = WfIODataTypeEnum.OPTIONS.getValue(); + private Boolean multiple; + + @Override + public boolean checkValue(NodeIOData data) { + if (!(data.getContent() instanceof NodeIODataOptionsContent optionsData)) { + return false; + } + Map value = optionsData.getValue(); + if (required && null == value) { + return false; + } + //如果设置了单选,传过来的值是多项,则检查不通过 + return multiple || null == value || value.size() <= 1; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOText.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOText.java new file mode 100644 index 00000000..83a0a15f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeIOText.java @@ -0,0 +1,35 @@ +package org.ruoyi.workflow.workflow.def; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.data.NodeIODataTextContent; + +/** + * 用户输入参数-文本类型 参数定义 + */ +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +public class WfNodeIOText extends WfNodeIO { + + protected Integer type = WfIODataTypeEnum.TEXT.getValue(); + + @JsonProperty("max_length") + private Integer maxLength; + + @Override + public boolean checkValue(NodeIOData data) { + if (!(data.getContent() instanceof NodeIODataTextContent optionsData)) { + return false; + } + String value = optionsData.getValue(); + if (required && null == value) { + return false; + } + return null == maxLength || value.length() <= maxLength; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeParamRef.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeParamRef.java new file mode 100644 index 00000000..82cf888e --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/def/WfNodeParamRef.java @@ -0,0 +1,23 @@ +package org.ruoyi.workflow.workflow.def; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; + +/** + * 输入参数-引用类型 参数定义
+ * 1.该参数的值是另一个节点的输出/或输入参数
+ * 2.该类型参数只在非开始节点中使用
+ * 3.通常做为输入参数使用 + */ +@Data +public class WfNodeParamRef implements Serializable { + + @JsonProperty("node_uuid") + private String nodeUuid; + @JsonProperty("node_param_name") + private String nodeParamName; + + private String name; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/edge/ConditionalEdge.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/edge/ConditionalEdge.java new file mode 100644 index 00000000..5be719cc --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/edge/ConditionalEdge.java @@ -0,0 +1,10 @@ +package org.ruoyi.workflow.workflow.edge; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +@Data +public class ConditionalEdge extends Edge { + private String sourceHandle; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/edge/Edge.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/edge/Edge.java new file mode 100644 index 00000000..0d9a0dc2 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/edge/Edge.java @@ -0,0 +1,11 @@ +package org.ruoyi.workflow.workflow.edge; + +import lombok.Data; + +import java.util.List; + +@Data +public class Edge { + private String sourceNodeUuid; + private List targetNodeUuid; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java new file mode 100644 index 00000000..d6119e5c --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/AbstractWfNode.java @@ -0,0 +1,213 @@ +package org.ruoyi.workflow.workflow.node; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import jakarta.validation.ConstraintViolation; +import lombok.Data; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.SerializationUtils; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.core.exception.base.BaseException; +import org.ruoyi.workflow.base.NodeInputConfigTypeHandler; +import org.ruoyi.workflow.entity.WorkflowComponent; +import org.ruoyi.workflow.entity.WorkflowNode; +import org.ruoyi.workflow.enums.WfIODataTypeEnum; +import org.ruoyi.workflow.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.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.def.WfNodeIO; +import org.ruoyi.workflow.workflow.def.WfNodeParamRef; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +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; + +/** + * 节点实例-运行时 + */ +@Data +@Slf4j +public abstract class AbstractWfNode { + + protected WorkflowComponent wfComponent; + protected WfState wfState; + @Getter + protected WfNodeState state; + protected WorkflowNode node; + + public AbstractWfNode(WorkflowComponent wfComponent, WorkflowNode node, WfState wfState, WfNodeState nodeState) { + this.wfState = wfState; + this.wfComponent = wfComponent; + this.state = nodeState; + this.node = node; + } + + public void initInput() { + WfNodeInputConfig nodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(node.getInputConfig()); + if (wfState.getCompletedNodes().isEmpty()) { + log.info("没有上游节点,当前节点为开始节点"); + state.getInputs().addAll(wfState.getInput()); + return; + } + + List inputs = new ArrayList<>(); + + //将上游节点的输出转成当前节点的输入 + List upstreamOutputs = wfState.getLatestOutputs(); + if (!upstreamOutputs.isEmpty()) { + inputs.addAll(new ArrayList<>(upstreamOutputs)); + } else { + log.warn("upstream output params is empty"); + } + //处理引用类型的输入参数,非开始节点只有引用类型输入参数 + List refInputDefs = nodeInputConfig.getRefInputs(); + inputs.addAll(changeRefersToNodeIODatas(refInputDefs)); + + //根据节点的输入参数定义,刷选出符合要求的输入参数 + WfNodeInputConfig inputConfig = JsonUtil.toBean(node.getInputConfig(), WfNodeInputConfig.class); + List defInputNames = inputConfig.getRefInputs().stream().map(WfNodeParamRef::getName).collect(Collectors.toList()); + defInputNames.addAll(inputConfig.getUserInputs().stream().map(WfNodeIO::getName).toList()); + List needInputs = inputs.stream().filter(item -> { + String needInputName = item.getName(); + //上流节点的默认输出参数(output),改成input即可 + if (DEFAULT_OUTPUT_PARAM_NAME.equals(needInputName)) { + item.setName(DEFAULT_INPUT_PARAM_NAME); + return true; + } + return defInputNames.contains(needInputName); + }).toList(); + state.getInputs().addAll(needInputs); + } + + /** + * 查找引用节点的参数并转成输入输出参数 + * + * @param referParams 引用类型的定义列表 + */ + private List changeRefersToNodeIODatas(List referParams) { + List result = new ArrayList<>(); + for (WfNodeParamRef referParam : referParams) { + String nodeUuid = referParam.getNodeUuid(); + String nodeParamName = referParam.getNodeParamName(); + NodeIOData newInput = createByReferParam(nodeUuid, nodeParamName); + if (null != newInput) { + newInput.setName(referParam.getName()); + result.add(newInput); + } else { + log.warn("Can not find reference node output param,refNodeId:{},refNodeOutputName:{}", nodeUuid, nodeParamName); + } + } + return result; + } + + public NodeIOData createByReferParam(String refNodeUuid, String refNodeParamName) { + Optional hitDataOpt = wfState.getIOByNodeUuid(refNodeUuid) + .stream() + .filter(wfNodeIOData -> wfNodeIOData.getName().equalsIgnoreCase(refNodeParamName)) + .findFirst(); + return hitDataOpt.map(SerializationUtils::clone).orElse(null); + } + + public NodeProcessResult process(Consumer inputConsumer, Consumer outputConsumer) { + log.info("↓↓↓↓↓ node process start,name:{},uuid:{}", node.getTitle(), node.getUuid()); + state.setProcessStatus(NODE_PROCESS_STATUS_DOING); + initInput(); + //HumanFeedback的情况 + Object humanFeedbackState = state.data().get(HUMAN_FEEDBACK_KEY); + if (null != humanFeedbackState) { + String userInput = humanFeedbackState.toString(); + if (StringUtils.isNotBlank(userInput)) { + state.getInputs().add(NodeIOData.createByText(HUMAN_FEEDBACK_KEY, "default", userInput)); + } + } + if (null != inputConsumer) { + inputConsumer.accept(state); + } + log.info("--node input:{}", JsonUtil.toJson(state.getInputs())); + NodeProcessResult processResult; + try { + processResult = onProcess(); + } catch (Exception e) { + state.setProcessStatus(NODE_PROCESS_STATUS_FAIL); + state.setProcessStatusRemark("process error:" + e.getMessage()); + wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_FAIL); + log.info("↑↑↑↑↑ node process error,name:{},uuid:{},error", node.getTitle(), node.getUuid(), e); + if (null != outputConsumer) { + outputConsumer.accept(state); + } + throw new RuntimeException(e); + } + + if (!processResult.getContent().isEmpty()) { + state.setOutputs(processResult.getContent()); + } + state.setProcessStatus(NODE_PROCESS_STATUS_SUCCESS); + wfState.getCompletedNodes().add(this); + log.info("↑↑↑↑↑ node process end,name:{},uuid:{},output:{}", + node.getTitle(), node.getUuid(), JsonUtil.toJson(state.getOutputs())); + if (null != outputConsumer) { + outputConsumer.accept(state); + } + return processResult; + } + + protected abstract NodeProcessResult onProcess(); + + protected String getFirstInputText() { + String firstInputText; + if (state.getInputs().size() > 1) { + firstInputText = state.getInputs() + .stream() + .filter(item -> WfIODataTypeEnum.TEXT.getValue().equals(item.getContent().getType()) && !DEFAULT_INPUT_PARAM_NAME.equals(item.getName())) + .map(NodeIOData::valueToString) + .findFirst() + .orElse(""); + } else { + firstInputText = state.getInputs().get(0).valueToString(); + } + return firstInputText; + } + + protected T checkAndGetConfig(Class clazz) { + ObjectNode configObj = JsonUtil.toBean(node.getNodeConfig(), ObjectNode.class); + if (configObj.isEmpty()) { + log.error("node config is empty,node uuid:{}", state.getUuid()); + throw new BaseException(A_WF_NODE_CONFIG_NOT_FOUND.getInfo()); + } + log.info("node config:{}", configObj); + T nodeConfig = JsonUtil.fromJson(configObj, clazz); + if (null == nodeConfig) { + log.warn("找不到节点的配置,node uuid:{}", state.getUuid()); + throw new BaseException(A_WF_NODE_CONFIG_ERROR.getInfo()); + } + boolean configValid = true; + try { + Set> violations = SpringUtil.getBean("beanValidator", LocalValidatorFactoryBean.class).validate(nodeConfig); + for (ConstraintViolation violation : violations) { + log.error(violation.getMessage()); + configValid = false; + } + } catch (Exception e) { + log.error("节点配置校验失败,node uuid:{},error:{}", state.getUuid(), e.getMessage()); + configValid = false; + } + if (!configValid) { + log.warn("节点配置错误,node uuid:{}", state.getUuid()); + throw new BaseException(A_WF_NODE_CONFIG_ERROR.getInfo()); + } + return nodeConfig; + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/EndNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/EndNode.java new file mode 100644 index 00000000..8b6c182f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/EndNode.java @@ -0,0 +1,41 @@ +package org.ruoyi.workflow.workflow.node; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.workflow.entity.WorkflowComponent; +import org.ruoyi.workflow.entity.WorkflowNode; +import org.ruoyi.workflow.util.JsonUtil; +import org.ruoyi.workflow.workflow.*; +import org.ruoyi.workflow.workflow.data.NodeIOData; + +import java.util.ArrayList; +import java.util.List; + +import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.DEFAULT_OUTPUT_PARAM_NAME; + + +@Slf4j +public class EndNode extends AbstractWfNode { + + public EndNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) { + super(wfComponent, nodeDef, wfState, nodeState); + } + + @Override + protected NodeProcessResult onProcess() { + List result = new ArrayList<>(); + ObjectNode objectConfig = JsonUtil.toBean(node.getNodeConfig(), ObjectNode.class); + JsonNode resultNode = objectConfig.get("result"); + String output = ""; + if (null == resultNode) { + log.warn("EndNode result config is empty, nodeUuid: {}, title: {}", node.getUuid(), node.getTitle()); + } else { + String resultTemplate = resultNode.asText(); + WfNodeIODataUtil.changeFilesContentToMarkdown(state.getInputs()); + output = WorkflowUtil.renderTemplate(resultTemplate, state.getInputs()); + } + result.add(NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "", output)); + return NodeProcessResult.builder().content(result).build(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java new file mode 100644 index 00000000..a9826112 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNode.java @@ -0,0 +1,52 @@ +package org.ruoyi.workflow.workflow.node.answer; + +import dev.langchain4j.data.message.UserMessage; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.workflow.entity.WorkflowComponent; +import org.ruoyi.workflow.entity.WorkflowNode; +import org.ruoyi.workflow.util.SpringUtil; +import org.ruoyi.workflow.workflow.NodeProcessResult; +import org.ruoyi.workflow.workflow.WfNodeState; +import org.ruoyi.workflow.workflow.WfState; +import org.ruoyi.workflow.workflow.WorkflowUtil; +import org.ruoyi.workflow.workflow.node.AbstractWfNode; + +import java.util.List; + +/** + * 【节点】LLM生成回答
+ * 节点内容固定格式:LLMAnswerNodeConfig + */ +@Slf4j +public class LLMAnswerNode extends AbstractWfNode { + + public LLMAnswerNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) { + super(wfComponent, nodeDef, wfState, nodeState); + } + + /** + * nodeConfig格式:
+ * {"prompt": "将以下内容翻译成英文:{input}","model_platform":"deepseek","model_name":"deepseek-chat"}
+ * + * @return LLM的返回内容 + */ + @Override + public NodeProcessResult onProcess() { + LLMAnswerNodeConfig nodeConfigObj = checkAndGetConfig(LLMAnswerNodeConfig.class); + String inputText = getFirstInputText(); + log.info("LLM answer node config:{}", nodeConfigObj); + String prompt = inputText; + if (StringUtils.isNotBlank(nodeConfigObj.getPrompt())) { + prompt = WorkflowUtil.renderTemplate(nodeConfigObj.getPrompt(), state.getInputs()); + } + log.info("LLM prompt:{}", prompt); + // 调用LLM + WorkflowUtil workflowUtil = SpringUtil.getBean(WorkflowUtil.class); + String modelName = nodeConfigObj.getModelName(); + String category = nodeConfigObj.getCategory(); + List systemMessage = List.of(UserMessage.from(prompt)); + workflowUtil.streamingInvokeLLM(wfState, state, node, category, modelName, systemMessage); + return new NodeProcessResult(); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNodeConfig.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNodeConfig.java new file mode 100644 index 00000000..fc90009f --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/answer/LLMAnswerNodeConfig.java @@ -0,0 +1,26 @@ +package org.ruoyi.workflow.workflow.node.answer; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode +@Data +public class LLMAnswerNodeConfig { + @NotBlank + private String prompt; + + /** + * TODO + */ + // @NotBlank + private String category; + + @NotNull + @JsonProperty("model_name") + private String modelName; + + private Boolean streaming; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/classifier/ClassifierNodeConfig.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/classifier/ClassifierNodeConfig.java new file mode 100644 index 00000000..a9432175 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/classifier/ClassifierNodeConfig.java @@ -0,0 +1,16 @@ +package org.ruoyi.workflow.workflow.node.classifier; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.ArrayList; +import java.util.List; + +@Data +public class ClassifierNodeConfig { + private List categories = new ArrayList<>(); + @JsonProperty("model_platform") + private String modelPlatform; + @JsonProperty("model_name") + private String modelName; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNode.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNode.java new file mode 100644 index 00000000..6f5df29e --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNode.java @@ -0,0 +1,49 @@ +package org.ruoyi.workflow.workflow.node.start; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.ruoyi.common.core.exception.base.BaseException; +import org.ruoyi.workflow.entity.WorkflowComponent; +import org.ruoyi.workflow.entity.WorkflowNode; +import org.ruoyi.workflow.util.JsonUtil; +import org.ruoyi.workflow.workflow.NodeProcessResult; +import org.ruoyi.workflow.workflow.WfNodeIODataUtil; +import org.ruoyi.workflow.workflow.WfNodeState; +import org.ruoyi.workflow.workflow.WfState; +import org.ruoyi.workflow.workflow.data.NodeIOData; +import org.ruoyi.workflow.workflow.node.AbstractWfNode; + +import java.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; + +@Slf4j +public class StartNode extends AbstractWfNode { + + public StartNode(WorkflowComponent wfComponent, WorkflowNode nodeDef, WfState wfState, WfNodeState nodeState) { + super(wfComponent, nodeDef, wfState, nodeState); + } + + @Override + public NodeProcessResult onProcess() { + String objectConfig = node.getNodeConfig(); + if (null == objectConfig) { + throw new BaseException(A_WF_NODE_CONFIG_NOT_FOUND.getInfo()); + } + List result; + StartNodeConfig nodeConfigObj = JsonUtil.fromJson(objectConfig, StartNodeConfig.class); + if (null == nodeConfigObj) { + log.warn("找不到开始节点的配置"); + throw new BaseException(A_WF_NODE_CONFIG_ERROR.getInfo()); + } + if (StringUtils.isNotBlank(nodeConfigObj.getPrologue())) { + result = List.of(NodeIOData.createByText(DEFAULT_OUTPUT_PARAM_NAME, "default", nodeConfigObj.getPrologue())); + } else { + result = WfNodeIODataUtil.changeInputsToOutputs(state.getInputs()); + } + return NodeProcessResult.builder().content(result).build(); + } + +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNodeConfig.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNodeConfig.java new file mode 100644 index 00000000..86de0fb1 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/start/StartNodeConfig.java @@ -0,0 +1,10 @@ +package org.ruoyi.workflow.workflow.node.start; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode +@Data +public class StartNodeConfig { + private String prologue; +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/LogicOperatorEnum.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/LogicOperatorEnum.java new file mode 100644 index 00000000..079e9a20 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/LogicOperatorEnum.java @@ -0,0 +1,17 @@ +package org.ruoyi.workflow.workflow.node.switcher; + +public enum LogicOperatorEnum { + AND("and"), + + OR("or"); + + private final String name; + + LogicOperatorEnum(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/OperatorEnum.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/OperatorEnum.java new file mode 100644 index 00000000..1e84e4c1 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/OperatorEnum.java @@ -0,0 +1,45 @@ +package org.ruoyi.workflow.workflow.node.switcher; + +import lombok.Getter; + +import java.util.Arrays; + +@Getter +public enum OperatorEnum { + CONTAINS("contains", "包含"), + + NOT_CONTAINS("not contains", "不包含"), + + START_WITH("start with", "开始内容是"), + + END_WITH("end with", "结束内容是"), + + EMPTY("empty", "为空"), + + NOT_EMPTY("not empty", "不为空"), + + EQUAL("=", "等于"), + + NOT_EQUAL("!=", "不等于"), + + GREATER(">", "大于"), + + GREATER_OR_EQUAL(">=", "大于或等于"), + + LESS("<", "小于"), + + LESS_OR_EQUAL("<=", "小于或等于"); + + private final String name; + + private final String desc; + + OperatorEnum(String name, String desc) { + this.name = name; + this.desc = desc; + } + + public static OperatorEnum getByName(String name) { + return Arrays.stream(OperatorEnum.values()).filter(item -> item.name.equals(name)).findFirst().orElse(null); + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherCase.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherCase.java new file mode 100644 index 00000000..fc35c377 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherCase.java @@ -0,0 +1,27 @@ +package org.ruoyi.workflow.workflow.node.switcher; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class SwitcherCase { + + private String uuid; + private String operator; + private List conditions; + @JsonProperty("target_node_uuid") + private String targetNodeUuid; + + @Data + public static class Condition { + private String uuid; + @JsonProperty("node_uuid") + private String nodeUuid; + @JsonProperty("node_param_name") + private String nodeParamName; + private String operator; + private String value; + } +} diff --git a/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNodeConfig.java b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNodeConfig.java new file mode 100644 index 00000000..40cf2f18 --- /dev/null +++ b/ruoyi-modules-api/ruoyi-workflow-api/src/main/java/org/ruoyi/workflow/workflow/node/switcher/SwitcherNodeConfig.java @@ -0,0 +1,14 @@ +package org.ruoyi.workflow.workflow.node.switcher; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +@Data +public class SwitcherNodeConfig { + + private List cases; + @JsonProperty("default_target_node_uuid") + private String defaultTargetNodeUuid; +} diff --git a/ruoyi-modules/pom.xml b/ruoyi-modules/pom.xml index af85f578..ee21cd0e 100644 --- a/ruoyi-modules/pom.xml +++ b/ruoyi-modules/pom.xml @@ -23,6 +23,9 @@ ruoyi-generator ruoyi-wechat ruoyi-graph + + ruoyi-aihuman + ruoyi-workflow diff --git a/ruoyi-modules/ruoyi-aihuman/pom.xml b/ruoyi-modules/ruoyi-aihuman/pom.xml new file mode 100644 index 00000000..d594c634 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + org.ruoyi + ruoyi-modules + ${revision} + ../pom.xml + + + ruoyi-aihuman + + + aihuman模块 + + + + 3.2.1 + + + + + + + org.ruoyi + ruoyi-common-core + + + + org.ruoyi + ruoyi-common-doc + + + + org.ruoyi + ruoyi-common-mybatis + + + + org.ruoyi + ruoyi-common-web + + + + org.ruoyi + ruoyi-common-log + + + + + org.apache.velocity + velocity-engine-core + + + + + + + + + + + + + + + + org.ruoyi + ruoyi-common-excel + + + \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanConfigController.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanConfigController.java new file mode 100644 index 00000000..4eecdf83 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanConfigController.java @@ -0,0 +1,125 @@ +package org.ruoyi.aihuman.controller; + +import java.util.List; + +import cn.dev33.satoken.annotation.SaIgnore; +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.ruoyi.common.idempotent.annotation.RepeatSubmit; +import org.ruoyi.common.log.annotation.Log; +import org.ruoyi.common.web.core.BaseController; +import org.ruoyi.core.page.PageQuery; +import org.ruoyi.common.core.domain.R; +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.aihuman.domain.vo.AihumanConfigVo; +import org.ruoyi.aihuman.domain.bo.AihumanConfigBo; +import org.ruoyi.aihuman.service.AihumanConfigService; +import org.ruoyi.core.page.TableDataInfo; + +/** + * 交互数字人配置 + * + * @author ageerle + * @date Fri Sep 26 22:27:00 GMT+08:00 2025 + */ + +//临时免登录 +@SaIgnore + +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/aihuman/aihumanConfig") +public class AihumanConfigController extends BaseController { + + private final AihumanConfigService aihumanConfigService; + +/** + * 查询交互数字人配置列表 + */ +@SaCheckPermission("aihuman:aihumanConfig:list") +@GetMapping("/list") + public TableDataInfo list(AihumanConfigBo bo, PageQuery pageQuery) { + return aihumanConfigService.queryPageList(bo, pageQuery); + } + + /** + * 导出交互数字人配置列表 + */ + @SaCheckPermission("aihuman:aihumanConfig:export") + @Log(title = "交互数字人配置", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(AihumanConfigBo bo, HttpServletResponse response) { + List list = aihumanConfigService.queryList(bo); + ExcelUtil.exportExcel(list, "交互数字人配置", AihumanConfigVo.class, response); + } + + /** + * 获取交互数字人配置详细信息 + * + * @param id 主键 + */ + @SaCheckPermission("aihuman:aihumanConfig:query") + @GetMapping("/{id}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Integer id) { + return R.ok(aihumanConfigService.queryById(id)); + } + + /** + * 新增交互数字人配置 + */ + @SaCheckPermission("aihuman:aihumanConfig:add") + @Log(title = "交互数字人配置", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody AihumanConfigBo bo) { + return toAjax(aihumanConfigService.insertByBo(bo)); + } + + /** + * 修改交互数字人配置 + */ + @SaCheckPermission("aihuman:aihumanConfig:edit") + @Log(title = "交互数字人配置", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody AihumanConfigBo bo) { + return toAjax(aihumanConfigService.updateByBo(bo)); + } + + /** + * 删除交互数字人配置 + * + * @param ids 主键串 + */ + @SaCheckPermission("aihuman:aihumanConfig:remove") + @Log(title = "交互数字人配置", businessType = BusinessType.DELETE) + @DeleteMapping("/{ids}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Integer[] ids) { + return toAjax(aihumanConfigService.deleteWithValidByIds(List.of(ids), true)); + } + + /** + * 查询已发布的交互数字人配置列表 + * 只返回 publish = 1 的数据 + */ + @GetMapping("/publishedList") + public TableDataInfo publishedList(PageQuery pageQuery) { + // 创建查询条件对象并设置publish=1 + AihumanConfigBo bo = new AihumanConfigBo(); + bo.setPublish(1); + // 调用现有的查询方法,传入预设了publish=1条件的bo对象 + return aihumanConfigService.queryPageList(bo, pageQuery); + } + + +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanInfoController.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanInfoController.java new file mode 100644 index 00000000..444e8ee2 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/controller/AihumanInfoController.java @@ -0,0 +1,80 @@ +package org.ruoyi.aihuman.controller; + +import cn.dev33.satoken.annotation.SaIgnore; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.ruoyi.aihuman.domain.AihumanInfo; +import org.ruoyi.aihuman.domain.vo.AihumanInfoVo; +import org.ruoyi.aihuman.service.IAihumanInfoService; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.core.page.TableDataInfo; +import org.ruoyi.core.page.PageQuery; + +import java.util.Arrays; + +/** + * AI人类交互信息Controller + * 免登录接口,方便验证 + * + * @author QingYunAI + */ +@SaIgnore +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/aihuman/info") +public class AihumanInfoController { + + private final IAihumanInfoService aihumanInfoService; + + /** + * 获取AI人类交互信息详情 + */ + @GetMapping("/{id}") + public R getInfo(@PathVariable Long id) { + return R.ok(aihumanInfoService.queryById(id)); + } + + /** + * 查询AI人类交互信息列表 + */ + @GetMapping("/list") + public R> list(AihumanInfo aihumanInfo, PageQuery pageQuery) { + TableDataInfo tableDataInfo = aihumanInfoService.queryPageList(aihumanInfo, pageQuery); + return R.ok(tableDataInfo); + } + + /** + * 新增AI人类交互信息 + */ + @PostMapping + public R add(@Validated @RequestBody AihumanInfo aihumanInfo) { + return R.ok(aihumanInfoService.insert(aihumanInfo)); + } + + /** + * 修改AI人类交互信息 + */ + @PutMapping + public R edit(@Validated @RequestBody AihumanInfo aihumanInfo) { + return R.ok(aihumanInfoService.update(aihumanInfo)); + } + + /** + * 删除AI人类交互信息 + */ + @DeleteMapping("/{ids}") + public R remove(@PathVariable Long[] ids) { + return R.ok(aihumanInfoService.deleteWithValidByIds(Arrays.asList(ids), true)); + } + + /** + * 测试接口 + * 提供一个简单的GET接口用于快速验证控制器是否正常工作 + */ + @GetMapping("/test") + public R test() { + return R.ok("AI Human Controller is working!"); + } +} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanConfig.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanConfig.java new file mode 100644 index 00000000..6b894806 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanConfig.java @@ -0,0 +1,71 @@ +package org.ruoyi.aihuman.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; +import java.io.Serializable; + +/** + * 交互数字人配置对象 aihuman_config + * + * @author ageerle + * @date Fri Sep 26 22:27:00 GMT+08:00 2025 + */ +@Data +@TableName("aihuman_config") +public class AihumanConfig implements Serializable { + + + /** + * id + */ + @TableId(value = "id", type = IdType.AUTO) + private Integer id; + + /** + * name + */ + private String name; + + /** + * modelName + */ + private String modelName; + + /** + * modelPath + */ + private String modelPath; + + /** + * modelParams + */ + private String modelParams; + + /** + * agentParams + */ + private String agentParams; + + /** + * createTime + */ + private LocalDateTime createTime; + + /** + * updateTime + */ + private LocalDateTime updateTime; + + /** + * status + */ + private Integer status; + + /** + * publish + */ + private Integer publish; + + +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanInfo.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanInfo.java new file mode 100644 index 00000000..73be9174 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/AihumanInfo.java @@ -0,0 +1,39 @@ +package org.ruoyi.aihuman.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.io.Serializable; +import java.util.Date; + +/** + * AI人类交互信息实体类 + * + * @author QingYunAI + */ +@Data +@TableName("aihuman_info") +public class AihumanInfo implements Serializable { + private static final long serialVersionUID = 1L; + + /** 主键ID */ + @TableId(value = "id", type = IdType.AUTO) + private Long id; + + /** 交互名称 */ + private String name; + + /** 交互内容 */ + private String content; + + /** 创建时间 */ + @TableField(fill = FieldFill.INSERT) + private Date createTime; + + /** 更新时间 */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private Date updateTime; + + /** 删除标志(0代表存在 2代表删除) */ + @TableLogic + private String delFlag; +} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanConfigBo.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanConfigBo.java new file mode 100644 index 00000000..5144b95b --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanConfigBo.java @@ -0,0 +1,70 @@ +package org.ruoyi.aihuman.domain.bo; + +import org.ruoyi.aihuman.domain.AihumanConfig; +import org.ruoyi.core.domain.BaseEntity; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; +import java.time.LocalDateTime; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import java.io.Serializable; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; + +/** + * 交互数字人配置业务对象 aihuman_config + * + * @author ageerle + * @date Fri Sep 26 22:27:00 GMT+08:00 2025 + */ +@Data + +@AutoMapper(target = AihumanConfig.class, reverseConvertGenerate = false) +public class AihumanConfigBo implements Serializable { + + private Integer id; + + /** + * name + */ + private String name; + /** + * modelName + */ + private String modelName; + /** + * modelPath + */ + private String modelPath; + /** + * modelParams + */ + private String modelParams; + /** + * agentParams + */ + private String agentParams; + /** + * createTime + */ + private LocalDateTime createTime; + /** + * updateTime + */ + private LocalDateTime updateTime; + /** + * status + */ + @NotNull(message = "status不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer status; + /** + * publish + */ + @NotNull(message = "publish不能为空", groups = { AddGroup.class, EditGroup.class }) + private Integer publish; + +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanInfoBo.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanInfoBo.java new file mode 100644 index 00000000..074f165a --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/bo/AihumanInfoBo.java @@ -0,0 +1,52 @@ +package org.ruoyi.aihuman.domain.bo; + +import org.ruoyi.aihuman.domain.AihumanInfo; +import org.ruoyi.core.domain.BaseEntity; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import jakarta.validation.constraints.*; +import java.time.LocalDateTime; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; +import java.io.Serializable; +import java.io.Serializable; +import org.ruoyi.common.core.validate.AddGroup; +import org.ruoyi.common.core.validate.EditGroup; + +/** + * 数字人信息管理业务对象 aihuman_info + * + * @author ageerle + * @date Fri Sep 26 20:03:06 GMT+08:00 2025 + */ +@Data + +@AutoMapper(target = AihumanInfo.class, reverseConvertGenerate = false) +public class AihumanInfoBo implements Serializable { + + private Long id; + + /** + * 交互名称 + */ + private String name; + /** + * 交互内容 + */ + private String content; + /** + * 创建时间 + */ + private LocalDateTime createTime; + /** + * 更新时间 + */ + private LocalDateTime updateTime; + /** + * 删除标志(0代表存在 2代表删除) + */ + private String delFlag; + +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanConfigVo.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanConfigVo.java new file mode 100644 index 00000000..0a8eea8e --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanConfigVo.java @@ -0,0 +1,76 @@ +package org.ruoyi.aihuman.domain.vo; + +import java.time.LocalDateTime; +import java.io.Serializable; +import org.ruoyi.aihuman.domain.AihumanConfig; +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.ruoyi.common.excel.annotation.ExcelDictFormat; +import org.ruoyi.common.excel.convert.ExcelDictConvert; + +import java.util.Date; + + +/** + * 交互数字人配置视图对象 aihuman_config + * + * @author ageerle + * @date Fri Sep 26 22:27:00 GMT+08:00 2025 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = AihumanConfig.class) +public class AihumanConfigVo implements Serializable { + + private Integer id; + /** + * name + */ + @ExcelProperty(value = "name") + private String name; + /** + * modelName + */ + @ExcelProperty(value = "modelName") + private String modelName; + /** + * modelPath + */ + @ExcelProperty(value = "modelPath") + private String modelPath; + /** + * modelParams + */ + @ExcelProperty(value = "modelParams") + private String modelParams; + /** + * agentParams + */ + @ExcelProperty(value = "agentParams") + private String agentParams; + /** + * createTime + */ + @ExcelProperty(value = "createTime") + private LocalDateTime createTime; + /** + * updateTime + */ + @ExcelProperty(value = "updateTime") + private LocalDateTime updateTime; + /** + * status + */ + @ExcelProperty(value = "status", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "sys_common_status") + private Integer status; + /** + * publish + */ + @ExcelProperty(value = "publish", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "sys_common_status") + private Integer publish; + +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanInfoVo.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanInfoVo.java new file mode 100644 index 00000000..e0868a5f --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/domain/vo/AihumanInfoVo.java @@ -0,0 +1,33 @@ +package org.ruoyi.aihuman.domain.vo; + +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.ruoyi.aihuman.domain.AihumanInfo; +import java.io.Serializable; +import java.util.Date; + +/** + * AI人类交互信息视图对象 + * + * @author QingYunAI + */ +@Data +@AutoMapper(target = AihumanInfo.class) +public class AihumanInfoVo implements Serializable { + private static final long serialVersionUID = 1L; + + /** 主键ID */ + private Long id; + + /** 交互名称 */ + private String name; + + /** 交互内容 */ + private String content; + + /** 创建时间 */ + private Date createTime; + + /** 更新时间 */ + private Date updateTime; +} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanConfigMapper.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanConfigMapper.java new file mode 100644 index 00000000..3dca6078 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanConfigMapper.java @@ -0,0 +1,17 @@ +package org.ruoyi.aihuman.mapper; + +import org.ruoyi.aihuman.domain.AihumanConfig; +import org.ruoyi.aihuman.domain.vo.AihumanConfigVo; +import org.ruoyi.core.mapper.BaseMapperPlus; +import org.apache.ibatis.annotations.Mapper; + +/** + * 交互数字人配置Mapper接口 + * + * @author ageerle + * @date Fri Sep 26 22:27:00 GMT+08:00 2025 + */ +@Mapper +public interface AihumanConfigMapper extends BaseMapperPlus { + +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanInfoMapper.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanInfoMapper.java new file mode 100644 index 00000000..afb8b414 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/mapper/AihumanInfoMapper.java @@ -0,0 +1,16 @@ +package org.ruoyi.aihuman.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.ruoyi.core.mapper.BaseMapperPlus; +import org.ruoyi.aihuman.domain.AihumanInfo; +import org.ruoyi.aihuman.domain.vo.AihumanInfoVo; + +/** + * AI人类交互信息Mapper接口 + * + * @author QingYunAI + */ +@Mapper +public interface AihumanInfoMapper extends BaseMapperPlus { + +} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/AihumanConfigService.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/AihumanConfigService.java new file mode 100644 index 00000000..3f155bc2 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/AihumanConfigService.java @@ -0,0 +1,48 @@ +package org.ruoyi.aihuman.service; + +import org.ruoyi.aihuman.domain.vo.AihumanConfigVo; +import org.ruoyi.aihuman.domain.bo.AihumanConfigBo; + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * 交互数字人配置Service接口 + * + * @author ageerle + * @date Fri Sep 26 22:27:00 GMT+08:00 2025 + */ +public interface AihumanConfigService { + + /** + * 查询交互数字人配置 + */ + AihumanConfigVo queryById(Integer id); + + /** + * 查询交互数字人配置列表 + */ + TableDataInfo queryPageList(AihumanConfigBo bo, PageQuery pageQuery); + + /** + * 查询交互数字人配置列表 + */ + List queryList(AihumanConfigBo bo); + + /** + * 新增交互数字人配置 + */ + Boolean insertByBo(AihumanConfigBo bo); + + /** + * 修改交互数字人配置 + */ + Boolean updateByBo(AihumanConfigBo bo); + + /** + * 校验并批量删除交互数字人配置信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/IAihumanInfoService.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/IAihumanInfoService.java new file mode 100644 index 00000000..f121ac4b --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/IAihumanInfoService.java @@ -0,0 +1,47 @@ +package org.ruoyi.aihuman.service; + +import org.ruoyi.aihuman.domain.AihumanInfo; +import org.ruoyi.aihuman.domain.vo.AihumanInfoVo; +import org.ruoyi.core.page.TableDataInfo; +import org.ruoyi.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * AI人类交互信息Service接口 + * + * @author QingYunAI + */ +public interface IAihumanInfoService { + + /** + * 查询AI人类交互信息 + */ + AihumanInfoVo queryById(Long id); + + /** + * 查询AI人类交互信息列表 + */ + TableDataInfo queryPageList(AihumanInfo record, PageQuery pageQuery); + + /** + * 查询AI人类交互信息列表 + */ + List queryList(AihumanInfo record); + + /** + * 新增AI人类交互信息 + */ + int insert(AihumanInfo record); + + /** + * 修改AI人类交互信息 + */ + int update(AihumanInfo record); + + /** + * 批量删除AI人类交互信息 + */ + int deleteWithValidByIds(Collection ids, Boolean isValid); +} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanConfigServiceImpl.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanConfigServiceImpl.java new file mode 100644 index 00000000..48c1f629 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanConfigServiceImpl.java @@ -0,0 +1,116 @@ +package org.ruoyi.aihuman.service.impl; + +import org.ruoyi.common.core.utils.MapstructUtils; + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.ruoyi.aihuman.domain.bo.AihumanConfigBo; +import org.ruoyi.aihuman.domain.vo.AihumanConfigVo; +import org.ruoyi.aihuman.domain.AihumanConfig; +import org.ruoyi.aihuman.mapper.AihumanConfigMapper; +import org.ruoyi.aihuman.service.AihumanConfigService; +import org.ruoyi.common.core.utils.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * 交互数字人配置Service业务层处理 + * + * @author ageerle + * @date Fri Sep 26 22:27:00 GMT+08:00 2025 + */ +@RequiredArgsConstructor +@Service +public class AihumanConfigServiceImpl implements AihumanConfigService { + + private final AihumanConfigMapper baseMapper; + + /** + * 查询交互数字人配置 + */ + @Override + public AihumanConfigVo queryById(Integer id) { + return baseMapper.selectVoById(id); + } + + /** + * 查询交互数字人配置列表 + */ + @Override + public TableDataInfo queryPageList(AihumanConfigBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询交互数字人配置列表 + */ + @Override + public List queryList(AihumanConfigBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(AihumanConfigBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(StringUtils.isNotBlank(bo.getName()), AihumanConfig::getName, bo.getName()); + lqw.eq(StringUtils.isNotBlank(bo.getModelName()), AihumanConfig::getModelName, bo.getModelName()); + lqw.eq(StringUtils.isNotBlank(bo.getModelPath()), AihumanConfig::getModelPath, bo.getModelPath()); + lqw.eq(StringUtils.isNotBlank(bo.getModelParams()), AihumanConfig::getModelParams, bo.getModelParams()); + lqw.eq(StringUtils.isNotBlank(bo.getAgentParams()), AihumanConfig::getAgentParams, bo.getAgentParams()); + lqw.eq(bo.getCreateTime() != null, AihumanConfig::getCreateTime, bo.getCreateTime()); + lqw.eq(bo.getUpdateTime() != null, AihumanConfig::getUpdateTime, bo.getUpdateTime()); + lqw.eq(bo.getStatus() != null, AihumanConfig::getStatus, bo.getStatus()); + lqw.eq(bo.getPublish() != null, AihumanConfig::getPublish, bo.getPublish()); + return lqw; + } + + /** + * 新增交互数字人配置 + */ + @Override + public Boolean insertByBo(AihumanConfigBo bo) { + AihumanConfig add = MapstructUtils.convert(bo, AihumanConfig. class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setId(add.getId()); + } + return flag; + } + + /** + * 修改交互数字人配置 + */ + @Override + public Boolean updateByBo(AihumanConfigBo bo) { + AihumanConfig update = MapstructUtils.convert(bo, AihumanConfig. class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(AihumanConfig entity) { + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除交互数字人配置 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } +} diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanInfoServiceImpl.java b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanInfoServiceImpl.java new file mode 100644 index 00000000..cb354c71 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/service/impl/AihumanInfoServiceImpl.java @@ -0,0 +1,96 @@ +package org.ruoyi.aihuman.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 org.springframework.stereotype.Service; +import org.ruoyi.aihuman.domain.AihumanInfo; +import org.ruoyi.aihuman.domain.vo.AihumanInfoVo; +import org.ruoyi.aihuman.mapper.AihumanInfoMapper; +import org.ruoyi.aihuman.service.IAihumanInfoService; +import org.ruoyi.common.core.utils.StringUtils; +import org.ruoyi.core.page.TableDataInfo; +import org.ruoyi.core.page.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * AI人类交互信息Service业务层处理 + * + * @author QingYunAI + */ +@RequiredArgsConstructor +@Service +public class AihumanInfoServiceImpl implements IAihumanInfoService { + + private final AihumanInfoMapper baseMapper; + + /** + * 查询AI人类交互信息 + */ + @Override + public AihumanInfoVo queryById(Long id) { + return baseMapper.selectVoById(id); + } + + /** + * 查询AI人类交互信息列表 + */ + @Override + public TableDataInfo queryPageList(AihumanInfo record, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(record); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询AI人类交互信息列表 + */ + @Override + public List queryList(AihumanInfo record) { + LambdaQueryWrapper lqw = buildQueryWrapper(record); + return baseMapper.selectVoList(lqw); + } + + /** + * 构建查询条件 + */ + private LambdaQueryWrapper buildQueryWrapper(AihumanInfo record) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(record.getId() != null, AihumanInfo::getId, record.getId()); + lqw.like(StringUtils.isNotBlank(record.getName()), AihumanInfo::getName, record.getName()); + lqw.like(StringUtils.isNotBlank(record.getContent()), AihumanInfo::getContent, record.getContent()); + lqw.orderByDesc(AihumanInfo::getCreateTime); + return lqw; + } + + /** + * 新增AI人类交互信息 + */ + @Override + public int insert(AihumanInfo record) { + return baseMapper.insert(record); + } + + /** + * 修改AI人类交互信息 + */ + @Override + public int update(AihumanInfo record) { + return baseMapper.updateById(record); + } + + /** + * 批量删除AI人类交互信息 + */ + @Override + public int deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // 如果需要逻辑删除,MyBatis-Plus会自动处理 + // 这里的@TableLogic注解已经在实体类中配置 + } + return baseMapper.deleteBatchIds(ids); + } +} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_config_menu.sql b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_config_menu.sql new file mode 100644 index 00000000..55453518 --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_config_menu.sql @@ -0,0 +1,19 @@ +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666752, '交互数字人配置', '2000', '1', 'aihumanConfig', 'aihuman/aihumanConfig/index', 1, 0, 'C', '0', '0', 'aihuman:aihumanConfig:list', '#', 103, 1, sysdate(), null, null, '交互数字人配置菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666753, '交互数字人配置查询', 1971582278942666752, '1', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:query', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666754, '交互数字人配置新增', 1971582278942666752, '2', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:add', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666755, '交互数字人配置修改', 1971582278942666752, '3', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:edit', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666756, '交互数字人配置删除', 1971582278942666752, '4', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:remove', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666757, '交互数字人配置导出', 1971582278942666752, '5', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:export', '#', 103, 1, sysdate(), null, null, ''); diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_info_menu.sql b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_info_menu.sql new file mode 100644 index 00000000..a2a80efd --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/java/org/ruoyi/aihuman/sql/aihuman_info_menu.sql @@ -0,0 +1,19 @@ +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597696, '数字人信息管理', '2000', '1', 'aihumanInfo', 'aihuman/aihumanInfo/index', 1, 0, 'C', '0', '0', 'aihuman:aihumanInfo:list', '#', 103, 1, sysdate(), null, null, '数字人信息管理菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597697, '数字人信息管理查询', 1971546066781597696, '1', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:query', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597698, '数字人信息管理新增', 1971546066781597696, '2', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:add', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597699, '数字人信息管理修改', 1971546066781597696, '3', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:edit', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597700, '数字人信息管理删除', 1971546066781597696, '4', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:remove', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597701, '数字人信息管理导出', 1971546066781597696, '5', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:export', '#', 103, 1, sysdate(), null, null, ''); diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/AihumanInfoMapper.xml b/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/AihumanInfoMapper.xml new file mode 100644 index 00000000..22bd47aa --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/AihumanInfoMapper.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/aihuman/AihumanConfigMapper.xml b/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/aihuman/AihumanConfigMapper.xml new file mode 100644 index 00000000..0cbe9dcb --- /dev/null +++ b/ruoyi-modules/ruoyi-aihuman/src/main/resources/mapper/aihuman/AihumanConfigMapper.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/ChatMessageController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/ChatMessageController.java index 10ddd3c6..40a5971e 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/ChatMessageController.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/controller/chat/ChatMessageController.java @@ -45,6 +45,18 @@ public class ChatMessageController extends BaseController { return chatMessageService.queryPageList(bo, pageQuery); } + /** + * 根据会话ID查询聊天消息列表 + */ + @GetMapping("/listBySession/{sessionId}") + public TableDataInfo listBySession(@NotNull(message = "会话ID不能为空") + @PathVariable Long sessionId, + PageQuery pageQuery) { + ChatMessageBo bo = new ChatMessageBo(); + bo.setSessionId(sessionId); + return chatMessageService.queryPageList(bo, pageQuery); + } + /** * 导出聊天消息列表 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java index 55ea2ac8..fe9861ab 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/IChatService.java @@ -1,8 +1,16 @@ package org.ruoyi.chat.service.chat; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import org.ruoyi.common.chat.request.ChatRequest; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import java.util.ArrayList; +import java.util.List; + /** * 对话Service接口 * @@ -13,9 +21,36 @@ public interface IChatService { /** * 客户端发送消息到服务端 + * * @param chatRequest 请求对象 */ - SseEmitter chat(ChatRequest chatRequest,SseEmitter emitter); + SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter); + + /** + * 工作流场景:支持 langchain4j 的 StreamingChatResponseHandler + * + * @param chatRequest ruoyi-ai 的请求对象 + * @param handler langchain4j 的流式响应处理器 + */ + default void chat(ChatRequest chatRequest, StreamingChatResponseHandler handler) { + throw new UnsupportedOperationException("此服务暂不支持工作流场景"); + } + + default dev.langchain4j.model.chat.request.ChatRequest convertToLangchainRequest(ChatRequest request) { + List messages = new ArrayList<>(); + for (org.ruoyi.common.chat.entity.chat.Message msg : request.getMessages()) { + // 简单转换,您可以根据实际需求调整 + if ("user".equals(msg.getRole())) { + messages.add(UserMessage.from(msg.getContent().toString())); + } else if ("system".equals(msg.getRole())) { + messages.add(SystemMessage.from(msg.getContent().toString())); + } else if ("assistant".equals(msg.getRole())) { + messages.add(AiMessage.from(msg.getContent().toString())); + } + } + return dev.langchain4j.model.chat.request.ChatRequest.builder().messages(messages).build(); + } + /** * 获取此服务支持的模型类别 */ diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java index 8ca63b33..44a43ee4 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/ChatCostServiceImpl.java @@ -110,7 +110,7 @@ public class ChatCostServiceImpl implements IChatCostService { BigDecimal numberCost = unitPrice .multiply(BigDecimal.valueOf(batches)) .setScale(2, RoundingMode.HALF_UP); - log.debug("deductToken->按token扣费,结算token数量: {},批次数: {},单价: {},费用: {}", + log.debug("deductToken->按token扣费,结算token数量: {},批次数: {},单价: {},费用: {}", billable, batches, unitPrice, numberCost); try { @@ -137,7 +137,7 @@ public class ChatCostServiceImpl implements IChatCostService { chatToken.setUserId(chatRequest.getUserId()); chatToken.setToken(totalTokens); chatTokenService.editToken(chatToken); - + // 虽未扣费,但要更新消息的基本信息(实际token数、计费类型等) updateMessageWithoutBilling(chatRequest, tokens, chatModelVo.getModelType()); } @@ -167,12 +167,6 @@ public class ChatCostServiceImpl implements IChatCostService { chatMessageBo.setContent(chatRequest.getPrompt().trim()); chatMessageBo.setModelName(chatRequest.getModel()); -// // 基础消息信息,计费相关数据(tokens、费用、计费类型等)在扣费时统一设置 -// chatMessageBo.setTotalTokens(0); // 初始设为0,扣费时更新 -// chatMessageBo.setDeductCost(null); -// chatMessageBo.setBillingType(null); -// chatMessageBo.setRemark("用户消息"); - try { chatMessageService.insertByBo(chatMessageBo); // 保存成功后,将生成的消息ID设置到ChatRequest中 @@ -444,11 +438,11 @@ public class ChatCostServiceImpl implements IChatCostService { preCheckBalance(chatRequest); return true; // 预检查通过,余额充足 } catch (ServiceException e) { - log.debug("checkBalanceSufficient->余额不足,用户ID: {}, 模型: {}, 错误: {}", + log.debug("checkBalanceSufficient->余额不足,用户ID: {}, 模型: {}, 错误: {}", chatRequest.getUserId(), chatRequest.getModel(), e.getMessage()); return false; // 预检查失败,余额不足 } catch (Exception e) { - log.error("checkBalanceSufficient->检查余额时发生异常,用户ID: {}, 模型: {}", + log.error("checkBalanceSufficient->检查余额时发生异常,用户ID: {}, 模型: {}", chatRequest.getUserId(), chatRequest.getModel(), e); return false; // 异常情况视为余额不足,保守处理 } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java index c2697c11..003455ec 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/DeepSeekChatImpl.java @@ -1,21 +1,46 @@ package org.ruoyi.chat.service.chat.impl; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.UserMessage; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.chat.response.ChatResponse; import dev.langchain4j.model.chat.response.StreamingChatResponseHandler; import dev.langchain4j.model.openai.OpenAiStreamingChatModel; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import okhttp3.Response; import org.ruoyi.chat.enums.ChatModeType; import org.ruoyi.chat.service.chat.IChatService; import org.ruoyi.chat.support.ChatServiceHelper; +import org.ruoyi.common.chat.entity.chat.Message; import org.ruoyi.common.chat.request.ChatRequest; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.service.IChatModelService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import com.fasterxml.jackson.databind.ObjectMapper; + + +import java.util.ArrayList; +import java.util.List; /** * deepseek */ @@ -26,9 +51,24 @@ public class DeepSeekChatImpl implements IChatService { @Autowired private IChatModelService chatModelService; + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + + // 创建一个用于直接API调用的OkHttpClient + private final OkHttpClient client = new OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .build(); + @Override public SseEmitter chat(ChatRequest chatRequest, SseEmitter emitter) { ChatModelVo chatModelVo = chatModelService.selectModelByName(chatRequest.getModel()); + + // 检查是否启用深度思考且是deepseek模型 + if (Boolean.TRUE.equals(chatRequest.getEnableThinking())) { + return handleDeepSeekWithThinking(chatRequest, emitter, chatModelVo); + } + StreamingChatModel chatModel = OpenAiStreamingChatModel.builder() .baseUrl(chatModelVo.getApiHost()) .apiKey(chatModelVo.getApiKey()) @@ -39,7 +79,31 @@ public class DeepSeekChatImpl implements IChatService { .build(); // 发送流式消息 try { - chatModel.chat(chatRequest.getPrompt(), new StreamingChatResponseHandler() { + // 构建消息列表,包含历史对话消息和当前用户消息 + List messages = new ArrayList<>(); + + // 添加历史对话消息 + if (chatRequest.getMessages() != null) { + for (Message message : chatRequest.getMessages()) { + // 检查消息内容是否有效 + if (message.getContent() == null || String.valueOf(message.getContent()).trim().isEmpty()) { + continue; // 跳过空消息 + } + + if (Message.Role.SYSTEM.getName().equals(message.getRole())) { + messages.add(new SystemMessage(String.valueOf(message.getContent()))); + } else if (Message.Role.USER.getName().equals(message.getRole())) { + messages.add(new UserMessage(String.valueOf(message.getContent()))); + } else if (Message.Role.ASSISTANT.getName().equals(message.getRole())) { + messages.add(new dev.langchain4j.data.message.AiMessage(String.valueOf(message.getContent()))); + } + } + } + + // 添加当前用户消息 + messages.add(new UserMessage(chatRequest.getPrompt())); + + chatModel.chat(messages, new StreamingChatResponseHandler() { @SneakyThrows @Override public void onPartialResponse(String partialResponse) { @@ -70,8 +134,236 @@ public class DeepSeekChatImpl implements IChatService { return emitter; } + /** + * 工作流场景:支持 langchain4j handler + */ + @Override + public void chat(ChatRequest request, StreamingChatResponseHandler handler) { + log.info("workflow chat, model: {}", request.getModel()); + + ChatModelVo chatModelVo = chatModelService.selectModelByName(request.getModel()); + + StreamingChatModel chatModel = OpenAiStreamingChatModel.builder() + .baseUrl(chatModelVo.getApiHost()) + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .logRequests(true) + .logResponses(true) + .temperature(0.7) + .build(); + + try { + // 将 ruoyi-ai 的 ChatRequest 转换为 langchain4j 的格式 + dev.langchain4j.model.chat.request.ChatRequest chatRequest = convertToLangchainRequest(request); + chatModel.chat(chatRequest, handler); + } catch (Exception e) { + log.error("workflow deepseek请求失败:{}", e.getMessage(), e); + throw new RuntimeException("DeepSeek workflow chat failed: " + e.getMessage(), e); + } + } + + /** + * 处理启用深度思考的deepseek模型请求 + */ + private SseEmitter handleDeepSeekWithThinking(ChatRequest chatRequest, SseEmitter emitter, ChatModelVo chatModelVo) { + try { + // 构建请求到外部API + String url = chatModelVo.getApiHost() + "/v1/chat/completions"; + String apiKey = chatModelVo.getApiKey(); + + // 构建请求体 + Map requestBody = new HashMap<>(); + requestBody.put("model", chatModelVo.getModelName()); + requestBody.put("response_format", Map.of("type", "text")); + requestBody.put("max_tokens", 4000); // 修复:将max_tokens从81920改为4000,符合API要求 + requestBody.put("temperature", 1); + requestBody.put("top_p", 1); + requestBody.put("top_k", 50); + requestBody.put("enable_thinking", chatRequest.getEnableThinking()); + requestBody.put("stream", chatRequest.getStream()); + + // 构建消息 - DeepSeek模型不需要系统提示词 + List> messages = new ArrayList<>(); + + // 添加历史对话消息 (只添加用户和助手消息) + if (chatRequest.getMessages() != null) { + for (Message message : chatRequest.getMessages()) { + // 检查消息内容是否有效 + if (message.getContent() == null || String.valueOf(message.getContent()).trim().isEmpty()) { + continue; // 跳过空消息 + } + + // DeepSeek模型在深度思考模式下只接受user和assistant角色的消息 + if (Message.Role.SYSTEM.getName().equals(message.getRole())) { + // 跳过系统消息 + continue; + } + + Map historyMessage = new HashMap<>(); + historyMessage.put("role", message.getRole()); + historyMessage.put("content", String.valueOf(message.getContent())); + messages.add(historyMessage); + } + } + + // 添加当前用户消息 + Map userMessage = new HashMap<>(); + userMessage.put("role", "user"); + userMessage.put("content", String.valueOf(chatRequest.getPrompt())); + messages.add(userMessage); + + requestBody.put("messages", messages); + + // 创建ObjectMapper实例 + com.fasterxml.jackson.databind.ObjectMapper objectMapper = new com.fasterxml.jackson.databind.ObjectMapper(); + String requestBodyStr = objectMapper.writeValueAsString(requestBody); + + // 打印请求体用于调试 + log.info("打印请求体: {}", requestBodyStr); + + // 创建请求 + Request request = new Request.Builder() + .url(url) + .header("Authorization", "Bearer " + apiKey) + .header("Content-Type", "application/json") + .post(RequestBody.create(requestBodyStr, JSON)) + .build(); + + // 执行异步请求 + this.client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + try { + log.error("深度思考请求失败: {}", e.getMessage(), e); + emitter.send("深度思考请求失败: " + e.getMessage()); + emitter.complete(); + } catch (IOException ioException) { + log.error("发送错误消息失败: {}", ioException.getMessage(), ioException); + } + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (!response.isSuccessful()) { + // 打印完整的错误响应体 + String errorBody = ""; + if (response.body() != null) { + errorBody = response.body().string(); + } + log.error("深度思考请求失败,状态码: {},响应体: {}", response.code(), errorBody); + try { + emitter.send("深度思考请求失败,状态码: " + response.code() + ",响应体: " + errorBody); + emitter.complete(); + return; + } catch (IOException e) { + log.error("发送错误消息失败: {}", e.getMessage(), e); + return; + } + } + + try (ResponseBody responseBody = response.body()) { + if (responseBody == null) { + log.error("响应体为空"); + emitter.send("响应体为空"); + emitter.complete(); + return; + } + + // 流式读取响应 + processThinkingResponse(responseBody, emitter); + } catch (Exception e) { + log.error("处理响应时出错: {}", e.getMessage(), e); + try { + emitter.send("处理响应时出错: " + e.getMessage()); + emitter.complete(); + } catch (IOException ioException) { + log.error("发送错误消息失败: {}", ioException.getMessage(), ioException); + } + } + } + }); + + } catch (Exception e) { + log.error("处理深度思考请求时出错: {}", e.getMessage(), e); + ChatServiceHelper.onStreamError(emitter, e.getMessage()); + } + return emitter; + } + + /** + * 处理深度思考的流式响应(边解析边推送) + */ + private void processThinkingResponse(ResponseBody responseBody, SseEmitter emitter) throws IOException { + // 标记是否进入正式回答阶段 + boolean thinkingComplete = false; + + try (BufferedReader reader = new BufferedReader(responseBody.charStream())) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.startsWith("data: ")) { + continue; + } + + String jsonData = line.substring(6).trim(); + if ("[DONE]".equals(jsonData)) { + break; + } + + try { + ObjectMapper mapper = new ObjectMapper(); + Map chunk = mapper.readValue(jsonData, Map.class); + + if (chunk.containsKey("choices") && chunk.get("choices") instanceof List) { + List> choices = (List>) chunk.get("choices"); + if (!choices.isEmpty()) { + Map choice = choices.get(0); + if (choice.containsKey("delta") && choice.get("delta") instanceof Map) { + Map delta = (Map) choice.get("delta"); + + // 推送思考过程 + if (delta.containsKey("reasoning_content") && delta.get("reasoning_content") != null) { + String reasoningChunk = delta.get("reasoning_content").toString(); + emitter.send(SseEmitter.event().data(reasoningChunk).name("thinking")); + log.debug("Reasoning Chunk: {}", reasoningChunk); + } + + // 推送正式回答 + if (delta.containsKey("content") && delta.get("content") != null) { + String content = delta.get("content").toString(); + + // 第一次进入回答阶段时,加个提示头 + if (!thinkingComplete) { + emitter.send(SseEmitter.event().data("\n\n回答内容:\n").name("answer-header")); + thinkingComplete = true; + } + + emitter.send(SseEmitter.event().data(content).name("answer")); + log.debug("Answer Chunk:{}", content); + } + } + } + } + } catch (Exception e) { + log.warn("解析JSON数据失败,忽略本行: {}", jsonData, e); + } + } + + emitter.complete(); + log.info("深度思考流式响应完成"); + } catch (IOException e) { + log.error("读取响应流时出错: {}", e.getMessage(), e); + try { + emitter.send(SseEmitter.event().data("读取响应流时出错: " + e.getMessage()).name("error")); + emitter.complete(); + } catch (IOException ioException) { + log.error("发送错误消息失败: {}", ioException.getMessage(), ioException); + } + } + } + + @Override public String getCategory() { return ChatModeType.DEEPSEEK.getCode(); } -} +} \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java index 0dcf3318..6b8f72d6 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/OpenAIServiceImpl.java @@ -43,7 +43,7 @@ public class OpenAIServiceImpl implements IChatService { this.chatClient = chatClientBuilder .defaultOptions( OpenAiChatOptions.builder().model("gpt-4o-mini").build()) - .defaultToolCallbacks(new SyncMcpToolCallbackProvider(mcpSyncClients)) + .defaultTools(new SyncMcpToolCallbackProvider(mcpSyncClients)) .build(); } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java index 4128b84a..8175c47d 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/QianWenAiChatServiceImpl.java @@ -8,13 +8,13 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.ruoyi.chat.enums.ChatModeType; import org.ruoyi.chat.service.chat.IChatService; +import org.ruoyi.chat.support.ChatServiceHelper; import org.ruoyi.common.chat.request.ChatRequest; import org.ruoyi.domain.vo.ChatModelVo; import org.ruoyi.service.IChatModelService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import org.ruoyi.chat.support.ChatServiceHelper; /** @@ -22,7 +22,7 @@ import org.ruoyi.chat.support.ChatServiceHelper; */ @Service @Slf4j -public class QianWenAiChatServiceImpl implements IChatService { +public class QianWenAiChatServiceImpl implements IChatService { @Autowired private IChatModelService chatModelService; @@ -37,7 +37,6 @@ public class QianWenAiChatServiceImpl implements IChatService { .build(); - // 发送流式消息 try { model.chat(chatRequest.getPrompt(), new StreamingChatResponseHandler() { @@ -70,11 +69,34 @@ public class QianWenAiChatServiceImpl implements IChatService { } + /** + * 工作流场景:支持 langchain4j handler + */ + @Override + public void chat(ChatRequest request, StreamingChatResponseHandler handler) { + log.info("workflow chat, model: {}", request.getModel()); + + ChatModelVo chatModelVo = chatModelService.selectModelByName(request.getModel()); + + StreamingChatModel model = QwenStreamingChatModel.builder() + .apiKey(chatModelVo.getApiKey()) + .modelName(chatModelVo.getModelName()) + .build(); + + try { + // 将 ruoyi-ai 的 ChatRequest 转换为 langchain4j 的格式 + dev.langchain4j.model.chat.request.ChatRequest chatRequest = convertToLangchainRequest(request); + model.chat(chatRequest, handler); + } catch (Exception e) { + log.error("workflow 千问请求失败:{}", e.getMessage(), e); + throw new RuntimeException("QianWen workflow chat failed: " + e.getMessage(), e); + } + } + @Override public String getCategory() { return ChatModeType.QIANWEN.getCode(); } - } diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java index 0250177a..e9a38259 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/chat/impl/SseServiceImpl.java @@ -100,24 +100,7 @@ public class SseServiceImpl implements ISseService { // 设置用户id chatRequest.setUserId(LoginHelper.getUserId()); - - //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) - //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) - //待优化的地方 (这里请前端提交send的时候传递uuid进来或者sessionId) - { - // 设置会话id - if (chatRequest.getUuid() == null) { - //暂时随机生成会话id - chatRequest.setSessionId(System.currentTimeMillis()); - } else { - //这里或许需要修改一下,这里应该用uuid 或者 前端传递 sessionId - chatRequest.setSessionId(chatRequest.getUuid()); - } - } - - - - chatRequest.setUserId(chatCostService.getUserId()); + // 设置会话id if (chatRequest.getSessionId() == null) { ChatSessionBo chatSessionBo = new ChatSessionBo(); chatSessionBo.setUserId(chatCostService.getUserId()); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java index 089a0ee8..6a588185 100644 --- a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/chat/service/knowledge/KnowledgeInfoServiceImpl.java @@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import org.ruoyi.chain.loader.ResourceLoader; import org.ruoyi.chain.loader.ResourceLoaderFactory; +import org.ruoyi.chat.enums.ChatModeType; import org.ruoyi.common.core.domain.model.LoginUser; import org.ruoyi.common.core.utils.MapstructUtils; import org.ruoyi.common.core.utils.StringUtils; @@ -91,45 +92,66 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService { public TableDataInfo queryPageListByRole(KnowledgeInfoBo bo, PageQuery pageQuery) { // 查询用户关联角色 LoginUser loginUser = LoginHelper.getLoginUser(); - if (StringUtils.isEmpty(loginUser.getKroleGroupIds()) || StringUtils.isEmpty(loginUser.getKroleGroupType())) { - return new TableDataInfo<>(); - } - - // 角色/角色组id列表 - List groupIdList = Arrays.stream(loginUser.getKroleGroupIds().split(",")) - .filter(StringUtils::isNotEmpty) - .toList(); - List knowledgeRoles; - LambdaQueryWrapper roleLqw = Wrappers.lambdaQuery(); - if ("role".equals(loginUser.getKroleGroupType())) { - roleLqw.in(KnowledgeRole::getId, groupIdList); - } else { - roleLqw.in(KnowledgeRole::getGroupId, groupIdList); - } - knowledgeRoles = knowledgeRoleMapper.selectList(roleLqw); - - if (CollectionUtils.isEmpty(knowledgeRoles)) { - return new TableDataInfo<>(); - } - - // 查询知识库id列表 - LambdaQueryWrapper relationLqw = Wrappers.lambdaQuery(); - relationLqw.in(KnowledgeRoleRelation::getKnowledgeRoleId, knowledgeRoles.stream().map(KnowledgeRole::getId).filter(Objects::nonNull).collect(Collectors.toList())); - List knowledgeRoleRelations = knowledgeRoleRelationMapper.selectList(relationLqw); - - if (CollectionUtils.isEmpty(knowledgeRoleRelations)) { - return new TableDataInfo<>(); - } - + + // 构建查询条件 LambdaQueryWrapper lqw = buildQueryWrapper(bo); - // 在查询用户创建的知识库条件下,拼接角色分配知识库 - lqw.or(q -> q.in( - KnowledgeInfo::getId, - knowledgeRoleRelations.stream() - .map(KnowledgeRoleRelation::getKnowledgeId) - .filter(Objects::nonNull) - .collect(Collectors.toList()) - )); + + // 管理员用户直接查询所有数据 + if (Objects.equals(loginUser.getUserId(), 1L)) { + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + // 检查用户是否配置了角色信息 + if (StringUtils.isNotEmpty(loginUser.getKroleGroupIds()) && StringUtils.isNotEmpty(loginUser.getKroleGroupType())) { + // 角色/角色组id列表 + List groupIdList = Arrays.stream(loginUser.getKroleGroupIds().split(",")) + .filter(StringUtils::isNotEmpty) + .toList(); + + // 查询用户关联的角色 + List knowledgeRoles = new ArrayList<>(); + LambdaQueryWrapper roleLqw = Wrappers.lambdaQuery(); + if ("role".equals(loginUser.getKroleGroupType())) { + roleLqw.in(KnowledgeRole::getId, groupIdList); + } else { + roleLqw.in(KnowledgeRole::getGroupId, groupIdList); + } + knowledgeRoles = knowledgeRoleMapper.selectList(roleLqw); + + // 如果用户有关联角色 + if (!CollectionUtils.isEmpty(knowledgeRoles)) { + // 查询这些角色关联的知识库 + LambdaQueryWrapper relationLqw = Wrappers.lambdaQuery(); + relationLqw.in(KnowledgeRoleRelation::getKnowledgeRoleId, + knowledgeRoles.stream().map(KnowledgeRole::getId).filter(Objects::nonNull).collect(Collectors.toList())); + List knowledgeRoleRelations = knowledgeRoleRelationMapper.selectList(relationLqw); + + // 如果角色关联了知识库 + if (!CollectionUtils.isEmpty(knowledgeRoleRelations)) { + // 查询用户自己的知识库和角色分配的知识库 + lqw.and(q -> q.eq(KnowledgeInfo::getUid, loginUser.getUserId()) + .or() + .in(KnowledgeInfo::getId, + knowledgeRoleRelations.stream() + .map(KnowledgeRoleRelation::getKnowledgeId) + .filter(Objects::nonNull) + .collect(Collectors.toList()) + ) + ); + } else { + // 用户没有关联任何知识库,只显示自己的 + lqw.eq(KnowledgeInfo::getUid, loginUser.getUserId()); + } + } else { + // 用户没有关联角色,只显示自己的 + lqw.eq(KnowledgeInfo::getUid, loginUser.getUserId()); + } + } else { + // 用户没有配置角色信息,只显示自己的 + lqw.eq(KnowledgeInfo::getUid, loginUser.getUserId()); + } + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); return TableDataInfo.build(result); } @@ -216,8 +238,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService { } baseMapper.insert(knowledgeInfo); if (knowledgeInfo != null) { - vectorStoreService.createSchema(String.valueOf(knowledgeInfo.getId()), - bo.getVectorModelName()); + vectorStoreService.createSchema(String.valueOf(knowledgeInfo.getId()), bo.getEmbeddingModelName()); } } else { baseMapper.updateById(knowledgeInfo); @@ -258,6 +279,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService { knowledgeAttach.setDocType(fileName.substring(fileName.lastIndexOf(".") + 1)); String content = ""; ResourceLoader resourceLoader = resourceLoaderFactory.getLoaderByFileType(knowledgeAttach.getDocType()); + // 文档分段入库 List fids = new ArrayList<>(); try { content = resourceLoader.getContent(file.getInputStream()); @@ -265,6 +287,7 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService { List knowledgeFragmentList = new ArrayList<>(); if (CollUtil.isNotEmpty(chunkList)) { for (int i = 0; i < chunkList.size(); i++) { + // 生成知识片段ID String fid = RandomUtil.randomString(10); fids.add(fid); KnowledgeFragment knowledgeFragment = new KnowledgeFragment(); @@ -291,13 +314,16 @@ public class KnowledgeInfoServiceImpl implements IKnowledgeInfoService { // 通过向量模型查询模型信息 ChatModelVo chatModelVo = chatModelService.selectModelByName(knowledgeInfoVo.getEmbeddingModelName()); - + // 未查到指定模型时,回退为向量分类最高优先级模型 + if (chatModelVo == null) { + chatModelVo = chatModelService.selectModelByCategoryWithHighestPriority(ChatModeType.VECTOR.getCode()); + } StoreEmbeddingBo storeEmbeddingBo = new StoreEmbeddingBo(); storeEmbeddingBo.setKid(kid); storeEmbeddingBo.setDocId(docId); storeEmbeddingBo.setFids(fids); storeEmbeddingBo.setChunkList(chunkList); - storeEmbeddingBo.setVectorModelName(knowledgeInfoVo.getVectorModelName()); + storeEmbeddingBo.setVectorStoreName(knowledgeInfoVo.getVectorModelName()); storeEmbeddingBo.setEmbeddingModelName(knowledgeInfoVo.getEmbeddingModelName()); storeEmbeddingBo.setApiKey(chatModelVo.getApiKey()); storeEmbeddingBo.setBaseUrl(chatModelVo.getApiHost()); diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/ChatClientConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/ChatClientConfig.java new file mode 100644 index 00000000..3138f381 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/ChatClientConfig.java @@ -0,0 +1,20 @@ +package org.ruoyi.mcp.config; + +import org.springframework.ai.chat.client.ChatClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ChatClientConfig { + + @Autowired + private DynamicMcpToolCallbackProvider dynamicMcpToolCallbackProvider; + + @Bean + public ChatClient chatClient(ChatClient.Builder builder) { + return builder + .defaultTools(java.util.List.of(dynamicMcpToolCallbackProvider.createToolCallbackProvider().getToolCallbacks())) + .build(); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/DynamicMcpToolCallbackProvider.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/DynamicMcpToolCallbackProvider.java new file mode 100644 index 00000000..32cb1c49 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/DynamicMcpToolCallbackProvider.java @@ -0,0 +1,97 @@ +package org.ruoyi.mcp.config; + +import org.ruoyi.mcp.service.McpInfoService; +import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.tool.ToolCallback; +import org.springframework.ai.tool.ToolCallbackProvider; +import org.springframework.ai.tool.definition.ToolDefinition; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.util.*; +/** + * 动态MCP工具回调提供者 + * + * 这个类有大问题 ,没有测试!!!!!!! + */ +@Component +public class DynamicMcpToolCallbackProvider { + + @Autowired + private McpInfoService mcpInfoService; + + @Autowired + private McpProcessManager mcpProcessManager; + + @Autowired + private McpToolInvoker mcpToolInvoker; + + /** + * 创建工具回调提供者 + */ + public ToolCallbackProvider createToolCallbackProvider() { + List callbacks = new ArrayList<>(); + List activeServerNames = mcpInfoService.getActiveServerNames(); + + for (String serverName : activeServerNames) { + FunctionCallback callback = createMcpToolCallback(serverName); + callbacks.add(callback); + } + + return ToolCallbackProvider.from(callbacks); + } + + private FunctionCallback createMcpToolCallback(String serverName) { + return new ToolCallback() { + @Override + public ToolDefinition getToolDefinition() { + // 获取工具配置 + McpServerConfig config = mcpInfoService.getToolConfigByName(serverName); + if (config == null) { + // 返回一个默认的ToolDefinition + return ToolDefinition.builder() + .name(serverName) + .description("MCP tool for " + serverName) + .build(); + } + // 根据config创建ToolDefinition + return ToolDefinition.builder() + .name(serverName) + .description(config.getDescription()) // 假设McpServerConfig有getDescription方法 + .build(); + } + + @Override + public String call(String toolInput) { + try { + // 获取工具配置 + McpServerConfig config = mcpInfoService.getToolConfigByName(serverName); + if (config == null) { + return "{\"error\": \"MCP tool not found: " + serverName + "\", \"serverName\": \"" + serverName + "\"}"; + } + + // 确保 MCP 服务器正在运行 + ensureMcpServerRunning(serverName, config); + + // 调用 MCP 工具 + Object result = mcpToolInvoker.invokeTool(serverName, toolInput); + + return "{\"result\": \"" + result.toString() + "\", \"serverName\": \"" + serverName + "\"}"; + } catch (Exception e) { + return "{\"error\": \"MCP tool execution failed: " + e.getMessage() + "\", \"serverName\": \"" + serverName + "\"}"; + } + } + }; + } + + private void ensureMcpServerRunning(String serverName, McpServerConfig config) { + if (!mcpProcessManager.isMcpServerRunning(serverName)) { + boolean started = mcpProcessManager.startMcpServer( + serverName, + config + ); + if (!started) { + throw new RuntimeException("Failed to start MCP server: " + serverName); + } + } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpConfig.java new file mode 100644 index 00000000..7f186388 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpConfig.java @@ -0,0 +1,20 @@ +package org.ruoyi.mcp.config; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import java.util.List; + +public class McpConfig { + @JsonProperty("mcpServers") + private Map mcpServers; + + // getters and setters + public Map getMcpServers() { + return mcpServers; + } + + public void setMcpServers(Map mcpServers) { + this.mcpServers = mcpServers; + } +} + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessManager.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessManager.java new file mode 100644 index 00000000..3c6ebee8 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessManager.java @@ -0,0 +1,341 @@ +package org.ruoyi.mcp.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.ruoyi.mcp.service.McpInfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import java.io.*; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.*; +@Slf4j +@Component +public class McpProcessManager { + + private final Map runningProcesses = new ConcurrentHashMap<>(); + private final Map mcpServerProcesses = new ConcurrentHashMap<>(); + private final ExecutorService executorService = Executors.newCachedThreadPool(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final Map processWriters = new ConcurrentHashMap<>(); + private final Map processReaders = new ConcurrentHashMap<>(); + + @Autowired + private McpInfoService mcpInfoService; + /** + * 启动 MCP 服务器进程(支持环境变量) + */ + public boolean startMcpServer(String serverName, McpServerConfig serverConfig) { + try { + log.info("启动MCP服务器进程: {}", serverName); + + ProcessBuilder processBuilder = new ProcessBuilder(); + + // 构建命令 + List commandList = buildCommandListWithFullPaths(serverConfig.getCommand(), serverConfig.getArgs()); + + + processBuilder.command(commandList); + + // 设置工作目录 + if (serverConfig.getWorkingDirectory() != null) { + processBuilder.directory(new File(serverConfig.getWorkingDirectory())); + } else { + processBuilder.directory(new File(System.getProperty("user.dir"))); + } + + // 设置环境变量 + if (serverConfig.getEnv() != null) { + processBuilder.environment().putAll(serverConfig.getEnv()); + } + // ===== 关键:在 start 之前打印完整的调试信息 ===== + System.out.println("=== ProcessBuilder 调试信息 ==="); + System.out.println("完整命令列表: " + commandList); + System.out.println("命令字符串: " + String.join(" ", commandList)); + System.out.println("工作目录: " + processBuilder.directory()); + System.out.println("================================"); + //https://www.modelscope.cn/mcp/servers/@worryzyy/howtocook-mcp + + // 启动进程 + Process process = processBuilder.start(); + // 获取输入输出流 + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + + processWriters.put(serverName, writer); + processReaders.put(serverName, reader); + + // 存储进程引用 + McpServerProcess serverProcess = new McpServerProcess(serverName, process, serverConfig); + mcpServerProcesses.put(serverName, serverProcess); + // 启动日志读取线程 + executorService.submit(() -> readProcessOutput(serverName, process)); + // 启动 MCP 通信监听线程 + executorService.submit(() -> listenMcpMessages(serverName, reader)); + + // 更新服务器状态 + mcpInfoService.enableTool(serverName); + boolean isAlive = process.isAlive(); + + if (isAlive) { + log.info("成功启动MCP服务器: {} 命令: {}", serverName, commandList); + } else { + System.err.println("✗ MCP server [" + serverName + "] failed to start"); + // 读取错误输出 + readErrorOutput(process); + } + return true; + + } catch (IOException e) { + log.error("启动MCP服务器进程失败: " + serverName, e); + + // 更新服务器状态为禁用 + //mcpInfoService.disableTool(serverName); + + throw new RuntimeException("Failed to start MCP server process: " + e.getMessage(), e); + + } + + } + /** + * 发送 MCP 消息 + */ + public boolean sendMcpMessage(String serverName, Map message) { + try { + BufferedWriter writer = processWriters.get(serverName); + if (writer == null) { + System.err.println("未找到服务器 [" + serverName + "] 的输出流"); + return false; + } + + String jsonMessage = objectMapper.writeValueAsString(message); + System.out.println("发送消息到 [" + serverName + "]: " + jsonMessage); + + writer.write(jsonMessage); + writer.newLine(); + writer.flush(); + + return true; + } catch (Exception e) { + System.err.println("发送消息到 [" + serverName + "] 失败: " + e.getMessage()); + return false; + } + } + + /** + * 监听 MCP 消息 + */ + private void listenMcpMessages(String serverName, BufferedReader reader) { + try { + String line; + while ((line = reader.readLine()) != null) { + try { + // 解析收到的 JSON 消息 + Map message = objectMapper.readValue(line, Map.class); + System.out.println("收到来自 [" + serverName + "] 的消息: " + message); + + // 处理不同类型的 MCP 消息 + handleMessage(serverName, message); + + } catch (Exception e) { + System.err.println("解析消息失败: " + line + ", 错误: " + e.getMessage()); + // 如果不是 JSON,当作普通日志输出 + System.out.println("[" + serverName + "] 日志: " + line); + } + } + } catch (IOException e) { + if (isMcpServerRunning(serverName)) { + System.err.println("监听 [" + serverName + "] 消息时出错: " + e.getMessage()); + } + } + } + + + /** + * 处理 MCP 消息(更新版本) + */ + private void handleMessage(String serverName, Map message) { + String type = (String) message.get("type"); + if (type == null) return; + + switch (type) { + case "ready": + System.out.println("MCP 服务器 [" + serverName + "] 准备就绪"); + break; + case "response": + System.out.println("MCP 服务器 [" + serverName + "] 响应: " + message.get("data")); + break; + case "error": + System.err.println("MCP 服务器 [" + serverName + "] 错误: " + message.get("message")); + break; + default: + System.out.println("MCP 服务器 [" + serverName + "] 未知消息类型: " + type); + break; + } + } + + /** + * 构建命令列表 + */ + private List buildCommandListWithFullPaths(String command, List args) { + List commandList = new ArrayList<>(); + + if (isWindows() && "npx".equalsIgnoreCase(command)) { + // 在 Windows 上使用 cmd.exe 包装以确保兼容性 + commandList.add("cmd.exe"); + commandList.add("/c"); + commandList.add("npx"); + commandList.addAll(args); + } else { + commandList.add(command); + commandList.addAll(args); + } + + return commandList; + } + /** + * 检查是否为 Windows 系统 + */ + private boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("windows"); + } + + + /** + * 读取错误输出 + */ + private void readErrorOutput(Process process) { + try { + InputStream errorStream = process.getErrorStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream)); + String line; + while ((line = reader.readLine()) != null) { + System.err.println("ERROR: " + line); + } + } catch (Exception e) { + System.err.println("Failed to read error output: " + e.getMessage()); + } + } + /** + * 停止 MCP 服务器进程 + */ + public boolean stopMcpServer(String serverName) { + Process process = runningProcesses.remove(serverName); + BufferedWriter writer = processWriters.remove(serverName); + BufferedReader reader = processReaders.remove(serverName); + try { + if (writer != null) { + writer.close(); + } + if (reader != null) { + reader.close(); + } + } catch (IOException e) { + System.err.println("关闭流时出错: " + e.getMessage()); + } + // 更新服务器状态为禁用 + mcpInfoService.disableTool(serverName); + + if (process != null && process.isAlive()) { + process.destroy(); + try { + if (!process.waitFor(5, TimeUnit.SECONDS)) { + process.destroyForcibly(); + process.waitFor(1, TimeUnit.SECONDS); + } + System.out.println("MCP server [" + serverName + "] stopped"); + return true; + } catch (InterruptedException e) { + process.destroyForcibly(); + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + + /** + * 重启 MCP 服务器进程 + */ + public boolean restartMcpServer(String serverName, String command, List args, Map env) { + stopMcpServer(serverName); + McpServerConfig mcpServerConfig = new McpServerConfig(); + mcpServerConfig.setCommand(command); + mcpServerConfig.setArgs(args); + mcpServerConfig.setEnv(env); + return startMcpServer(serverName, mcpServerConfig); + } + + /** + * 检查 MCP 服务器是否运行 + */ + public boolean isMcpServerRunning(String serverName) { + Process process = runningProcesses.get(serverName); + return process != null && process.isAlive(); + } + + /** + * 获取所有运行中的 MCP 服务器 + */ + public Set getRunningMcpServers() { + Set running = new HashSet<>(); + for (Map.Entry entry : runningProcesses.entrySet()) { + if (entry.getValue().isAlive()) { + running.add(entry.getKey()); + } + } + return running; + } + + /** + * 获取进程信息 + */ + public McpServerProcess getProcessInfo(String serverName) { + return mcpServerProcesses.get(serverName); + } + + private void readProcessOutput(String serverName, Process process) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null && process.isAlive()) { + System.out.println("[" + serverName + "] " + line); + } + } catch (IOException e) { + System.err.println("Error reading output from " + serverName + ": " + e.getMessage()); + } + } + + private String getProcessId(Process process) { + try { + // Java 9+ 可以直接获取 PID + return String.valueOf(process.pid()); + } catch (Exception e) { + // Java 8 兼容处理 + return "unknown"; + } + } + + /** + * MCP服务器进程信息 + */ + public static class McpServerProcess { + private final String name; + private final Process process; + private final McpServerConfig config; + private final LocalDateTime startTime; + + public McpServerProcess(String name, Process process, McpServerConfig config) { + this.name = name; + this.process = process; + this.config = config; + this.startTime = LocalDateTime.now(); + } + + // Getters + public String getName() { return name; } + public Process getProcess() { return process; } + public McpServerConfig getConfig() { return config; } + public LocalDateTime getStartTime() { return startTime; } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessSSEManager.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessSSEManager.java new file mode 100644 index 00000000..4ac1fab5 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpProcessSSEManager.java @@ -0,0 +1,287 @@ +package org.ruoyi.mcp.config; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.info.ProcessInfo; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.Disposable; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + + + +@Component +public class McpProcessSSEManager { + + private final Map runningProcesses = new ConcurrentHashMap<>(); + private final Map processInfos = new ConcurrentHashMap<>(); + private final Map sseClients = new ConcurrentHashMap<>(); + private final Map sseSubscriptions = new ConcurrentHashMap<>(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private McpSSEToolInvoker mcpToolInvoker; + + /** + * 启动 MCP 服务器进程(SSE 模式) + */ + public boolean startMcpServer(String serverName, String command, List args, Map env) { + try { + System.out.println("准备启动 MCP 服务器 (SSE 模式): " + serverName); + + // 如果已经运行,先停止 + if (isMcpServerRunning(serverName)) { + stopMcpServer(serverName); + } + + // 构建命令 + List commandList = buildCommandList(command, args); + + // 创建 ProcessBuilder + ProcessBuilder processBuilder = new ProcessBuilder(commandList); + processBuilder.redirectErrorStream(true); + + // 设置工作目录 + String workingDir = System.getProperty("user.dir"); + processBuilder.directory(new File(workingDir)); + + // 打印调试信息 + System.out.println("=== ProcessBuilder 调试信息 ==="); + System.out.println("完整命令列表: " + commandList); + System.out.println("================================"); + + // 执行命令 + Process process = processBuilder.start(); + runningProcesses.put(serverName, process); + + ProcessInfo processInfo = new ProcessInfo(); + processInfo.setStartTime(System.currentTimeMillis()); + processInfo.setPid(getProcessId(process)); + processInfos.put(serverName, processInfo); + + // 启动日志读取线程 + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.submit(() -> readProcessOutput(serverName, process)); + + // 等待进程启动 + Thread.sleep(3000); + boolean isAlive = process.isAlive(); + + if (isAlive) { + System.out.println("✓ MCP 服务器 [" + serverName + "] 启动成功"); + // 初始化 SSE 连接 + initializeSseConnection(serverName); + } else { + System.err.println("✗ MCP 服务器 [" + serverName + "] 启动失败"); + readErrorOutput(process); + } + + return isAlive; + + } catch (Exception e) { + System.err.println("✗ 启动 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + private String getProcessId(Process process) { + try { + return String.valueOf(process.pid()); + } catch (Exception e) { + return "unknown"; + } + } + private void readProcessOutput(String serverName, Process process) { + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null && process.isAlive()) { + System.out.println("[" + serverName + "] " + line); + } + } catch (IOException e) { + System.err.println("Error reading output from " + serverName + ": " + e.getMessage()); + } + } + /** + * 读取错误输出 + */ + private void readErrorOutput(Process process) { + try { + InputStream errorStream = process.getErrorStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(errorStream)); + String line; + while ((line = reader.readLine()) != null) { + System.err.println("ERROR: " + line); + } + } catch (Exception e) { + System.err.println("Failed to read error output: " + e.getMessage()); + } + } + /** + * 初始化 SSE 连接 + */ + private void initializeSseConnection(String serverName) { + try { + // 创建 WebClient 用于 SSE 连接 + WebClient webClient = WebClient.builder() + .baseUrl("http://localhost:3000") // 假设默认端口 3000 + .build(); + + sseClients.put(serverName, webClient); + + // 建立 SSE 连接 + String sseUrl = "/sse/" + serverName; // SSE 端点 + + Disposable subscription = webClient.get() + .uri(sseUrl) + .accept(MediaType.TEXT_EVENT_STREAM) + .retrieve() + .bodyToFlux(String.class) + .subscribe( + event -> handleSseEvent(serverName, event), + error -> System.err.println("SSE 连接错误 [" + serverName + "]: " + error.getMessage()), + () -> System.out.println("SSE 连接完成 [" + serverName + "]") + ); + + sseSubscriptions.put(serverName, subscription); + System.out.println("✓ SSE 连接建立成功 [" + serverName + "]"); + + } catch (Exception e) { + System.err.println("✗ 建立 SSE 连接失败 [" + serverName + "]: " + e.getMessage()); + } + } + + /** + * 处理 SSE 事件 + */ + private void handleSseEvent(String serverName, String event) { + try { + System.out.println("收到来自 [" + serverName + "] 的 SSE 事件: " + event); + + // 解析 SSE 事件 + if (event.startsWith("data: ")) { + String jsonData = event.substring(6); // 移除 "data: " 前缀 + Map message = objectMapper.readValue(jsonData, Map.class); + + // 处理不同类型的事件 + String type = (String) message.get("type"); + if ("tool_response".equals(type)) { + mcpToolInvoker.handleSseResponse(serverName, message); + } else if ("tool_error".equals(type)) { + mcpToolInvoker.handleSseError(serverName, message); + } else if ("progress".equals(type)) { + handleProgressEvent(serverName, message); + } else { + System.out.println("[" + serverName + "] 未知事件类型: " + type); + } + } + + } catch (Exception e) { + System.err.println("处理 SSE 事件失败 [" + serverName + "]: " + e.getMessage()); + } + } + + /** + * 处理进度事件 + */ + private void handleProgressEvent(String serverName, Map message) { + Object progress = message.get("progress"); + Object messageText = message.get("message"); + System.out.println("[" + serverName + "] 进度: " + progress + " - " + messageText); + } + + + + /** + * 构建命令列表 + */ + private List buildCommandList(String command, List args) { + List commandList = new ArrayList<>(); + + if (isWindows() && "npx".equalsIgnoreCase(command)) { + commandList.add("cmd.exe"); + commandList.add("/c"); + commandList.add("npx"); + commandList.addAll(args); + } else { + commandList.add(command); + commandList.addAll(args); + } + + return commandList; + } + /** + * 检查是否为 Windows 系统 + */ + private boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("windows"); + } + + /** + * 停止 MCP 服务器进程 + */ + public boolean stopMcpServer(String serverName) { + // 停止 SSE 连接 + Disposable subscription = sseSubscriptions.remove(serverName); + if (subscription != null && !subscription.isDisposed()) { + subscription.dispose(); + } + + sseClients.remove(serverName); + + // 停止进程 + Process process = runningProcesses.remove(serverName); + ProcessInfo processInfo = processInfos.remove(serverName); + + if (process != null && process.isAlive()) { + process.destroy(); + try { + if (!process.waitFor(10, TimeUnit.SECONDS)) { + process.destroyForcibly(); + process.waitFor(2, TimeUnit.SECONDS); + } + System.out.println("MCP 服务器 [" + serverName + "] 已停止"); + return true; + } catch (InterruptedException e) { + process.destroyForcibly(); + Thread.currentThread().interrupt(); + return false; + } + } + return false; + } + /** + * 检查 MCP 服务器是否运行 + */ + public boolean isMcpServerRunning(String serverName) { + Process process = runningProcesses.get(serverName); + return process != null && process.isAlive(); + } + /** + * 进程信息类 + */ + public static class ProcessInfo { + private String pid; + private long startTime; + + public String getPid() { return pid; } + public void setPid(String pid) { this.pid = pid; } + + public long getStartTime() { return startTime; } + public void setStartTime(long startTime) { this.startTime = startTime; } + + public long getUptime() { + return System.currentTimeMillis() - startTime; + } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpSSEToolInvoker.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpSSEToolInvoker.java new file mode 100644 index 00000000..682f74b7 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpSSEToolInvoker.java @@ -0,0 +1,206 @@ +package org.ruoyi.mcp.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Component +public class McpSSEToolInvoker { + + + private final Map> pendingRequests = new ConcurrentHashMap<>(); + private final AtomicLong requestIdCounter = new AtomicLong(0); + + /** + * 调用 MCP 工具(SSE 模式) + */ + public Object invokeTool(String serverName, Object parameters) { + try { + // 生成请求ID + String requestId = "req_" + requestIdCounter.incrementAndGet(); + + // 创建 CompletableFuture 等待响应 + CompletableFuture future = new CompletableFuture<>(); + pendingRequests.put(requestId, future); + + // 构造 MCP 调用请求 + Map callRequest = new HashMap<>(); + callRequest.put("requestId", requestId); + callRequest.put("serverName", serverName); + callRequest.put("parameters", convertToMap(parameters)); + callRequest.put("timestamp", System.currentTimeMillis()); + + System.out.println("通过 SSE 调用 MCP 工具 [" + serverName + "] 参数: " + parameters); + + // 发送请求到 MCP 服务器(通过 HTTP POST) + sendSseToolCall(serverName, callRequest); + + // 等待响应(超时 30 秒) + Object result = future.get(30, TimeUnit.SECONDS); + + System.out.println("MCP 工具 [" + serverName + "] 调用成功,响应: " + result); + + return result; + + } catch (Exception e) { + System.err.println("调用 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage()); + e.printStackTrace(); + + return Map.of( + "serverName", serverName, + "status", "failed", + "message", "Tool invocation failed: " + e.getMessage(), + "parameters", parameters + ); + } + } + + /** + * 发送 SSE 工具调用请求 + */ + private void sendSseToolCall(String serverName, Map callRequest) { + try { + // 通过 HTTP POST 发送工具调用请求 + WebClient webClient = WebClient.builder() + .baseUrl("http://localhost:3000") + .build(); + + String toolCallUrl = "/tool/" + serverName; + + webClient.post() + .uri(toolCallUrl) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(callRequest) + .retrieve() + .bodyToMono(String.class) + .timeout(Duration.ofSeconds(5)) + .subscribe( + response -> System.out.println("工具调用请求发送成功: " + response), + error -> System.err.println("工具调用请求发送失败: " + error.getMessage()) + ); + + } catch (Exception e) { + System.err.println("发送 SSE 工具调用请求失败: " + e.getMessage()); + } + } + + /** + * 处理 SSE 响应 + */ + public void handleSseResponse(String serverName, Map message) { + String requestId = (String) message.get("requestId"); + if (requestId != null) { + CompletableFuture future = pendingRequests.remove(requestId); + if (future != null) { + Object data = message.get("data"); + future.complete(data != null ? data : message); + } + } + } + + /** + * 处理 SSE 错误 + */ + public void handleSseError(String serverName, Map message) { + String requestId = (String) message.get("requestId"); + if (requestId != null) { + CompletableFuture future = pendingRequests.remove(requestId); + if (future != null) { + String errorMessage = (String) message.get("message"); + future.completeExceptionally(new RuntimeException(errorMessage)); + } + } + } + + /** + * 流式调用 MCP 工具(支持实时进度) + */ + public Flux invokeToolStream(String serverName, Object parameters) { + return Flux.create(emitter -> { + try { + // 生成请求ID + String requestId = "req_" + requestIdCounter.incrementAndGet(); + + // 构造 MCP 调用请求 + Map callRequest = new HashMap<>(); + callRequest.put("requestId", requestId); + callRequest.put("serverName", serverName); + callRequest.put("parameters", convertToMap(parameters)); + callRequest.put("stream", true); // 标记为流式调用 + callRequest.put("timestamp", System.currentTimeMillis()); + + // 创建流式处理器 + StreamHandler streamHandler = new StreamHandler(emitter); + pendingRequests.put(requestId + "_stream", null); // 占位符 + + // 发送流式调用请求 + sendSseToolCall(serverName, callRequest); + + // 注册流式处理器 + registerStreamHandler(requestId, streamHandler); + + emitter.onDispose(() -> { + // 清理资源 + pendingRequests.remove(requestId + "_stream"); + }); + + } catch (Exception e) { + emitter.error(e); + } + }); + } + + /** + * 流式处理器 + */ + private static class StreamHandler { + private final FluxSink emitter; + + public StreamHandler(FluxSink emitter) { + this.emitter = emitter; + } + + public void onNext(Object data) { + emitter.next(data); + } + + public void onComplete() { + emitter.complete(); + } + + public void onError(Throwable error) { + emitter.error(error); + } + } + + @SuppressWarnings("unchecked") + private Map convertToMap(Object parameters) { + if (parameters instanceof Map) { + Map result = new HashMap<>(); + Map paramMap = (Map) parameters; + for (Map.Entry entry : paramMap.entrySet()) { + if (entry.getKey() instanceof String) { + result.put((String) entry.getKey(), entry.getValue()); + } + } + return result; + } + return new HashMap<>(); + } + + private void registerStreamHandler(String requestId, StreamHandler streamHandler) { + // 实现流式处理器注册逻辑 + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java new file mode 100644 index 00000000..123d3181 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpServerConfig.java @@ -0,0 +1,61 @@ +package org.ruoyi.mcp.config; + +import java.util.List; +import java.util.Map; + +public class McpServerConfig { + private String command; + private List args; + private Map env; + private String Description; + private String workingDirectory; + // getters and setters + public String getCommand() { + return command; + } + + public void setCommand(String command) { + this.command = command; + } + + public List getArgs() { + return args; + } + + public void setArgs(List args) { + this.args = args; + } + + public Map getEnv() { + return env; + } + + public void setEnv(Map env) { + this.env = env; + } + + public String getDescription() { + return Description; + } + + public void setDescription(String description) { + Description = description; + } + public String getWorkingDirectory() { + return workingDirectory; + } + + public void setWorkingDirectory(String workingDirectory) { + this.workingDirectory = workingDirectory; + } + @Override + public String toString() { + return "McpServerConfig{" + + "command='" + command + '\'' + + ", args=" + args + + ", env=" + env + + ", Description='" + Description + '\'' + + ", workingDirectory='" + workingDirectory + '\'' + + '}'; + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpStartupConfig.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpStartupConfig.java new file mode 100644 index 00000000..66e6f468 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpStartupConfig.java @@ -0,0 +1,27 @@ +package org.ruoyi.mcp.config; + +import org.ruoyi.mcp.service.McpToolManagementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class McpStartupConfig { + + @Autowired + private McpToolManagementService mcpToolManagementService; + + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + // 应用启动时自动初始化 MCP 工具 + try { + System.out.println("Starting MCP tools initialization..."); + mcpToolManagementService.initializeMcpTools(); + System.out.println("MCP tools initialization completed successfully"); + } catch (Exception e) { + System.err.println("Failed to initialize MCP tools: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpToolInvoker.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpToolInvoker.java new file mode 100644 index 00000000..ee75bc88 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/config/McpToolInvoker.java @@ -0,0 +1,113 @@ +package org.ruoyi.mcp.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +@Component +public class McpToolInvoker { + + private final Map> pendingRequests = new ConcurrentHashMap<>(); + private final AtomicLong requestIdCounter = new AtomicLong(0); + @Autowired + private McpProcessManager mcpProcessManager; + + + /** + * 调用 MCP 工具(Studio 模式) + */ + public Object invokeTool(String serverName, Object parameters) { + try { + // 生成请求ID + String requestId = "req_" + requestIdCounter.incrementAndGet(); + + // 创建 CompletableFuture 等待响应 + CompletableFuture future = new CompletableFuture<>(); + pendingRequests.put(requestId, future); + + // 构造 MCP 调用消息 + Map callMessage = new HashMap<>(); + callMessage.put("type", "tool_call"); + callMessage.put("requestId", requestId); + callMessage.put("serverName", serverName); + callMessage.put("parameters", convertToMap(parameters)); + callMessage.put("timestamp", System.currentTimeMillis()); + + System.out.println("调用 MCP 工具 [" + serverName + "] 参数: " + parameters); + + // 发送消息到 MCP 服务器 + boolean sent = mcpProcessManager.sendMcpMessage(serverName, callMessage); + if (!sent) { + pendingRequests.remove(requestId); + throw new RuntimeException("无法发送消息到 MCP 服务器: " + serverName); + } + + // 等待响应(超时 30 秒) + Object result = future.get(30, TimeUnit.SECONDS); + + System.out.println("MCP 工具 [" + serverName + "] 调用成功,响应: " + result); + + return result; + + } catch (Exception e) { + System.err.println("调用 MCP 服务器 [" + serverName + "] 失败: " + e.getMessage()); + e.printStackTrace(); + + return Map.of( + "serverName", serverName, + "status", "failed", + "message", "Tool invocation failed: " + e.getMessage(), + "parameters", parameters + ); + } + } + /** + * 处理 MCP 服务器的响应消息 + */ + public void handleMcpResponse(String serverName, Map message) { + String type = (String) message.get("type"); + if ("tool_response".equals(type)) { + String requestId = (String) message.get("requestId"); + if (requestId != null) { + CompletableFuture future = pendingRequests.remove(requestId); + if (future != null) { + Object data = message.get("data"); + future.complete(data != null ? data : message); + } + } + } else if ("tool_error".equals(type)) { + String requestId = (String) message.get("requestId"); + if (requestId != null) { + CompletableFuture future = pendingRequests.remove(requestId); + if (future != null) { + String errorMessage = (String) message.get("message"); + future.completeExceptionally(new RuntimeException(errorMessage)); + } + } + } + } + + + @SuppressWarnings("unchecked") + private Map convertToMap(Object parameters) { + if (parameters instanceof Map) { + Map result = new HashMap<>(); + Map paramMap = (Map) parameters; + for (Map.Entry entry : paramMap.entrySet()) { + if (entry.getKey() instanceof String) { + result.put((String) entry.getKey(), entry.getValue()); + } + } + return result; + } + return new HashMap<>(); + } +} + + diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/MCPSseController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/MCPSseController.java new file mode 100644 index 00000000..0ecb2dbb --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/MCPSseController.java @@ -0,0 +1,66 @@ +package org.ruoyi.mcp.controller; + +import org.ruoyi.mcp.config.McpSSEToolInvoker; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.Map; + +@RestController +@RequestMapping("/api/sse") +public class MCPSseController { + + @Autowired + private McpSSEToolInvoker mcpToolInvoker; + + /** + * SSE 流式响应端点 + */ + @GetMapping(value = "/{serverName}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter streamMcpResponse(@PathVariable String serverName) { + SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); + + try { + // 发送连接建立消息 + emitter.send(SseEmitter.event() + .name("connected") + .data(Map.of("serverName", serverName, "status", "connected"))); + + } catch (Exception e) { + emitter.completeWithError(e); + } + + return emitter; + } + + /** + * 调用 MCP 工具(流式) + */ + @PostMapping("/tool/{serverName}") + public ResponseEntity callMcpTool( + @PathVariable String serverName, + @RequestBody Map request) { + + try { + boolean isStream = (Boolean) request.getOrDefault("stream", false); + Object parameters = request.get("parameters"); + + if (isStream) { + // 流式调用 + return ResponseEntity.ok(Map.of("status", "streaming_started")); + } else { + // 普通调用 + Object result = mcpToolInvoker.invokeTool(serverName, parameters); + return ResponseEntity.ok(result); + } + + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", e.getMessage())); + } + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/McpInfoController.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/McpInfoController.java new file mode 100644 index 00000000..62978741 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/controller/McpInfoController.java @@ -0,0 +1,162 @@ +package org.ruoyi.mcp.controller; + +import java.util.List; +import java.util.Map; + +import lombok.RequiredArgsConstructor; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.constraints.*; +import cn.dev33.satoken.annotation.SaCheckPermission; +import org.ruoyi.domain.McpInfo; +import org.ruoyi.domain.bo.McpInfoBo; +import org.ruoyi.domain.vo.McpInfoVo; +import org.ruoyi.mcp.config.McpConfig; +import org.ruoyi.mcp.config.McpServerConfig; +import org.ruoyi.mcp.domain.McpInfoRequest; +import org.ruoyi.mcp.service.McpInfoService; +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import org.ruoyi.common.idempotent.annotation.RepeatSubmit; +import org.ruoyi.common.log.annotation.Log; +import org.ruoyi.common.web.core.BaseController; +import org.ruoyi.core.page.PageQuery; +import org.ruoyi.common.core.domain.R; +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.core.page.TableDataInfo; + +/** + * MCP + * + * @author ageerle + * @date Sat Aug 09 16:50:58 CST 2025 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/operator/mcpInfo") +public class McpInfoController extends BaseController { + + private final McpInfoService mcpInfoService; + +/** + * 查询MCP列表 + */ +@SaCheckPermission("operator:mcpInfo:list") +@GetMapping("/list") + public TableDataInfo list(McpInfoBo bo, PageQuery pageQuery) { + return mcpInfoService.queryPageList(bo, pageQuery); + } + + /** + * 导出MCP列表 + */ + @SaCheckPermission("operator:mcpInfo:export") + @Log(title = "MCP", businessType = BusinessType.EXPORT) + @PostMapping("/export") + public void export(McpInfoBo bo, HttpServletResponse response) { + List list = mcpInfoService.queryList(bo); + ExcelUtil.exportExcel(list, "MCP", McpInfoVo.class, response); + } + + /** + * 获取MCP详细信息 + * + * @param mcpId 主键 + */ + @SaCheckPermission("operator:mcpInfo:query") + @GetMapping("/{mcpId}") + public R getInfo(@NotNull(message = "主键不能为空") + @PathVariable Integer mcpId) { + return R.ok(mcpInfoService.queryById(mcpId)); + } + + /** + * 新增MCP + */ + @SaCheckPermission("operator:mcpInfo:add") + @Log(title = "MCP", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping() + public R add(@Validated(AddGroup.class) @RequestBody McpInfoBo bo) { + return toAjax(mcpInfoService.insertByBo(bo)); + } + + /** + * 修改MCP + */ + @SaCheckPermission("operator:mcpInfo:edit") + @Log(title = "MCP", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping() + public R edit(@Validated(EditGroup.class) @RequestBody McpInfoBo bo) { + return toAjax(mcpInfoService.updateByBo(bo)); + } + + /** + * 删除MCP + * + * @param mcpIds 主键串 + */ + @SaCheckPermission("operator:mcpInfo:remove") + @Log(title = "MCP", businessType = BusinessType.DELETE) + @DeleteMapping("/{mcpIds}") + public R remove(@NotEmpty(message = "主键不能为空") + @PathVariable Integer[] mcpIds) { + return toAjax(mcpInfoService.deleteWithValidByIds(List.of(mcpIds), true)); + } + + /** + * 添加或更新 MCP 工具 + */ + @PostMapping("/tools") + public R saveToolConfig(@RequestBody McpInfoRequest request) { + return R.ok(mcpInfoService.saveToolConfig(request)); + } + + /** + * 获取所有活跃服务器名称 + */ + @GetMapping("/tools/names") + public R> getActiveServerNames() { + return R.ok(mcpInfoService.getActiveServerNames()); + } + + /** + * 根据名称获取工具配置 + */ + @GetMapping("/tools/{serverName}") + public R getToolConfig(@PathVariable String serverName) { + return R.ok(mcpInfoService.getToolConfigByName(serverName)); + } + + /** + * 启用工具 + */ + @PostMapping("/tools/{serverName}/enable") + public Map enableTool(@PathVariable String serverName) { + boolean success = mcpInfoService.enableTool(serverName); + return Map.of("success", success); + } + + /** + * 禁用工具 + */ + @PostMapping("/tools/{serverName}/disable") + public Map disableTool(@PathVariable String serverName) { + boolean success = mcpInfoService.disableTool(serverName); + return Map.of("success", success); + } + + /** + * 删除工具 + */ + @DeleteMapping("/tools/{serverName}") + public Map deleteTool(@PathVariable String serverName) { + boolean success = mcpInfoService.deleteToolConfig(serverName); + return Map.of("success", success); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/domain/McpInfoRequest.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/domain/McpInfoRequest.java new file mode 100644 index 00000000..d5525ed6 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/domain/McpInfoRequest.java @@ -0,0 +1,28 @@ +package org.ruoyi.mcp.domain; + +import java.util.List; +import java.util.Map; + +public class McpInfoRequest { + private String serverName; + private String command; + private List args; + private Map env; + private String description; + + // getters and setters + public String getServerName() { return serverName; } + public void setServerName(String serverName) { this.serverName = serverName; } + + public String getCommand() { return command; } + public void setCommand(String command) { this.command = command; } + + public List getArgs() { return args; } + public void setArgs(List args) { this.args = args; } + + public Map getEnv() { return env; } + public void setEnv(Map env) { this.env = env; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpInfoService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpInfoService.java new file mode 100644 index 00000000..aa926d79 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpInfoService.java @@ -0,0 +1,68 @@ +package org.ruoyi.mcp.service; + + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + import org.ruoyi.domain.McpInfo; + import org.ruoyi.domain.bo.McpInfoBo; + import org.ruoyi.domain.vo.McpInfoVo; + import org.ruoyi.mcp.config.McpConfig; + import org.ruoyi.mcp.config.McpServerConfig; + import org.ruoyi.mcp.domain.McpInfoRequest; + + import java.util.Collection; +import java.util.List; + +/** + * MCPService接口 + * + * @author ageerle + * @date Sat Aug 09 16:50:58 CST 2025 + */ +public interface McpInfoService { + + /** + * 查询MCP + */ + McpInfoVo queryById(Integer mcpId); + + /** + * 查询MCP列表 + */ + TableDataInfo queryPageList(McpInfoBo bo, PageQuery pageQuery); + + /** + * 查询MCP列表 + */ + List queryList(McpInfoBo bo); + + /** + * 新增MCP + */ + Boolean insertByBo(McpInfoBo bo); + + /** + * 修改MCP + */ + Boolean updateByBo(McpInfoBo bo); + + /** + * 校验并批量删除MCP信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + McpServerConfig getToolConfigByName(String serverName); + + McpConfig getAllActiveMcpConfig(); + + List getActiveServerNames(); + + McpInfo saveToolConfig(McpInfoRequest request); + + boolean deleteToolConfig(String serverName); + + boolean updateToolStatus(String serverName, Boolean status); + + boolean enableTool(String serverName); + + boolean disableTool(String serverName); +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpToolManagementService.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpToolManagementService.java new file mode 100644 index 00000000..4b7ff701 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/McpToolManagementService.java @@ -0,0 +1,134 @@ +package org.ruoyi.mcp.service; + +import org.ruoyi.domain.McpInfo; +import org.ruoyi.mcp.config.McpConfig; +import org.ruoyi.mcp.config.McpProcessManager; +import org.ruoyi.mcp.config.McpServerConfig; +import org.ruoyi.mcp.domain.McpInfoRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; + +@Service +public class McpToolManagementService { + + @Autowired + private McpInfoService mcpInfoService; + + @Autowired + private McpProcessManager mcpProcessManager; + + /** + * 初始化所有 MCP 工具(应用启动时调用) + */ + public void initializeMcpTools() { + System.out.println("Initializing MCP tools..."); + + McpConfig config = mcpInfoService.getAllActiveMcpConfig(); + if (config.getMcpServers() != null) { + int successCount = 0; + int totalCount = config.getMcpServers().size(); + + for (Map.Entry entry : config.getMcpServers().entrySet()) { + String serverName = entry.getKey(); + McpServerConfig serverConfig = entry.getValue(); + + System.out.println("Starting MCP server: " + serverName); + System.out.println("Starting MCP serverConfig: " + serverConfig); + // 启动 MCP 服务器进程 + boolean started = mcpProcessManager.startMcpServer(serverName,serverConfig); + + if (started) { + successCount++; + System.out.println("✓ MCP server [" + serverName + "] started successfully"); + } else { + System.err.println("✗ Failed to start MCP server [" + serverName + "]"); + } + } + + System.out.println("MCP tools initialization completed. " + + successCount + "/" + totalCount + " tools started."); + } + } + + /** + * 添加新的 MCP 工具并启动 + */ + public boolean addMcpTool(McpInfoRequest request) { + try { + McpInfo tool = mcpInfoService.saveToolConfig(request); + + // 启动新添加的工具 + McpServerConfig config = new McpServerConfig(); + config.setCommand(request.getCommand()); + config.setArgs(request.getArgs()); + config.setEnv(request.getEnv()); + + boolean started = mcpProcessManager.startMcpServer( + request.getServerName(), + config + ); + + return started; + } catch (Exception e) { + System.err.println("Failed to add MCP tool: " + e.getMessage()); + e.printStackTrace(); + return false; + } + } + + /** + * 获取 MCP 工具状态 + */ + public Map getMcpToolStatus() { + List activeTools = mcpInfoService.getActiveServerNames(); + Map status = new HashMap<>(); + + for (String serverName : activeTools) { + boolean isRunning = mcpProcessManager.isMcpServerRunning(serverName); + McpProcessManager.McpServerProcess processInfo = mcpProcessManager.getProcessInfo(serverName); + + Map toolStatus = new HashMap<>(); + toolStatus.put("running", isRunning); + toolStatus.put("processInfo", processInfo); + + status.put(serverName, toolStatus); + } + + return status; + } + + /** + * 重启指定的 MCP 工具 + */ + public boolean restartMcpTool(String serverName) { + McpServerConfig config = mcpInfoService.getToolConfigByName(serverName); + if (config == null) { + return false; + } + + return mcpProcessManager.restartMcpServer( + serverName, + config.getCommand(), + config.getArgs(), + config.getEnv() + ); + } + + /** + * 停止指定的 MCP 工具 + */ + public boolean stopMcpTool(String serverName) { + return mcpProcessManager.stopMcpServer(serverName); + } + + /** + * 获取所有运行中的工具 + */ + public Set getRunningTools() { + return mcpProcessManager.getRunningMcpServers(); + } +} diff --git a/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/impl/McpInfoServiceImpl.java b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/impl/McpInfoServiceImpl.java new file mode 100644 index 00000000..95704717 --- /dev/null +++ b/ruoyi-modules/ruoyi-chat/src/main/java/org/ruoyi/mcp/service/impl/McpInfoServiceImpl.java @@ -0,0 +1,252 @@ +package org.ruoyi.mcp.service.impl; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.ruoyi.common.core.utils.MapstructUtils; + import org.ruoyi.core.page.TableDataInfo; + import org.ruoyi.core.page.PageQuery; + import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.ruoyi.domain.McpInfo; +import org.ruoyi.domain.bo.McpInfoBo; +import org.ruoyi.domain.vo.McpInfoVo; +import org.ruoyi.mapper.McpInfoMapper; +import org.ruoyi.mcp.config.McpConfig; +import org.ruoyi.mcp.config.McpServerConfig; +import org.ruoyi.mcp.domain.McpInfoRequest; +import org.ruoyi.mcp.service.McpInfoService; +import org.springframework.stereotype.Service; + +import org.ruoyi.common.core.utils.StringUtils; + +import java.util.*; + +/** + * MCPService业务层处理 + * + * @author ageerle + * @date Sat Aug 09 16:50:58 CST 2025 + */ +@RequiredArgsConstructor +@Service +public class McpInfoServiceImpl implements McpInfoService { + + private final McpInfoMapper baseMapper; + private final ObjectMapper objectMapper = new ObjectMapper(); + /** + * 查询MCP + */ + @Override + public McpInfoVo queryById(Integer mcpId) { + return baseMapper.selectVoById(mcpId); + } + + /** + * 查询MCP列表 + */ + @Override + public TableDataInfo queryPageList(McpInfoBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询MCP列表 + */ + @Override + public List queryList(McpInfoBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(McpInfoBo bo) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.like(StringUtils.isNotBlank(bo.getServerName()), McpInfo::getServerName, bo.getServerName()); + lqw.eq(StringUtils.isNotBlank(bo.getTransportType()), McpInfo::getTransportType, bo.getTransportType()); + lqw.eq(StringUtils.isNotBlank(bo.getCommand()), McpInfo::getCommand, bo.getCommand()); + lqw.eq(bo.getStatus() != null, McpInfo::getStatus, bo.getStatus()); + return lqw; + } + + /** + * 新增MCP + */ + @Override + public Boolean insertByBo(McpInfoBo bo) { + McpInfo add = MapstructUtils.convert(bo, McpInfo. class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setMcpId(add.getMcpId()); + } + return flag; + } + + /** + * 修改MCP + */ + @Override + public Boolean updateByBo(McpInfoBo bo) { + McpInfo update = MapstructUtils.convert(bo, McpInfo. class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(McpInfo entity) { + //TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除MCP + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + //TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 根据服务器名称获取工具配置 + */ + @Override + public McpServerConfig getToolConfigByName(String serverName) { + McpInfo tool = baseMapper.selectByServerName(serverName); + if (tool != null) { + return convertToMcpServerConfig(tool); + } + return null; + } + + /** + * 获取所有活跃的 MCP 工具配置 + */ + @Override + public McpConfig getAllActiveMcpConfig() { + List activeTools = baseMapper.selectActiveServers(); + Map servers = new HashMap<>(); + + for (McpInfo tool : activeTools) { + McpServerConfig serverConfig = convertToMcpServerConfig(tool); + servers.put(tool.getServerName(), serverConfig); + } + + McpConfig config = new McpConfig(); + config.setMcpServers(servers); + return config; + } + + /** + * 获取所有活跃服务器名称 + */ + @Override + public List getActiveServerNames() { + return baseMapper.selectActiveServerNames(); + } + + /** + * 保存或更新 MCP 工具配置 + */ + @Override + public McpInfo saveToolConfig(McpInfoRequest request) { + McpInfo existingTool = baseMapper.selectByServerName(request.getServerName()); + + McpInfo tool; + if (existingTool != null) { + tool = existingTool; + } else { + tool = new McpInfo(); + } + + tool.setServerName(request.getServerName()); + tool.setCommand(request.getCommand()); + + try { + tool.setArguments(objectMapper.writeValueAsString(request.getArgs())); + if (request.getEnv() != null) { + tool.setEnv(objectMapper.writeValueAsString(request.getEnv())); + } + } catch (Exception e) { + throw new RuntimeException("Failed to serialize JSON data", e); + } + + tool.setDescription(request.getDescription()); + tool.setStatus(true); // 默认启用 + + if (existingTool != null) { + baseMapper.updateById(tool); + } else { + baseMapper.insert(tool); + } + + return tool; + } + + /** + * 删除工具配置 + */ + @Override + public boolean deleteToolConfig(String serverName) { + return baseMapper.deleteByServerName(serverName) > 0; + } + + /** + * 更新工具状态 + */ + @Override + public boolean updateToolStatus(String serverName, Boolean status) { + return baseMapper.updateActiveStatus(serverName, status) > 0; + } + + /** + * 启用工具 + */ + @Override + public boolean enableTool(String serverName) { + return updateToolStatus(serverName, true); + } + + /** + * 禁用工具 + */ + @Override + public boolean disableTool(String serverName) { + return updateToolStatus(serverName, false); + } + + private McpServerConfig convertToMcpServerConfig(McpInfo tool) { + McpServerConfig config = new McpServerConfig(); + config.setCommand(tool.getCommand()); + + try { + // 解析 args + if (tool.getArguments() != null && !tool.getArguments().isEmpty()) { + List args = objectMapper.readValue(tool.getArguments(), new TypeReference>() {}); + config.setArgs(args); + } else { + config.setArgs(new ArrayList<>()); + } + + // 解析 env + if (tool.getEnv() != null && !tool.getEnv().isEmpty()) { + Map env = objectMapper.readValue(tool.getEnv(), new TypeReference>() {}); + config.setEnv(env); + } else { + config.setEnv(new HashMap<>()); + } + + } catch (Exception e) { + config.setArgs(new ArrayList<>()); + config.setEnv(new HashMap<>()); + } + + return config; + } +} diff --git a/ruoyi-modules/ruoyi-workflow/pom.xml b/ruoyi-modules/ruoyi-workflow/pom.xml new file mode 100644 index 00000000..51c23ed1 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.ruoyi + ruoyi-modules + ${revision} + ../pom.xml + + + ruoyi-workflow + + + 工作流模块 + + + + 17 + 17 + UTF-8 + + + + + org.ruoyi + ruoyi-workflow-api + + + + org.ruoyi + ruoyi-system-api + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + com.github.xiaoymin + knife4j-openapi3-jakarta-spring-boot-starter + 4.4.0 + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${springdoc.version} + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + jakarta.servlet + jakarta.servlet-api + + + + com.talanlabs + avatar-generator + 1.1.0 + + + + com.talanlabs + avatar-generator-cat + 1.1.0 + + + + + \ No newline at end of file diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/WorkflowController.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/WorkflowController.java new file mode 100644 index 00000000..02fd3c96 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/WorkflowController.java @@ -0,0 +1,114 @@ +package org.ruoyi.workflow.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.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; +import org.ruoyi.workflow.service.WorkflowService; +import org.ruoyi.workflow.workflow.WorkflowStarter; +import org.ruoyi.workflow.workflow.node.switcher.OperatorEnum; +import org.springframework.http.MediaType; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/workflow") +@Validated +public class WorkflowController { + + @Resource + private WorkflowStarter workflowStarter; + + @Resource + private WorkflowService workflowService; + + @Resource + private WorkflowComponentService workflowComponentService; + + @PostMapping("/add") + public R add(@RequestBody @Validated WfAddReq addReq) { + return R.ok(workflowService.add(addReq.getTitle(), addReq.getRemark(), addReq.getIsPublic())); + } + + @PostMapping("/set-public/{wfUuid}") + public R setPublic(@PathVariable String wfUuid, @RequestParam(defaultValue = "true") Boolean isPublic) { + workflowService.setPublic(wfUuid, isPublic); + return R.ok(); + } + + @PostMapping("/update") + public R update(@RequestBody @Validated WorkflowUpdateReq req) { + return R.ok(workflowService.update(req)); + } + + @PostMapping("/del/{uuid}") + public R delete(@PathVariable String uuid) { + workflowService.softDelete(uuid); + return R.ok(); + } + + @PostMapping("/enable/{uuid}") + public R enable(@PathVariable String uuid, @RequestParam Boolean enable) { + workflowService.enable(uuid, enable); + return R.ok(); + } + + @PostMapping("/base-info/update") + public R updateBaseInfo(@RequestBody @Validated WfBaseInfoUpdateReq req) { + return R.ok(workflowService.updateBaseInfo(req.getUuid(), req.getTitle(), req.getRemark(), req.getIsPublic())); + } + + @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()); + } + + @GetMapping("/mine/search") + public R> searchMine(@RequestParam(defaultValue = "") String keyword, + @RequestParam(required = false) Boolean isPublic, + @NotNull @Min(1) Integer currentPage, + @NotNull @Min(10) Integer pageSize) { + return R.ok(workflowService.search(keyword, isPublic, null, currentPage, pageSize)); + } + + /** + * 搜索公开工作流 + * + * @param keyword 搜索关键词 + * @param currentPage 当前页数 + * @param pageSize 每页数量 + * @return 工作流列表 + */ + @GetMapping("/public/search") + public R> searchPublic(@RequestParam(defaultValue = "") String keyword, + @NotNull @Min(1) Integer currentPage, + @NotNull @Min(10) Integer pageSize) { + return R.ok(workflowService.searchPublic(keyword, currentPage, pageSize)); + } + + @GetMapping("/public/operators") + public R>> searchPublic() { + List> result = new ArrayList<>(); + for (OperatorEnum operator : OperatorEnum.values()) { + result.add(Map.of("name", operator.getName(), "desc", operator.getDesc())); + } + return R.ok(result); + } + + @GetMapping("/public/component/list") + public R> component() { + return R.ok(workflowComponentService.getAllEnable()); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/WorkflowRuntimeController.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/WorkflowRuntimeController.java new file mode 100644 index 00000000..34fc0065 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/WorkflowRuntimeController.java @@ -0,0 +1,58 @@ +package org.ruoyi.workflow.controller; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +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.core.domain.R; +import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto; +import org.ruoyi.workflow.dto.workflow.WfRuntimeResp; +import org.ruoyi.workflow.dto.workflow.WorkflowResumeReq; +import org.ruoyi.workflow.service.WorkflowRuntimeService; +import org.ruoyi.workflow.workflow.WorkflowStarter; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/workflow/runtime") +@Validated +public class WorkflowRuntimeController { + + @Resource + private WorkflowRuntimeService workflowRuntimeService; + + @Resource + private WorkflowStarter workflowStarter; + + @Operation(summary = "接收用户输入以继续执行剩余流程") + @PostMapping(value = "/resume/{runtimeUuid}") + public R resume(@PathVariable String runtimeUuid, @RequestBody WorkflowResumeReq resumeReq) { + workflowStarter.resumeFlow(runtimeUuid, resumeReq.getFeedbackContent()); + return R.ok(); + } + + @GetMapping("/page") + public R> search(@RequestParam String wfUuid, + @NotNull @Min(1) Integer currentPage, + @NotNull @Min(10) Integer pageSize) { + return R.ok(workflowRuntimeService.page(wfUuid, currentPage, pageSize)); + } + + @GetMapping("/nodes/{runtimeUuid}") + public R> listByRuntimeId(@PathVariable String runtimeUuid) { + return R.ok(workflowRuntimeService.listByRuntimeUuid(runtimeUuid)); + } + + @PostMapping("/clear") + public R clear(@RequestParam(defaultValue = "") String wfUuid) { + return R.ok(workflowRuntimeService.deleteAll(wfUuid)); + } + + @PostMapping("/del/{wfRuntimeUuid}") + public R delete(@PathVariable String wfRuntimeUuid) { + return R.ok(workflowRuntimeService.softDelete(wfRuntimeUuid)); + } +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/admin/AdminWorkflowComponentController.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/admin/AdminWorkflowComponentController.java new file mode 100644 index 00000000..7e1d8c71 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/admin/AdminWorkflowComponentController.java @@ -0,0 +1,45 @@ +package org.ruoyi.workflow.controller.admin; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.workflow.dto.workflow.WfComponentReq; +import org.ruoyi.workflow.dto.workflow.WfComponentSearchReq; +import org.ruoyi.workflow.entity.WorkflowComponent; +import org.ruoyi.workflow.service.WorkflowComponentService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/admin/workflow/component") +@Validated +public class AdminWorkflowComponentController { + @Resource + private WorkflowComponentService workflowComponentService; + + @PostMapping("/search") + public R> search(@RequestBody WfComponentSearchReq searchReq, @NotNull @Min(1) Integer currentPage, @NotNull @Min(10) Integer pageSize) { + return R.ok(workflowComponentService.search(searchReq, currentPage, pageSize)); + } + + @PostMapping("/enable") + public R enable(@RequestParam String uuid, @RequestParam Boolean isEnable) { + workflowComponentService.enable(uuid, isEnable); + return R.ok(); + } + + @PostMapping("/del/{uuid}") + public R del(@PathVariable String uuid) { + workflowComponentService.deleteByUuid(uuid); + return R.ok(); + } + + + @PostMapping("/addOrUpdate") + public R addOrUpdate(@Validated @RequestBody WfComponentReq req) { + return R.ok(workflowComponentService.addOrUpdate(req)); + } + +} diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/admin/AdminWorkflowController.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/admin/AdminWorkflowController.java new file mode 100644 index 00000000..fe193d86 --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/ruoyi/workflow/controller/admin/AdminWorkflowController.java @@ -0,0 +1,35 @@ +package org.ruoyi.workflow.controller.admin; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import jakarta.annotation.Resource; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.ruoyi.common.core.domain.R; +import org.ruoyi.workflow.dto.workflow.WfSearchReq; +import org.ruoyi.workflow.dto.workflow.WorkflowResp; +import org.ruoyi.workflow.service.WorkflowService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/admin/workflow") +@Validated +public class AdminWorkflowController { + + @Resource + private WorkflowService workflowService; + + @PostMapping("/search") + public R> search(@RequestBody WfSearchReq req, + @RequestParam @NotNull @Min(1) Integer currentPage, + @RequestParam @NotNull @Min(10) Integer pageSize) { + return R.ok(workflowService.search(req.getTitle(), req.getIsPublic(), + req.getIsEnable(), currentPage, pageSize)); + } + + @PostMapping("/enable") + public R enable(@RequestParam String uuid, @RequestParam Boolean isEnable) { + workflowService.enable(uuid, isEnable); + return R.ok(); + } +} diff --git a/script/sql/update/2025-09-30-流程编排.sql b/script/sql/update/2025-09-30-流程编排.sql new file mode 100644 index 00000000..8532a817 --- /dev/null +++ b/script/sql/update/2025-09-30-流程编排.sql @@ -0,0 +1,162 @@ +CREATE TABLE t_workflow +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + title VARCHAR(100) NOT NULL DEFAULT '', + remark TEXT NOT NULL, + user_id BIGINT NOT NULL DEFAULT 0, + is_public TINYINT(1) NOT NULL DEFAULT 0, + is_enable TINYINT(1) NOT NULL DEFAULT 1, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0 +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COMMENT ='工作流定义(用户定义的工作流)| Workflow Definition'; + + +CREATE TABLE t_workflow_node +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + workflow_id BIGINT NOT NULL DEFAULT 0, + workflow_component_id BIGINT NOT NULL DEFAULT 0, + user_id BIGINT NOT NULL DEFAULT 0, + title VARCHAR(100) NOT NULL DEFAULT '', + remark VARCHAR(500) NOT NULL DEFAULT '', + input_config JSON NOT NULL DEFAULT ('{}'), + node_config JSON NOT NULL DEFAULT ('{}'), + position_x DOUBLE NOT NULL DEFAULT 0, + position_y DOUBLE NOT NULL DEFAULT 0, + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + INDEX idx_workflow_node_workflow_id (workflow_id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COMMENT ='工作流定义的节点 | Node of Workflow Definition'; + + +CREATE TABLE t_workflow_edge +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + workflow_id BIGINT NOT NULL DEFAULT 0, + source_node_uuid VARCHAR(32) NOT NULL DEFAULT '', + source_handle VARCHAR(32) NOT NULL DEFAULT '', + target_node_uuid VARCHAR(32) NOT NULL DEFAULT '', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + INDEX idx_workflow_edge_workflow_id (workflow_id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; + + +CREATE TABLE t_workflow_runtime +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + user_id BIGINT NOT NULL DEFAULT 0, + workflow_id BIGINT NOT NULL DEFAULT 0, + input JSON NOT NULL DEFAULT ('{}'), + output JSON NOT NULL DEFAULT ('{}'), + status SMALLINT NOT NULL DEFAULT 1 COMMENT '执行状态,1:就绪,2:执行中,3:成功,4:失败', + status_remark VARCHAR(250) NOT NULL DEFAULT '' COMMENT '状态备注', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + INDEX idx_workflow_runtime_workflow_id (workflow_id), + INDEX idx_workflow_runtime_user_id (user_id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COMMENT ='工作流实例(运行时)| Workflow Runtime'; + + +CREATE TABLE t_workflow_runtime_node +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) NOT NULL DEFAULT '', + user_id BIGINT NOT NULL DEFAULT 0, + workflow_runtime_id BIGINT NOT NULL DEFAULT 0, + node_id BIGINT NOT NULL DEFAULT 0, + input JSON NOT NULL DEFAULT ('{}'), + output JSON NOT NULL DEFAULT ('{}'), + status SMALLINT NOT NULL DEFAULT 1 COMMENT '执行状态,1:进行中,2:失败,3:成功', + status_remark VARCHAR(250) NOT NULL DEFAULT '' COMMENT '状态备注', + create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + is_deleted TINYINT(1) NOT NULL DEFAULT 0, + INDEX idx_runtime_node_runtime_id (workflow_runtime_id), + INDEX idx_runtime_node_node_id (node_id) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COMMENT ='工作流实例(运行时)- 节点 | Workflow Runtime Node'; + + +CREATE TABLE t_workflow_component +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + uuid VARCHAR(32) DEFAULT '' NOT NULL, + name VARCHAR(32) DEFAULT '' NOT NULL, + title VARCHAR(100) DEFAULT '' NOT NULL, + remark TEXT NOT NULL, + display_order INT DEFAULT 0 NOT NULL, + is_enable TINYINT(1) DEFAULT 0 NOT NULL, + create_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + update_time DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, + is_deleted TINYINT(1) DEFAULT 0 NOT NULL, + INDEX idx_display_order (display_order) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COMMENT '工作流组件库 | Workflow Component'; + + +-- workflow +-- 如果不定义输入的变量名,则默认设置为input +-- 如果不定义输出的变量名,则默认设置为output +insert into t_workflow_component(uuid, name, title, remark, is_enable) +values (replace(uuid(), '-', ''), 'Start', '开始', '流程由此开始', true); +insert into t_workflow_component(uuid, name, title, remark, is_enable) +values (replace(uuid(), '-', ''), 'End', '结束', '流程由此结束', true); +insert into t_workflow_component(uuid, name, title, remark, is_enable) +values (replace(uuid(), '-', ''), 'Answer', '生成回答', '调用大语言模型回答问题', true); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'Dalle3', 'DALL-E 3 画图', '调用Dall-e-3生成图片', 11, false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'DocumentExtractor', '文档提取', '从文档中提取信息', 4, false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'KeywordExtractor', '关键词提取', + '从内容中提取关键词,Top N指定需要提取的关键词数量', 5, false); +insert into t_workflow_component(uuid, name, title, remark, is_enable) +values (replace(uuid(), '-', ''), 'KnowledgeRetrieval', '知识检索', '从知识库中检索信息,需选中知识库', + true); +insert into t_workflow_component(uuid, name, title, remark, is_enable) +values (replace(uuid(), '-', ''), 'Switcher', '条件分支', '根据设置的条件引导执行不同的流程', false); +insert into t_workflow_component(uuid, name, title, remark, is_enable) +values (replace(uuid(), '-', ''), 'Classifier', '内容归类', + '使用大语言模型对输入信息进行分析并归类,根据类别调用对应的下游节点', false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'Template', '模板转换', + '将多个变量合并成一个输出内容', 10, true); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'Google', 'Google搜索', '从Google中检索信息', 13, false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'FaqExtractor', '常见问题提取', + '从内容中提取出常见问题及对应的答案,Top N为提取的数量', + 6, false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'Tongyiwanx', '通义万相-画图', '调用文生图模型生成图片', 12, false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'HumanFeedback', '人机交互', + '中断执行中的流程并等待用户的输入,用户输入后继续执行后续流程', 10, false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'MailSend', '邮件发送', '发送邮件到指定邮箱', 10, false); +insert into t_workflow_component(uuid, name, title, remark, display_order, is_enable) +values (replace(uuid(), '-', ''), 'HttpRequest', 'Http请求', + '通过Http协议发送请求,可将其他组件的输出作为参数,也可设置常量作为参数。', 10, false); + + +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 (1976160997656043521, '流程管理', 0, 1, 'flow', '', null, 1, 0, 'M', '0', '0', null, 'ph:user-fill', null, null, '2025-10-09 13:41:12', 1, '2025-10-20 20:59:25', ''); +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 (1976161221409579010, '工作流编排', 1976160997656043521, 0, 'workflow', 'workflow/index', null, 1, 0, 'C', '0', '0', null, 'ph:user-fill', null, null, '2025-10-09 13:42:05', 1, '2025-10-20 20:59:16', ''); + diff --git a/script/sql/update/2025-10-10-实时交互数字人集成.sql b/script/sql/update/2025-10-10-实时交互数字人集成.sql new file mode 100644 index 00000000..d6832230 --- /dev/null +++ b/script/sql/update/2025-10-10-实时交互数字人集成.sql @@ -0,0 +1,395 @@ +-- Description: 实时交互数字人集成模块 + +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666752, '交互数字人配置', '2000', '1', 'aihumanConfig', 'aihuman/aihumanConfig/index', 1, 0, 'C', '0', '0', 'aihuman:aihumanConfig:list', '#', 103, 1, sysdate(), null, null, '交互数字人配置菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666753, '交互数字人配置查询', 1971582278942666752, '1', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:query', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666754, '交互数字人配置新增', 1971582278942666752, '2', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:add', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666755, '交互数字人配置修改', 1971582278942666752, '3', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:edit', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666756, '交互数字人配置删除', 1971582278942666752, '4', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:remove', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971582278942666757, '交互数字人配置导出', 1971582278942666752, '5', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:export', '#', 103, 1, sysdate(), null, null, ''); + +-- Description: 实时交互数字人集成模块 + +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597696, '数字人信息管理', '2000', '1', 'aihumanInfo', 'aihuman/aihumanInfo/index', 1, 0, 'C', '0', '0', 'aihuman:aihumanInfo:list', '#', 103, 1, sysdate(), null, null, '数字人信息管理菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597697, '数字人信息管理查询', 1971546066781597696, '1', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:query', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597698, '数字人信息管理新增', 1971546066781597696, '2', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:add', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597699, '数字人信息管理修改', 1971546066781597696, '3', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:edit', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597700, '数字人信息管理删除', 1971546066781597696, '4', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:remove', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1971546066781597701, '数字人信息管理导出', 1971546066781597696, '5', '#', '', 1, 0, 'F', '0', '0', 'aihuman:aihumanInfo:export', '#', 103, 1, sysdate(), null, null, ''); + + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for aihuman_info +-- ---------------------------- +DROP TABLE IF EXISTS `aihuman_info`; +CREATE TABLE `aihuman_info` ( + `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '交互名称', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '交互内容', + `create_time` datetime(0) DEFAULT NULL COMMENT '创建时间', + `update_time` datetime(0) DEFAULT NULL COMMENT '更新时间', + `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'AI人类交互信息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of aihuman_info +-- ---------------------------- +INSERT INTO `aihuman_info` VALUES (1, '1', '1', '2025-09-26 18:02:00', '2025-09-26 18:02:02', '0'); + +SET FOREIGN_KEY_CHECKS = 1; + + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for aihuman_config +-- ---------------------------- +DROP TABLE IF EXISTS `aihuman_config`; +CREATE TABLE `aihuman_config` ( + `id` int(0) NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `model_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `model_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `model_params` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `agent_params` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci, + `create_time` datetime(0) DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `update_time` datetime(0) DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP(0), + `status` int(0) DEFAULT NULL, + `publish` int(0) DEFAULT NULL, + `create_dept` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + `update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of aihuman_config +-- ---------------------------- +INSERT INTO `aihuman_config` VALUES (9, '关爱老婆数字人(梅朵)', '梅朵吉祥物', '/Live2D/models/梅朵吉祥物/梅朵吉祥物.model3.json', '{\n \"Version\": 3,\n \"FileReferences\": {\n \"Moc\": \"梅朵吉祥物.moc3\",\n \"Textures\": [\n \"梅朵吉祥物.4096/texture_00.png\",\n \"梅朵吉祥物.4096/texture_01.png\"\n ],\n \"Physics\": \"梅朵吉祥物.physics3.json\",\n \"DisplayInfo\": \"梅朵吉祥物.cdi3.json\",\n \"MotionSync\": \"梅朵吉祥物.motionsync3.json\",\n \"Expressions\": [\n {\n \"Name\": \"kaixin\",\n \"File\": \"kaixin.exp3.json\"\n },\n {\n \"Name\": \"maozi\",\n \"File\": \"maozi.exp3.json\"\n },\n {\n \"Name\": \"mouth open\",\n \"File\": \"mouth open.exp3.json\"\n },\n {\n \"Name\": \"shibai\",\n \"File\": \"shibai.exp3.json\"\n },\n {\n \"Name\": \"yinchen\",\n \"File\": \"yinchen.exp3.json\"\n }\n ],\n \"Motions\": {\n \"\": [\n {\n \"File\": \"mouth.motion3.json\"\n }\n ]\n }\n },\n \"Groups\": [\n {\n \"Target\": \"Parameter\",\n \"Name\": \"LipSync\",\n \"Ids\": [\n \"ParamMouthForm\",\n \"ParamMouthOpenY\"\n ]\n },\n {\n \"Target\": \"Parameter\",\n \"Name\": \"EyeBlink\",\n \"Ids\": [\n \"ParamEyeLOpen\",\n \"ParamEyeROpen\"\n ]\n }\n ],\n \"HitAreas\": []\n}', '{\n \"bot_id\": \"7504596188201746470\",\n \"user_id\": \"7376476310010937396\",\n \"stream\": true,\n \"auto_save_history\": true\n}', '2025-09-29 16:36:46', '2025-09-29 16:36:46', 0, 1, NULL, NULL, '1'); +INSERT INTO `aihuman_config` VALUES (10, '关爱老婆数字人(K)', 'kei_vowels_pro', '/Live2D/models/kei_vowels_pro/kei_vowels_pro.model3.json', '{\n \"Version\": 3,\n \"FileReferences\": {\n \"Moc\": \"kei_vowels_pro.moc3\",\n \"Textures\": [\n \"kei_vowels_pro.2048/texture_00.png\"\n ],\n \"Physics\": \"kei_vowels_pro.physics3.json\",\n \"DisplayInfo\": \"kei_vowels_pro.cdi3.json\",\n \"MotionSync\": \"kei_vowels_pro.motionsync3.json\",\n \"Motions\": {\n \"\": [\n {\n \"File\": \"motions/01_kei_en.motion3.json\",\n \"Sound\": \"sounds/01_kei_en.wav\",\n \"MotionSync\": \"Vowels_CRI\"\n },\n {\n \"File\": \"motions/01_kei_jp.motion3.json\",\n \"Sound\": \"sounds/01_kei_jp.wav\",\n \"MotionSync\": \"Vowels_CRI\"\n },\n {\n \"File\": \"motions/01_kei_ko.motion3.json\",\n \"Sound\": \"sounds/01_kei_ko.wav\",\n \"MotionSync\": \"Vowels_CRI\"\n },\n {\n \"File\": \"motions/01_kei_zh.motion3.json\",\n \"Sound\": \"sounds/01_kei_zh.wav\",\n \"MotionSync\": \"Vowels_CRI\"\n }\n ]\n }\n },\n \"Groups\": [\n {\n \"Target\": \"Parameter\",\n \"Name\": \"LipSync\",\n \"Ids\": []\n },\n {\n \"Target\": \"Parameter\",\n \"Name\": \"EyeBlink\",\n \"Ids\": [\n \"ParamEyeLOpen\",\n \"ParamEyeROpen\"\n ]\n }\n ],\n \"HitAreas\": [\n {\n \"Id\": \"HitAreaHead\",\n \"Name\": \"Head\"\n }\n ]\n}', '3', '2025-09-29 16:35:27', '2025-09-29 16:35:27', 0, 1, NULL, NULL, '1'); +INSERT INTO `aihuman_config` VALUES (11, '关爱老婆数字人(March 7th)', 'March 7th', '/Live2D/models/March 7th/March 7th.model3.json', '{\n \"Version\": 3,\n \"FileReferences\": {\n \"Moc\": \"March 7th.moc3\",\n \"Textures\": [\n \"March 7th.4096/texture_00.png\",\n \"March 7th.4096/texture_01.png\"\n ],\n \"Physics\": \"March 7th.physics3.json\",\n \"DisplayInfo\": \"March 7th.cdi3.json\",\n \"Expressions\": [\n {\n \"Name\": \"捂脸\",\n \"File\": \"1.exp3.json\"\n },\n {\n \"Name\": \"比耶\",\n \"File\": \"2.exp3.json\"\n },\n {\n \"Name\": \"照相\",\n \"File\": \"3.exp3.json\"\n },\n {\n \"Name\": \"脸红\",\n \"File\": \"4.exp3.json\"\n },\n {\n \"Name\": \"黑脸\",\n \"File\": \"5.exp3.json\"\n },\n {\n \"Name\": \"哭\",\n \"File\": \"6.exp3.json\"\n },\n {\n \"Name\": \"流汗\",\n \"File\": \"7.exp3.json\"\n },\n {\n \"Name\": \"星星\",\n \"File\": \"8.exp3.json\"\n }\n ]\n },\n \"Groups\": [\n {\n \"Target\": \"Parameter\",\n \"Name\": \"EyeBlink\",\n \"Ids\": [\n \"ParamEyeLOpen\",\n \"ParamEyeROpen\"\n ]\n },\n {\n \"Target\": \"Parameter\",\n \"Name\": \"LipSync\",\n \"Ids\": [\n \"ParamMouthOpenY\"\n ]\n }\n ],\n \"HitAreas\": []\n}', '3', '2025-09-29 21:09:26', '2025-09-29 21:09:28', 0, 1, NULL, NULL, NULL); +INSERT INTO `aihuman_config` VALUES (12, '关爱老婆数字人(pachan)', 'pachan', '/Live2D/models/pachan/pachan.model3.json', '{\n \"Version\": 3,\n \"FileReferences\": {\n \"Moc\": \"pachirisu anime girl - top half.moc3\",\n \"Textures\": [\n \"pachirisu anime girl - top half.4096/texture_00.png\"\n ],\n \"Physics\": \"pachirisu anime girl - top half.physics3.json\",\n \"DisplayInfo\": \"pachirisu anime girl - top half.cdi3.json\"\n },\n \"Groups\": [\n {\n \"Target\": \"Parameter\",\n \"Name\": \"EyeBlink\",\n \"Ids\": []\n },\n {\n \"Target\": \"Parameter\",\n \"Name\": \"LipSync\",\n \"Ids\": []\n }\n ]\n}', NULL, '2025-10-05 19:49:56', '2025-10-05 19:49:56', 0, 1, NULL, NULL, NULL); +INSERT INTO `aihuman_config` VALUES (13, '关爱老婆数字人(230108)', '230108', '/Live2D/models/230108/230108.model3.json', '{\n \"Version\": 3,\n \"FileReferences\": {\n \"Moc\": \"230108.moc3\",\n \"Textures\": [\n \"230108.4096/texture_00.png\"\n ],\n \"Physics\": \"230108.physics3.json\",\n \"DisplayInfo\": \"230108.cdi3.json\"\n },\n \"Groups\": [\n {\n \"Target\": \"Parameter\",\n \"Name\": \"LipSync\",\n \"Ids\": [\n \"ParamMouthOpenY\"\n ]\n },\n {\n \"Target\": \"Parameter\",\n \"Name\": \"EyeBlink\",\n \"Ids\": [\n \"ParamEyeLOpen\",\n \"ParamEyeROpen\"\n ]\n }\n ]\n}', NULL, '2025-10-06 19:28:20', '2025-10-06 19:28:23', 0, 1, NULL, NULL, NULL); + +SET FOREIGN_KEY_CHECKS = 1; + + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sys_dict_data +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict_data`; +CREATE TABLE `sys_dict_data` ( + `dict_code` bigint(0) NOT NULL COMMENT '字典编码', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', + `dict_sort` int(0) DEFAULT 0 COMMENT '字典排序', + `dict_label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典标签', + `dict_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典键值', + `dict_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典类型', + `css_class` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '样式属性(其他样式扩展)', + `list_class` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '表格回显样式', + `is_default` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT 'N' COMMENT '是否默认(Y是 N否)', + `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '状态(0正常 1停用)', + `create_dept` bigint(0) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(0) DEFAULT NULL COMMENT '创建者', + `create_time` datetime(0) DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(0) DEFAULT NULL COMMENT '更新者', + `update_time` datetime(0) DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`dict_code`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '字典数据表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_dict_data +-- ---------------------------- +INSERT INTO `sys_dict_data` VALUES (1, '000000', 1, '男', '0', 'sys_user_sex', '', '', 'Y', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '性别男'); +INSERT INTO `sys_dict_data` VALUES (2, '000000', 2, '女', '1', 'sys_user_sex', '', '', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '性别女'); +INSERT INTO `sys_dict_data` VALUES (3, '000000', 3, '未知', '2', 'sys_user_sex', '', '', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '性别未知'); +INSERT INTO `sys_dict_data` VALUES (4, '000000', 1, '显示', '0', 'sys_show_hide', '', 'primary', 'Y', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '显示菜单'); +INSERT INTO `sys_dict_data` VALUES (5, '000000', 2, '隐藏', '1', 'sys_show_hide', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '隐藏菜单'); +INSERT INTO `sys_dict_data` VALUES (6, '000000', 1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '正常状态'); +INSERT INTO `sys_dict_data` VALUES (7, '000000', 2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '停用状态'); +INSERT INTO `sys_dict_data` VALUES (12, '000000', 1, '是', 'Y', 'sys_yes_no', '', 'primary', 'Y', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '系统默认是'); +INSERT INTO `sys_dict_data` VALUES (13, '000000', 2, '否', 'N', 'sys_yes_no', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '系统默认否'); +INSERT INTO `sys_dict_data` VALUES (14, '000000', 1, '通知', '1', 'sys_notice_type', '', 'warning', 'Y', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '通知'); +INSERT INTO `sys_dict_data` VALUES (15, '000000', 2, '公告', '2', 'sys_notice_type', '', 'success', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '公告'); +INSERT INTO `sys_dict_data` VALUES (16, '000000', 1, '正常', '0', 'sys_notice_status', '', 'primary', 'Y', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '正常状态'); +INSERT INTO `sys_dict_data` VALUES (17, '000000', 2, '关闭', '1', 'sys_notice_status', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '关闭状态'); +INSERT INTO `sys_dict_data` VALUES (18, '000000', 1, '新增', '1', 'sys_oper_type', '', 'info', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '新增操作'); +INSERT INTO `sys_dict_data` VALUES (19, '000000', 2, '修改', '2', 'sys_oper_type', '', 'info', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '修改操作'); +INSERT INTO `sys_dict_data` VALUES (20, '000000', 3, '删除', '3', 'sys_oper_type', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '删除操作'); +INSERT INTO `sys_dict_data` VALUES (21, '000000', 4, '授权', '4', 'sys_oper_type', '', 'primary', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '授权操作'); +INSERT INTO `sys_dict_data` VALUES (22, '000000', 5, '导出', '5', 'sys_oper_type', '', 'warning', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '导出操作'); +INSERT INTO `sys_dict_data` VALUES (23, '000000', 6, '导入', '6', 'sys_oper_type', '', 'warning', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '导入操作'); +INSERT INTO `sys_dict_data` VALUES (24, '000000', 7, '强退', '7', 'sys_oper_type', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '强退操作'); +INSERT INTO `sys_dict_data` VALUES (25, '000000', 8, '生成代码', '8', 'sys_oper_type', '', 'warning', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '生成操作'); +INSERT INTO `sys_dict_data` VALUES (26, '000000', 9, '清空数据', '9', 'sys_oper_type', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '清空操作'); +INSERT INTO `sys_dict_data` VALUES (27, '000000', 1, '成功', '0', 'sys_common_status', '', 'primary', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '正常状态'); +INSERT INTO `sys_dict_data` VALUES (28, '000000', 2, '失败', '1', 'sys_common_status', '', 'danger', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '停用状态'); +INSERT INTO `sys_dict_data` VALUES (29, '000000', 99, '其他', '0', 'sys_oper_type', '', 'info', 'N', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '其他操作'); +INSERT INTO `sys_dict_data` VALUES (1775756996568993793, '000000', 1, '免费用户', '0', 'sys_user_grade', '', 'info', 'N', '0', 103, 1, '2024-04-04 13:27:15', 1, '2024-04-04 13:30:09', ''); +INSERT INTO `sys_dict_data` VALUES (1775757116970684418, '000000', 2, '高级会员', '1', 'sys_user_grade', '', 'success', 'N', '0', 103, 1, '2024-04-04 13:27:43', 1, '2024-04-04 13:30:15', ''); +INSERT INTO `sys_dict_data` VALUES (1776109770934677506, '000000', 0, 'token计费', '1', 'sys_model_billing', '', 'primary', 'N', '0', 103, 1, '2024-04-05 12:49:03', 1, '2024-04-21 00:05:41', ''); +INSERT INTO `sys_dict_data` VALUES (1776109853377916929, '000000', 0, '次数计费', '2', 'sys_model_billing', '', 'success', 'N', '0', 103, 1, '2024-04-05 12:49:22', 1, '2024-04-05 12:49:22', ''); +INSERT INTO `sys_dict_data` VALUES (1780264338471858177, '000000', 0, '未支付', '1', 'pay_state', '', 'info', 'N', '0', 103, 1, '2024-04-16 23:57:49', 1, '2024-04-16 23:58:29', ''); +INSERT INTO `sys_dict_data` VALUES (1780264431589601282, '000000', 2, '已支付', '2', 'pay_state', '', 'success', 'N', '0', 103, 1, '2024-04-16 23:58:11', 1, '2024-04-16 23:58:21', ''); +INSERT INTO `sys_dict_data` VALUES (1933094189606670338, '000000', 0, '知识库', 'vector', 'prompt_template_type', NULL, '', 'N', '0', 103, 1, '2025-06-12 17:29:05', 1, '2025-06-12 17:29:05', NULL); +INSERT INTO `sys_dict_data` VALUES (1938226101050925057, '000000', 1, '中转模型-chat', 'chat', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:21:28', 1, '2025-06-26 21:22:26', NULL); +INSERT INTO `sys_dict_data` VALUES (1938226833825193985, '000000', 1, '本地部署模型-ollama', 'ollama', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:24:22', 1, '2025-06-26 21:24:22', NULL); +INSERT INTO `sys_dict_data` VALUES (1938226919661625345, '000000', 1, 'DIFY-dify', 'dify', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:24:43', 1, '2025-06-26 21:24:43', NULL); +INSERT INTO `sys_dict_data` VALUES (1938226981422751746, '000000', 1, '扣子-coze', 'coze', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:24:58', 1, '2025-06-26 21:24:58', NULL); +INSERT INTO `sys_dict_data` VALUES (1938227034350673922, '000000', 1, '智谱清言-zhipu', 'zhipu', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:25:10', 1, '2025-06-26 21:25:10', NULL); +INSERT INTO `sys_dict_data` VALUES (1938227086750113793, '000000', 1, '深度求索-deepseek', 'deepseek', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:25:23', 1, '2025-06-26 21:25:23', NULL); +INSERT INTO `sys_dict_data` VALUES (1938227141741633537, '000000', 1, '通义千问-qianwen', 'qianwen', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:25:36', 1, '2025-06-26 21:25:36', NULL); +INSERT INTO `sys_dict_data` VALUES (1938227191834206209, '000000', 1, '知识库向量模型-vector', 'vector', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:25:48', 1, '2025-06-26 21:25:48', NULL); +INSERT INTO `sys_dict_data` VALUES (1938227249417805826, '000000', 1, '图片识别模型-image', 'image', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-06-26 21:26:01', 1, '2025-06-26 21:26:01', NULL); +INSERT INTO `sys_dict_data` VALUES (1940594785010503681, '000000', 1, 'FASTGPT-fastgpt', 'fastgpt', 'chat_model_category', NULL, '', 'N', '0', 103, 1, '2025-07-03 10:13:46', 1, '2025-07-03 10:13:46', NULL); +INSERT INTO `sys_dict_data` VALUES (1971580207002615809, '000000', 0, '未发布', '0', 'aihuman_is_publish', NULL, '#949494', 'N', '0', NULL, NULL, '2025-09-26 22:18:46', NULL, '2025-09-26 22:18:46', NULL); +INSERT INTO `sys_dict_data` VALUES (1971580286589534210, '000000', 1, '已发布', '1', 'aihuman_is_publish', NULL, '#00a89d', 'N', '0', NULL, NULL, '2025-09-26 22:19:05', 1, '2025-09-26 22:19:25', NULL); + +SET FOREIGN_KEY_CHECKS = 1; + + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sys_dict_type +-- ---------------------------- +DROP TABLE IF EXISTS `sys_dict_type`; +CREATE TABLE `sys_dict_type` ( + `dict_id` bigint(0) NOT NULL COMMENT '字典主键', + `tenant_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户编号', + `dict_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典名称', + `dict_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '字典类型', + `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '状态(0正常 1停用)', + `create_dept` bigint(0) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(0) DEFAULT NULL COMMENT '创建者', + `create_time` datetime(0) DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(0) DEFAULT NULL COMMENT '更新者', + `update_time` datetime(0) DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`dict_id`) USING BTREE, + UNIQUE INDEX `tenant_id`(`tenant_id`, `dict_type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '字典类型表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_dict_type +-- ---------------------------- +INSERT INTO `sys_dict_type` VALUES (1, '000000', '用户性别', 'sys_user_sex', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '用户性别列表'); +INSERT INTO `sys_dict_type` VALUES (2, '000000', '菜单状态', 'sys_show_hide', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '菜单状态列表'); +INSERT INTO `sys_dict_type` VALUES (3, '000000', '系统开关', 'sys_normal_disable', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '系统开关列表'); +INSERT INTO `sys_dict_type` VALUES (6, '000000', '系统是否', 'sys_yes_no', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '系统是否列表'); +INSERT INTO `sys_dict_type` VALUES (7, '000000', '通知类型', 'sys_notice_type', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '通知类型列表'); +INSERT INTO `sys_dict_type` VALUES (8, '000000', '通知状态', 'sys_notice_status', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '通知状态列表'); +INSERT INTO `sys_dict_type` VALUES (9, '000000', '操作类型', 'sys_oper_type', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '操作类型列表'); +INSERT INTO `sys_dict_type` VALUES (10, '000000', '系统状态', 'sys_common_status', '0', 103, 1, '2023-05-14 15:19:41', NULL, NULL, '登录状态列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083714, '911866', '用户性别', 'sys_user_sex', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '用户性别列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083715, '911866', '菜单状态', 'sys_show_hide', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '菜单状态列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083716, '911866', '系统开关', 'sys_normal_disable', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '系统开关列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083717, '911866', '系统是否', 'sys_yes_no', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '系统是否列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083718, '911866', '通知类型', 'sys_notice_type', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '通知类型列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083719, '911866', '通知状态', 'sys_notice_status', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '通知状态列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083720, '911866', '操作类型', 'sys_oper_type', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '操作类型列表'); +INSERT INTO `sys_dict_type` VALUES (1729685494468083721, '911866', '系统状态', 'sys_common_status', '0', 103, 1, '2023-05-14 15:19:41', 1, '2023-05-14 15:19:41', '登录状态列表'); +INSERT INTO `sys_dict_type` VALUES (1775756736895438849, '000000', '用户等级', 'sys_user_grade', '0', 103, 1, '2024-04-04 13:26:13', 1, '2024-04-04 13:26:13', ''); +INSERT INTO `sys_dict_type` VALUES (1776109665045278721, '000000', '模型计费方式', 'sys_model_billing', '0', 103, 1, '2024-04-05 12:48:37', 1, '2024-04-08 11:22:18', '模型计费方式'); +INSERT INTO `sys_dict_type` VALUES (1780263881368219649, '000000', '支付状态', 'pay_state', '0', 103, 1, '2024-04-16 23:56:00', 1, '2025-03-29 15:21:57', '支付状态'); +INSERT INTO `sys_dict_type` VALUES (1904565568803217409, '000000', '状态类型', 'status_type', '0', 103, 1, '2025-03-26 00:06:31', 1, '2025-03-26 00:06:31', NULL); +INSERT INTO `sys_dict_type` VALUES (1933093946274123777, '000000', '提示词模板分类', 'prompt_template_type', '0', 103, 1, '2025-06-12 17:28:07', 1, '2025-06-12 17:28:07', '提示词模板类型'); +INSERT INTO `sys_dict_type` VALUES (1938225899023884289, '000000', '模型分类', 'chat_model_category', '0', 103, 1, '2025-06-26 21:20:39', 1, '2025-06-26 21:20:39', '模型分类'); +INSERT INTO `sys_dict_type` VALUES (1971579935501123586, '000000', '发布状态', 'aihuman_is_publish', '0', NULL, NULL, '2025-09-26 22:17:41', NULL, '2025-09-26 22:17:41', '0 代表未发布,1代表发布'); + +SET FOREIGN_KEY_CHECKS = 1; + + + + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sys_menu +-- ---------------------------- +DROP TABLE IF EXISTS `sys_menu`; +CREATE TABLE `sys_menu` ( + `menu_id` bigint(0) NOT NULL COMMENT '菜单ID', + `menu_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称', + `parent_id` bigint(0) DEFAULT 0 COMMENT '父菜单ID', + `order_num` int(0) DEFAULT 0 COMMENT '显示顺序', + `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '路由地址', + `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '组件路径', + `query_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由参数', + `is_frame` int(0) DEFAULT 1 COMMENT '是否为外链(0是 1否)', + `is_cache` int(0) DEFAULT 0 COMMENT '是否缓存(0缓存 1不缓存)', + `menu_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)', + `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '显示状态(0显示 1隐藏)', + `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', + `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '权限标识', + `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '#' COMMENT '菜单图标', + `create_dept` bigint(0) DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(0) DEFAULT NULL COMMENT '创建者', + `create_time` datetime(0) DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(0) DEFAULT NULL COMMENT '更新者', + `update_time` datetime(0) DEFAULT NULL COMMENT '更新时间', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '备注', + PRIMARY KEY (`menu_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '菜单权限表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_menu +-- ---------------------------- +INSERT INTO `sys_menu` VALUES (1, '系统管理', 0, 5, 'system', '', '', 1, 0, 'M', '0', '0', '', 'eos-icons:system-group', 103, 1, '2023-05-14 15:19:39', 1, '2025-09-26 20:19:31', '系统管理目录'); +INSERT INTO `sys_menu` VALUES (100, '用户管理', 1775500307898949634, 1, 'user', 'operator/user/index', '', 1, 0, 'C', '0', '0', 'system:user:list', 'ph:user-fill', 103, 1, '2023-05-14 15:19:39', 1, '2024-10-07 21:29:29', '用户管理菜单'); +INSERT INTO `sys_menu` VALUES (101, '角色管理', 1, 2, 'role', 'system/role/index', '', 1, 0, 'C', '0', '0', 'system:role:list', 'ri:user-3-fill', 103, 1, '2023-05-14 15:19:39', 1, '2024-10-07 21:04:59', '角色管理菜单'); +INSERT INTO `sys_menu` VALUES (102, '菜单管理', 1, 3, 'menu', 'system/menu/index', '', 1, 0, 'C', '0', '0', 'system:menu:list', 'typcn:th-menu-outline', 103, 1, '2023-05-14 15:19:39', 1, '2024-10-07 21:06:06', '菜单管理菜单'); +INSERT INTO `sys_menu` VALUES (103, '部门管理', 1, 4, 'dept', 'system/dept/index', '', 1, 0, 'C', '1', '1', 'system:dept:list', 'mdi:company', 103, 1, '2023-05-14 15:19:39', 1, '2024-10-07 21:07:38', '部门管理菜单'); +INSERT INTO `sys_menu` VALUES (104, '岗位管理', 1, 5, 'post', 'system/post/index', '', 1, 0, 'C', '1', '1', 'system:post:list', 'post', 103, 1, '2023-05-14 15:19:39', 1, '2024-04-04 22:36:15', '岗位管理菜单'); +INSERT INTO `sys_menu` VALUES (105, '字典管理', 1, 6, 'dict', 'system/dict/index', '', 1, 0, 'C', '0', '0', 'system:dict:list', 'fluent-mdl2:dictionary', 103, 1, '2023-05-14 15:19:40', 1, '2024-10-07 21:14:33', '字典管理菜单'); +INSERT INTO `sys_menu` VALUES (106, '系统参数', 1, 10, 'config', 'system/config/index', '', 1, 0, 'C', '0', '0', 'system:config:list', 'tdesign:system-code', 103, 1, '2023-05-14 15:19:40', 1, '2024-10-07 21:11:07', '参数设置菜单'); +INSERT INTO `sys_menu` VALUES (107, '通知公告', 1, 14, 'notice', 'system/notice/index', '', 1, 0, 'C', '0', '0', 'system:notice:list', 'icon-park-solid:volume-notice', 103, 1, '2023-05-14 15:19:40', 1, '2024-10-07 21:11:42', '通知公告菜单'); +INSERT INTO `sys_menu` VALUES (108, '日志管理', 1, 9, 'log', '', '', 1, 0, 'M', '0', '0', '', 'icon-park-solid:log', 103, 1, '2023-05-14 15:19:40', 1, '2024-10-07 21:10:41', '日志管理菜单'); +INSERT INTO `sys_menu` VALUES (113, '缓存监控', 1, 5, 'cache', 'monitor/cache/index', '', 1, 0, 'C', '0', '0', 'monitor:cache:list', 'octicon:cache-24', 103, 1, '2023-05-14 15:19:40', 1, '2024-10-07 21:09:44', '缓存监控菜单'); +INSERT INTO `sys_menu` VALUES (500, '操作日志', 108, 1, 'operlog', 'monitor/operlog/index', '', 1, 0, 'C', '0', '0', 'monitor:operlog:list', 'icon-park-solid:log', 103, 1, '2023-05-14 15:19:40', 1, '2024-10-07 21:13:20', '操作日志菜单'); +INSERT INTO `sys_menu` VALUES (501, '登录日志', 108, 2, 'logininfor', 'monitor/logininfor/index', '', 1, 0, 'C', '0', '0', 'monitor:logininfor:list', 'icon-park-solid:log', 103, 1, '2023-05-14 15:19:40', 1, '2024-10-07 21:13:33', '登录日志菜单'); +INSERT INTO `sys_menu` VALUES (1001, '用户查询', 100, 1, '', '', '', 1, 0, 'F', '0', '0', 'system:user:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1002, '用户新增', 100, 2, '', '', '', 1, 0, 'F', '0', '0', 'system:user:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1003, '用户修改', 100, 3, '', '', '', 1, 0, 'F', '0', '0', 'system:user:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1004, '用户删除', 100, 4, '', '', '', 1, 0, 'F', '0', '0', 'system:user:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1005, '用户导出', 100, 5, '', '', '', 1, 0, 'F', '0', '0', 'system:user:export', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1006, '用户导入', 100, 6, '', '', '', 1, 0, 'F', '0', '0', 'system:user:import', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1007, '重置密码', 100, 7, '', '', '', 1, 0, 'F', '0', '0', 'system:user:resetPwd', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1008, '角色查询', 101, 1, '', '', '', 1, 0, 'F', '0', '0', 'system:role:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1009, '角色新增', 101, 2, '', '', '', 1, 0, 'F', '0', '0', 'system:role:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1010, '角色修改', 101, 3, '', '', '', 1, 0, 'F', '0', '0', 'system:role:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1011, '角色删除', 101, 4, '', '', '', 1, 0, 'F', '0', '0', 'system:role:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1012, '角色导出', 101, 5, '', '', '', 1, 0, 'F', '0', '0', 'system:role:export', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1013, '菜单查询', 102, 1, '', '', '', 1, 0, 'F', '0', '0', 'system:menu:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1014, '菜单新增', 102, 2, '', '', '', 1, 0, 'F', '0', '0', 'system:menu:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1015, '菜单修改', 102, 3, '', '', '', 1, 0, 'F', '0', '0', 'system:menu:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1016, '菜单删除', 102, 4, '', '', '', 1, 0, 'F', '0', '0', 'system:menu:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1017, '部门查询', 103, 1, '', '', '', 1, 0, 'F', '0', '0', 'system:dept:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1018, '部门新增', 103, 2, '', '', '', 1, 0, 'F', '0', '0', 'system:dept:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1019, '部门修改', 103, 3, '', '', '', 1, 0, 'F', '0', '0', 'system:dept:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1020, '部门删除', 103, 4, '', '', '', 1, 0, 'F', '0', '0', 'system:dept:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1021, '岗位查询', 104, 1, '', '', '', 1, 0, 'F', '0', '0', 'system:post:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1022, '岗位新增', 104, 2, '', '', '', 1, 0, 'F', '0', '0', 'system:post:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1023, '岗位修改', 104, 3, '', '', '', 1, 0, 'F', '0', '0', 'system:post:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1024, '岗位删除', 104, 4, '', '', '', 1, 0, 'F', '0', '0', 'system:post:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1025, '岗位导出', 104, 5, '', '', '', 1, 0, 'F', '0', '0', 'system:post:export', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1026, '字典查询', 105, 1, '#', '', '', 1, 0, 'F', '0', '0', 'system:dict:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1027, '字典新增', 105, 2, '#', '', '', 1, 0, 'F', '0', '0', 'system:dict:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1028, '字典修改', 105, 3, '#', '', '', 1, 0, 'F', '0', '0', 'system:dict:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1029, '字典删除', 105, 4, '#', '', '', 1, 0, 'F', '0', '0', 'system:dict:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1030, '字典导出', 105, 5, '#', '', '', 1, 0, 'F', '0', '0', 'system:dict:export', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1031, '参数查询', 106, 1, '#', '', '', 1, 0, 'F', '0', '0', 'system:config:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1032, '参数新增', 106, 2, '#', '', '', 1, 0, 'F', '0', '0', 'system:config:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1033, '参数修改', 106, 3, '#', '', '', 1, 0, 'F', '0', '0', 'system:config:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1034, '参数删除', 106, 4, '#', '', '', 1, 0, 'F', '0', '0', 'system:config:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1035, '参数导出', 106, 5, '#', '', '', 1, 0, 'F', '0', '0', 'system:config:export', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1036, '公告查询', 107, 1, '#', '', '', 1, 0, 'F', '0', '0', 'system:notice:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1037, '公告新增', 107, 2, '#', '', '', 1, 0, 'F', '0', '0', 'system:notice:add', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1038, '公告修改', 107, 3, '#', '', '', 1, 0, 'F', '0', '0', 'system:notice:edit', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1039, '公告删除', 107, 4, '#', '', '', 1, 0, 'F', '0', '0', 'system:notice:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1040, '操作查询', 500, 1, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1041, '操作删除', 500, 2, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1042, '日志导出', 500, 4, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:operlog:export', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1043, '登录查询', 501, 1, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:query', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1044, '登录删除', 501, 2, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:remove', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1045, '日志导出', 501, 3, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:export', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1050, '账户解锁', 501, 4, '#', '', '', 1, 0, 'F', '0', '0', 'monitor:logininfor:unlock', '#', 103, 1, '2023-05-14 15:19:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (2000, '在线开发', 0, 20, 'dev', '', '', 1, 0, 'M', '0', '0', '', 'carbon:development', 103, 1, '2025-07-11 19:38:05', 1, '2025-07-11 19:43:03', '在线开发目录'); +INSERT INTO `sys_menu` VALUES (1775500307898949634, '运营管理', 0, 3, 'operate', '', NULL, 1, 0, 'M', '0', '0', NULL, 'icon-park-outline:appointment', 103, 1, '2024-04-03 20:27:15', 1, '2025-09-26 20:19:38', ''); +INSERT INTO `sys_menu` VALUES (1775895273104068610, '系统模型', 1775500307898949634, 2, 'model', 'operator/model/index', NULL, 1, 0, 'C', '0', '0', 'system:model:list', 'ph:list-fill', 103, 1, '2024-04-05 12:00:38', 1, '2024-10-07 21:36:00', '系统模型菜单'); +INSERT INTO `sys_menu` VALUES (1775895273104068611, '系统模型查询', 1775895273104068610, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:model:query', '#', 103, 1, '2024-04-05 12:00:38', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1775895273104068612, '系统模型新增', 1775895273104068610, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:model:add', '#', 103, 1, '2024-04-05 12:00:38', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1775895273104068613, '系统模型修改', 1775895273104068610, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:model:edit', '#', 103, 1, '2024-04-05 12:00:38', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1775895273104068614, '系统模型删除', 1775895273104068610, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:model:remove', '#', 103, 1, '2024-04-05 12:00:38', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1775895273104068615, '系统模型导出', 1775895273104068610, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:model:export', '#', 103, 1, '2024-04-05 12:00:38', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780240077690507266, '聊天消息', 1775500307898949634, 5, 'chatMessage', 'operator/message/index', NULL, 1, 0, 'C', '0', '0', 'system:message:list', 'bx:chat', 103, 1, '2024-04-16 22:24:48', 1, '2024-10-07 21:38:49', '聊天消息菜单'); +INSERT INTO `sys_menu` VALUES (1780240077690507267, '聊天消息查询', 1780240077690507266, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:message:query', '#', 103, 1, '2024-04-16 22:24:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780240077690507268, '聊天消息新增', 1780240077690507266, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:message:add', '#', 103, 1, '2024-04-16 22:24:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780240077690507269, '聊天消息修改', 1780240077690507266, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:message:edit', '#', 103, 1, '2024-04-16 22:24:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780240077690507270, '聊天消息删除', 1780240077690507266, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:message:remove', '#', 103, 1, '2024-04-16 22:24:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780240077690507271, '聊天消息导出', 1780240077690507266, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:message:export', '#', 103, 1, '2024-04-16 22:24:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780255628576018433, '支付订单', 1775500307898949634, 6, 'order', 'operator/payOrder/index', NULL, 1, 0, 'C', '0', '0', 'system:order:list', 'material-symbols:order-approve', 103, 1, '2024-04-16 23:32:48', 1, '2025-03-30 21:12:38', '支付订单菜单'); +INSERT INTO `sys_menu` VALUES (1780255628576018434, '支付订单查询', 1780255628576018433, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:orders:query', '#', 103, 1, '2024-04-16 23:32:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780255628576018435, '支付订单新增', 1780255628576018433, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:orders:add', '#', 103, 1, '2024-04-16 23:32:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780255628576018436, '支付订单修改', 1780255628576018433, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:orders:edit', '#', 103, 1, '2024-04-16 23:32:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780255628576018437, '支付订单删除', 1780255628576018433, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:orders:remove', '#', 103, 1, '2024-04-16 23:32:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1780255628576018438, '支付订单导出', 1780255628576018433, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:orders:export', '#', 103, 1, '2024-04-16 23:32:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1843281231381852162, '文件管理', 1775500307898949634, 20, 'file', 'operator/oss/index', NULL, 1, 0, 'C', '0', '0', NULL, 'material-symbols-light:folder', 103, 1, '2024-10-07 21:24:27', 1, '2024-12-27 23:03:04', ''); +INSERT INTO `sys_menu` VALUES (1898286496441393153, '知识库管理', 1775500307898949634, 10, 'knowledgeBase', 'operator/knowledgeBase/index', NULL, 1, 0, 'C', '0', '0', '', 'garden:knowledge-base-26', 103, 1, '2025-03-08 16:15:44', 1, '2025-03-10 00:21:26', ''); +INSERT INTO `sys_menu` VALUES (1906674838461321217, '配置信息', 1775500307898949634, 13, 'configurationManage', 'operator/configurationManage/index', '', 1, 0, 'C', '0', '0', 'system:config:list', 'mdi:archive-cog-outline', 103, 1, '2025-03-31 19:48:48', 1, '2025-03-31 19:59:58', '配置信息菜单'); +INSERT INTO `sys_menu` VALUES (1906674838461321218, '配置信息查询', 1906674838461321217, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:query', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1906674838461321219, '配置信息新增', 1906674838461321217, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:add', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1906674838461321220, '配置信息修改', 1906674838461321217, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:edit', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1906674838461321221, '配置信息删除', 1906674838461321217, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:remove', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1906674838461321222, '配置信息导出', 1906674838461321217, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:config:export', '#', 103, 1, '2025-03-31 19:48:48', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1929170702299045890, '提示词模板', 1775500307898949634, 1, 'promptTemplate', 'operator/promptTemplate/index', '', 1, 0, 'C', '0', '0', 'system:promptTemplate:list', 'fluent:prompt-16-filled', 103, 1, '2025-09-17 16:43:40', NULL, NULL, '提示词模板菜单'); +INSERT INTO `sys_menu` VALUES (1929170702299045891, '提示词模板查询', 1929170702299045890, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:query', '#', 103, 1, '2025-09-17 16:43:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1929170702299045892, '提示词模板新增', 1929170702299045890, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:add', '#', 103, 1, '2025-09-17 16:43:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1929170702299045893, '提示词模板修改', 1929170702299045890, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:edit', '#', 103, 1, '2025-09-17 16:43:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1929170702299045894, '提示词模板删除', 1929170702299045890, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:remove', '#', 103, 1, '2025-09-17 16:43:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1929170702299045895, '提示词模板导出', 1929170702299045890, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'system:promptTemplate:export', '#', 103, 1, '2025-09-17 16:43:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944213468857495553, '模型分组', 2000, 1, 'schemaGroup', 'dev/schemaGroup/index', NULL, 1, 0, 'C', '0', '0', NULL, '#', 103, 1, '2025-07-13 09:53:07', 1, '2025-07-13 09:54:45', '模型分组菜单'); +INSERT INTO `sys_menu` VALUES (1944213468857495554, '模型分组查询', 1944213468857495553, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaGroup:list', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944213468857495555, '模型分组新增', 1944213468857495553, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaGroup:add', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944213468857495556, '模型分组修改', 1944213468857495553, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaGroup:edit', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944213468857495557, '模型分组删除', 1944213468857495553, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaGroup:remove', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944229086906281985, '数据模型', 2000, 2, 'schema', 'dev/schema/index', NULL, 1, 0, 'C', '0', '0', NULL, '#', 103, 1, '2025-07-13 10:55:11', NULL, '2025-07-13 10:55:11', '数据模型菜单'); +INSERT INTO `sys_menu` VALUES (1944229086906281986, '模型数据查询', 1944229086906281985, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schema:list', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944229086906281987, '模型数据新增', 1944229086906281985, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schema:add', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944229086906281988, '模型数据修改', 1944229086906281985, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schema:edit', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1944229086906281989, '模型数据删除', 1944229086906281985, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schema:remove', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1946466176918249473, '模型字段管理', 2000, 3, 'schemaField', 'dev/schemaField/index', NULL, 1, 0, 'C', '0', '0', NULL, '#', 103, 1, '2025-07-19 15:04:35', NULL, '2025-07-19 15:04:35', '模型字段管理菜单'); +INSERT INTO `sys_menu` VALUES (1946466176918249474, '模型字段管理查询', 1946466176918249473, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaField:list', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1946466176918249475, '模型字段管理新增', 1946466176918249473, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaField:add', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1946466176918249476, '模型字段管理修改', 1946466176918249473, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaField:edit', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1946466176918249477, '模型字段管理删除', 1946466176918249473, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'dev:schemaField:remove', '#', 103, 1, '2025-06-24 19:06:58', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1946483381643743233, '知识库角色管理', 1775500307898949634, 12, 'knowledgeRole', 'operator/knowledgeRole/index', NULL, 1, 0, 'C', '0', '0', NULL, 'ri:user-3-fill', 103, 1, '2025-07-19 16:41:17', NULL, NULL, '知识库角色管理'); +INSERT INTO `sys_menu` VALUES (1971550631887237121, '数字人管理', 0, 0, 'aihuman', '', NULL, 1, 0, 'M', '0', '0', NULL, 'mdi:account-cog', NULL, NULL, '2025-09-26 20:21:15', NULL, '2025-09-26 20:21:15', ''); +INSERT INTO `sys_menu` VALUES (1971582278942666752, '交互数字人配置', 1971550631887237121, 1, 'aihumanConfig', 'aihuman/aihumanConfig/index', NULL, 1, 0, 'C', '0', '0', 'aihuman:aihumanConfig:list', '#', 103, 1, '2025-09-26 22:40:40', 1, '2025-09-26 22:43:10', '交互数字人配置菜单'); +INSERT INTO `sys_menu` VALUES (1971582278942666753, '交互数字人配置查询', 1971582278942666752, 1, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:query', '#', 103, 1, '2025-09-26 22:40:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971582278942666754, '交互数字人配置新增', 1971582278942666752, 2, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:add', '#', 103, 1, '2025-09-26 22:40:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971582278942666755, '交互数字人配置修改', 1971582278942666752, 3, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:edit', '#', 103, 1, '2025-09-26 22:40:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971582278942666756, '交互数字人配置删除', 1971582278942666752, 4, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:remove', '#', 103, 1, '2025-09-26 22:40:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1971582278942666757, '交互数字人配置导出', 1971582278942666752, 5, '#', '', NULL, 1, 0, 'F', '0', '0', 'aihuman:aihumanConfig:export', '#', 103, 1, '2025-09-26 22:40:40', NULL, NULL, ''); +INSERT INTO `sys_menu` VALUES (1972543718952386561, 'Live2D数字人体验', 1971550631887237121, 10, 'aihumanPublish', 'aihuman/aihumanPublish/index', NULL, 1, 0, 'C', '0', '0', NULL, '#', NULL, NULL, '2025-09-29 14:07:25', 1, '2025-09-29 14:36:28', ''); + +SET FOREIGN_KEY_CHECKS = 1; + + + diff --git a/script/sql/update/2025-10-13-mcp工具管理.sql b/script/sql/update/2025-10-13-mcp工具管理.sql new file mode 100644 index 00000000..933ee11b --- /dev/null +++ b/script/sql/update/2025-10-13-mcp工具管理.sql @@ -0,0 +1,54 @@ +-- 菜单 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954103099019309056, 'MCP', '2000', '1', 'mcpInfo', 'operator/mcpInfo/index', 1, 0, 'C', '0', '0', 'operator:mcpInfo:list', '#', 103, 1, sysdate(), null, null, 'MCP菜单'); + +-- 按钮 SQL +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954103099019309057, 'MCP查询', 1954103099019309056, '1', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:query', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954103099019309058, 'MCP新增', 1954103099019309056, '2', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:add', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954103099019309059, 'MCP修改', 1954103099019309056, '3', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:edit', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954103099019309060, 'MCP删除', 1954103099019309056, '4', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:remove', '#', 103, 1, sysdate(), null, null, ''); + +insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_dept, create_by, create_time, update_by, update_time, remark) +values(1954103099019309061, 'MCP导出', 1954103099019309056, '5', '#', '', 1, 0, 'F', '0', '0', 'operator:mcpInfo:export', '#', 103, 1, sysdate(), null, null, ''); + + +-- ---------------------------- +-- Table structure for mcp_info +-- ---------------------------- +DROP TABLE IF EXISTS `mcp_info`; +CREATE TABLE `mcp_info` ( + `mcp_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', + `server_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '服务器名称', + `transport_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '链接方式', + `command` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '命令', + `arguments` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '参数', + `env` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '环境', + `status` tinyint(1) NULL DEFAULT NULL COMMENT '是否启用', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '工具描述', + `create_dept` bigint(20) NULL DEFAULT NULL COMMENT '创建部门', + `create_by` bigint(20) NULL DEFAULT NULL COMMENT '创建者', + `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', + `update_by` bigint(20) NULL DEFAULT NULL COMMENT '更新者', + `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', + PRIMARY KEY (`mcp_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'mcp工具管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of mcp_info +-- ---------------------------- +INSERT INTO `mcp_info` VALUES (1, 'howtocook-mcp', 'STDIO', 'npx', '[\"-y\", \"howtocook-mcp\"]', NULL, 1, NULL, NULL, NULL, '2025-08-11 17:19:25', 1, '2025-08-11 18:24:22', NULL); + + +INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098808913211393, '000000', 0, 'STDIO', 'STDIO', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:33:56', 1, '2025-08-09 16:34:19', NULL); +INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098960432443394, '000000', 1, 'SSE', 'SSE', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:34:32', NULL, '2025-08-09 16:34:32', NULL); +INSERT INTO `ruoyi-ai`.`sys_dict_data` (`dict_code`, `tenant_id`, `dict_sort`, `dict_label`, `dict_value`, `dict_type`, `css_class`, `list_class`, `is_default`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954099421436784642, '000000', 2, 'HTTP', 'HTTP', 'mcp_transport_type', NULL, '', 'N', '0', NULL, NULL, '2025-08-09 16:36:22', NULL, '2025-08-09 16:36:22', NULL); +INSERT INTO `ruoyi-ai`.`sys_dict_type` (`dict_id`, `tenant_id`, `dict_name`, `dict_type`, `status`, `create_dept`, `create_by`, `create_time`, `update_by`, `update_time`, `remark`) VALUES (1954098639622713345, '000000', 'mcp链接方式', 'mcp_transport_type', '0', NULL, NULL, '2025-08-09 16:33:16', NULL, '2025-08-09 16:33:16', NULL); + diff --git a/script/sql/update/2025-10-4-多供应商嵌入模型集成.sql b/script/sql/update/2025-10-4-多供应商嵌入模型集成.sql new file mode 100644 index 00000000..6619c20f --- /dev/null +++ b/script/sql/update/2025-10-4-多供应商嵌入模型集成.sql @@ -0,0 +1,13 @@ +-- 为 chat_model 表添加 provider_name 字段 +-- 变更日期: 2025-10-04 +-- 负责人: Robust_H +-- 说明: 嵌入模型供应商 (用于实现动态选择嵌入模型实现类) +ALTER TABLE `ruoyi-ai`.chat_model + ADD COLUMN `provider_name` varchar(20) DEFAULT NULL COMMENT '模型供应商' AFTER `model_name`; + +-- 修改 knowledge_info 中的 ‘embedding_model_name’ 为 ‘embedding_model_id’ +-- 变更日期: 2025-10-04 +-- 负责人: Robust_H +-- 说明: 用于区分多个供应商实现同一嵌入模型的情况 +ALTER TABLE `ruoyi-ai`.knowledge_info + ADD COLUMN `embedding_model_id` bigint DEFAULT NULL COMMENT '模型id' AFTER `embedding_model_name`; diff --git a/script/sql/update/fix-upload-bucket.sql b/script/sql/update/fix-upload-bucket.sql deleted file mode 100644 index faa2dc37..00000000 --- a/script/sql/update/fix-upload-bucket.sql +++ /dev/null @@ -1,8 +0,0 @@ --- 默认开启本地minio -UPDATE `ruoyi-ai`.sys_oss_config t -SET t.status = '1' -WHERE t.oss_config_id = 4; - -UPDATE `ruoyi-ai`.sys_oss_config t -SET t.status = '0' -WHERE t.oss_config_id = 1; \ No newline at end of file