mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-04-11 02:37:06 +00:00
feat: 工作流第一版提交
This commit is contained in:
432
docs/工作流模块说明.md
Normal file
432
docs/工作流模块说明.md
Normal file
@@ -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<ObjectNode> 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<WfNodeState> 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<NodeIOData> 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);
|
||||||
|
```
|
||||||
@@ -43,6 +43,11 @@
|
|||||||
<artifactId>ruoyi-system-api</artifactId>
|
<artifactId>ruoyi-system-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.ruoyi</groupId>
|
||||||
|
<artifactId>ruoyi-common-satoken</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.ruoyi</groupId>
|
<groupId>org.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-common-mail</artifactId>
|
<artifactId>ruoyi-common-mail</artifactId>
|
||||||
|
|||||||
@@ -1,67 +1,122 @@
|
|||||||
package org.ruoyi.workflow.base;
|
package org.ruoyi.workflow.base;
|
||||||
|
|
||||||
import io.micrometer.common.util.StringUtils;
|
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.entity.User;
|
||||||
import org.ruoyi.workflow.enums.UserStatusEnum;
|
import org.ruoyi.workflow.enums.UserStatusEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
|
|
||||||
import static org.ruoyi.workflow.enums.ErrorEnum.A_USER_NOT_FOUND;
|
import static org.ruoyi.workflow.enums.ErrorEnum.A_USER_NOT_FOUND;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 线程上下文适配器,统一接入 Sa-Token 登录态。
|
||||||
|
*/
|
||||||
public class ThreadContext {
|
public class ThreadContext {
|
||||||
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
|
|
||||||
private static final ThreadLocal<String> currentToken = new ThreadLocal<>();
|
private static final ThreadLocal<User> CURRENT_USER = new ThreadLocal<>();
|
||||||
|
private static final ThreadLocal<String> CURRENT_TOKEN = new ThreadLocal<>();
|
||||||
|
|
||||||
private ThreadContext() {
|
private ThreadContext() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录的工作流用户。
|
||||||
|
*/
|
||||||
public static User getCurrentUser() {
|
public static User getCurrentUser() {
|
||||||
User user = new User();
|
User cached = CURRENT_USER.get();
|
||||||
user.setName("admin");
|
if (cached != null) {
|
||||||
user.setEmail("12345@qq.com");
|
return cached;
|
||||||
user.setUuid("123456789");
|
}
|
||||||
user.setUnderstandContextMsgPairNum(1);
|
LoginUser loginUser = LoginHelper.getLoginUser();
|
||||||
user.setQuotaByTokenDaily(1);
|
if (loginUser == null) {
|
||||||
user.setQuotaByTokenMonthly(1);
|
throw new BaseException(A_USER_NOT_FOUND.getInfo());
|
||||||
user.setQuotaByRequestDaily(1);
|
}
|
||||||
user.setQuotaByRequestMonthly(1);
|
User mapped = mapToWorkflowUser(loginUser);
|
||||||
user.setQuotaByImageDaily(1);
|
CURRENT_USER.set(mapped);
|
||||||
user.setQuotaByImageMonthly(1);
|
return mapped;
|
||||||
user.setUserStatus(UserStatusEnum.NORMAL);
|
|
||||||
user.setIsAdmin(true);
|
|
||||||
user.setId(1L);
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 允许在测试或特殊场景下显式设置当前用户。
|
||||||
|
*/
|
||||||
public static void setCurrentUser(User user) {
|
public static void setCurrentUser(User user) {
|
||||||
currentUser.set(user);
|
if (user == null) {
|
||||||
|
CURRENT_USER.remove();
|
||||||
|
} else {
|
||||||
|
CURRENT_USER.set(user);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户 ID。
|
||||||
|
*/
|
||||||
public static Long getCurrentUserId() {
|
public static Long getCurrentUserId() {
|
||||||
return 1L;
|
Long userId = LoginHelper.getUserId();
|
||||||
|
if (userId != null) {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
return getCurrentUser().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前访问 token。
|
||||||
|
*/
|
||||||
public static String getToken() {
|
public static String getToken() {
|
||||||
return currentToken.get();
|
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) {
|
public static void setToken(String token) {
|
||||||
currentToken.set(token);
|
if (StringUtils.isBlank(token)) {
|
||||||
|
CURRENT_TOKEN.remove();
|
||||||
|
} else {
|
||||||
|
CURRENT_TOKEN.set(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isLogin() {
|
public static boolean isLogin() {
|
||||||
return StringUtils.isNotBlank(currentToken.get());
|
return LoginHelper.isLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static User getExistCurrentUser() {
|
public static User getExistCurrentUser() {
|
||||||
User user = ThreadContext.getCurrentUser();
|
return getCurrentUser();
|
||||||
if (null == user) {
|
|
||||||
throw new WorkflowBaseException(A_USER_NOT_FOUND);
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unload() {
|
public static void unload() {
|
||||||
currentUser.remove();
|
CURRENT_USER.remove();
|
||||||
currentToken.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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package org.ruoyi.workflow.exception;
|
|
||||||
|
|
||||||
|
|
||||||
import org.ruoyi.workflow.enums.ErrorEnum;
|
|
||||||
|
|
||||||
import java.text.MessageFormat;
|
|
||||||
|
|
||||||
public class WorkflowBaseException extends RuntimeException {
|
|
||||||
private final String code;
|
|
||||||
private final String info;
|
|
||||||
|
|
||||||
private Object data;
|
|
||||||
|
|
||||||
public WorkflowBaseException(String code, String info) {
|
|
||||||
super(code + ":" + info);
|
|
||||||
this.code = code;
|
|
||||||
this.info = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public WorkflowBaseException(ErrorEnum errorEnum, String... infoValues) {
|
|
||||||
super(errorEnum.getCode() + ":" + MessageFormat.format(errorEnum.getInfo(), infoValues));
|
|
||||||
this.code = errorEnum.getCode();
|
|
||||||
if (infoValues.length > 0) {
|
|
||||||
this.info = MessageFormat.format(errorEnum.getInfo(), infoValues);
|
|
||||||
} else {
|
|
||||||
this.info = errorEnum.getInfo();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getCode() {
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getInfo() {
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object getData() {
|
|
||||||
if (null != data) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
return getMessage();
|
|
||||||
}
|
|
||||||
|
|
||||||
public WorkflowBaseException setData(Object data) {
|
|
||||||
this.data = data;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,11 +7,11 @@ import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
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.WfComponentReq;
|
||||||
import org.ruoyi.workflow.dto.workflow.WfComponentSearchReq;
|
import org.ruoyi.workflow.dto.workflow.WfComponentSearchReq;
|
||||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||||
import org.ruoyi.workflow.enums.ErrorEnum;
|
import org.ruoyi.workflow.enums.ErrorEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.mapper.WorkflowComponentMapper;
|
import org.ruoyi.workflow.mapper.WorkflowComponentMapper;
|
||||||
import org.ruoyi.workflow.util.PrivilegeUtil;
|
import org.ruoyi.workflow.util.PrivilegeUtil;
|
||||||
import org.ruoyi.workflow.util.UuidUtil;
|
import org.ruoyi.workflow.util.UuidUtil;
|
||||||
@@ -74,11 +74,18 @@ public class WorkflowComponentService extends ServiceImpl<WorkflowComponentMappe
|
|||||||
|
|
||||||
@CacheEvict(cacheNames = {WORKFLOW_COMPONENTS, WORKFLOW_COMPONENT_START_KEY})
|
@CacheEvict(cacheNames = {WORKFLOW_COMPONENTS, WORKFLOW_COMPONENT_START_KEY})
|
||||||
public void deleteByUuid(String uuid) {
|
public void deleteByUuid(String uuid) {
|
||||||
|
WorkflowComponent component = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_COMPONENT_NOT_FOUND);
|
||||||
Integer refNodeCount = baseMapper.countRefNodes(uuid);
|
Integer refNodeCount = baseMapper.countRefNodes(uuid);
|
||||||
if (refNodeCount > 0) {
|
if (refNodeCount != null && refNodeCount > 0) {
|
||||||
throw new WorkflowBaseException(C_WF_COMPONENT_DELETED_FAIL_BY_USED);
|
throw new BaseException(C_WF_COMPONENT_DELETED_FAIL_BY_USED.getInfo());
|
||||||
} else {
|
}
|
||||||
// PrivilegeUtil.checkAndDelete(uuid, this.query(), ChainWrappers.updateChain(baseMapper), ErrorEnum.A_WF_COMPONENT_NOT_FOUND);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +113,7 @@ public class WorkflowComponentService extends ServiceImpl<WorkflowComponentMappe
|
|||||||
return components.stream()
|
return components.stream()
|
||||||
.filter(component -> WfComponentNameEnum.START.getName().equals(component.getName()))
|
.filter(component -> WfComponentNameEnum.START.getName().equals(component.getName()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> new WorkflowBaseException(ErrorEnum.B_WF_NODE_DEFINITION_NOT_FOUND));
|
.orElseThrow(() -> new BaseException(ErrorEnum.B_WF_NODE_DEFINITION_NOT_FOUND.getInfo()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorkflowComponent getComponent(Long id) {
|
public WorkflowComponent getComponent(Long id) {
|
||||||
@@ -114,6 +121,6 @@ public class WorkflowComponentService extends ServiceImpl<WorkflowComponentMappe
|
|||||||
return components.stream()
|
return components.stream()
|
||||||
.filter(component -> component.getId().equals(id))
|
.filter(component -> component.getId().equals(id))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.orElseThrow(() -> new WorkflowBaseException(ErrorEnum.B_WF_NODE_DEFINITION_NOT_FOUND));
|
.orElseThrow(() -> new BaseException(ErrorEnum.B_WF_NODE_DEFINITION_NOT_FOUND.getInfo()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
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.dto.workflow.WfEdgeReq;
|
||||||
import org.ruoyi.workflow.entity.WorkflowEdge;
|
import org.ruoyi.workflow.entity.WorkflowEdge;
|
||||||
import org.ruoyi.workflow.enums.ErrorEnum;
|
import org.ruoyi.workflow.enums.ErrorEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.mapper.WorkflowEdgeMapper;
|
import org.ruoyi.workflow.mapper.WorkflowEdgeMapper;
|
||||||
import org.ruoyi.workflow.util.MPPageUtil;
|
import org.ruoyi.workflow.util.MPPageUtil;
|
||||||
import org.ruoyi.workflow.util.UuidUtil;
|
import org.ruoyi.workflow.util.UuidUtil;
|
||||||
@@ -98,7 +98,7 @@ public class WorkflowEdgeService extends ServiceImpl<WorkflowEdgeMapper, Workflo
|
|||||||
WorkflowEdge old = self.getByUuid(uuid);
|
WorkflowEdge old = self.getByUuid(uuid);
|
||||||
if (null != old && !old.getWorkflowId().equals(workflowId)) {
|
if (null != old && !old.getWorkflowId().equals(workflowId)) {
|
||||||
log.error("该边不属于指定的工作流,删除失败,workflowId:{},node workflowId:{}", workflowId, workflowId);
|
log.error("该边不属于指定的工作流,删除失败,workflowId:{},node workflowId:{}", workflowId, workflowId);
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_PARAMS_ERROR);
|
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
ChainWrappers.lambdaUpdateChain(baseMapper)
|
ChainWrappers.lambdaUpdateChain(baseMapper)
|
||||||
.eq(WorkflowEdge::getWorkflowId, workflowId)
|
.eq(WorkflowEdge::getWorkflowId, workflowId)
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
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.dto.workflow.WfNodeDto;
|
||||||
import org.ruoyi.workflow.entity.Workflow;
|
import org.ruoyi.workflow.entity.Workflow;
|
||||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
import org.ruoyi.workflow.enums.ErrorEnum;
|
import org.ruoyi.workflow.enums.ErrorEnum;
|
||||||
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.mapper.WorkflowNodeMapper;
|
import org.ruoyi.workflow.mapper.WorkflowNodeMapper;
|
||||||
import org.ruoyi.workflow.util.JsonUtil;
|
import org.ruoyi.workflow.util.JsonUtil;
|
||||||
import org.ruoyi.workflow.util.MPPageUtil;
|
import org.ruoyi.workflow.util.MPPageUtil;
|
||||||
@@ -135,7 +135,7 @@ public class WorkflowNodeService extends ServiceImpl<WorkflowNodeMapper, Workflo
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (null == component) {
|
if (null == component) {
|
||||||
log.error("节点不存在,uuid:{},title:{}", workflowNode.getUuid(), workflowNode.getTitle());
|
log.error("节点不存在,uuid:{},title:{}", workflowNode.getUuid(), workflowNode.getTitle());
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_PARAMS_ERROR);
|
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
if (component.getName().equals(WfComponentNameEnum.MAIL_SEND.getName())) {
|
if (component.getName().equals(WfComponentNameEnum.MAIL_SEND.getName())) {
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ public class WorkflowNodeService extends ServiceImpl<WorkflowNodeMapper, Workflo
|
|||||||
.orElse(null);
|
.orElse(null);
|
||||||
if (null == component) {
|
if (null == component) {
|
||||||
log.error("节点不存在,uuid:{},title:{}", workflowNode.getUuid(), workflowNode.getTitle());
|
log.error("节点不存在,uuid:{},title:{}", workflowNode.getUuid(), workflowNode.getTitle());
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_PARAMS_ERROR);
|
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
if (component.getName().equals(WfComponentNameEnum.MAIL_SEND.getName())) {
|
if (component.getName().equals(WfComponentNameEnum.MAIL_SEND.getName())) {
|
||||||
// MailSendNodeConfig mailSendNodeConfig = JsonUtil.fromJson(workflowNode.getNodeConfig(), MailSendNodeConfig.class);
|
// MailSendNodeConfig mailSendNodeConfig = JsonUtil.fromJson(workflowNode.getNodeConfig(), MailSendNodeConfig.class);
|
||||||
@@ -189,7 +189,7 @@ public class WorkflowNodeService extends ServiceImpl<WorkflowNodeMapper, Workflo
|
|||||||
}
|
}
|
||||||
if (!old.getWorkflowId().equals(workflowId)) {
|
if (!old.getWorkflowId().equals(workflowId)) {
|
||||||
log.error("节点不属于指定的工作流,删除失败,workflowId:{},node workflowId:{}", workflowId, workflowId);
|
log.error("节点不属于指定的工作流,删除失败,workflowId:{},node workflowId:{}", workflowId, workflowId);
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_PARAMS_ERROR);
|
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
if (workflowComponentService.getStartComponent().getId().equals(old.getWorkflowComponentId())) {
|
if (workflowComponentService.getStartComponent().getId().equals(old.getWorkflowComponentId())) {
|
||||||
log.warn("开始节点不能删除,uuid:{}", old.getUuid());
|
log.warn("开始节点不能删除,uuid:{}", old.getUuid());
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.extension.toolkit.ChainWrappers;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.ruoyi.common.core.exception.base.BaseException;
|
||||||
import org.ruoyi.workflow.base.ThreadContext;
|
import org.ruoyi.workflow.base.ThreadContext;
|
||||||
import org.ruoyi.workflow.dto.workflow.WfEdgeReq;
|
import org.ruoyi.workflow.dto.workflow.WfEdgeReq;
|
||||||
import org.ruoyi.workflow.dto.workflow.WfNodeDto;
|
import org.ruoyi.workflow.dto.workflow.WfNodeDto;
|
||||||
@@ -14,7 +15,6 @@ import org.ruoyi.workflow.dto.workflow.WorkflowUpdateReq;
|
|||||||
import org.ruoyi.workflow.entity.User;
|
import org.ruoyi.workflow.entity.User;
|
||||||
import org.ruoyi.workflow.entity.Workflow;
|
import org.ruoyi.workflow.entity.Workflow;
|
||||||
import org.ruoyi.workflow.enums.ErrorEnum;
|
import org.ruoyi.workflow.enums.ErrorEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.mapper.WorkflowMapper;
|
import org.ruoyi.workflow.mapper.WorkflowMapper;
|
||||||
import org.ruoyi.workflow.util.MPPageUtil;
|
import org.ruoyi.workflow.util.MPPageUtil;
|
||||||
import org.ruoyi.workflow.util.PrivilegeUtil;
|
import org.ruoyi.workflow.util.PrivilegeUtil;
|
||||||
@@ -70,7 +70,7 @@ public class WorkflowService extends ServiceImpl<WorkflowMapper, Workflow> {
|
|||||||
|
|
||||||
public WorkflowResp updateBaseInfo(String wfUuid, String title, String remark, Boolean isPublic) {
|
public WorkflowResp updateBaseInfo(String wfUuid, String title, String remark, Boolean isPublic) {
|
||||||
if (StringUtils.isAnyBlank(wfUuid, title)) {
|
if (StringUtils.isAnyBlank(wfUuid, title)) {
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_PARAMS_ERROR);
|
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
ChainWrappers.lambdaUpdateChain(baseMapper)
|
ChainWrappers.lambdaUpdateChain(baseMapper)
|
||||||
.eq(Workflow::getUuid, wfUuid)
|
.eq(Workflow::getUuid, wfUuid)
|
||||||
@@ -108,7 +108,7 @@ public class WorkflowService extends ServiceImpl<WorkflowMapper, Workflow> {
|
|||||||
.last("limit 1")
|
.last("limit 1")
|
||||||
.one();
|
.one();
|
||||||
if (null == workflow) {
|
if (null == workflow) {
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_WF_NOT_FOUND);
|
throw new BaseException(ErrorEnum.A_WF_NOT_FOUND.getInfo());
|
||||||
}
|
}
|
||||||
return workflow;
|
return workflow;
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ public class WorkflowService extends ServiceImpl<WorkflowMapper, Workflow> {
|
|||||||
|
|
||||||
public void enable(String uuid, Boolean enable) {
|
public void enable(String uuid, Boolean enable) {
|
||||||
if (null == enable) {
|
if (null == enable) {
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_PARAMS_ERROR);
|
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
Workflow workflow = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_NOT_FOUND);
|
Workflow workflow = PrivilegeUtil.checkAndGetByUuid(uuid, this.query(), ErrorEnum.A_WF_NOT_FOUND);
|
||||||
ChainWrappers.lambdaUpdateChain(baseMapper)
|
ChainWrappers.lambdaUpdateChain(baseMapper)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.ruoyi.workflow.util;
|
package org.ruoyi.workflow.util;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.core.type.TypeReference;
|
import com.fasterxml.jackson.core.type.TypeReference;
|
||||||
@@ -17,7 +16,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package org.ruoyi.workflow.util;
|
package org.ruoyi.workflow.util;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
|
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.base.ThreadContext;
|
||||||
import org.ruoyi.workflow.enums.ErrorEnum;
|
import org.ruoyi.workflow.enums.ErrorEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
|
|
||||||
import static org.ruoyi.workflow.cosntant.AdiConstant.*;
|
import static org.ruoyi.workflow.cosntant.AdiConstant.*;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ public class PrivilegeUtil {
|
|||||||
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);
|
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) {
|
if (null == target) {
|
||||||
throw new WorkflowBaseException(exceptionMessage);
|
throw new BaseException(exceptionMessage.getInfo());
|
||||||
}
|
}
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package org.ruoyi.workflow.workflow;
|
package org.ruoyi.workflow.workflow;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.util.ObjectUtil;
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import org.apache.commons.collections4.CollectionUtils;
|
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.ErrorEnum;
|
||||||
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.util.JsonUtil;
|
import org.ruoyi.workflow.util.JsonUtil;
|
||||||
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
import org.ruoyi.workflow.workflow.data.NodeIOData;
|
||||||
import org.ruoyi.workflow.workflow.data.NodeIODataFilesContent;
|
import org.ruoyi.workflow.workflow.data.NodeIODataFilesContent;
|
||||||
@@ -36,7 +35,7 @@ public class WfNodeIODataUtil {
|
|||||||
JsonNode nameObj = data.get("name");
|
JsonNode nameObj = data.get("name");
|
||||||
JsonNode content = data.get("content");
|
JsonNode content = data.get("content");
|
||||||
if (null == nameObj || null == content) {
|
if (null == nameObj || null == content) {
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_PARAMS_ERROR);
|
throw new BaseException(ErrorEnum.A_PARAMS_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
String name = nameObj.asText();
|
String name = nameObj.asText();
|
||||||
Integer type = content.get("type").asInt();
|
Integer type = content.get("type").asInt();
|
||||||
|
|||||||
@@ -10,16 +10,15 @@ import org.bsc.async.AsyncGenerator;
|
|||||||
import org.bsc.langgraph4j.*;
|
import org.bsc.langgraph4j.*;
|
||||||
import org.bsc.langgraph4j.checkpoint.MemorySaver;
|
import org.bsc.langgraph4j.checkpoint.MemorySaver;
|
||||||
import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator;
|
import org.bsc.langgraph4j.langchain4j.generators.StreamingChatGenerator;
|
||||||
import org.bsc.langgraph4j.serializer.std.ObjectStreamStateSerializer;
|
|
||||||
import org.bsc.langgraph4j.state.AgentState;
|
import org.bsc.langgraph4j.state.AgentState;
|
||||||
import org.bsc.langgraph4j.state.StateSnapshot;
|
import org.bsc.langgraph4j.state.StateSnapshot;
|
||||||
import org.bsc.langgraph4j.streaming.StreamingOutput;
|
import org.bsc.langgraph4j.streaming.StreamingOutput;
|
||||||
|
import org.ruoyi.common.core.exception.base.BaseException;
|
||||||
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
||||||
import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto;
|
import org.ruoyi.workflow.dto.workflow.WfRuntimeNodeDto;
|
||||||
import org.ruoyi.workflow.dto.workflow.WfRuntimeResp;
|
import org.ruoyi.workflow.dto.workflow.WfRuntimeResp;
|
||||||
import org.ruoyi.workflow.entity.*;
|
import org.ruoyi.workflow.entity.*;
|
||||||
import org.ruoyi.workflow.enums.ErrorEnum;
|
import org.ruoyi.workflow.enums.ErrorEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
||||||
import org.ruoyi.workflow.service.WorkflowRuntimeNodeService;
|
import org.ruoyi.workflow.service.WorkflowRuntimeNodeService;
|
||||||
import org.ruoyi.workflow.service.WorkflowRuntimeService;
|
import org.ruoyi.workflow.service.WorkflowRuntimeService;
|
||||||
@@ -34,12 +33,8 @@ import java.util.*;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import static org.bsc.langgraph4j.StateGraph.END;
|
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.cosntant.AdiConstant.WorkflowConstant.*;
|
import static org.ruoyi.workflow.cosntant.AdiConstant.WorkflowConstant.*;
|
||||||
import static org.ruoyi.workflow.enums.ErrorEnum.*;
|
import static org.ruoyi.workflow.enums.ErrorEnum.*;
|
||||||
import static org.ruoyi.workflow.workflow.WfComponentNameEnum.HUMAN_FEEDBACK;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class WorkflowEngine {
|
public class WorkflowEngine {
|
||||||
@@ -50,11 +45,6 @@ public class WorkflowEngine {
|
|||||||
private final SSEEmitterHelper sseEmitterHelper;
|
private final SSEEmitterHelper sseEmitterHelper;
|
||||||
private final WorkflowRuntimeService workflowRuntimeService;
|
private final WorkflowRuntimeService workflowRuntimeService;
|
||||||
private final WorkflowRuntimeNodeService workflowRuntimeNodeService;
|
private final WorkflowRuntimeNodeService workflowRuntimeNodeService;
|
||||||
private final ObjectStreamStateSerializer<WfNodeState> stateSerializer = new ObjectStreamStateSerializer<>(WfNodeState::new);
|
|
||||||
private final Map<String, List<StateGraph<WfNodeState>>> stateGraphNodes = new HashMap<>();
|
|
||||||
private final Map<String, List<StateGraph<WfNodeState>>> stateGraphEdges = new HashMap<>();
|
|
||||||
private final Map<String, String> rootToSubGraph = new HashMap<>();
|
|
||||||
private final Map<String, GraphCompileNode> nodeToParallelBranch = new HashMap<>();
|
|
||||||
private CompiledGraph<WfNodeState> app;
|
private CompiledGraph<WfNodeState> app;
|
||||||
private SseEmitter sseEmitter;
|
private SseEmitter sseEmitter;
|
||||||
private User user;
|
private User user;
|
||||||
@@ -84,7 +74,7 @@ public class WorkflowEngine {
|
|||||||
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
|
log.info("WorkflowEngine run,userId:{},workflowUuid:{},userInputs:{}", user.getId(), workflow.getUuid(), userInputs);
|
||||||
if (!this.workflow.getIsEnable()) {
|
if (!this.workflow.getIsEnable()) {
|
||||||
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, ErrorEnum.A_WF_DISABLED.getInfo());
|
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, ErrorEnum.A_WF_DISABLED.getInfo());
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_WF_DISABLED);
|
throw new BaseException(ErrorEnum.A_WF_DISABLED.getInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
Long workflowId = this.workflow.getId();
|
Long workflowId = this.workflow.getId();
|
||||||
@@ -96,20 +86,17 @@ public class WorkflowEngine {
|
|||||||
Pair<WorkflowNode, Set<WorkflowNode>> startAndEnds = findStartAndEndNode();
|
Pair<WorkflowNode, Set<WorkflowNode>> startAndEnds = findStartAndEndNode();
|
||||||
WorkflowNode startNode = startAndEnds.getLeft();
|
WorkflowNode startNode = startAndEnds.getLeft();
|
||||||
List<NodeIOData> wfInputs = getAndCheckUserInput(userInputs, startNode);
|
List<NodeIOData> wfInputs = getAndCheckUserInput(userInputs, startNode);
|
||||||
//工作流运行实例状态
|
|
||||||
this.wfState = new WfState(user, wfInputs, runtimeUuid);
|
this.wfState = new WfState(user, wfInputs, runtimeUuid);
|
||||||
workflowRuntimeService.updateInput(this.wfRuntimeResp.getId(), wfState);
|
workflowRuntimeService.updateInput(this.wfRuntimeResp.getId(), wfState);
|
||||||
CompileNode rootCompileNode = new CompileNode();
|
|
||||||
rootCompileNode.setId(startNode.getUuid());
|
|
||||||
|
|
||||||
//构建整棵树
|
|
||||||
buildCompileNode(rootCompileNode, startNode);
|
|
||||||
|
|
||||||
//主状态图
|
WorkflowGraphBuilder graphBuilder = new WorkflowGraphBuilder(
|
||||||
StateGraph<WfNodeState> mainStateGraph = new StateGraph<>(stateSerializer);
|
components,
|
||||||
this.wfState.addEdge(START, startNode.getUuid());
|
wfNodes,
|
||||||
//构建包括所有节点的状态图
|
wfEdges,
|
||||||
buildStateGraph(null, mainStateGraph, rootCompileNode);
|
this::runNode,
|
||||||
|
this.wfState);
|
||||||
|
StateGraph<WfNodeState> mainStateGraph = graphBuilder.build(startNode);
|
||||||
|
|
||||||
MemorySaver saver = new MemorySaver();
|
MemorySaver saver = new MemorySaver();
|
||||||
CompileConfig compileConfig = CompileConfig.builder().checkpointSaver(saver)
|
CompileConfig compileConfig = CompileConfig.builder().checkpointSaver(saver)
|
||||||
@@ -130,13 +117,13 @@ public class WorkflowEngine {
|
|||||||
|
|
||||||
StateSnapshot<WfNodeState> stateSnapshot = app.getState(invokeConfig);
|
StateSnapshot<WfNodeState> stateSnapshot = app.getState(invokeConfig);
|
||||||
String nextNode = stateSnapshot.config().nextNode().orElse("");
|
String nextNode = stateSnapshot.config().nextNode().orElse("");
|
||||||
//还有下个节点,表示进入中断状态,等待用户输入后继续执行
|
//还有下个节点,表示进入中断状态,等待用户输入后继续执<EFBFBD>?
|
||||||
if (StringUtils.isNotBlank(nextNode) && !nextNode.equalsIgnoreCase(END)) {
|
if (StringUtils.isNotBlank(nextNode) && !nextNode.equalsIgnoreCase(END)) {
|
||||||
String intTip = WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes);
|
String intTip = WorkflowUtil.getHumanFeedbackTip(nextNode, wfNodes);
|
||||||
//将等待输入信息[事件与提示词]发送到到客户端
|
//将等待输入信息[事件与提示词]发送到到客户端
|
||||||
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_WAIT_FEEDBACK_BY_" + nextNode + "]", intTip);
|
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_WAIT_FEEDBACK_BY_" + nextNode + "]", intTip);
|
||||||
InterruptedFlow.RUNTIME_TO_GRAPH.put(wfState.getUuid(), this);
|
InterruptedFlow.RUNTIME_TO_GRAPH.put(wfState.getUuid(), this);
|
||||||
//更新状态
|
//更新状<EFBFBD>?
|
||||||
wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_WAITING_INPUT);
|
wfState.setProcessStatus(WORKFLOW_PROCESS_STATUS_WAITING_INPUT);
|
||||||
workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
|
workflowRuntimeService.updateOutput(wfRuntimeResp.getId(), wfState);
|
||||||
} else {
|
} else {
|
||||||
@@ -170,7 +157,7 @@ public class WorkflowEngine {
|
|||||||
log.error("error", e);
|
log.error("error", e);
|
||||||
String errorMsg = e.getMessage();
|
String errorMsg = e.getMessage();
|
||||||
if (errorMsg.contains("parallel node doesn't support conditional branch")) {
|
if (errorMsg.contains("parallel node doesn't support conditional branch")) {
|
||||||
errorMsg = "并行节点中不能包含条件分支";
|
errorMsg = "并行节点中不能包含条件分<EFBFBD>?";
|
||||||
}
|
}
|
||||||
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg);
|
sseEmitterHelper.sendErrorAndComplete(user.getId(), sseEmitter, errorMsg);
|
||||||
workflowRuntimeService.updateStatus(wfRuntimeResp.getId(), WORKFLOW_PROCESS_STATUS_FAIL, errorMsg);
|
workflowRuntimeService.updateStatus(wfRuntimeResp.getId(), WORKFLOW_PROCESS_STATUS_FAIL, errorMsg);
|
||||||
@@ -214,7 +201,7 @@ public class WorkflowEngine {
|
|||||||
}
|
}
|
||||||
}, (is) -> {
|
}, (is) -> {
|
||||||
workflowRuntimeNodeService.updateOutput(runtimeNodeDto.getId(), nodeState);
|
workflowRuntimeNodeService.updateOutput(runtimeNodeDto.getId(), nodeState);
|
||||||
//并行节点内部的节点执行结束后,需要主动向客户端发送输出结果
|
//并行节点内部的节点执行结束后,需要主动向客户端发送输出结<EFBFBD>?
|
||||||
String nodeUuid = wfNode.getUuid();
|
String nodeUuid = wfNode.getUuid();
|
||||||
List<NodeIOData> nodeOutputs = nodeState.getOutputs();
|
List<NodeIOData> nodeOutputs = nodeState.getOutputs();
|
||||||
for (NodeIOData output : nodeOutputs) {
|
for (NodeIOData output : nodeOutputs) {
|
||||||
@@ -227,10 +214,10 @@ public class WorkflowEngine {
|
|||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Node run error", e);
|
log.error("Node run error", e);
|
||||||
throw new WorkflowBaseException(ErrorEnum.B_WF_RUN_ERROR);
|
throw new BaseException(ErrorEnum.B_WF_RUN_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
resultMap.put("name", wfNode.getTitle());
|
resultMap.put("name", wfNode.getTitle());
|
||||||
//langgraph4j state中的data不做数据存储,只存储元数据
|
//langgraph4j state中的data不做数据存储,只存储元数<EFBFBD>?
|
||||||
StreamingChatGenerator<AgentState> generator = wfState.getNodeToStreamingGenerator().get(wfNode.getUuid());
|
StreamingChatGenerator<AgentState> generator = wfState.getNodeToStreamingGenerator().get(wfNode.getUuid());
|
||||||
if (null != generator) {
|
if (null != generator) {
|
||||||
resultMap.put("_streaming_messages", generator);
|
resultMap.put("_streaming_messages", generator);
|
||||||
@@ -251,6 +238,9 @@ public class WorkflowEngine {
|
|||||||
String node = streamingOutput.node();
|
String node = streamingOutput.node();
|
||||||
String chunk = streamingOutput.chunk();
|
String chunk = streamingOutput.chunk();
|
||||||
log.info("node:{},chunk:{}", node, chunk);
|
log.info("node:{},chunk:{}", node, chunk);
|
||||||
|
Map<String, String> strMap = new HashMap<>();
|
||||||
|
strMap.put("ck", chunk);
|
||||||
|
// SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", strMap.toString());
|
||||||
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
|
SSEEmitterHelper.parseAndSendPartialMsg(sseEmitter, "[NODE_CHUNK_" + node + "]", chunk);
|
||||||
} else {
|
} else {
|
||||||
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()
|
AbstractWfNode abstractWfNode = wfState.getCompletedNodes().stream()
|
||||||
@@ -274,8 +264,8 @@ public class WorkflowEngine {
|
|||||||
* 校验用户输入并组装成工作流的输入
|
* 校验用户输入并组装成工作流的输入
|
||||||
*
|
*
|
||||||
* @param userInputs 用户输入
|
* @param userInputs 用户输入
|
||||||
* @param startNode 开始节点定义
|
* @param startNode 开始节点定<EFBFBD>?
|
||||||
* @return 正确的用户输入列表
|
* @return 正确的用户输入列<EFBFBD>?
|
||||||
*/
|
*/
|
||||||
private List<NodeIOData> getAndCheckUserInput(List<ObjectNode> userInputs, WorkflowNode startNode) {
|
private List<NodeIOData> getAndCheckUserInput(List<ObjectNode> userInputs, WorkflowNode startNode) {
|
||||||
WfNodeInputConfig wfNodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(startNode.getInputConfig());
|
WfNodeInputConfig wfNodeInputConfig = NodeInputConfigTypeHandler.fillNodeInputConfig(startNode.getInputConfig());
|
||||||
@@ -292,19 +282,19 @@ public class WorkflowEngine {
|
|||||||
}
|
}
|
||||||
Integer dataType = nodeIOData.getContent().getType();
|
Integer dataType = nodeIOData.getContent().getType();
|
||||||
if (null == dataType) {
|
if (null == dataType) {
|
||||||
throw new WorkflowBaseException(A_WF_INPUT_INVALID);
|
throw new BaseException(A_WF_INPUT_INVALID.getInfo());
|
||||||
}
|
}
|
||||||
requiredParamMissing = false;
|
requiredParamMissing = false;
|
||||||
boolean valid = paramDefinition.checkValue(nodeIOData);
|
boolean valid = paramDefinition.checkValue(nodeIOData);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
log.error("用户输入无效,workflowId:{}", startNode.getWorkflowId());
|
log.error("用户输入无效,workflowId:{}", startNode.getWorkflowId());
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_WF_INPUT_INVALID);
|
throw new BaseException(ErrorEnum.A_WF_INPUT_INVALID.getInfo());
|
||||||
}
|
}
|
||||||
wfInputs.add(nodeIOData);
|
wfInputs.add(nodeIOData);
|
||||||
}
|
}
|
||||||
if (requiredParamMissing) {
|
if (requiredParamMissing) {
|
||||||
log.error("在流程定义中必填的参数没有传进来,name:{}", paramNameFromDef);
|
log.error("在流程定义中必填的参数没有传进来,name:{}", paramNameFromDef);
|
||||||
throw new WorkflowBaseException(A_WF_INPUT_MISSING);
|
throw new BaseException(A_WF_INPUT_MISSING.getInfo());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return wfInputs;
|
return wfInputs;
|
||||||
@@ -323,7 +313,7 @@ public class WorkflowEngine {
|
|||||||
Optional<WorkflowComponent> wfComponent = components.stream().filter(item -> item.getId().equals(node.getWorkflowComponentId())).findFirst();
|
Optional<WorkflowComponent> wfComponent = components.stream().filter(item -> item.getId().equals(node.getWorkflowComponentId())).findFirst();
|
||||||
if (wfComponent.isPresent() && WfComponentNameEnum.START.getName().equals(wfComponent.get().getName())) {
|
if (wfComponent.isPresent() && WfComponentNameEnum.START.getName().equals(wfComponent.get().getName())) {
|
||||||
if (null != startNode) {
|
if (null != startNode) {
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_WF_MULTIPLE_START_NODE);
|
throw new BaseException(ErrorEnum.A_WF_MULTIPLE_START_NODE.getInfo());
|
||||||
}
|
}
|
||||||
startNode = node;
|
startNode = node;
|
||||||
} else if (wfComponent.isPresent() && WfComponentNameEnum.END.getName().equals(wfComponent.get().getName())) {
|
} else if (wfComponent.isPresent() && WfComponentNameEnum.END.getName().equals(wfComponent.get().getName())) {
|
||||||
@@ -331,8 +321,8 @@ public class WorkflowEngine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (null == startNode) {
|
if (null == startNode) {
|
||||||
log.error("没有开始节点,workflowId:{}", wfNodes.get(0).getWorkflowId());
|
log.error("没有开始节点, workflowId:{}", wfNodes.get(0).getWorkflowId());
|
||||||
throw new WorkflowBaseException(ErrorEnum.A_WF_START_NODE_NOT_FOUND);
|
throw new BaseException(ErrorEnum.A_WF_START_NODE_NOT_FOUND.getInfo());
|
||||||
}
|
}
|
||||||
//Find all end nodes
|
//Find all end nodes
|
||||||
wfNodes.forEach(item -> {
|
wfNodes.forEach(item -> {
|
||||||
@@ -354,217 +344,11 @@ public class WorkflowEngine {
|
|||||||
log.info("end nodes:{}", endNodes);
|
log.info("end nodes:{}", endNodes);
|
||||||
if (endNodes.isEmpty()) {
|
if (endNodes.isEmpty()) {
|
||||||
log.error("没有结束节点,workflowId:{}", startNode.getWorkflowId());
|
log.error("没有结束节点,workflowId:{}", startNode.getWorkflowId());
|
||||||
throw new WorkflowBaseException(A_WF_END_NODE_NOT_FOUND);
|
throw new BaseException(A_WF_END_NODE_NOT_FOUND.getInfo());
|
||||||
}
|
}
|
||||||
return Pair.of(startNode, endNodes);
|
return Pair.of(startNode, endNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buildCompileNode(
|
|
||||||
CompileNode parentNode,
|
|
||||||
WorkflowNode node) {
|
|
||||||
log.info("buildByNode, parentNode:{}, node:{},title:{}", parentNode.getId(), node.getUuid(), node.getTitle());
|
|
||||||
CompileNode newNode;
|
|
||||||
List<String> 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 (null == newNode) {
|
|
||||||
log.error("节点{}不存在", node.getUuid());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
List<String> downstreamUuids = getDownstreamNodeUuids(node.getUuid());
|
|
||||||
for (String downstream : downstreamUuids) {
|
|
||||||
Optional<WorkflowNode> n = wfNodes.stream().filter(item -> item.getUuid().equals(downstream)).findFirst();
|
|
||||||
n.ifPresent(workflowNode -> buildCompileNode(newNode, workflowNode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建完整的stategraph
|
|
||||||
*
|
|
||||||
* @param upstreamCompileNode 上游节点
|
|
||||||
* @param stateGraph 当前状态图
|
|
||||||
* @param compileNode 当前节点
|
|
||||||
* @throws GraphStateException 状态图异常
|
|
||||||
*/
|
|
||||||
private void buildStateGraph(CompileNode upstreamCompileNode, StateGraph<WfNodeState> stateGraph, CompileNode compileNode) throws GraphStateException {
|
|
||||||
log.info("buildStateGraph,upstreamCompileNode:{},node:{}", upstreamCompileNode, compileNode.getId());
|
|
||||||
String stateGraphNodeUuid = compileNode.getId();
|
|
||||||
if (null == upstreamCompileNode) {
|
|
||||||
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<WfNodeState> 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
//ConditionalEdge 的创建另外处理
|
|
||||||
if (Boolean.FALSE.equals(upstreamCompileNode.getConditional())) {
|
|
||||||
addEdgeToStateGraph(stateGraph, upstreamCompileNode.getId(), stateGraphNodeUuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
List<CompileNode> 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);
|
|
||||||
}
|
|
||||||
//节点是"条件分支"或"分类"的情况下不支持并行执行,所以直接使用条件ConditionalEdge
|
|
||||||
if (conditional) {
|
|
||||||
List<String> targets = nextNodes.stream().map(CompileNode::getId).toList();
|
|
||||||
Map<String, String> 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 (null == exist) {
|
|
||||||
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<String> getUpstreamNodeUuids(String nodeUuid) {
|
|
||||||
return this.wfEdges.stream()
|
|
||||||
.filter(edge -> edge.getTargetNodeUuid().equals(nodeUuid))
|
|
||||||
.map(WorkflowEdge::getSourceNodeUuid)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getDownstreamNodeUuids(String nodeUuid) {
|
|
||||||
return this.wfEdges.stream()
|
|
||||||
.filter(edge -> edge.getSourceNodeUuid().equals(nodeUuid))
|
|
||||||
.map(WorkflowEdge::getTargetNodeUuid)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
//判断节点是否属于子图
|
|
||||||
private boolean pointToParallelBranch(String nodeUuid) {
|
|
||||||
int edgeCount = 0;
|
|
||||||
for (WorkflowEdge edge : this.wfEdges) {
|
|
||||||
if (edge.getSourceNodeUuid().equals(nodeUuid) && StringUtils.isBlank(edge.getSourceHandle())) {
|
|
||||||
edgeCount = edgeCount + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return edgeCount > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加节点到状态图
|
|
||||||
*
|
|
||||||
* @param stateGraph
|
|
||||||
* @param stateGraphNodeUuid
|
|
||||||
* @throws GraphStateException
|
|
||||||
*/
|
|
||||||
private void addNodeToStateGraph(StateGraph<WfNodeState> stateGraph, String stateGraphNodeUuid) throws GraphStateException {
|
|
||||||
List<StateGraph<WfNodeState>> 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) -> runNode(wfNode, state)));
|
|
||||||
stateGraphList.add(stateGraph);
|
|
||||||
|
|
||||||
//记录人机交互节点
|
|
||||||
WorkflowComponent wfComponent = components.stream().filter(item -> item.getId().equals(wfNode.getWorkflowComponentId())).findFirst().orElseThrow();
|
|
||||||
if (HUMAN_FEEDBACK.getName().equals(wfComponent.getName())) {
|
|
||||||
this.wfState.addInterruptNode(stateGraphNodeUuid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addEdgeToStateGraph(StateGraph<WfNodeState> stateGraph, String source, String target) throws GraphStateException {
|
|
||||||
String key = source + "_" + target;
|
|
||||||
List<StateGraph<WfNodeState>> 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) {
|
|
||||||
return wfNodes.stream()
|
|
||||||
.filter(item -> item.getUuid().equals(nodeUuid))
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow(() -> new WorkflowBaseException(ErrorEnum.A_WF_NODE_NOT_FOUND));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public CompiledGraph<WfNodeState> getApp() {
|
public CompiledGraph<WfNodeState> getApp() {
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 负责构建工作流运行所依赖的状态图<E68081>?
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class WorkflowGraphBuilder {
|
||||||
|
|
||||||
|
private final Map<Long, WorkflowComponent> componentIndex;
|
||||||
|
private final Map<String, WorkflowNode> nodeIndex;
|
||||||
|
private final Map<String, List<WorkflowEdge>> edgesBySource;
|
||||||
|
private final Map<String, List<WorkflowEdge>> edgesByTarget;
|
||||||
|
private final WorkflowNodeRunner nodeRunner;
|
||||||
|
private final WfState wfState;
|
||||||
|
|
||||||
|
private final ObjectStreamStateSerializer<WfNodeState> stateSerializer = new ObjectStreamStateSerializer<>(WfNodeState::new);
|
||||||
|
private final Map<String, List<StateGraph<WfNodeState>>> stateGraphNodes = new HashMap<>();
|
||||||
|
private final Map<String, List<StateGraph<WfNodeState>>> stateGraphEdges = new HashMap<>();
|
||||||
|
private final Map<String, String> rootToSubGraph = new HashMap<>();
|
||||||
|
private final Map<String, GraphCompileNode> nodeToParallelBranch = new HashMap<>();
|
||||||
|
|
||||||
|
public WorkflowGraphBuilder(
|
||||||
|
List<WorkflowComponent> components,
|
||||||
|
List<WorkflowNode> nodes,
|
||||||
|
List<WorkflowEdge> 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<WfNodeState> build(WorkflowNode startNode) throws GraphStateException {
|
||||||
|
CompileNode rootCompileNode = new CompileNode();
|
||||||
|
rootCompileNode.setId(startNode.getUuid());
|
||||||
|
buildCompileNode(rootCompileNode, startNode);
|
||||||
|
|
||||||
|
StateGraph<WfNodeState> 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<String> 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("节点:{}不存<E4B88D>?", 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<WfNodeState> 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<WfNodeState> 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<CompileNode> 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<String> targets = nextNodes.stream().map(CompileNode::getId).toList();
|
||||||
|
Map<String, String> 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<String> getUpstreamNodeUuids(String nodeUuid) {
|
||||||
|
return edgesByTarget.getOrDefault(nodeUuid, List.of())
|
||||||
|
.stream()
|
||||||
|
.map(WorkflowEdge::getSourceNodeUuid)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> 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<WfNodeState> stateGraph, String stateGraphNodeUuid) throws GraphStateException {
|
||||||
|
List<StateGraph<WfNodeState>> 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<WfNodeState> stateGraph, String source, String target) throws GraphStateException {
|
||||||
|
String key = source + "_" + target;
|
||||||
|
List<StateGraph<WfNodeState>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package org.ruoyi.workflow.workflow;
|
||||||
|
|
||||||
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回调接口,负责执行业务节点并返回下游编排所需的元数据。
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface WorkflowNodeRunner {
|
||||||
|
|
||||||
|
Map<String, Object> run(WorkflowNode node, WfNodeState nodeState);
|
||||||
|
}
|
||||||
@@ -3,8 +3,8 @@ package org.ruoyi.workflow.workflow;
|
|||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.ruoyi.common.core.exception.base.BaseException;
|
||||||
import org.ruoyi.workflow.entity.*;
|
import org.ruoyi.workflow.entity.*;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
import org.ruoyi.workflow.helper.SSEEmitterHelper;
|
||||||
import org.ruoyi.workflow.service.*;
|
import org.ruoyi.workflow.service.*;
|
||||||
import org.springframework.context.annotation.Lazy;
|
import org.springframework.context.annotation.Lazy;
|
||||||
@@ -87,7 +87,7 @@ public class WorkflowStarter {
|
|||||||
WorkflowEngine workflowEngine = InterruptedFlow.RUNTIME_TO_GRAPH.get(runtimeUuid);
|
WorkflowEngine workflowEngine = InterruptedFlow.RUNTIME_TO_GRAPH.get(runtimeUuid);
|
||||||
if (null == workflowEngine) {
|
if (null == workflowEngine) {
|
||||||
log.error("工作流恢复执行时失败,runtime:{}", runtimeUuid);
|
log.error("工作流恢复执行时失败,runtime:{}", runtimeUuid);
|
||||||
throw new WorkflowBaseException(A_WF_RESUME_FAIL);
|
throw new BaseException(A_WF_RESUME_FAIL.getInfo());
|
||||||
}
|
}
|
||||||
workflowEngine.resume(userInput);
|
workflowEngine.resume(userInput);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.ruoyi.workflow.workflow.node;
|
package org.ruoyi.workflow.workflow.node;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import jakarta.validation.ConstraintViolation;
|
import jakarta.validation.ConstraintViolation;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
@@ -8,11 +7,11 @@ import lombok.Getter;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.SerializationUtils;
|
import org.apache.commons.lang3.SerializationUtils;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.ruoyi.common.core.exception.base.BaseException;
|
||||||
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
import org.ruoyi.workflow.base.NodeInputConfigTypeHandler;
|
||||||
import org.ruoyi.workflow.entity.WorkflowComponent;
|
import org.ruoyi.workflow.entity.WorkflowComponent;
|
||||||
import org.ruoyi.workflow.entity.WorkflowNode;
|
import org.ruoyi.workflow.entity.WorkflowNode;
|
||||||
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
import org.ruoyi.workflow.enums.WfIODataTypeEnum;
|
||||||
import org.ruoyi.workflow.exception.WorkflowBaseException;
|
|
||||||
import org.ruoyi.workflow.util.JsonUtil;
|
import org.ruoyi.workflow.util.JsonUtil;
|
||||||
import org.ruoyi.workflow.util.SpringUtil;
|
import org.ruoyi.workflow.util.SpringUtil;
|
||||||
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
import org.ruoyi.workflow.workflow.NodeProcessResult;
|
||||||
@@ -185,13 +184,13 @@ public abstract class AbstractWfNode {
|
|||||||
ObjectNode configObj = JsonUtil.toBean(node.getNodeConfig(), ObjectNode.class);
|
ObjectNode configObj = JsonUtil.toBean(node.getNodeConfig(), ObjectNode.class);
|
||||||
if (configObj.isEmpty()) {
|
if (configObj.isEmpty()) {
|
||||||
log.error("node config is empty,node uuid:{}", state.getUuid());
|
log.error("node config is empty,node uuid:{}", state.getUuid());
|
||||||
throw new WorkflowBaseException(A_WF_NODE_CONFIG_NOT_FOUND);
|
throw new BaseException(A_WF_NODE_CONFIG_NOT_FOUND.getInfo());
|
||||||
}
|
}
|
||||||
log.info("node config:{}", configObj);
|
log.info("node config:{}", configObj);
|
||||||
T nodeConfig = JsonUtil.fromJson(configObj, clazz);
|
T nodeConfig = JsonUtil.fromJson(configObj, clazz);
|
||||||
if (null == nodeConfig) {
|
if (null == nodeConfig) {
|
||||||
log.warn("找不到节点的配置,node uuid:{}", state.getUuid());
|
log.warn("找不到节点的配置,node uuid:{}", state.getUuid());
|
||||||
throw new WorkflowBaseException(A_WF_NODE_CONFIG_ERROR);
|
throw new BaseException(A_WF_NODE_CONFIG_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
boolean configValid = true;
|
boolean configValid = true;
|
||||||
try {
|
try {
|
||||||
@@ -206,7 +205,7 @@ public abstract class AbstractWfNode {
|
|||||||
}
|
}
|
||||||
if (!configValid) {
|
if (!configValid) {
|
||||||
log.warn("节点配置错误,node uuid:{}", state.getUuid());
|
log.warn("节点配置错误,node uuid:{}", state.getUuid());
|
||||||
throw new WorkflowBaseException(A_WF_NODE_CONFIG_ERROR);
|
throw new BaseException(A_WF_NODE_CONFIG_ERROR.getInfo());
|
||||||
}
|
}
|
||||||
return nodeConfig;
|
return nodeConfig;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user