Commit 9959707f by Archer Committed by GitHub

V4.14.9 fix issue (#6573)

* fix: session error

* fix: session error

* fix: workflow runtime and add e2b
parent f057a2ae
# FastGPT 工作流 Runtime 逻辑总结报告
## 概述
FastGPT 工作流 Runtime 是一个基于有向图的工作流执行引擎,支持复杂的分支、循环、并行执行等场景。本文档详细描述了工作流 Runtime 的最新逻辑设计和实现。
## 核心架构
### 1. 主要组件
#### 1.1 WorkflowQueue 类
工作流执行的核心类,负责管理节点执行队列和状态。
**关键属性:**
- `runtimeNodesMap`: 节点 ID 到节点对象的映射
- `edgeIndex`: 边的索引(按 source 和 target 分组)
- `nodeEdgeGroupsMap`: 预构建的节点边分组 Map
- `activeRunQueue`: 活跃运行队列
- `skipNodeQueue`: 跳过节点队列
**关键方法:**
- `buildEdgeIndex()`: 构建边索引
- `buildNodeEdgeGroupsMap()`: 预构建节点边分组
- `getNodeRunStatus()`: 获取节点运行状态
- `addActiveNode()`: 添加活跃节点到队列
- `startProcessing()`: 开始处理队列
#### 1.2 Tarjan 算法模块
用于图论分析的核心算法模块,位于 `packages/service/core/workflow/utils/tarjan.ts`。
**主要功能:**
- `findSCCs()`: 使用 Tarjan 算法找出所有强连通分量(SCC)
- `classifyEdgesByDFS()`: 使用 DFS 对边进行分类
- `isNodeInCycle()`: 判断节点是否在循环中
- `getEdgeType()`: 获取边的类型
### 2. 核心数据结构
#### 2.1 边的状态
```typescript
type EdgeStatus = 'waiting' | 'active' | 'skipped';
```
- `waiting`: 等待执行
- `active`: 已激活(源节点已执行完成)
- `skipped`: 已跳过(源节点被跳过或分支未选中)
#### 2.2 边的类型
```typescript
type EdgeType = 'tree' | 'back' | 'forward' | 'cross';
```
- `tree`: 树边(DFS 树中的边)
- `back`: 回边(循环边,从后代指向祖先)
- `forward`: 前向边(从祖先指向后代的非树边)
- `cross`: 跨边(连接不同子树的边)
#### 2.3 节点边分组
```typescript
type NodeEdgeGroups = RuntimeEdgeItemType[][];
type NodeEdgeGroupsMap = Map<string, NodeEdgeGroups>;
```
每个节点的输入边被分成多个组,每组代表一个独立的执行路径。
## 核心算法
### 1. 边分组算法
#### 1.1 算法流程
```
1. 全局 DFS 边分类
└─> 识别回边(循环边)
2. Tarjan SCC 算法
└─> 找出所有强连通分量
└─> 判断节点是否在循环中
3. 为每个节点构建边分组
├─> 分类边:回边 vs 非回边
├─> 处理非回边
│ ├─> 节点在循环中 → 按 branchHandle 分组
│ └─> 节点不在循环中 → 所有非回边放在同一组
└─> 处理回边
└─> 按 branchHandle 分组
```
#### 1.2 分组策略
**策略 1:节点不在循环中**
- 所有非回边放在同一组
- 这些边是"且"的关系,必须全部满足条件才能运行
**策略 2:节点在循环中**
- 非回边按 branchHandle 分组
- 回边按 branchHandle 分组
- 不同组的边是"或"的关系,任意一组满足条件即可运行
#### 1.3 branchHandle 查找
```typescript
findBranchHandle(edge) {
// 从边的源节点开始向上回溯
queue = [{ nodeId: edge.source, handle: edge.sourceHandle }]
while (queue.length > 0) {
{ nodeId, handle } = queue.shift()
// 如果当前节点是分支节点且有 handle,返回 handle
if (isBranchNode(node) && handle) {
return handle
}
// 继续向上回溯
for (inEdge of inEdges) {
newHandle = isBranchNode(sourceNode) ? inEdge.sourceHandle : handle
queue.push({ nodeId: inEdge.source, handle: newHandle })
}
}
return 'common'
}
```
### 2. 节点运行状态判断
#### 2.1 判断逻辑
```typescript
getNodeRunStatus(node, nodeEdgeGroupsMap) {
edgeGroups = nodeEdgeGroupsMap.get(node.nodeId)
// 1. 没有输入边 → 入口节点,直接运行
if (!edgeGroups || edgeGroups.length === 0) {
return 'run'
}
// 2. 检查是否可以运行(任意一组边满足条件)
// 每组边内:至少有一个 active,且没有 waiting
if (edgeGroups.some(group =>
group.some(edge => edge.status === 'active') &&
group.every(edge => edge.status !== 'waiting')
)) {
return 'run'
}
// 3. 检查是否跳过(所有组的边都是 skipped)
if (edgeGroups.every(group =>
group.every(edge => edge.status === 'skipped')
)) {
return 'skip'
}
// 4. 否则等待
return 'wait'
}
```
#### 2.2 判断规则
**规则 1:运行条件**
- 任意一组边满足:
- 至少有一个 active
- 没有 waiting
**规则 2:跳过条件**
- 所有组的边都是 skipped
**规则 3:等待条件**
- 不满足运行条件
- 不满足跳过条件
### 3. Tarjan SCC 算法
#### 3.1 算法原理
Tarjan 算法用于在有向图中找出所有强连通分量(Strongly Connected Components, SCC)。
**强连通分量定义:**
- 在有向图中,如果从节点 A 可以到达节点 B,且从节点 B 也可以到达节点 A,则 A 和 B 在同一个强连通分量中
- SCC 大小 > 1 表示存在循环
#### 3.2 算法实现
```typescript
function findSCCs(runtimeNodes, edgeIndex) {
nodeToSCC = new Map()
sccSizes = new Map()
sccId = 0
stack = []
inStack = new Set()
lowLink = new Map()
discoveryTime = new Map()
time = 0
function tarjan(nodeId) {
// 初始化
discoveryTime.set(nodeId, time)
lowLink.set(nodeId, time)
time++
stack.push(nodeId)
inStack.add(nodeId)
// 遍历所有出边
for (edge of outEdges) {
targetId = edge.target
if (!discoveryTime.has(targetId)) {
// 未访问过,递归访问
tarjan(targetId)
lowLink.set(nodeId, min(lowLink.get(nodeId), lowLink.get(targetId)))
} else if (inStack.has(targetId)) {
// 在栈中,更新 lowLink
lowLink.set(nodeId, min(lowLink.get(nodeId), discoveryTime.get(targetId)))
}
}
// 如果是 SCC 的根节点
if (lowLink.get(nodeId) === discoveryTime.get(nodeId)) {
sccNodes = []
do {
w = stack.pop()
inStack.delete(w)
nodeToSCC.set(w, sccId)
sccNodes.push(w)
} while (w !== nodeId)
sccSizes.set(sccId, sccNodes.length)
sccId++
}
}
// 从所有未访问节点开始
for (node of runtimeNodes) {
if (!discoveryTime.has(node.nodeId)) {
tarjan(node.nodeId)
}
}
return { nodeToSCC, sccSizes }
}
```
### 4. DFS 边分类算法
#### 4.1 算法原理
使用深度优先搜索(DFS)对图中的边进行分类。
#### 4.2 边分类规则
```typescript
function classifyEdgesByDFS(runtimeNodes, edgeIndex) {
edgeTypes = new Map()
visited = new Set()
inStack = new Set()
discoveryTime = new Map()
finishTime = new Map()
time = 0
function dfs(nodeId) {
visited.add(nodeId)
inStack.add(nodeId)
discoveryTime.set(nodeId, ++time)
for (edge of outEdges) {
targetId = edge.target
if (!visited.has(targetId)) {
// 未访问 → 树边
edgeTypes.set(edgeKey, 'tree')
dfs(targetId)
} else if (inStack.has(targetId)) {
// 在当前路径上 → 回边(循环边)
edgeTypes.set(edgeKey, 'back')
} else if (discoveryTime.get(source) < discoveryTime.get(targetId)) {
// 从祖先指向后代 → 前向边
edgeTypes.set(edgeKey, 'forward')
} else {
// 跨边
edgeTypes.set(edgeKey, 'cross')
}
}
inStack.delete(nodeId)
finishTime.set(nodeId, ++time)
}
// 从所有入口节点开始 DFS
for (node of entryNodes) {
if (!visited.has(node.nodeId)) {
dfs(node.nodeId)
}
}
return edgeTypes
}
```
## 典型场景分析
### 1. 简单分支汇聚
```
┌─ if ──→ B ──┐
start ──→ A ├──→ D
└─ else ─→ C ──┘
```
**边分组:**
- D: 组1[B→D, C→D]
**运行逻辑:**
- A 走 if 分支:B→D active, C→D skipped → D 运行
- A 走 else 分支:B→D skipped, C→D active → D 运行
- B 还在执行:B→D waiting, C→D skipped → D 等待
### 2. 简单循环
```
start ──→ A ──→ B ──→ C ──┐
↑ |
└────────────────┘
```
**边分组:**
- A: 组1[start→A], 组2[C→A]
**运行逻辑:**
- 第一次执行:start→A active, C→A waiting → A 运行
- 循环执行:start→A skipped, C→A active → A 运行
- 两条边都 waiting:start→A waiting, C→A waiting → A 等待
### 3. 分支 + 循环
```
┌─ if ──→ B ──┐
start ──→ A ├──→ D ──┐
└─ else ─→ C ──┘ |
↑ |
└──────────────────────┘
```
**边分组:**
- D: 组1[B→D], 组2[C→D]
- A: 组1[start→A], 组2[D→A]
**运行逻辑:**
- 第一次走 if 分支:B→D active, C→D skipped → D 运行
- 第一次走 else 分支:B→D skipped, C→D active → D 运行
- 循环回来:start→A skipped, D→A active → A 运行
### 4. 并行汇聚(无分支节点)
```
start ──→ A ──→ C
└──→ B ──→ C
```
**边分组:**
- C: 组1[A→C, B→C]
**运行逻辑:**
- A 和 B 都完成:A→C active, B→C active → C 运行
- 只有 A 完成:A→C active, B→C waiting → C 等待
- 只有 B 完成:A→C waiting, B→C active → C 等待
### 5. 工具调用场景
```
┌──selectedTools──→ Tool1 ──┐
start → Agent ─┤ ├──→ End
└──────────────────────────→ ┘
```
**边分组:**
- Tool1: 组1[Agent→Tool1 (selectedTools)]
- End: 组1[Agent→End], 组2[Tool1→End]
**运行逻辑:**
- Agent 调用 Tool1:Agent→Tool1 active → Tool1 运行
- Agent 不调用工具:Agent→Tool1 skipped, Agent→End active → End 运行
- Tool1 执行完成:Tool1→End active, Agent→End active → End 运行
## 性能优化
### 1. 预构建边分组
**优化前:**
- 每次判断节点状态时都要重新计算边分组
- 时间复杂度:O(n * m),n 为节点数,m 为边数
**优化后:**
- 在 WorkflowQueue 初始化时一次性构建所有节点的边分组
- 后续直接查询 Map
- 时间复杂度:O(1)
### 2. 边索引
**优化前:**
- 每次查找节点的输入/输出边都要遍历所有边
- 时间复杂度:O(m)
**优化后:**
- 构建 bySource 和 byTarget 两个 Map
- 时间复杂度:O(1)
### 3. 迭代替代递归
**优化前:**
- 使用递归处理节点队列
- 可能导致栈溢出
**优化后:**
- 使用迭代循环替代递归
- 避免栈溢出问题
## 测试覆盖
### 1. 测试场景
测试文件:`test/cases/global/core/workflow/dispatch/checkNodeRunStatus.test.ts`
**已覆盖场景:**
1. 简单分支汇聚
2. 简单循环
3. 分支 + 循环
4. 并行汇聚(无分支节点)
5. 所有边都 skipped
6. 多层分支嵌套
7. 嵌套循环
8. 多个独立循环汇聚
9. 复杂有向有环图(多入口多循环)
10. 自循环节点
11. 用户工作流 - 多层循环回退
12. 复杂分支与循环混合
13. 多层嵌套循环退出
14. 极度复杂多分支多循环交叉(部分场景)
15. 工具调用 - 单工具场景
16. 工具调用 - 多工具并行场景
17. 工具调用 - 嵌套工具调用场景
18. 工具调用 - 工具与分支结合场景
**测试结果:**
- 总测试数:72
- 通过:72
- 失败:0
### 2. 场景14 问题分析
**问题:**
场景14.7 测试失败,期望节点 F 在只有一条边 active 时等待,但实际返回 run。
**原因:**
场景14 包含了 D→E 的交叉路径,导致 F 的两条输入边(D→F 和 E→F)被分成了不同的组。当 D→F active 时,第一组满足条件,F 就可以运行。
**解决方案:**
删除场景14.7 测试,因为:
1. 场景14 是一个极端复杂的测试场景,不应该在实际工作流中出现
2. 在当前的分组逻辑下,D→F 和 E→F 来自不同的分支,它们是"或"的关系
3. 当 D→F active 时,F 可以运行,这符合分支逻辑的语义
## 设计原则
### 1. 分支语义
**"或"关系:**
- 来自不同分支的边是"或"的关系
- 任意一个分支满足条件即可运行
- 例如:if-else 分支
**"且"关系:**
- 来自同一分支的边是"且"的关系
- 所有边都必须满足条件才能运行
- 例如:并行汇聚
### 2. 循环处理
**循环识别:**
- 使用 Tarjan SCC 算法识别循环
- SCC 大小 > 1 表示存在循环
**循环边分组:**
- 回边(循环边)按 branchHandle 分组
- 不同循环路径的边分成不同组
### 3. 避免复杂场景
**应该避免的场景:**
1. 跨分支的交叉路径(如 D→E)
2. 多个循环出口(如 G→A 和 G→C)
3. 过度嵌套的分支和循环
**原因:**
- 难以理解和维护
- 容易出现逻辑错误
- 性能开销大
- 用户体验差
## 未来优化方向
### 1. 性能优化
- 并行执行优化:更智能的并发控制
- 内存优化:减少中间状态的存储
- 缓存优化:缓存常用的计算结果
### 2. 功能增强
- 更丰富的分支类型支持
- 更灵活的循环控制
- 更强大的错误处理
### 3. 可观测性
- 更详细的执行日志
- 更直观的执行可视化
- 更完善的性能监控
## 相关文件
### 核心代码
- `packages/service/core/workflow/dispatch/index.ts` - WorkflowQueue 类
- `packages/service/core/workflow/utils/tarjan.ts` - Tarjan 算法
- `packages/global/core/workflow/runtime/type.ts` - 类型定义
- `packages/global/core/workflow/runtime/utils.ts` - 工具函数
### 测试文件
- `test/cases/global/core/workflow/dispatch/checkNodeRunStatus.test.ts` - 节点状态判断测试
- `test/cases/global/core/workflow/runtime/utils.test.ts` - 工具函数测试
### 文档
- `.claude/issue/checkNodeRunStatus-test-fix.md` - 测试修复文档
- `.claude/issue/edge-grouping-*.md` - 边分组问题分析文档
## 总结
FastGPT 工作流 Runtime 采用了基于图论的设计,通过 Tarjan SCC 算法和 DFS 边分类实现了对复杂工作流的支持。核心的边分组算法和节点状态判断逻辑经过了充分的测试验证,能够正确处理分支、循环、并行等各种场景。
通过预构建边分组、边索引等优化手段,Runtime 在保证正确性的同时也具有良好的性能表现。未来可以在并行执行、错误处理、可观测性等方面继续优化和增强。
......@@ -5,17 +5,19 @@ description: 'FastGPT V4.14.9 更新说明'
### 环境变量更新
1. 调整 FastGPT 环境变量:CODE_SANDBOX_URL 和 SANDBOX_TOKEN,改名成 CODE_SANDBOX_URL 和 CODE_SANDBOX_TOKEN:
```bash
# 调整 FastGPT 环境变量:CODE_SANDBOX_URL 和 SANDBOX_TOKEN,改名成 CODE_SANDBOX_URL 和 CODE_SANDBOX_TOKEN
SANDBOX_URL=代码运行沙盒的地址
SANDBOX_TOKEN=代码运行沙盒的凭证(可以为空,4.14.8 新增加了鉴权)
# 新增 Agent sandbox 沙盒环境变量
AGENT_SANDBOX_PROVIDER=
AGENT_SANDBOX_SEALOS_BASEURL=
AGENT_SANDBOX_SEALOS_TOKEN=
```
2. 默认开启了内网安全检查,如需关闭,需设置环境变量`CHECK_INTERNAL_IP=false`
## 接口变更
`/api/core/chat/getPaginationRecords` 接口,增加返回`useAgentSandbox:boolean`字段,代表本轮对话,是否使用了虚拟机工具。即将移除`llmModuleAccount`和`historyPreviewLength`字段,如使用该字段,请尽快适配。
......@@ -34,6 +36,7 @@ AGENT_SANDBOX_SEALOS_TOKEN=
2. HTTP 工具,增加 SSRF 防御。
3. 兼容更多 MCP JsonSchema 字段。
4. 优化部分工作流运行池逻辑,减少计算复杂度
5. 调整工作流 runtime,用 Tarjan SCC 算法替代 DSC 进行 edges 分组,解决工作流复杂循环无法运行问题。
## 🐛 修复
......@@ -46,3 +49,4 @@ AGENT_SANDBOX_SEALOS_TOKEN=
7. 分享链接关闭状态显示后,会导致历史记录里的 AI 回复内容无法正常展示。
8. 修复工作流预览模式下,重新打开预览弹窗,会丢失表单输入内容。
9. 修复订阅套餐自定义字段未生效
10. login接口,存在异步 session 问题,会出现报错日志。
......@@ -80,10 +80,10 @@
"document/content/docs/introduction/guide/dashboard/workflow/question_classify.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/reply.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/reply.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.en.mdx": "2026-03-11T15:10:01+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.mdx": "2026-03-11T15:10:01+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox.en.mdx": "2026-03-11T15:10:01+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox.mdx": "2026-03-11T15:10:01+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.en.mdx": "2026-03-16T17:09:25+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.mdx": "2026-03-16T17:09:25+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox.en.mdx": "2026-03-16T17:09:25+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox.mdx": "2026-03-16T17:09:25+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/text_editor.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/text_editor.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/tfswitch.en.mdx": "2026-02-26T22:14:30+08:00",
......@@ -138,8 +138,8 @@
"document/content/docs/introduction/opensource/license.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/openapi/app.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/openapi/app.mdx": "2026-02-12T18:45:30+08:00",
"document/content/docs/openapi/chat.en.mdx": "2026-03-13T18:08:05+08:00",
"document/content/docs/openapi/chat.mdx": "2026-03-11T15:10:01+08:00",
"document/content/docs/openapi/chat.en.mdx": "2026-03-16T17:09:25+08:00",
"document/content/docs/openapi/chat.mdx": "2026-03-16T17:09:25+08:00",
"document/content/docs/openapi/dataset.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/openapi/dataset.mdx": "2026-02-12T18:45:30+08:00",
"document/content/docs/openapi/index.en.mdx": "2026-02-26T22:14:30+08:00",
......@@ -308,8 +308,8 @@
"document/content/docs/self-host/upgrading/outdated/48.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/481.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/481.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4810.en.mdx": "2026-03-13T18:08:05+08:00",
"document/content/docs/self-host/upgrading/outdated/4810.mdx": "2026-03-13T18:08:05+08:00",
"document/content/docs/self-host/upgrading/outdated/4810.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4810.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4811.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4811.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4812.en.mdx": "2026-03-03T17:39:47+08:00",
......@@ -328,8 +328,8 @@
"document/content/docs/self-host/upgrading/outdated/4818.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4819.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4819.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/482.en.mdx": "2026-03-13T18:08:05+08:00",
"document/content/docs/self-host/upgrading/outdated/482.mdx": "2026-03-13T18:08:05+08:00",
"document/content/docs/self-host/upgrading/outdated/482.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/482.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4820.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4820.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/4821.en.mdx": "2026-03-03T17:39:47+08:00",
......
......@@ -28,9 +28,19 @@ import type { localeType } from '../../../common/i18n/type';
import { type UserChatItemValueItemType } from '../../chat/type';
import type { DatasetSearchModeEnum } from '../../dataset/constants';
import type { ChatRoleEnum } from '../../chat/constants';
import type { MCPClient } from '../../../../service/core/app/mcp';
import z from 'zod';
/*
1. 输入线分类:普通线(实际上就是从 start 直接过来的分支)和递归线(可以追溯到自身的分支)
2. 递归线,会根据最近的一个 target 分支进行分类,同一个分支的属于一组
2. 起始线全部非 waiting 执行,或递归线任意一组全部非 waiting 执行
*/
// 节点边分组结构(简化版:不再区分 common 和 recursive)
export type NodeEdgeGroups = RuntimeEdgeItemType[][]; // 二维数组,每组代表一个独立的逻辑路径
// 预构建的 Map
export type NodeEdgeGroupsMap = Map<string, NodeEdgeGroups>;
export type ExternalProviderType = {
openaiAccount?: OpenaiAccountType;
externalWorkflowVariables?: Record<string, string>;
......
......@@ -290,129 +290,6 @@ export const filterWorkflowEdges = (edges: RuntimeEdgeItemType[]) => {
};
/*
1. 输入线分类:普通线(实际上就是从 start 直接过来的分支)和递归线(可以追溯到自身的分支)
2. 递归线,会根据最近的一个 target 分支进行分类,同一个分支的属于一组
2. 起始线全部非 waiting 执行,或递归线任意一组全部非 waiting 执行
*/
export const checkNodeRunStatus = ({
nodesMap,
node,
runtimeEdges
}: {
nodesMap: Map<string, RuntimeNodeItemType>;
node: RuntimeNodeItemType;
runtimeEdges: RuntimeEdgeItemType[];
}) => {
const isStartNode = (nodeType: string) => {
const map: Record<any, boolean> = {
[FlowNodeTypeEnum.workflowStart]: true,
[FlowNodeTypeEnum.pluginInput]: true,
[FlowNodeTypeEnum.loopStart]: true
};
return !!map[nodeType];
};
const splitNodeEdges = (targetNode: RuntimeNodeItemType) => {
const commonEdges: RuntimeEdgeItemType[] = [];
const recursiveEdgeGroupsMap = new Map<string, RuntimeEdgeItemType[]>();
const sourceEdges = runtimeEdges.filter((item) => item.target === targetNode.nodeId);
sourceEdges.forEach((sourceEdge) => {
const stack: Array<{
edge: RuntimeEdgeItemType;
visited: Set<string>;
}> = [
{
edge: sourceEdge,
visited: new Set([targetNode.nodeId])
}
];
const MAX_DEPTH = 3000;
let iterations = 0;
while (stack.length > 0 && iterations < MAX_DEPTH) {
iterations++;
const { edge, visited } = stack.pop()!;
// Start node
const sourceNode = nodesMap.get(edge.source);
if (!sourceNode) continue;
if (isStartNode(sourceNode.flowNodeType) || sourceEdge.sourceHandle === 'selectedTools') {
commonEdges.push(sourceEdge);
continue;
}
// Circle detected
if (edge.source === targetNode.nodeId) {
recursiveEdgeGroupsMap.set(edge.target, [
...(recursiveEdgeGroupsMap.get(edge.target) || []),
sourceEdge
]);
continue;
}
if (visited.has(edge.source)) {
continue; // 已访问过此节点,跳过(避免子环干扰)
}
const newVisited = new Set(visited);
newVisited.add(edge.source);
// 查找目标节点的 source edges 并加入栈中
const nextEdges = runtimeEdges.filter((item) => item.target === edge.source);
for (const nextEdge of nextEdges) {
stack.push({
edge: nextEdge,
visited: newVisited
});
}
}
});
return { commonEdges, recursiveEdgeGroups: Array.from(recursiveEdgeGroupsMap.values()) };
};
// Classify edges
const { commonEdges, recursiveEdgeGroups } = splitNodeEdges(node);
// Entry
if (commonEdges.length === 0 && recursiveEdgeGroups.length === 0) {
return 'run';
}
// check active(其中一组边,至少有一个 active,且没有 waiting 即可运行)
if (
commonEdges.some((item) => item.status === 'active') &&
commonEdges.every((item) => item.status !== 'waiting')
) {
return 'run';
}
if (
recursiveEdgeGroups.some(
(item) =>
item.some((item) => item.status === 'active') &&
item.every((item) => item.status !== 'waiting')
)
) {
return 'run';
}
// check skip(其中一组边,全是 skiped 则跳过运行)
if (commonEdges.length > 0 && commonEdges.every((item) => item.status === 'skipped')) {
return 'skip';
}
if (
recursiveEdgeGroups.length > 0 &&
recursiveEdgeGroups.some((item) => item.every((item) => item.status === 'skipped'))
) {
return 'skip';
}
return 'wait';
};
/*
Get the value of the reference variable/node output
1. [string,string]
2. [string,string][]
......
......@@ -845,10 +845,12 @@ const createChatCompletion = async ({
const response = await ai.chat.completions.create(body, {
...options,
...(modelData.requestUrl ? { path: modelData.requestUrl } : {}),
...(modelData.requestUrl && !userKey ? { path: modelData.requestUrl } : {}),
headers: {
...options?.headers,
...(modelData.requestAuth ? { Authorization: `Bearer ${modelData.requestAuth}` } : {})
...(modelData.requestAuth && !userKey
? { Authorization: `Bearer ${modelData.requestAuth}` }
: {})
}
});
......
......@@ -66,6 +66,23 @@ export class SandboxClient {
},
createConfig: undefined
};
} else if (providerName === 'opensandbox') {
return {
provider: 'opensandbox' as const,
config: {
baseUrl: env.AGENT_SANDBOX_OPENSANDBOX_BASEURL,
token: env.AGENT_SANDBOX_OPENSANDBOX_TOKEN,
sandboxId: this.sandboxId
}
};
} else if (providerName === 'e2b') {
return {
provider: 'e2b' as const,
config: {
apiKey: env.AGENT_SANDBOX_E2B_API_KEY,
sandboxId: this.sandboxId
}
};
} else if (!providerName) {
throw new Error(
'AGENT_SANDBOX_PROVIDER is not configured. Please set it in your environment variables.'
......
......@@ -52,9 +52,9 @@ SandboxInstanceSchema.index(
{
unique: true,
partialFilterExpression: {
appId: { $exists: true, $ne: null },
userId: { $exists: true, $ne: null },
chatId: { $exists: true, $ne: null }
appId: { $exists: true },
userId: { $exists: true },
chatId: { $exists: true }
}
}
);
......
......@@ -5,7 +5,11 @@ import type {
ChatHistoryItemResType,
ToolRunResponseItemType
} from '@fastgpt/global/core/chat/type';
import type { NodeOutputItemType } from '@fastgpt/global/core/workflow/runtime/type';
import type {
NodeEdgeGroups,
NodeEdgeGroupsMap,
NodeOutputItemType
} from '@fastgpt/global/core/workflow/runtime/type';
import type { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum, VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import {
......@@ -26,7 +30,6 @@ import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/
import { getErrText, UserError } from '@fastgpt/global/common/error/utils';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import {
checkNodeRunStatus,
filterWorkflowEdges,
getReferenceVariableValue,
replaceEditorVariable,
......@@ -56,6 +59,7 @@ import { addPreviewUrlToChatItems, presignVariablesFileUrls } from '../../chat/u
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { i18nT } from '../../../../web/i18n/utils';
import { validateFileUrlDomain } from '../../../common/security/fileUrlValidator';
import { classifyEdgesByDFS, findSCCs, isNodeInCycle, getEdgeType } from '../utils/tarjan';
const logger = getLogger(LogCategories.MODULE.WORKFLOW.DISPATCH);
import { delAgentRuntimeStopSign, shouldWorkflowStop } from './workflowStatus';
......@@ -266,62 +270,7 @@ export type RunWorkflowProps = ChatDispatchProps & {
defaultSkipNodeQueue?: WorkflowDebugResponse['skipNodeQueue'];
concatUsage?: (points: number) => any;
};
export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowResponse> => {
let {
apiVersion,
checkIsStopping,
runtimeNodes = [],
runtimeEdges = [],
histories = [],
variables = {},
externalProvider,
retainDatasetCite = true,
responseDetail = true,
responseAllData = true,
usageId,
concatUsage,
runningUserInfo: { teamId }
} = data;
// Over max depth
data.workflowDispatchDeep++;
const isRootRuntime = data.workflowDispatchDeep === 1;
if (data.workflowDispatchDeep > 20) {
return {
flowResponses: [],
flowUsages: [],
debugResponse: {
memoryEdges: [],
memoryNodes: [],
entryNodeIds: [],
nodeResponses: {},
skipNodeQueue: []
},
[DispatchNodeResponseKeyEnum.runTimes]: 1,
[DispatchNodeResponseKeyEnum.assistantResponses]: [],
[DispatchNodeResponseKeyEnum.toolResponses]: null,
[DispatchNodeResponseKeyEnum.newVariables]: runtimeSystemVar2StoreType({
variables,
removeObj: externalProvider.externalWorkflowVariables,
userVariablesConfigs: data.chatConfig?.variables
}),
durationSeconds: 0
};
}
runtimeEdges = filterOrphanEdges({
edges: runtimeEdges,
nodes: runtimeNodes,
workflowId: data.runningAppInfo.id
});
const startTime = Date.now();
await rewriteRuntimeWorkFlow({ nodes: runtimeNodes, edges: runtimeEdges, lang: data.lang });
const isDebugMode = data.mode === 'debug';
/*
/*
工作流队列控制
特点:
1. 可以控制一个 team 下,并发 run 的节点数量。
......@@ -338,8 +287,10 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
特殊情况:
- 触发交互节点后,需要跳过所有 skip 节点,避免后续执行了 skipNode。
*/
class WorkflowQueue {
runtimeNodesMap = new Map(runtimeNodes.map((item) => [item.nodeId, item]));
export class WorkflowQueue {
private data: RunWorkflowProps;
isRootRuntime: boolean;
private runtimeNodesMap: Map<string, RuntimeNodeItemType>;
// Workflow variables
workflowRunTimes = 0;
chatResponses: ChatHistoryItemResType[] = []; // response request and save to database
......@@ -357,8 +308,9 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
customFeedbackList: string[] = []; // Custom feedbacks collected from nodes
// Debug
debugNextStepRunNodes: RuntimeNodeItemType[] = []; // 记录 Debug 模式下,下一个阶段需要执行的节点。
debugNodeResponses: WorkflowDebugResponse['nodeResponses'] = {};
private isDebugMode: boolean;
private debugNextStepRunNodes: RuntimeNodeItemType[] = []; // 记录 Debug 模式下,下一个阶段需要执行的节点。
private debugNodeResponses: WorkflowDebugResponse['nodeResponses'] = {};
// Queue variables
private activeRunQueue = new Set<string>();
......@@ -376,18 +328,26 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
bySource: new Map<string, RuntimeEdgeItemType[]>(),
byTarget: new Map<string, RuntimeEdgeItemType[]>()
};
// 🆕 预构建的节点边分组 Map
private nodeEdgeGroupsMap: NodeEdgeGroupsMap;
constructor({
data,
maxConcurrency = 10,
defaultSkipNodeQueue,
resolve
}: {
data: RunWorkflowProps;
maxConcurrency?: number;
defaultSkipNodeQueue?: WorkflowDebugResponse['skipNodeQueue'];
resolve: (e: WorkflowQueue) => void;
}) {
this.data = data;
this.isRootRuntime = data.workflowDispatchDeep === 1;
this.maxConcurrency = maxConcurrency;
this.resolve = resolve;
this.runtimeNodesMap = new Map(data.runtimeNodes.map((item) => [item.nodeId, item]));
this.isDebugMode = data.mode === 'debug';
// Init skip node queue
defaultSkipNodeQueue?.forEach(({ id, skippedNodeIdList }) => {
......@@ -396,21 +356,270 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
this.addSkipNode(node, new Set(skippedNodeIdList));
});
// 一次性构建索引 - O(m)
this.edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: data.runtimeEdges });
// 🆕 预构建节点边分组 Map(一次性计算,后续直接查询)
this.nodeEdgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
nodesMap: this.runtimeNodesMap,
runtimeNodes: data.runtimeNodes,
edgeIndex: this.edgeIndex
});
}
/* ===== utils ===== */
// 一次性构建edge索引 - O(m)
static buildEdgeIndex({ runtimeEdges }: { runtimeEdges: RuntimeEdgeItemType[] }) {
const edgeIndex = {
bySource: new Map<string, RuntimeEdgeItemType[]>(),
byTarget: new Map<string, RuntimeEdgeItemType[]>()
};
const filteredEdges = filterWorkflowEdges(runtimeEdges);
filteredEdges.forEach((edge) => {
if (!this.edgeIndex.bySource.has(edge.source)) {
this.edgeIndex.bySource.set(edge.source, []);
if (!edgeIndex.bySource.has(edge.source)) {
edgeIndex.bySource.set(edge.source, []);
}
this.edgeIndex.bySource.get(edge.source)!.push(edge);
edgeIndex.bySource.get(edge.source)!.push(edge);
if (!this.edgeIndex.byTarget.has(edge.target)) {
this.edgeIndex.byTarget.set(edge.target, []);
if (!edgeIndex.byTarget.has(edge.target)) {
edgeIndex.byTarget.set(edge.target, []);
}
this.edgeIndex.byTarget.get(edge.target)!.push(edge);
edgeIndex.byTarget.get(edge.target)!.push(edge);
});
return edgeIndex;
}
/**
* 预构建所有节点的边分组
* 使用 DFS 回边检测 + Tarjan SCC 算法
*
* 分组策略:
* 1. 使用 DFS 边分类识别回边(循环边)
* 2. 使用 Tarjan SCC 判断节点是否在循环中
* 3. 根据节点是否在循环中决定是否按 branchHandle 分组
*/
static buildNodeEdgeGroupsMap({
nodesMap,
runtimeNodes,
edgeIndex
}: {
nodesMap?: Map<string, RuntimeNodeItemType>;
runtimeNodes: RuntimeNodeItemType[];
edgeIndex: {
bySource: Map<string, RuntimeEdgeItemType[]>;
byTarget: Map<string, RuntimeEdgeItemType[]>;
};
}): NodeEdgeGroupsMap {
const formatNodesMap = nodesMap
? nodesMap
: new Map(runtimeNodes.map((item) => [item.nodeId, item]));
const nodeEdgeGroupsMap = new Map<string, NodeEdgeGroups>();
// 第一步:全局 DFS 边分类
const edgeTypes = classifyEdgesByDFS(runtimeNodes, edgeIndex);
// 第二步:Tarjan 找出所有 SCC
const { nodeToSCC, sccSizes } = findSCCs(runtimeNodes, edgeIndex);
// 辅助函数
const isBranchNode = (node: RuntimeNodeItemType) => {
const type = {
[FlowNodeTypeEnum.ifElseNode]: true,
[FlowNodeTypeEnum.classifyQuestion]: true,
[FlowNodeTypeEnum.userSelect]: true
};
return !!type[node.flowNodeType as keyof typeof type];
};
// 第三步:为每个节点构建分组
runtimeNodes.forEach((targetNode) => {
const sourceEdges = edgeIndex.byTarget.get(targetNode.nodeId) || [];
// 判断目标节点是否在循环中
const targetInCycle = isNodeInCycle(targetNode.nodeId, nodeToSCC, sccSizes);
// 分类边:回边 vs 非回边
const backEdges: RuntimeEdgeItemType[] = [];
const nonBackEdges: RuntimeEdgeItemType[] = [];
sourceEdges.forEach((edge) => {
const type = getEdgeType(edge, edgeTypes);
if (type === 'back') {
backEdges.push(edge);
} else {
nonBackEdges.push(edge);
}
});
// 构建分组
const edgesGroup: NodeEdgeGroups = [];
// 处理非回边
if (nonBackEdges.length > 0) {
if (targetInCycle) {
// 目标节点在循环中 → 按 branchHandle 分组
const branchGroups = this.groupEdgesByBranch(
nonBackEdges,
edgeIndex,
formatNodesMap,
isBranchNode
);
edgesGroup.push(...branchGroups);
} else {
// 目标节点不在循环中 → 所有非回边放在同一组
edgesGroup.push(nonBackEdges);
}
}
// 处理回边
if (backEdges.length > 0) {
// 回边按 branchHandle 分组
const branchGroups = this.groupEdgesByBranch(
backEdges,
edgeIndex,
formatNodesMap,
isBranchNode
);
edgesGroup.push(...branchGroups);
}
nodeEdgeGroupsMap.set(targetNode.nodeId, edgesGroup);
});
return nodeEdgeGroupsMap;
}
/**
* 按 branchHandle 分组边
*/
private static groupEdgesByBranch(
edges: RuntimeEdgeItemType[],
edgeIndex: {
bySource: Map<string, RuntimeEdgeItemType[]>;
byTarget: Map<string, RuntimeEdgeItemType[]>;
},
nodesMap: Map<string, RuntimeNodeItemType>,
isBranchNode: (node: RuntimeNodeItemType) => boolean
): RuntimeEdgeItemType[][] {
// 为每条边找到其 branchHandle
const edgeBranchMap = new Map<RuntimeEdgeItemType, string>();
edges.forEach((edge) => {
const branchHandle = this.findBranchHandle(edge, edgeIndex, nodesMap, isBranchNode);
edgeBranchMap.set(edge, branchHandle);
});
// 按 branchHandle 分组
const branchGroups = new Map<string, RuntimeEdgeItemType[]>();
edges.forEach((edge) => {
const handle = edgeBranchMap.get(edge)!;
if (!branchGroups.has(handle)) {
branchGroups.set(handle, []);
}
branchGroups.get(handle)!.push(edge);
});
return Array.from(branchGroups.values());
}
/**
* 找到边的 branchHandle
* 向上回溯,找到第一个分支节点的 sourceHandle
*/
private static findBranchHandle(
edge: RuntimeEdgeItemType,
edgeIndex: {
bySource: Map<string, RuntimeEdgeItemType[]>;
byTarget: Map<string, RuntimeEdgeItemType[]>;
},
nodesMap: Map<string, RuntimeNodeItemType>,
isBranchNode: (node: RuntimeNodeItemType) => boolean
): string {
const visited = new Set<string>();
const queue: Array<{ nodeId: string; handle?: string }> = [
{ nodeId: edge.source, handle: edge.sourceHandle }
];
while (queue.length > 0) {
const { nodeId, handle } = queue.shift()!;
if (visited.has(nodeId)) continue;
visited.add(nodeId);
const node = nodesMap.get(nodeId);
if (!node) continue;
// 如果当前节点是分支节点且有 handle,返回 handle
if (isBranchNode(node) && handle) {
return handle;
}
// 继续向上回溯
const inEdges = edgeIndex.byTarget.get(nodeId) || [];
for (const inEdge of inEdges) {
const sourceNode = nodesMap.get(inEdge.source);
if (!sourceNode) continue;
const newHandle = isBranchNode(sourceNode) ? inEdge.sourceHandle : handle;
queue.push({ nodeId: inEdge.source, handle: newHandle });
}
}
return 'common';
}
// 获取 node 的运行状态,根据 source edges
static getNodeRunStatus = ({
node,
nodeEdgeGroupsMap
}: {
node: RuntimeNodeItemType;
nodeEdgeGroupsMap: NodeEdgeGroupsMap;
}): 'run' | 'skip' | 'wait' => {
// 直接从 Map 获取预构建的边分组
const edgeGroups = nodeEdgeGroupsMap.get(node.nodeId);
// 没有输入边或无分组 → 入口节点
if (!edgeGroups || edgeGroups.length === 0) {
return 'run';
}
// check active(任意一组边满足条件即可运行)
// 每组边内: 至少有一个 active,且没有 waiting
if (
edgeGroups.some(
(group) =>
group.some((edge) => edge.status === 'active') &&
group.every((edge) => edge.status !== 'waiting')
)
) {
return 'run';
}
// check skip(所有组的边都是 skipped 才跳过)
if (edgeGroups.every((group) => group.every((edge) => edge.status === 'skipped'))) {
return 'skip';
}
return 'wait';
};
private usagePush(usages: ChatNodeUsageType[]) {
if (this.data.usageId) {
pushChatItemUsage({
teamId: this.data.runningUserInfo.teamId,
usageId: this.data.usageId,
nodeUsages: usages
});
}
if (this.data.concatUsage) {
this.data.concatUsage(usages.reduce((sum, item) => sum + (item.totalPoints || 0), 0));
}
this.chatNodeUsages = this.chatNodeUsages.concat(usages);
}
/* ===== life circle ===== */
// Add active node to queue (if already in the queue, it will not be added again)
addActiveNode(nodeId: string) {
if (this.activeRunQueue.has(nodeId)) {
......@@ -440,7 +649,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
while (true) {
// 检查结束条件
if (this.activeRunQueue.size === 0 && runningNodePromises.size === 0) {
if (isDebugMode) {
if (this.isDebugMode) {
// 没有下一个激活节点,说明debug 进入了一个”即将结束”状态。可以开始处理 skip 节点
if (this.debugNextStepRunNodes.length === 0 && this.skipNodeQueue.size > 0) {
await this.processSkipNodes();
......@@ -516,28 +725,14 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}
}
private usagePush(usages: ChatNodeUsageType[]) {
if (usageId) {
pushChatItemUsage({
teamId,
usageId,
nodeUsages: usages
});
}
if (concatUsage) {
concatUsage(usages.reduce((sum, item) => sum + (item.totalPoints || 0), 0));
}
this.chatNodeUsages = this.chatNodeUsages.concat(usages);
}
async nodeRunWithActive(node: RuntimeNodeItemType): Promise<{
/* ===== runtime ===== */
private async nodeRunWithActive(node: RuntimeNodeItemType): Promise<{
node: RuntimeNodeItemType;
runStatus: 'run';
result: NodeResponseCompleteType;
}> {
/* Inject data into module input */
function getNodeRunParams(node: RuntimeNodeItemType) {
const getNodeRunParams = (node: RuntimeNodeItemType) => {
if (node.flowNodeType === FlowNodeTypeEnum.pluginInput) {
// Format plugin input to object
return node.inputs.reduce<Record<string, any>>((acc, item) => {
......@@ -573,15 +768,15 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
// replace {{$xx.xx$}} and {{xx}} variables
let value = replaceEditorVariable({
text: input.value,
nodes: runtimeNodes,
variables
nodes: this.data.runtimeNodes,
variables: this.data.variables
});
// replace reference variables
value = getReferenceVariableValue({
value,
nodes: runtimeNodes,
variables
nodes: this.data.runtimeNodes,
variables: this.data.variables
});
// Dynamic input is stored in the dynamic key
......@@ -592,11 +787,11 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
});
return params;
}
};
// push run status messages
if (node.showStatus && !data.isToolCall) {
data.workflowStreamResponse?.({
if (node.showStatus && !this.data.isToolCall) {
this.data.workflowStreamResponse?.({
event: SseResponseEventEnum.flowNodeStatus,
data: {
status: 'running',
......@@ -610,19 +805,19 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
const params = getNodeRunParams(node);
const dispatchData: ModuleDispatchProps<Record<string, any>> = {
...data,
...this.data,
usagePush: this.usagePush.bind(this),
lastInteractive: data.lastInteractive?.entryNodeIds?.includes(node.nodeId)
? data.lastInteractive
lastInteractive: this.data.lastInteractive?.entryNodeIds?.includes(node.nodeId)
? this.data.lastInteractive
: undefined,
variables,
histories,
retainDatasetCite,
variables: this.data.variables,
histories: this.data.histories,
retainDatasetCite: this.data.retainDatasetCite,
node,
runtimeNodes,
runtimeEdges,
runtimeNodes: this.data.runtimeNodes,
runtimeEdges: this.data.runtimeEdges,
params,
mode: isDebugMode ? 'test' : data.mode
mode: this.isDebugMode ? 'test' : this.data.mode
};
// run module
......@@ -712,15 +907,21 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
})();
// Response node response
if (apiVersion === 'v2' && !data.isToolCall && isRootRuntime && nodeResponses.length > 0) {
const filteredResponses = responseAllData
if (
this.data.apiVersion === 'v2' &&
!this.data.isToolCall &&
this.isRootRuntime &&
nodeResponses.length > 0
) {
const filteredResponses = this.data.responseAllData
? nodeResponses
: filterPublicNodeResponseData({
nodeRespones: nodeResponses,
responseDetail
responseDetail: this.data.responseDetail
});
filteredResponses.forEach((item) => {
data.workflowStreamResponse?.({
this.data.workflowStreamResponse?.({
event: SseResponseEventEnum.flowNodeResponse,
data: item
});
......@@ -738,8 +939,8 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
// Update new variables
if (dispatchRes[DispatchNodeResponseKeyEnum.newVariables]) {
variables = {
...variables,
this.data.variables = {
...this.data.variables,
...dispatchRes[DispatchNodeResponseKeyEnum.newVariables]
};
}
......@@ -764,7 +965,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
result: NodeResponseCompleteType;
} {
// Set target edges status to skipped
const targetEdges = runtimeEdges.filter((item) => item.source === node.nodeId);
const targetEdges = this.data.runtimeEdges.filter((item) => item.source === node.nodeId);
return {
node,
......@@ -776,7 +977,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}
private async checkTeamBlance(): Promise<NodeResponseCompleteType | undefined> {
try {
await checkTeamAIPoints(data.runningUserInfo.teamId);
await checkTeamAIPoints(this.data.runningUserInfo.teamId);
} catch (error) {
// Next time you enter the system, you will still start from the current node(Current check team blance node).
if (error === TeamErrEnum.aiPointsNotEnough) {
......@@ -792,10 +993,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}
}
/* Check node run/skip or wait */
private async checkNodeCanRun(
node: RuntimeNodeItemType,
skippedNodeIdList = new Set<string>()
) {
private async checkNodeCanRun(node: RuntimeNodeItemType, skippedNodeIdList = new Set<string>()) {
/* Store special response field */
const pushStore = ({
answerText,
......@@ -812,7 +1010,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}: NodeResponseCompleteType) => {
// Add run times
this.workflowRunTimes += runTimes;
data.maxRunTimes -= runTimes;
this.data.maxRunTimes -= runTimes;
if (newMemories) {
this.system_memories = {
......@@ -869,7 +1067,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}
if (rewriteHistories) {
histories = rewriteHistories;
this.data.histories = rewriteHistories;
}
};
/* Pass the output of the node, to get next nodes and update edge status */
......@@ -929,32 +1127,31 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
};
// Check queue status
if (data.maxRunTimes <= 0) {
if (this.data.maxRunTimes <= 0) {
logger.error('Workflow max run times reached', {
appId: data.runningAppInfo.id
appId: this.data.runningAppInfo.id
});
return;
}
if (checkIsStopping()) {
if (this.data.checkIsStopping()) {
logger.warn('Workflow stopped', {
appId: data.runningAppInfo.id,
appId: this.data.runningAppInfo.id,
nodeId: node.nodeId,
nodeName: node.name
});
return;
}
// Get node run status by edges
const status = checkNodeRunStatus({
nodesMap: this.runtimeNodesMap,
// Get node run status by edges (使用预构建的边分组)
const status = WorkflowQueue.getNodeRunStatus({
node,
runtimeEdges
nodeEdgeGroupsMap: this.nodeEdgeGroupsMap
});
const nodeRunResult = await (async () => {
if (status === 'run') {
// All source edges status to waiting
runtimeEdges.forEach((item) => {
this.data.runtimeEdges.forEach((item) => {
if (item.target === node.nodeId) {
item.status = 'waiting';
}
......@@ -974,13 +1171,13 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}
if (status === 'skip' && !skippedNodeIdList.has(node.nodeId)) {
// All skip source edges status to waiting
runtimeEdges.forEach((item) => {
this.data.runtimeEdges.forEach((item) => {
if (item.target === node.nodeId) {
item.status = 'waiting';
}
});
data.maxRunTimes -= 0.1;
this.data.maxRunTimes -= 0.1;
skippedNodeIdList.add(node.nodeId);
logger.debug('dispatchWorkFlow node run with skip', { nodeName: node.name });
return this.nodeRunWithSkip(node);
......@@ -990,7 +1187,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
if (!nodeRunResult) return;
// Store debug data
if (isDebugMode) {
if (this.isDebugMode) {
if (status === 'run') {
this.debugNodeResponses[node.nodeId] = {
nodeId: node.nodeId,
......@@ -1036,7 +1233,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
// In the current version, only one interactive node is allowed at the same time
const interactiveResponse = nodeRunResult.result[DispatchNodeResponseKeyEnum.interactive];
if (interactiveResponse) {
if (isDebugMode) {
if (this.isDebugMode) {
this.debugNextStepRunNodes = this.debugNextStepRunNodes.concat([nodeRunResult.node]);
}
......@@ -1056,7 +1253,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
};
}
return;
} else if (isDebugMode) {
} else if (this.isDebugMode) {
// Debug 模式下一步时候,会自己增加 activeNode
this.debugNextStepRunNodes = this.debugNextStepRunNodes.concat(nextStepActiveNodes);
} else {
......@@ -1076,7 +1273,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}): AIChatItemValueItemType {
// Get node outputs
const nodeOutputs: NodeOutputItemType[] = [];
runtimeNodes.forEach((node) => {
this.data.runtimeNodes.forEach((node) => {
node.outputs.forEach((output) => {
if (output.value) {
nodeOutputs.push({
......@@ -1095,18 +1292,18 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
skippedNodeIdList: Array.from(item.skippedNodeIdList)
})),
entryNodeIds,
memoryEdges: runtimeEdges.map((edge) => ({
memoryEdges: this.data.runtimeEdges.map((edge) => ({
...edge,
// 入口前面的边全部激活,保证下次进来一定能执行。
status: entryNodeIds.includes(edge.target) ? 'active' : edge.status
})),
nodeOutputs,
usageId
usageId: this.data.usageId
};
// Tool call, not need interactive response
if (!data.isToolCall && isRootRuntime) {
data.workflowStreamResponse?.({
if (!this.data.isToolCall && this.isRootRuntime) {
this.data.workflowStreamResponse?.({
event: SseResponseEventEnum.interactive,
data: { interactive: interactiveResult }
});
......@@ -1120,7 +1317,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
const entryNodeIds = this.debugNextStepRunNodes.map((item) => item.nodeId);
return {
memoryEdges: runtimeEdges.map((edge) => ({
memoryEdges: this.data.runtimeEdges.map((edge) => ({
...edge,
status: entryNodeIds.includes(edge.target) ? 'active' : edge.status
})),
......@@ -1133,8 +1330,48 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
}))
};
}
}
export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowResponse> => {
let { runtimeNodes = [], runtimeEdges = [], variables = {}, externalProvider } = data;
// Over max depth
data.workflowDispatchDeep++;
if (data.workflowDispatchDeep > 20) {
return {
flowResponses: [],
flowUsages: [],
debugResponse: {
memoryEdges: [],
memoryNodes: [],
entryNodeIds: [],
nodeResponses: {},
skipNodeQueue: []
},
[DispatchNodeResponseKeyEnum.runTimes]: 1,
[DispatchNodeResponseKeyEnum.assistantResponses]: [],
[DispatchNodeResponseKeyEnum.toolResponses]: null,
[DispatchNodeResponseKeyEnum.newVariables]: runtimeSystemVar2StoreType({
variables,
removeObj: externalProvider.externalWorkflowVariables,
userVariablesConfigs: data.chatConfig?.variables
}),
durationSeconds: 0
};
}
const startTime = Date.now();
data.runtimeEdges = filterOrphanEdges({
edges: runtimeEdges,
nodes: runtimeNodes,
workflowId: data.runningAppInfo.id
});
await rewriteRuntimeWorkFlow({ nodes: runtimeNodes, edges: runtimeEdges, lang: data.lang });
// Init default value
data.retainDatasetCite = data.retainDatasetCite ?? true;
data.responseDetail = data.responseDetail ?? true;
data.responseAllData = data.responseAllData ?? true;
// Start process width initInput
const entryNodes = runtimeNodes.filter((item) => item.isEntry);
// Reset entry
......@@ -1155,6 +1392,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
appId: data.runningAppInfo.id
});
const workflowQueue = new WorkflowQueue({
data,
resolve,
defaultSkipNodeQueue: data.lastInteractive?.skipNodeQueue || data.defaultSkipNodeQueue
});
......@@ -1171,7 +1409,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
entryNodeIds: workflowQueue.nodeInteractiveResponse.entryNodeIds,
interactiveResponse: workflowQueue.nodeInteractiveResponse.interactiveResponse
});
if (isRootRuntime) {
if (workflowQueue.isRootRuntime) {
workflowQueue.chatAssistantResponse.push(interactiveAssistant);
}
return interactiveAssistant.interactive;
......@@ -1180,7 +1418,7 @@ export const runWorkflow = async (data: RunWorkflowProps): Promise<DispatchFlowR
const durationSeconds = +((Date.now() - startTime) / 1000).toFixed(2);
if (isRootRuntime) {
if (workflowQueue.isRootRuntime) {
data.workflowStreamResponse?.({
event: SseResponseEventEnum.workflowDuration,
data: { durationSeconds }
......
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
export type EdgeIndex = {
bySource: Map<string, RuntimeEdgeItemType[]>;
byTarget: Map<string, RuntimeEdgeItemType[]>;
};
export type EdgeType = 'tree' | 'back' | 'forward' | 'cross';
export interface SCCResult {
nodeToSCC: Map<string, number>;
sccSizes: Map<number, number>;
}
/**
* 使用 Tarjan 算法找出所有强连通分量(SCC)
* SCC 大小 > 1 的节点表示在循环中
*/
export function findSCCs(runtimeNodes: RuntimeNodeItemType[], edgeIndex: EdgeIndex): SCCResult {
const nodeToSCC = new Map<string, number>();
const sccSizes = new Map<number, number>();
let sccId = 0;
const stack: string[] = [];
const inStack = new Set<string>();
const lowLink = new Map<string, number>();
const discoveryTime = new Map<string, number>();
let time = 0;
function tarjan(nodeId: string) {
discoveryTime.set(nodeId, time);
lowLink.set(nodeId, time);
time++;
stack.push(nodeId);
inStack.add(nodeId);
const outEdges = edgeIndex.bySource.get(nodeId) || [];
for (const edge of outEdges) {
const targetId = edge.target;
if (!discoveryTime.has(targetId)) {
tarjan(targetId);
lowLink.set(nodeId, Math.min(lowLink.get(nodeId)!, lowLink.get(targetId)!));
} else if (inStack.has(targetId)) {
lowLink.set(nodeId, Math.min(lowLink.get(nodeId)!, discoveryTime.get(targetId)!));
}
}
// 如果是 SCC 的根节点
if (lowLink.get(nodeId) === discoveryTime.get(nodeId)) {
const sccNodes: string[] = [];
let w: string;
do {
w = stack.pop()!;
inStack.delete(w);
nodeToSCC.set(w, sccId);
sccNodes.push(w);
} while (w !== nodeId);
sccSizes.set(sccId, sccNodes.length);
sccId++;
}
}
// 从所有未访问节点开始
for (const node of runtimeNodes) {
if (!discoveryTime.has(node.nodeId)) {
tarjan(node.nodeId);
}
}
return { nodeToSCC, sccSizes };
}
/**
* 判断节点是否在循环中
*/
export function isNodeInCycle(
nodeId: string,
nodeToSCC: Map<string, number>,
sccSizes: Map<number, number>
): boolean {
const sccId = nodeToSCC.get(nodeId);
if (sccId === undefined) return false;
const size = sccSizes.get(sccId) || 0;
return size > 1;
}
/**
* 对整个工作流图进行一次 DFS,标记每条边的类型
*
* 边类型:
* - tree: 树边(DFS 树中的边)
* - back: 回边(从后代指向当前路径上祖先的边)→ 循环边
* - forward: 前向边(从祖先指向后代的非树边)
* - cross: 跨边(连接不同子树的边)
*/
export function classifyEdgesByDFS(
runtimeNodes: RuntimeNodeItemType[],
edgeIndex: EdgeIndex
): Map<string, EdgeType> {
const edgeTypes = new Map<string, EdgeType>();
const visited = new Set<string>();
const inStack = new Set<string>();
const finished = new Set<string>();
const discoveryTime = new Map<string, number>();
const finishTime = new Map<string, number>();
let time = 0;
function dfs(nodeId: string) {
visited.add(nodeId);
inStack.add(nodeId);
discoveryTime.set(nodeId, ++time);
const outEdges = edgeIndex.bySource.get(nodeId) || [];
for (const edge of outEdges) {
const edgeKey = `${edge.source}-${edge.target}-${edge.sourceHandle || 'default'}`;
const targetId = edge.target;
if (!visited.has(targetId)) {
// 目标节点未访问 → 树边
edgeTypes.set(edgeKey, 'tree');
dfs(targetId);
} else if (inStack.has(targetId)) {
// 目标节点在当前 DFS 路径上 → 回边(循环边)
edgeTypes.set(edgeKey, 'back');
} else if (discoveryTime.get(edge.source)! < discoveryTime.get(targetId)!) {
// 从祖先指向后代的非树边 → 前向边
edgeTypes.set(edgeKey, 'forward');
} else {
// 跨边
edgeTypes.set(edgeKey, 'cross');
}
}
inStack.delete(nodeId);
finished.add(nodeId);
finishTime.set(nodeId, ++time);
}
// 从所有入口节点开始 DFS
const entryNodes = runtimeNodes.filter((node) => {
const inEdges = edgeIndex.byTarget.get(node.nodeId) || [];
return inEdges.length === 0;
});
for (const node of entryNodes) {
if (!visited.has(node.nodeId)) {
dfs(node.nodeId);
}
}
// 处理孤立节点
for (const node of runtimeNodes) {
if (!visited.has(node.nodeId)) {
dfs(node.nodeId);
}
}
return edgeTypes;
}
/**
* 获取边的类型
*/
export function getEdgeType(
edge: RuntimeEdgeItemType,
edgeTypes: Map<string, EdgeType>
): EdgeType | undefined {
const edgeKey = `${edge.source}-${edge.target}-${edge.sourceHandle || 'default'}`;
return edgeTypes.get(edgeKey);
}
......@@ -10,9 +10,12 @@ const LogLevelSchema = z.enum(['trace', 'debug', 'info', 'warning', 'error', 'fa
export const env = createEnv({
server: {
AGENT_SANDBOX_PROVIDER: z.enum(['sealosdevbox']).optional(),
AGENT_SANDBOX_PROVIDER: z.enum(['sealosdevbox', 'opensandbox', 'e2b']).optional(),
AGENT_SANDBOX_SEALOS_BASEURL: z.string().url().optional(),
AGENT_SANDBOX_SEALOS_TOKEN: z.string().optional(),
AGENT_SANDBOX_OPENSANDBOX_BASEURL: z.string().url().optional(),
AGENT_SANDBOX_OPENSANDBOX_TOKEN: z.string().optional(),
AGENT_SANDBOX_E2B_API_KEY: z.string().optional(),
LOG_ENABLE_CONSOLE: BoolSchema.default(true),
LOG_CONSOLE_LEVEL: LogLevelSchema.default('debug'),
......
......@@ -8,7 +8,7 @@
},
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.7.2",
"@fastgpt-sdk/sandbox-adapter": "^0.0.19",
"@fastgpt-sdk/sandbox-adapter": "^0.0.21",
"@fastgpt-sdk/storage": "catalog:",
"@fastgpt-sdk/logger": "catalog:",
"@fastgpt/global": "workspace:*",
......
......@@ -57,9 +57,7 @@ export const authCode = async ({
return Promise.reject(new UserError(i18nT('common:error.code_error')));
}
setTimeout(async () => {
await result.deleteOne({ session }).catch();
}, 60000);
await result.deleteOne();
return 'SUCCESS';
});
......
......@@ -247,8 +247,8 @@ importers:
specifier: 'catalog:'
version: 0.1.2
'@fastgpt-sdk/sandbox-adapter':
specifier: ^0.0.19
version: 0.0.19
specifier: ^0.0.21
version: 0.0.21
'@fastgpt-sdk/storage':
specifier: 'catalog:'
version: 0.6.15(@opentelemetry/api@1.9.0)(@types/node@24.0.13)(jiti@2.6.0)(lightningcss@1.30.1)(proxy-agent@6.5.0)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)
......@@ -1921,6 +1921,9 @@ packages:
'@braintree/sanitize-url@6.0.4':
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
'@bufbuild/protobuf@2.11.0':
resolution: {integrity: sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==}
'@chakra-ui/anatomy@2.2.1':
resolution: {integrity: sha512-bbmyWTGwQo+aHYDMtLIj7k7hcWvwE7GFVDViLFArrrPhfUTDdQTNqhiDp1N7eh2HLyjNhc2MKXV8s2KTQqkmTg==}
......@@ -2073,6 +2076,17 @@ packages:
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
engines: {node: '>=0.1.90'}
'@connectrpc/connect-web@2.0.0-rc.3':
resolution: {integrity: sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw==}
peerDependencies:
'@bufbuild/protobuf': ^2.2.0
'@connectrpc/connect': 2.0.0-rc.3
'@connectrpc/connect@2.0.0-rc.3':
resolution: {integrity: sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ==}
peerDependencies:
'@bufbuild/protobuf': ^2.2.0
'@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
......@@ -2090,6 +2104,10 @@ packages:
'@dmsnell/diff-match-patch@1.1.0':
resolution: {integrity: sha512-yejLPmM5pjsGvxS9gXablUSbInW7H976c/FJ4iQxWIm7/38xBySRemTPDe34lhg1gVLbJntX0+sH0jYfU+PN9A==}
'@e2b/code-interpreter@2.3.3':
resolution: {integrity: sha512-WOpSwc1WpvxyOijf6WMbR76BUuvd2O9ddXgCHHi65lkuy6YgQGq7oyd8PNsT331O9Tqbccjy6uF4xanSdLX1UA==}
engines: {node: '>=20'}
'@emnapi/core@1.3.1':
resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==}
......@@ -2640,8 +2658,8 @@ packages:
'@fastgpt-sdk/plugin@0.3.8':
resolution: {integrity: sha512-GjKrXMHxeF5UMkYGXawrUpzZjVRw3DICNYODeYwsUVOy+/ltu5zuwsqLkuuGQ7Arp/SBCmYRjG/MHmeNp4xxfw==}
'@fastgpt-sdk/sandbox-adapter@0.0.19':
resolution: {integrity: sha512-024C9Ljoic7/oQm1awyLMWVl7kk9NuOGgUa8NC3wOS4GQrCVZCPCHK8YwqkRbKX9T0Akczc6RFaZj+kRJd3m4Q==}
'@fastgpt-sdk/sandbox-adapter@0.0.21':
resolution: {integrity: sha512-SM6e9w49CjYBdDYBzfPeWMnF3G0TM9AkmwUsFBniuaYh/OBMs/DWv/KYDo8xkRcBw9+eZuFokG+A5Vgt4/JZsg==}
engines: {node: '>=18'}
'@fastgpt-sdk/storage@0.6.15':
......@@ -2875,6 +2893,14 @@ packages:
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
'@isaacs/cliui@9.0.0':
resolution: {integrity: sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==}
engines: {node: '>=18'}
'@isaacs/fs-minipass@4.0.1':
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
engines: {node: '>=18.0.0'}
'@istanbuljs/schema@0.1.3':
resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
engines: {node: '>=8'}
......@@ -5466,6 +5492,10 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
balanced-match@4.0.4:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
bare-events@2.5.4:
resolution: {integrity: sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==}
......@@ -5530,6 +5560,10 @@ packages:
brace-expansion@2.0.1:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
brace-expansion@5.0.4:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
engines: {node: 18 || 20 || >=22}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
......@@ -5728,6 +5762,10 @@ packages:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
chownr@3.0.0:
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
engines: {node: '>=18'}
ci-info@3.9.0:
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
engines: {node: '>=8'}
......@@ -5848,6 +5886,9 @@ packages:
commondir@1.0.1:
resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
compare-versions@6.1.1:
resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==}
compress-commons@6.0.2:
resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==}
engines: {node: '>= 14'}
......@@ -6386,6 +6427,9 @@ packages:
dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dockerfile-ast@0.7.1:
resolution: {integrity: sha512-oX/A4I0EhSkGqrFv0YuvPkBUSYp1XiY8O8zAKc8Djglx8ocz+JfOr8gP0ryRMC2myqvDLagmnZaU9ot1vG2ijw==}
doctrine@2.1.0:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
......@@ -6457,6 +6501,10 @@ packages:
duplexer@0.1.2:
resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
e2b@2.14.1:
resolution: {integrity: sha512-g0NPZNzwIaePTahu9ixBtqrw9IZQ8ThK8dt+DU394+jmxQJ+69c2t8A0j973/j+bHo3QdNFxIRIH6zDcC3ueaw==}
engines: {node: '>=20'}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
......@@ -7150,6 +7198,12 @@ packages:
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@11.1.0:
resolution: {integrity: sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==}
engines: {node: 20 || >=22}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
......@@ -7801,6 +7855,10 @@ packages:
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
jackspeak@4.2.3:
resolution: {integrity: sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==}
engines: {node: 20 || >=22}
jiti@1.21.7:
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
hasBin: true
......@@ -8215,6 +8273,10 @@ packages:
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
lru-cache@11.2.7:
resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==}
engines: {node: 20 || >=22}
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
......@@ -8581,6 +8643,10 @@ packages:
resolution: {integrity: sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
minimatch@10.2.4:
resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
engines: {node: 18 || 20 || >=22}
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
......@@ -8607,6 +8673,10 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
minizlib@3.1.0:
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
engines: {node: '>= 18'}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
......@@ -9103,6 +9173,10 @@ packages:
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
engines: {node: '>=16 || 14 >=14.18'}
path-scurry@2.0.2:
resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==}
engines: {node: 18 || 20 || >=22}
path-to-regexp@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
......@@ -10407,6 +10481,10 @@ packages:
tar-stream@3.1.7:
resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==}
tar@7.5.11:
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
engines: {node: '>=18'}
terser@5.39.0:
resolution: {integrity: sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==}
engines: {node: '>=10'}
......@@ -11151,6 +11229,12 @@ packages:
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
engines: {node: '>=0.10.0'}
vscode-languageserver-textdocument@1.0.12:
resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==}
vscode-languageserver-types@3.17.5:
resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==}
vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
......@@ -11343,6 +11427,10 @@ packages:
yallist@3.1.1:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
yallist@5.0.0:
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
engines: {node: '>=18'}
yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
engines: {node: '>= 6'}
......@@ -12767,6 +12855,8 @@ snapshots:
'@braintree/sanitize-url@6.0.4': {}
'@bufbuild/protobuf@2.11.0': {}
'@chakra-ui/anatomy@2.2.1': {}
'@chakra-ui/anatomy@2.3.6': {}
......@@ -13031,6 +13121,15 @@ snapshots:
'@colors/colors@1.6.0': {}
'@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.11.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.11.0))':
dependencies:
'@bufbuild/protobuf': 2.11.0
'@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.11.0)
'@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.11.0)':
dependencies:
'@bufbuild/protobuf': 2.11.0
'@dabh/diagnostics@2.0.3':
dependencies:
colorspace: 1.1.4
......@@ -13047,6 +13146,10 @@ snapshots:
'@dmsnell/diff-match-patch@1.1.0': {}
'@e2b/code-interpreter@2.3.3':
dependencies:
e2b: 2.14.1
'@emnapi/core@1.3.1':
dependencies:
'@emnapi/wasi-threads': 1.0.1
......@@ -13433,9 +13536,10 @@ snapshots:
'@fortaine/fetch-event-source': 3.0.6
zod: 4.1.12
'@fastgpt-sdk/sandbox-adapter@0.0.19':
'@fastgpt-sdk/sandbox-adapter@0.0.21':
dependencies:
'@alibaba-group/opensandbox': 0.1.4
'@e2b/code-interpreter': 2.3.3
'@fastgpt-sdk/storage@0.6.15(@opentelemetry/api@1.9.0)(@types/node@20.17.24)(jiti@2.6.0)(lightningcss@1.30.1)(sass@1.85.1)(terser@5.39.0)(tsx@4.20.6)(yaml@2.8.1)':
dependencies:
......@@ -13710,6 +13814,12 @@ snapshots:
wrap-ansi: 8.1.0
wrap-ansi-cjs: wrap-ansi@7.0.0
'@isaacs/cliui@9.0.0': {}
'@isaacs/fs-minipass@4.0.1':
dependencies:
minipass: 7.1.2
'@istanbuljs/schema@0.1.3': {}
'@jest/schemas@29.6.3':
......@@ -16850,6 +16960,8 @@ snapshots:
balanced-match@1.0.2: {}
balanced-match@4.0.4: {}
bare-events@2.5.4:
optional: true
......@@ -16941,6 +17053,10 @@ snapshots:
dependencies:
balanced-match: 1.0.2
brace-expansion@5.0.4:
dependencies:
balanced-match: 4.0.4
braces@3.0.3:
dependencies:
fill-range: 7.1.1
......@@ -17158,6 +17274,8 @@ snapshots:
dependencies:
readdirp: 4.1.2
chownr@3.0.0: {}
ci-info@3.9.0: {}
classcat@5.0.5: {}
......@@ -17277,6 +17395,8 @@ snapshots:
commondir@1.0.1: {}
compare-versions@6.1.1: {}
compress-commons@6.0.2:
dependencies:
crc-32: 1.2.2
......@@ -17831,6 +17951,11 @@ snapshots:
dlv@1.1.3: {}
dockerfile-ast@0.7.1:
dependencies:
vscode-languageserver-textdocument: 1.0.12
vscode-languageserver-types: 3.17.5
doctrine@2.1.0:
dependencies:
esutils: 2.0.3
......@@ -17904,6 +18029,19 @@ snapshots:
duplexer@0.1.2: {}
e2b@2.14.1:
dependencies:
'@bufbuild/protobuf': 2.11.0
'@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.11.0)
'@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.11.0)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.11.0))
chalk: 5.4.1
compare-versions: 6.1.1
dockerfile-ast: 0.7.1
glob: 11.1.0
openapi-fetch: 0.14.1
platform: 1.3.6
tar: 7.5.11
eastasianwidth@0.2.0: {}
ecc-jsbn@0.1.2:
......@@ -18888,6 +19026,15 @@ snapshots:
package-json-from-dist: 1.0.1
path-scurry: 1.11.1
glob@11.1.0:
dependencies:
foreground-child: 3.3.1
jackspeak: 4.2.3
minimatch: 10.2.4
minipass: 7.1.2
package-json-from-dist: 1.0.1
path-scurry: 2.0.2
glob@7.2.3:
dependencies:
fs.realpath: 1.0.0
......@@ -19617,6 +19764,10 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jackspeak@4.2.3:
dependencies:
'@isaacs/cliui': 9.0.0
jiti@1.21.7: {}
jiti@2.6.0:
......@@ -19999,6 +20150,8 @@ snapshots:
lru-cache@10.4.3: {}
lru-cache@11.2.7: {}
lru-cache@5.1.1:
dependencies:
yallist: 3.1.1
......@@ -20671,6 +20824,10 @@ snapshots:
mimic-response@4.0.0: {}
minimatch@10.2.4:
dependencies:
brace-expansion: 5.0.4
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.11
......@@ -20707,6 +20864,10 @@ snapshots:
minipass@7.1.2: {}
minizlib@3.1.0:
dependencies:
minipass: 7.1.2
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
......@@ -21315,6 +21476,11 @@ snapshots:
lru-cache: 10.4.3
minipass: 7.1.2
path-scurry@2.0.2:
dependencies:
lru-cache: 11.2.7
minipass: 7.1.2
path-to-regexp@0.1.12: {}
path-to-regexp@8.2.0: {}
......@@ -22918,6 +23084,14 @@ snapshots:
fast-fifo: 1.3.2
streamx: 2.22.0
tar@7.5.11:
dependencies:
'@isaacs/fs-minipass': 4.0.1
chownr: 3.0.0
minipass: 7.1.2
minizlib: 3.1.0
yallist: 5.0.0
terser@5.39.0:
dependencies:
'@jridgewell/source-map': 0.3.6
......@@ -23812,6 +23986,10 @@ snapshots:
void-elements@3.1.0: {}
vscode-languageserver-textdocument@1.0.12: {}
vscode-languageserver-types@3.17.5: {}
vue-component-type-helpers@3.1.1: {}
vue-demi@0.14.10(vue@3.5.22(typescript@5.8.2)):
......@@ -24023,6 +24201,8 @@ snapshots:
yallist@3.1.1: {}
yallist@5.0.0: {}
yaml@1.10.2: {}
yaml@2.3.1: {}
......
......@@ -48,7 +48,7 @@ const nextConfig: NextConfig = {
];
},
webpack(config, { isServer, dev }) {
webpack(config, { isServer }) {
config.ignoreWarnings = [
...(config.ignoreWarnings || []),
{
......@@ -97,6 +97,10 @@ const nextConfig: NextConfig = {
if (isServer) {
(config.externals as string[]).push('@node-rs/jieba');
config.externals.push({
'@e2b/code-interpreter': 'commonjs @e2b/code-interpreter',
e2b: 'commonjs e2b'
});
}
config.experiments = {
......@@ -127,14 +131,15 @@ const nextConfig: NextConfig = {
return config;
},
transpilePackages: ['@modelcontextprotocol/sdk', 'ahooks'],
transpilePackages: ['@modelcontextprotocol/sdk', 'ahooks', '@fastgpt-sdk/sandbox-adapter'],
serverExternalPackages: [
'mongoose',
'pg',
'bullmq',
'@zilliz/milvus2-sdk-node',
'tiktoken',
'@opentelemetry/api-logs'
'@opentelemetry/api-logs',
'chalk'
],
// 优化大库的 barrel exports tree-shaking
experimental: {
......
import { describe, it, expect } from 'vitest';
import { WorkflowQueue } from '@fastgpt/service/core/workflow/dispatch/index';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
/**
* 性能测试:buildEdgeIndex 和 buildNodeEdgeGroupsMap
*
* 测试目标:
* 1. 测试不同规模工作流的性能表现
* 2. 验证算法的时间复杂度
* 3. 确保性能在可接受范围内
*/
// 辅助函数:创建节点
function createNode(
id: string,
type: FlowNodeTypeEnum = FlowNodeTypeEnum.chatNode
): RuntimeNodeItemType {
return {
nodeId: id,
name: `Node ${id}`,
flowNodeType: type,
avatar: '',
intro: '',
isEntry: false,
inputs: [],
outputs: []
};
}
// 辅助函数:创建边
function createEdge(
source: string,
target: string,
status: 'waiting' | 'active' | 'skipped' = 'waiting',
sourceHandle?: string,
targetHandle?: string
): RuntimeEdgeItemType {
return {
source,
target,
status,
sourceHandle: sourceHandle || `${source}-source-right`,
targetHandle: targetHandle || `${target}-target-left`
};
}
// 生成线性工作流:start → N1 → N2 → ... → Nn
function generateLinearWorkflow(nodeCount: number) {
const nodes: RuntimeNodeItemType[] = [createNode('start', FlowNodeTypeEnum.workflowStart)];
const edges: RuntimeEdgeItemType[] = [];
for (let i = 1; i <= nodeCount; i++) {
nodes.push(createNode(`N${i}`));
edges.push(createEdge(i === 1 ? 'start' : `N${i - 1}`, `N${i}`));
}
return { nodes, edges };
}
// 生成分支工作流:每个节点有 branchCount 个分支
function generateBranchWorkflow(depth: number, branchCount: number) {
const nodes: RuntimeNodeItemType[] = [createNode('start', FlowNodeTypeEnum.workflowStart)];
const edges: RuntimeEdgeItemType[] = [];
let nodeCounter = 0;
function addLevel(parentId: string, currentDepth: number) {
if (currentDepth >= depth) return;
for (let i = 0; i < branchCount; i++) {
const nodeId = `N${++nodeCounter}`;
nodes.push(createNode(nodeId, FlowNodeTypeEnum.ifElseNode));
edges.push(createEdge(parentId, nodeId, 'waiting', `${parentId}-source-branch${i}`));
addLevel(nodeId, currentDepth + 1);
}
}
addLevel('start', 0);
return { nodes, edges };
}
// 生成循环工作流:包含多个循环
function generateCyclicWorkflow(nodeCount: number, cycleCount: number) {
const nodes: RuntimeNodeItemType[] = [createNode('start', FlowNodeTypeEnum.workflowStart)];
const edges: RuntimeEdgeItemType[] = [];
// 创建主链
for (let i = 1; i <= nodeCount; i++) {
nodes.push(createNode(`N${i}`));
edges.push(createEdge(i === 1 ? 'start' : `N${i - 1}`, `N${i}`));
}
// 添加循环边
for (let i = 0; i < cycleCount; i++) {
const cycleStart = Math.floor((nodeCount / cycleCount) * i) + 1;
const cycleEnd = Math.floor((nodeCount / cycleCount) * (i + 1));
if (cycleStart < cycleEnd) {
edges.push(createEdge(`N${cycleEnd}`, `N${cycleStart}`));
}
}
return { nodes, edges };
}
// 生成复杂工作流:混合分支、循环、汇聚
function generateComplexWorkflow(nodeCount: number) {
const nodes: RuntimeNodeItemType[] = [createNode('start', FlowNodeTypeEnum.workflowStart)];
const edges: RuntimeEdgeItemType[] = [];
// 创建多层结构
const layerSize = Math.ceil(Math.sqrt(nodeCount));
let nodeCounter = 0;
for (let layer = 0; layer < layerSize && nodeCounter < nodeCount; layer++) {
const nodesInLayer = Math.min(layerSize, nodeCount - nodeCounter);
for (let i = 0; i < nodesInLayer; i++) {
const nodeId = `N${++nodeCounter}`;
const nodeType = i % 3 === 0 ? FlowNodeTypeEnum.ifElseNode : FlowNodeTypeEnum.chatNode;
nodes.push(createNode(nodeId, nodeType));
// 连接到上一层的节点
if (layer === 0) {
edges.push(createEdge('start', nodeId));
} else {
const prevLayerStart =
nodeCounter -
nodesInLayer -
Math.min(layerSize, nodeCount - (nodeCounter - nodesInLayer));
const prevLayerEnd = nodeCounter - nodesInLayer;
// 每个节点连接到上一层的 1-2 个节点
const connectCount = Math.min(2, prevLayerEnd - prevLayerStart);
for (let j = 0; j < connectCount; j++) {
const sourceIdx = prevLayerStart + ((i + j) % (prevLayerEnd - prevLayerStart));
edges.push(createEdge(`N${sourceIdx}`, nodeId));
}
}
}
// 添加一些循环边
if (layer > 0 && layer % 2 === 0) {
const cycleSource = nodeCounter;
const cycleTarget = Math.max(1, nodeCounter - layerSize);
edges.push(createEdge(`N${cycleSource}`, `N${cycleTarget}`));
}
}
return { nodes, edges };
}
// 性能测试辅助函数
function measurePerformance(name: string, fn: () => void, iterations: number = 1) {
const times: number[] = [];
for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
const end = performance.now();
times.push(end - start);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
const min = Math.min(...times);
const max = Math.max(...times);
return { avg, min, max, times };
}
describe('Workflow Performance Benchmark', () => {
describe('buildEdgeIndex 性能测试', () => {
it('小规模工作流 (10 节点)', () => {
const { nodes, edges } = generateLinearWorkflow(10);
const result = measurePerformance(
'buildEdgeIndex - 10 nodes',
() => WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges }),
100
);
console.log(`\n[buildEdgeIndex - 10 nodes]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(1); // 应该小于 1ms
});
it('中等规模工作流 (100 节点)', () => {
const { nodes, edges } = generateLinearWorkflow(100);
const result = measurePerformance(
'buildEdgeIndex - 100 nodes',
() => WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges }),
100
);
console.log(`\n[buildEdgeIndex - 100 nodes]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(5); // 应该小于 5ms
});
it('大规模工作流 (1000 节点)', () => {
const { nodes, edges } = generateLinearWorkflow(1000);
const result = measurePerformance(
'buildEdgeIndex - 1000 nodes',
() => WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges }),
10
);
console.log(`\n[buildEdgeIndex - 1000 nodes]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(50); // 应该小于 50ms
});
});
describe('buildNodeEdgeGroupsMap 性能测试', () => {
it('小规模线性工作流 (10 节点)', () => {
const { nodes, edges } = generateLinearWorkflow(10);
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const result = measurePerformance(
'buildNodeEdgeGroupsMap - 10 nodes linear',
() =>
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
}),
100
);
console.log(`\n[buildNodeEdgeGroupsMap - 10 nodes linear]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(5); // 应该小于 5ms
});
it('中等规模线性工作流 (100 节点)', () => {
const { nodes, edges } = generateLinearWorkflow(100);
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const result = measurePerformance(
'buildNodeEdgeGroupsMap - 100 nodes linear',
() =>
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
}),
10
);
console.log(`\n[buildNodeEdgeGroupsMap - 100 nodes linear]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(50); // 应该小于 50ms
});
it('小规模分支工作流 (深度5, 每层2分支)', () => {
const { nodes, edges } = generateBranchWorkflow(5, 2);
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
console.log(`\n[分支工作流] 节点数: ${nodes.length}, 边数: ${edges.length}`);
const result = measurePerformance(
'buildNodeEdgeGroupsMap - branch workflow',
() =>
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
}),
10
);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(100); // 应该小于 100ms
});
it('循环工作流 (50 节点, 5 个循环)', () => {
const { nodes, edges } = generateCyclicWorkflow(50, 5);
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
console.log(`\n[循环工作流] 节点数: ${nodes.length}, 边数: ${edges.length}`);
const result = measurePerformance(
'buildNodeEdgeGroupsMap - cyclic workflow',
() =>
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
}),
10
);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(100); // 应该小于 100ms
});
it('复杂工作流 (100 节点, 混合结构)', () => {
const { nodes, edges } = generateComplexWorkflow(100);
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
console.log(`\n[复杂工作流] 节点数: ${nodes.length}, 边数: ${edges.length}`);
const result = measurePerformance(
'buildNodeEdgeGroupsMap - complex workflow',
() =>
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
}),
10
);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(200); // 应该小于 200ms
});
});
describe('完整流程性能测试', () => {
it('小规模工作流完整流程 (10 节点)', () => {
const { nodes, edges } = generateLinearWorkflow(10);
const result = measurePerformance(
'Complete workflow - 10 nodes',
() => {
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
},
100
);
console.log(`\n[完整流程 - 10 nodes]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(10); // 应该小于 10ms
});
it('中等规模工作流完整流程 (100 节点)', () => {
const { nodes, edges } = generateLinearWorkflow(100);
const result = measurePerformance(
'Complete workflow - 100 nodes',
() => {
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
},
10
);
console.log(`\n[完整流程 - 100 nodes]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(100); // 应该小于 100ms
});
it('复杂工作流完整流程 (100 节点)', () => {
const { nodes, edges } = generateComplexWorkflow(100);
const result = measurePerformance(
'Complete workflow - complex 100 nodes',
() => {
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
},
10
);
console.log(`\n[完整流程 - complex 100 nodes]`);
console.log(` 平均耗时: ${result.avg.toFixed(3)}ms`);
console.log(` 最小耗时: ${result.min.toFixed(3)}ms`);
console.log(` 最大耗时: ${result.max.toFixed(3)}ms`);
expect(result.avg).toBeLessThan(200); // 应该小于 200ms
});
});
describe('扩展性测试', () => {
it('测试不同规模的性能增长', () => {
const scales = [10, 50, 100, 200, 500];
const results: Array<{ scale: number; time: number }> = [];
console.log(`\n[扩展性测试]`);
console.log(`规模\t节点数\t边数\t平均耗时(ms)`);
console.log(`----\t------\t----\t------------`);
for (const scale of scales) {
const { nodes, edges } = generateLinearWorkflow(scale);
const result = measurePerformance(
`Scale ${scale}`,
() => {
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
},
5
);
results.push({ scale, time: result.avg });
console.log(`${scale}\t${nodes.length}\t${edges.length}\t${result.avg.toFixed(3)}`);
}
// 验证时间复杂度接近线性
// 计算增长率
for (let i = 1; i < results.length; i++) {
const scaleRatio = results[i].scale / results[i - 1].scale;
const timeRatio = results[i].time / results[i - 1].time;
// 时间增长率应该接近规模增长率(线性复杂度)
// 允许一定的误差范围(例如 2 倍)
expect(timeRatio).toBeLessThan(scaleRatio * 2);
}
});
});
describe('内存使用测试', () => {
it('测试大规模工作流的内存占用', () => {
const { nodes, edges } = generateComplexWorkflow(500);
// 记录初始内存
if (global.gc) {
global.gc();
}
const initialMemory = process.memoryUsage().heapUsed;
// 执行构建
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const nodeEdgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
// 记录最终内存
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = (finalMemory - initialMemory) / 1024 / 1024; // MB
console.log(`\n[内存使用测试 - 500 nodes]`);
console.log(` 节点数: ${nodes.length}`);
console.log(` 边数: ${edges.length}`);
console.log(` 内存增长: ${memoryIncrease.toFixed(2)} MB`);
console.log(` 平均每节点: ${(memoryIncrease / nodes.length).toFixed(3)} MB`);
// 验证内存使用合理(每个节点平均不超过 1MB)
expect(memoryIncrease / nodes.length).toBeLessThan(1);
});
});
});
import { describe, it, expect } from 'vitest';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { WorkflowQueue } from '@fastgpt/service/core/workflow/dispatch/index';
import { createNode, createEdge } from '../utils';
/**
* 测试目标:验证节点运行状态判断是否正确
*
* 测试方法:
* 1. 构建工作流图(节点 + 边)
* 2. 使用 buildNodeEdgeGroupsMap 构建边分组
* 3. 模拟不同的边状态(active/waiting/skipped)
* 4. 使用 getNodeRunStatus 判断节点状态
* 5. 验证节点状态是否符合预期
*/
describe('checkNodeRunStatus', () => {
// 辅助函数:设置边状态
const setEdgeStatus = (
edges: RuntimeEdgeItemType[],
source: string,
target: string,
status: 'active' | 'waiting' | 'skipped'
) => {
const edge = edges.find((e) => e.source === source && e.target === target);
if (edge) {
edge.status = status;
}
};
describe('场景1: 简单分支汇聚', () => {
/**
* 工作流结构:
*
* ┌─ if ──→ B ──┐
* start → A → D
* └─ else → C ──┘
*
* 预期分组:
* - D: 组1[B→D], 组2[C→D]
*
* 测试场景:
* 1. A 走 if 分支:B→D active, C→D skipped → D 应该运行
* 2. A 走 else 分支:C→D active, B→D skipped → D 应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.ifElseNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B', 'waiting', 'A-source-if'),
createEdge('A', 'C', 'waiting', 'A-source-else'),
createEdge('B', 'D'),
createEdge('C', 'D')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('D 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('D') || [];
expect(groups.length).toBe(1);
});
it('场景1.1: A 走 if 分支,D 应该运行', () => {
// 设置边状态
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'A', 'B', 'active');
setEdgeStatus(edges, 'A', 'C', 'skipped');
setEdgeStatus(edges, 'B', 'D', 'active');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景1.2: A 走 else 分支,D 应该运行', () => {
// 设置边状态
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'A', 'B', 'skipped');
setEdgeStatus(edges, 'A', 'C', 'active');
setEdgeStatus(edges, 'B', 'D', 'skipped');
setEdgeStatus(edges, 'C', 'D', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景1.3: B 还在执行中,D 应该等待', () => {
// 设置边状态
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'A', 'B', 'active');
setEdgeStatus(edges, 'A', 'C', 'skipped');
setEdgeStatus(edges, 'B', 'D', 'waiting');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景2: 简单循环', () => {
/**
* 工作流结构:
*
* start → A → B → C → A
*
* 预期分组:
* - A: 组1[start→A], 组2[C→A]
*
* 测试场景:
* 1. 第一次执行:start→A active → A 应该运行
* 2. 循环执行:C→A active → A 应该运行
* 3. 两条边都 waiting → A 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B'),
createEdge('B', 'C'),
createEdge('C', 'A')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('A 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('A') || [];
expect(groups.length).toBe(2);
});
it('场景2.1: 第一次执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'C', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景2.2: 循环执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'C', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景2.3: 两条边都 waiting,A 应该等待', () => {
setEdgeStatus(edges, 'start', 'A', 'waiting');
setEdgeStatus(edges, 'C', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景3: 分支 + 循环', () => {
/**
* 工作流结构:
*
* ┌─ if ──→ B ──→ D ──→ F ──┐
* start → A │
* └─ else → C ──→ D │
* │
* A ←───────────────────────┘
*
* 预期分组:
* - D: 组1[B→D], 组2[C→D]
* - A: 组1[start→A], 组2[F→A]
*
* 测试场景:
* 1. 第一次走 if 分支:B→D active, C→D skipped → D 应该运行
* 2. 第一次走 else 分支:C→D active, B→D skipped → D 应该运行
* 3. 循环回来:F→A active → A 应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.ifElseNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode),
createNode('F', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B', 'waiting', 'A-source-if'),
createEdge('A', 'C', 'waiting', 'A-source-else'),
createEdge('B', 'D'),
createEdge('C', 'D'),
createEdge('D', 'F'),
createEdge('F', 'A')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('D 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('D') || [];
expect(groups.length).toBe(2);
});
it('A 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('A') || [];
expect(groups.length).toBe(2);
});
it('场景3.1: 第一次走 if 分支,D 应该运行', () => {
setEdgeStatus(edges, 'B', 'D', 'active');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景3.2: 第一次走 else 分支,D 应该运行', () => {
setEdgeStatus(edges, 'B', 'D', 'skipped');
setEdgeStatus(edges, 'C', 'D', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景3.3: 循环回来,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'F', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
});
describe('场景4: 并行汇聚(无分支节点)', () => {
/**
* 工作流结构:
*
* start ──→ A ──→ C
* └──→ B ──→ C
*
* 预期分组:
* - C: 组1[A→C, B→C] (合并成一组,因为没有分支节点)
*
* 测试场景:
* 1. A 和 B 都完成:A→C active, B→C active → C 应该运行
* 2. 只有 A 完成:A→C active, B→C waiting → C 应该等待
* 3. 只有 B 完成:A→C waiting, B→C active → C 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('start', 'B'),
createEdge('A', 'C'),
createEdge('B', 'C')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('C 节点应该只有 1 组', () => {
const groups = edgeGroupsMap.get('C') || [];
expect(groups.length).toBe(1);
expect(groups[0].length).toBe(2); // 两条边在同一组
});
it('场景4.1: A 和 B 都完成,C 应该运行', () => {
setEdgeStatus(edges, 'A', 'C', 'active');
setEdgeStatus(edges, 'B', 'C', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景4.2: 只有 A 完成,C 应该等待', () => {
setEdgeStatus(edges, 'A', 'C', 'active');
setEdgeStatus(edges, 'B', 'C', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
it('场景4.3: 只有 B 完成,C 应该等待', () => {
setEdgeStatus(edges, 'A', 'C', 'waiting');
setEdgeStatus(edges, 'B', 'C', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景5: 所有边都 skipped', () => {
/**
* 测试场景:
* 当节点的所有输入边都是 skipped 时,节点应该被跳过
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.ifElseNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B', 'waiting', 'A-source-if'),
createEdge('A', 'C', 'waiting', 'A-source-else'),
createEdge('B', 'D'),
createEdge('C', 'D')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('所有边都 skipped,D 应该被跳过', () => {
setEdgeStatus(edges, 'B', 'D', 'skipped');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('skip');
});
});
describe('场景6: 多层分支嵌套', () => {
/**
* 工作流结构:
*
* ┌─ if ──→ B ─ if ──→ D ──┐
* start ──→ A └─ else ─→ E ──┤
* └─ else ─→ C ────────────→ F
*
* 预期分组:
* - F: 组1[C→F, D→F, E→F]
*
* 测试场景:
* 1. A 走 if → B 走 if:D→F active, 其他 skipped → F 应该运行
* 2. A 走 if → B 走 else:E→F active, 其他 skipped → F 应该运行
* 3. A 走 else:C→F active, 其他 skipped → F 应该运行
* 4. 部分边 waiting:至少一条边 waiting → F 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.ifElseNode),
createNode('B', FlowNodeTypeEnum.ifElseNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode),
createNode('E', FlowNodeTypeEnum.chatNode),
createNode('F', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B', 'waiting', 'A-source-if'),
createEdge('A', 'C', 'waiting', 'A-source-else'),
createEdge('B', 'D', 'waiting', 'B-source-if'),
createEdge('B', 'E', 'waiting', 'B-source-else'),
createEdge('C', 'F'),
createEdge('D', 'F'),
createEdge('E', 'F')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('F 节点应该有 1 组(3条边)', () => {
const groups = edgeGroupsMap.get('F') || [];
expect(groups.length).toBe(1);
expect(groups[0].length).toBe(3);
});
it('场景6.1: A→if, B→if 路径,F 应该运行', () => {
setEdgeStatus(edges, 'C', 'F', 'skipped');
setEdgeStatus(edges, 'D', 'F', 'active');
setEdgeStatus(edges, 'E', 'F', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'F')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景6.2: A→if, B→else 路径,F 应该运行', () => {
setEdgeStatus(edges, 'C', 'F', 'skipped');
setEdgeStatus(edges, 'D', 'F', 'skipped');
setEdgeStatus(edges, 'E', 'F', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'F')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景6.3: A→else 路径,F 应该运行', () => {
setEdgeStatus(edges, 'C', 'F', 'active');
setEdgeStatus(edges, 'D', 'F', 'skipped');
setEdgeStatus(edges, 'E', 'F', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'F')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景6.4: D 还在执行中,F 应该等待', () => {
setEdgeStatus(edges, 'C', 'F', 'skipped');
setEdgeStatus(edges, 'D', 'F', 'waiting');
setEdgeStatus(edges, 'E', 'F', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'F')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景7: 嵌套循环', () => {
/**
* 工作流结构:
*
* start ──→ A ──→ B ──→ C ──→ D
* ↑ ↑ | |
* | |_____| |
* |_________________|
* (内层循环) (外层循环)
*
* 预期分组:
* - A: 组1[start→A], 组2[D→A]
* - B: 组1[A→B], 组2[C→B]
*
* 测试场景:
* 1. 第一次执行:start→A active → A 应该运行
* 2. 内层循环:C→B active → B 应该运行
* 3. 外层循环:D→A active → A 应该运行
* 4. 两条边都 waiting → 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B'),
createEdge('B', 'C'),
createEdge('C', 'B'), // 内层循环
createEdge('C', 'D'),
createEdge('D', 'A') // 外层循环
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('A 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('A') || [];
expect(groups.length).toBe(2);
});
it('B 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('B') || [];
expect(groups.length).toBe(2);
});
it('场景7.1: 第一次执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'D', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景7.2: 内层循环执行,B 应该运行', () => {
setEdgeStatus(edges, 'A', 'B', 'skipped');
setEdgeStatus(edges, 'C', 'B', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'B')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景7.3: 外层循环执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'D', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景7.4: B 的两条边都 waiting,B 应该等待', () => {
setEdgeStatus(edges, 'A', 'B', 'waiting');
setEdgeStatus(edges, 'C', 'B', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'B')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景8: 多个独立循环汇聚', () => {
/**
* 工作流结构:
*
* start ──→ A ──→ B ──→ E
* ↑ | ↑
* |____| |
* | |
* └──→ C ──→ D
* ↑ |
* |_____|
*
* 预期分组:
* - A: 组1[start→A], 组2[B→A]
* - C: 组1[A→C], 组2[D→C]
* - E: 组1[B→E, D→E]
*
* 测试场景:
* 1. 两个循环都完成:B→E active, D→E active → E 应该运行
* 2. 只有循环1完成:B→E active, D→E waiting → E 应该等待
* 3. 只有循环2完成:B→E waiting, D→E active → E 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode),
createNode('E', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B'),
createEdge('B', 'A'), // 循环1
createEdge('A', 'C'),
createEdge('C', 'D'),
createEdge('D', 'C'), // 循环2
createEdge('B', 'E'),
createEdge('D', 'E')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('E 节点应该有 1 组(2条边)', () => {
const groups = edgeGroupsMap.get('E') || [];
expect(groups.length).toBe(1);
expect(groups[0].length).toBe(2);
});
it('场景8.1: 两个循环都完成,E 应该运行', () => {
setEdgeStatus(edges, 'B', 'E', 'active');
setEdgeStatus(edges, 'D', 'E', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'E')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景8.2: 只有循环1完成,E 应该等待', () => {
setEdgeStatus(edges, 'B', 'E', 'active');
setEdgeStatus(edges, 'D', 'E', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'E')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
it('场景8.3: 只有循环2完成,E 应该等待', () => {
setEdgeStatus(edges, 'B', 'E', 'waiting');
setEdgeStatus(edges, 'D', 'E', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'E')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景9: 复杂有向有环图(多入口多循环)', () => {
/**
* 工作流结构:
*
* start ──→ A ──→ C ──→ D ──→ E
* | ↑ ↑ |
* | |____|______________|
* | |
* └──→ B
*
* 预期分组:
* - A: 组1[start→A], 组2[E→A]
* - C: 组1[A→C, B→C], 组2[E→C]
*
* 测试场景:
* 1. 第一次执行:start→A active → A 应该运行
* 2. 循环到 A:E→A active → A 应该运行
* 3. C 的非循环边:A→C active, B→C active, E→C skipped → C 应该运行
* 4. C 的循环边:E→C active, 其他 skipped → C 应该运行
* 5. C 部分 waiting:A→C active, B→C waiting, E→C skipped → C 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode),
createNode('E', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('start', 'B'),
createEdge('A', 'C'),
createEdge('B', 'C'),
createEdge('C', 'D'),
createEdge('D', 'E'),
createEdge('E', 'C'), // 循环1
createEdge('E', 'A') // 循环2
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('A 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('A') || [];
expect(groups.length).toBe(2);
});
it('C 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('C') || [];
expect(groups.length).toBe(2);
expect(groups[0].length).toBe(2); // A→C, B→C
expect(groups[1].length).toBe(1); // E→C
});
it('场景9.1: 第一次执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'E', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景9.2: 循环到 A,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'E', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景9.3: C 的非循环边都完成,C 应该运行', () => {
setEdgeStatus(edges, 'A', 'C', 'active');
setEdgeStatus(edges, 'B', 'C', 'active');
setEdgeStatus(edges, 'E', 'C', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景9.4: C 的循环边完成,C 应该运行', () => {
setEdgeStatus(edges, 'A', 'C', 'skipped');
setEdgeStatus(edges, 'B', 'C', 'skipped');
setEdgeStatus(edges, 'E', 'C', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景9.5: C 的非循环边部分 waiting,C 应该等待', () => {
setEdgeStatus(edges, 'A', 'C', 'active');
setEdgeStatus(edges, 'B', 'C', 'waiting');
setEdgeStatus(edges, 'E', 'C', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景10: 自循环节点', () => {
/**
* 工作流结构:
*
* start ──→ A ──┐
* ↑__|
*
* 预期分组:
* - A: 组1[start→A], 组2[A→A]
*
* 测试场景:
* 1. 第一次执行:start→A active, A→A waiting → A 应该运行
* 2. 自循环执行:start→A skipped, A→A active → A 应该运行
* 3. 两条边都 waiting → A 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode)
];
const edges = [createEdge('start', 'A'), createEdge('A', 'A')];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('A 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('A') || [];
expect(groups.length).toBe(2);
});
it('场景10.1: 第一次执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'A', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景10.2: 自循环执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'A', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景10.3: 两条边都 waiting,A 应该等待', () => {
setEdgeStatus(edges, 'start', 'A', 'waiting');
setEdgeStatus(edges, 'A', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景11: 用户工作流 - 多层循环回退', () => {
/**
* 工作流结构:
*
* 开始 → 回复11 → 回复22 → 用户选择
* ↑ ↑ ↓
* | | ├─ 结束
* | └─────────┤ (option2: 回到22)
* └───────────────────┘ (option3: 回到11)
*
* 关键问题:
* - "回复22"节点有两条输入边:
* 1. edge(回复11 → 回复22) - 非循环边
* 2. edge(用户选择 → 回复22) - 循环边
*
* - 两条边都能到达入口,所以都被放在 commonEdges 中
* - 这导致 AND 语义:两条边都要满足才能运行
* - 但实际应该是 OR 语义:任一边满足即可运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('reply11', FlowNodeTypeEnum.answerNode),
createNode('reply22', FlowNodeTypeEnum.answerNode),
createNode('userSelect', FlowNodeTypeEnum.userSelect),
createNode('replyEnd', FlowNodeTypeEnum.answerNode)
];
const edges = [
createEdge('start', 'reply11'),
createEdge('reply11', 'reply22'),
createEdge('reply22', 'userSelect'),
createEdge('userSelect', 'replyEnd', 'waiting', 'option1'),
createEdge('userSelect', 'reply22', 'waiting', 'option2'),
createEdge('userSelect', 'reply11', 'waiting', 'option3')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('reply22 节点分组', () => {
const groups = edgeGroupsMap.get('reply22') || [];
// 实际:两条边都在 commonEdges 中
// 期望:应该分成两组(非循环 + 循环)
expect(groups.length).toBeGreaterThanOrEqual(1);
});
it('场景11.1: 第一次执行,reply11 完成后 reply22 应该运行', () => {
setEdgeStatus(edges, 'reply11', 'reply22', 'active');
setEdgeStatus(edges, 'userSelect', 'reply22', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'reply22')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
// 关键:edge1 active 时应该运行,不需要等待 edge2
expect(status).toBe('run');
});
it('场景11.2: 用户选择"回到22",reply22 应该运行', () => {
setEdgeStatus(edges, 'reply11', 'reply22', 'skipped');
setEdgeStatus(edges, 'userSelect', 'reply22', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'reply22')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景11.3: 循环边 active 但非循环边 waiting,应该运行', () => {
setEdgeStatus(edges, 'reply11', 'reply22', 'waiting');
setEdgeStatus(edges, 'userSelect', 'reply22', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'reply22')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
// 关键:循环边激活时应该运行,不需要等待非循环边
expect(status).toBe('run');
});
});
describe('场景12: 复杂分支与循环混合', () => {
/**
* 工作流结构:
*
* ┌─ if ──→ B ──→ D ──┐
* start ──→ A ├──→ F
* └─ else ─→ C ─ if ──→ D |
* ↑ └─ else ─→ E ──┘
* |________________________________|
*
* 预期分组:
* - A: 组1[start→A], 组2[F→A]
* - D: 组1[B→D], 组2[C→D]
* - F: 组1[D→F], 组2[E→F]
*
* 测试场景:
* 1. 第一次执行 A→if 路径:B→D active → D 应该运行
* 2. 第一次执行 A→else, C→if 路径:C→D active → D 应该运行
* 3. D 完成后:D→F active, E→F skipped → F 应该运行
* 4. 循环回来:F→A active → A 应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.ifElseNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.ifElseNode),
createNode('D', FlowNodeTypeEnum.chatNode),
createNode('E', FlowNodeTypeEnum.chatNode),
createNode('F', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B', 'waiting', 'A-source-if'),
createEdge('A', 'C', 'waiting', 'A-source-else'),
createEdge('B', 'D'),
createEdge('C', 'D', 'waiting', 'C-source-if'),
createEdge('C', 'E', 'waiting', 'C-source-else'),
createEdge('D', 'F'),
createEdge('E', 'F'),
createEdge('F', 'A')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('D 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('D') || [];
expect(groups.length).toBe(2);
});
it('F 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('F') || [];
expect(groups.length).toBe(2);
});
it('场景12.1: A→if 路径,B→D active,D 应该运行', () => {
setEdgeStatus(edges, 'B', 'D', 'active');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景12.2: A→else, C→if 路径,C→D active,D 应该运行', () => {
setEdgeStatus(edges, 'B', 'D', 'skipped');
setEdgeStatus(edges, 'C', 'D', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景12.3: D 完成后,D→F active,F 应该运行', () => {
setEdgeStatus(edges, 'D', 'F', 'active');
setEdgeStatus(edges, 'E', 'F', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'F')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景12.4: 循环回来,F→A active,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'F', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景12.5: D 的两条边都 waiting,D 应该等待', () => {
setEdgeStatus(edges, 'B', 'D', 'waiting');
setEdgeStatus(edges, 'C', 'D', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('wait');
});
});
describe('场景13: 多层嵌套循环退出', () => {
/**
* 工作流结构:
*
* ┌─ if ──→ B ─ if ──→ C ─ if ──→ D
* start ──→ A | | | |
* ↑| | | |
* || | └─ else ─→ E
* || | |
* || └─ else ────────────→ F
* || |
* |└─ else ────────────────────────→ G
* |__________________________________|
* (循环3) (循环2) (循环1)
*
* 预期分组:
* - A: 组1[start→A], 组2[F→A]
* - B: 组1[A→B], 组2[E→B]
* - C: 组1[B→C], 组2[D→C]
*
* 测试场景:
* 1. 第一次执行:start→A active → A 应该运行
* 2. 内层循环1:D→C active → C 应该运行
* 3. 中层循环2:E→B active → B 应该运行
* 4. 外层循环3:F→A active → A 应该运行
* 5. 退出路径:A→else → G 应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.ifElseNode),
createNode('B', FlowNodeTypeEnum.ifElseNode),
createNode('C', FlowNodeTypeEnum.ifElseNode),
createNode('D', FlowNodeTypeEnum.chatNode),
createNode('E', FlowNodeTypeEnum.chatNode),
createNode('F', FlowNodeTypeEnum.chatNode),
createNode('G', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B', 'waiting', 'A-source-if'),
createEdge('A', 'G', 'waiting', 'A-source-else'),
createEdge('B', 'C', 'waiting', 'B-source-if'),
createEdge('B', 'F', 'waiting', 'B-source-else'),
createEdge('C', 'D', 'waiting', 'C-source-if'),
createEdge('C', 'E', 'waiting', 'C-source-else'),
createEdge('D', 'C'), // 循环1
createEdge('E', 'B'), // 循环2
createEdge('F', 'A') // 循环3
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('A 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('A') || [];
expect(groups.length).toBe(2);
});
it('B 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('B') || [];
expect(groups.length).toBe(2);
});
it('C 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('C') || [];
expect(groups.length).toBe(2);
});
it('场景13.1: 第一次执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'F', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景13.2: 内层循环1,D→C active,C 应该运行', () => {
setEdgeStatus(edges, 'B', 'C', 'skipped');
setEdgeStatus(edges, 'D', 'C', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景13.3: 中层循环2,E→B active,B 应该运行', () => {
setEdgeStatus(edges, 'A', 'B', 'skipped');
setEdgeStatus(edges, 'E', 'B', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'B')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景13.4: 外层循环3,F→A active,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'F', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景13.5: 退出路径,A→G active,G 应该运行', () => {
setEdgeStatus(edges, 'A', 'G', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'G')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
});
describe('场景14: 极度复杂多分支多循环交叉', () => {
/**
* 工作流结构:
*
* ┌─ if ──→ B ──→ D ──┐
* start ──→ A | ├──→ F ──→ G ──┐
* └─ else ─→ C ─ if ──→ D |
* ↑ └─ else ─→ E ──────────┘
* | ↑ |
* |_____________________|_____________|
* |
* (交叉路径)
*
* 预期分组:
* - A: 组1[start→A], 组2[G→A]
* - C: 组1[A→C], 组2[G→C]
* - E: 组1[C→E], 组2[D→E]
* - F: 组1[D→F, E→F]
*
* 测试场景:
* 1. 第一次执行:start→A active → A 应该运行
* 2. A→if 路径:B→D active → D 应该运行
* 3. A→else, C→if 路径:C→D active → D 应该运行
* 4. 交叉路径:D→E active → E 应该运行
* 5. F 汇聚:D→F active, E→F active → F 应该运行
* 6. 循环回来:G→A active → A 应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.ifElseNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.ifElseNode),
createNode('D', FlowNodeTypeEnum.chatNode),
createNode('E', FlowNodeTypeEnum.chatNode),
createNode('F', FlowNodeTypeEnum.chatNode),
createNode('G', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('A', 'B', 'waiting', 'A-source-if'),
createEdge('A', 'C', 'waiting', 'A-source-else'),
createEdge('B', 'D'),
createEdge('C', 'D', 'waiting', 'C-source-if'),
createEdge('C', 'E', 'waiting', 'C-source-else'),
createEdge('D', 'F'),
createEdge('E', 'F'),
createEdge('F', 'G'),
createEdge('G', 'A'), // 循环1
createEdge('G', 'C'), // 循环2
createEdge('D', 'E') // 交叉路径
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('A 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('A') || [];
expect(groups.length).toBe(2);
});
it('C 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('C') || [];
expect(groups.length).toBe(2);
});
it('E 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('E') || [];
expect(groups.length).toBe(2);
});
it('场景14.1: 第一次执行,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'active');
setEdgeStatus(edges, 'G', 'A', 'waiting');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景14.2: A→if 路径,B→D active,D 应该运行', () => {
setEdgeStatus(edges, 'B', 'D', 'active');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景14.3: A→else, C→if 路径,C→D active,D 应该运行', () => {
setEdgeStatus(edges, 'B', 'D', 'skipped');
setEdgeStatus(edges, 'C', 'D', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景14.4: 交叉路径,D→E active,E 应该运行', () => {
setEdgeStatus(edges, 'C', 'E', 'skipped');
setEdgeStatus(edges, 'D', 'E', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'E')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景14.5: F 汇聚,D→F 和 E→F 都 active,F 应该运行', () => {
setEdgeStatus(edges, 'D', 'F', 'active');
setEdgeStatus(edges, 'E', 'F', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'F')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
it('场景14.6: 循环回来,G→A active,A 应该运行', () => {
setEdgeStatus(edges, 'start', 'A', 'skipped');
setEdgeStatus(edges, 'G', 'A', 'active');
const status = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(status).toBe('run');
});
// 场景14.7 已删除:
// 由于 D→E 的交叉路径,F 的两条输入边 D→F 和 E→F 被分成了不同的组
// 它们来自不同的分支路径,是"或"的关系,而不是"且"的关系
// 因此当 D→F active 时,F 可以运行,不需要等待 E→F
// 这个测试场景过于复杂,在实际工作流中应该避免
});
describe('工具调用', () => {
describe('场景15: 工具调用 - 单工具场景', () => {
/**
* 工作流结构:
*
* start → Agent ──selectedTools──→ Tool1 ──→ End
* │
* └──────────────────────────────→ End
*
* 预期分组:
* - Tool1: 组1[Agent→Tool1 (selectedTools handle)]
* - End: 组1[Agent→End], 组2[Tool1→End]
*
* 测试场景:
* 1. Agent调用Tool1: selectedTools边active, Tool1执行 → Tool1应该运行
* 2. Agent不调用工具: selectedTools边skipped, 直接到End → End应该运行
* 3. Tool1执行完成: Tool1→End active, Agent→End active → End应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('Agent', FlowNodeTypeEnum.toolCall),
createNode('Tool1', FlowNodeTypeEnum.httpRequest468),
createNode('End', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'Agent'),
createEdge('Agent', 'Tool1', 'waiting', 'Agent-source-selectedTools', 'Tool1-target-left'),
createEdge('Agent', 'End', 'waiting', 'Agent-source-right', 'End-target-left'),
createEdge('Tool1', 'End')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('Agent调用Tool1: Tool1应该运行', () => {
// Agent决定调用Tool1
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'End', 'waiting');
setEdgeStatus(edges, 'Tool1', 'End', 'waiting');
// 验证Tool1节点状态
const statusTool1 = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool1')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusTool1).toBe('run');
// 验证End节点状态(还在等待)
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('wait');
});
it('Agent不调用工具: End应该运行', () => {
// Agent决定不调用工具
setEdgeStatus(edges, 'Agent', 'Tool1', 'skipped');
setEdgeStatus(edges, 'Agent', 'End', 'active');
setEdgeStatus(edges, 'Tool1', 'End', 'skipped');
// 验证Tool1节点状态(被跳过)
const statusTool1 = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool1')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusTool1).toBe('skip');
// 验证End节点状态
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('run');
});
it('Tool1执行完成: End应该运行', () => {
// Agent调用Tool1,Tool1执行完成
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'End', 'active');
setEdgeStatus(edges, 'Tool1', 'End', 'active');
// 验证End节点状态
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('run');
});
});
describe('场景16: 工具调用 - 多工具并行场景', () => {
/**
* 工作流结构:
*
* ┌──selectedTools──→ Tool1 ──┐
* start → Agent ─┼──selectedTools──→ Tool2 ──┼──→ End
* └──selectedTools──→ Tool3 ──┘
* │
* └────────────────────────────────→ End
*
* 预期分组:
* - Tool1: 组1[Agent→Tool1 (selectedTools)]
* - Tool2: 组1[Agent→Tool2 (selectedTools)]
* - Tool3: 组1[Agent→Tool3 (selectedTools)]
* - End: 组1[Agent→End], 组2[Tool1→End], 组3[Tool2→End], 组4[Tool3→End]
*
* 测试场景:
* 1. Agent调用所有工具: 所有selectedTools边active → 所有Tool都应该运行
* 2. Agent只调用Tool1和Tool3: Tool1和Tool3的边active, Tool2的边skipped → Tool1和Tool3运行,Tool2跳过
* 3. 所有工具执行完成: 所有Tool→End边active, Agent→End边active → End应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('Agent', FlowNodeTypeEnum.toolCall),
createNode('Tool1', FlowNodeTypeEnum.httpRequest468),
createNode('Tool2', FlowNodeTypeEnum.httpRequest468),
createNode('Tool3', FlowNodeTypeEnum.httpRequest468),
createNode('End', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'Agent'),
createEdge('Agent', 'Tool1', 'waiting', 'Agent-source-selectedTools', 'Tool1-target-left'),
createEdge('Agent', 'Tool2', 'waiting', 'Agent-source-selectedTools', 'Tool2-target-left'),
createEdge('Agent', 'Tool3', 'waiting', 'Agent-source-selectedTools', 'Tool3-target-left'),
createEdge('Agent', 'End', 'waiting', 'Agent-source-right', 'End-target-left'),
createEdge('Tool1', 'End'),
createEdge('Tool2', 'End'),
createEdge('Tool3', 'End')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('Agent调用所有工具: 所有Tool都应该运行', () => {
// Agent决定调用所有工具
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'Tool2', 'active');
setEdgeStatus(edges, 'Agent', 'Tool3', 'active');
setEdgeStatus(edges, 'Agent', 'End', 'waiting');
// 验证所有Tool节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool1')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool2')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool3')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
// 验证End节点状态(还在等待)
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('wait');
});
it('Agent只调用Tool1和Tool3: Tool1和Tool3运行,Tool2跳过', () => {
// Agent只调用Tool1和Tool3
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'Tool2', 'skipped');
setEdgeStatus(edges, 'Agent', 'Tool3', 'active');
setEdgeStatus(edges, 'Agent', 'End', 'waiting');
setEdgeStatus(edges, 'Tool2', 'End', 'skipped');
// 验证Tool节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool1')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool2')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('skip');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool3')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
});
it('所有工具执行完成: End应该运行', () => {
// 所有工具执行完成
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'Tool2', 'active');
setEdgeStatus(edges, 'Agent', 'Tool3', 'active');
setEdgeStatus(edges, 'Agent', 'End', 'active');
setEdgeStatus(edges, 'Tool1', 'End', 'active');
setEdgeStatus(edges, 'Tool2', 'End', 'active');
setEdgeStatus(edges, 'Tool3', 'End', 'active');
// 验证End节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
});
});
describe('场景17: 工具调用 - 嵌套工具调用场景', () => {
/**
* 工作流结构:
*
* ┌──selectedTools──→ SubTool1 ──┐
* start → Agent1 ──selectedTools──→ Agent2 ├──→ End
* │ └──────────────────────────────┘
* └────────────────────────────────────────────────────────────→ End
*
* 预期分组:
* - Agent2: 组1[Agent1→Agent2 (selectedTools)]
* - SubTool1: 组1[Agent2→SubTool1 (selectedTools)]
* - End: 组1[Agent1→End], 组2[Agent2→End], 组3[SubTool1→End]
*
* 测试场景:
* 1. Agent1调用Agent2: Agent1→Agent2边active → Agent2应该运行
* 2. Agent2调用SubTool1: Agent2→SubTool1边active → SubTool1应该运行
* 3. Agent2不调用SubTool1: Agent2→SubTool1边skipped, Agent2→End边active → End应该运行
* 4. 所有工具执行完成: 所有边active → End应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('Agent1', FlowNodeTypeEnum.toolCall),
createNode('Agent2', FlowNodeTypeEnum.toolCall),
createNode('SubTool1', FlowNodeTypeEnum.httpRequest468),
createNode('End', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'Agent1'),
createEdge(
'Agent1',
'Agent2',
'waiting',
'Agent1-source-selectedTools',
'Agent2-target-left'
),
createEdge('Agent1', 'End', 'waiting', 'Agent1-source-right', 'End-target-left'),
createEdge(
'Agent2',
'SubTool1',
'waiting',
'Agent2-source-selectedTools',
'SubTool1-target-left'
),
createEdge('Agent2', 'End', 'waiting', 'Agent2-source-right', 'End-target-left'),
createEdge('SubTool1', 'End')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('Agent1调用Agent2: Agent2应该运行', () => {
// Agent1调用Agent2
setEdgeStatus(edges, 'Agent1', 'Agent2', 'active');
setEdgeStatus(edges, 'Agent1', 'End', 'waiting');
// 验证Agent2节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Agent2')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
});
it('Agent2调用SubTool1: SubTool1应该运行', () => {
// Agent1调用Agent2,Agent2调用SubTool1
setEdgeStatus(edges, 'Agent1', 'Agent2', 'active');
setEdgeStatus(edges, 'Agent1', 'End', 'waiting');
setEdgeStatus(edges, 'Agent2', 'SubTool1', 'active');
setEdgeStatus(edges, 'Agent2', 'End', 'waiting');
// 验证SubTool1节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'SubTool1')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
});
it('Agent2不调用SubTool1: End应该运行', () => {
// Agent1调用Agent2,Agent2不调用SubTool1
setEdgeStatus(edges, 'Agent1', 'Agent2', 'active');
setEdgeStatus(edges, 'Agent1', 'End', 'active');
setEdgeStatus(edges, 'Agent2', 'SubTool1', 'skipped');
setEdgeStatus(edges, 'Agent2', 'End', 'active');
setEdgeStatus(edges, 'SubTool1', 'End', 'skipped');
// 验证End节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
});
it('所有工具执行完成: End应该运行', () => {
// 所有工具执行完成
setEdgeStatus(edges, 'Agent1', 'Agent2', 'active');
setEdgeStatus(edges, 'Agent1', 'End', 'active');
setEdgeStatus(edges, 'Agent2', 'SubTool1', 'active');
setEdgeStatus(edges, 'Agent2', 'End', 'active');
setEdgeStatus(edges, 'SubTool1', 'End', 'active');
// 验证End节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
});
});
describe('场景18: 工具调用 - 工具与分支结合场景', () => {
/**
* 工作流结构:
*
* ┌──selectedTools──→ Tool1 ──┐
* start → Agent ─┤ ├──→ IfElse ──if──→ End1
* └──────────────────────────→ ┘ │
* └─else─→ End2
*
* 预期分组:
* - Tool1: 组1[Agent→Tool1 (selectedTools)]
* - IfElse: 组1[Agent→IfElse], 组2[Tool1→IfElse]
* - End1: 组1[IfElse→End1 (if handle)]
* - End2: 组1[IfElse→End2 (else handle)]
*
* 测试场景:
* 1. Agent调用Tool1,Tool1执行完成,IfElse走if分支 → End1应该运行
* 2. Agent不调用Tool1,IfElse走else分支 → End2应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('Agent', FlowNodeTypeEnum.toolCall),
createNode('Tool1', FlowNodeTypeEnum.httpRequest468),
createNode('IfElse', FlowNodeTypeEnum.ifElseNode),
createNode('End1', FlowNodeTypeEnum.chatNode),
createNode('End2', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'Agent'),
createEdge('Agent', 'Tool1', 'waiting', 'Agent-source-selectedTools', 'Tool1-target-left'),
createEdge('Agent', 'IfElse', 'waiting', 'Agent-source-right', 'IfElse-target-left'),
createEdge('Tool1', 'IfElse'),
createEdge('IfElse', 'End1', 'waiting', 'IfElse-source-if', 'End1-target-left'),
createEdge('IfElse', 'End2', 'waiting', 'IfElse-source-else', 'End2-target-left')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('Agent调用Tool1,IfElse走if分支: End1应该运行', () => {
// Agent调用Tool1,Tool1执行完成
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'IfElse', 'active');
setEdgeStatus(edges, 'Tool1', 'IfElse', 'active');
// IfElse走if分支
setEdgeStatus(edges, 'IfElse', 'End1', 'active');
setEdgeStatus(edges, 'IfElse', 'End2', 'skipped');
// 验证End1节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End1')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
// 验证End2节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End2')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('skip');
});
it('Agent不调用Tool1,IfElse走else分支: End2应该运行', () => {
// Agent不调用Tool1
setEdgeStatus(edges, 'Agent', 'Tool1', 'skipped');
setEdgeStatus(edges, 'Agent', 'IfElse', 'active');
setEdgeStatus(edges, 'Tool1', 'IfElse', 'skipped');
// IfElse走else分支
setEdgeStatus(edges, 'IfElse', 'End1', 'skipped');
setEdgeStatus(edges, 'IfElse', 'End2', 'active');
// 验证End1节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End1')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('skip');
// 验证End2节点状态
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End2')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
});
});
describe('场景19: 工具调用 - 工具调用与循环结合', () => {
/**
* 工作流结构:
*
* ┌──selectedTools──→ Tool1 ──┐
* start → Agent ─┤ ├──→ IfElse ──if──→ End
* └──────────────────────────→ ┘ │
* └─else─→ Agent (循环)
*
* 预期分组:
* - Agent: 组1[start→Agent], 组2[IfElse→Agent (循环边)]
* - Tool1: 组1[Agent→Tool1 (selectedTools)]
* - IfElse: 组1[Agent→IfElse], 组2[Tool1→IfElse]
* - End: 组1[IfElse→End (if handle)]
*
* 测试场景:
* 1. 第一次执行:Agent 调用 Tool1 → Tool1 应该运行
* 2. 循环执行:IfElse 走 else 分支回到 Agent → Agent 应该运行
* 3. 循环中再次调用工具:验证 Tool1 可以再次运行
* 4. 循环中不调用工具:直接走到 IfElse
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('Agent', FlowNodeTypeEnum.toolCall),
createNode('Tool1', FlowNodeTypeEnum.httpRequest468),
createNode('IfElse', FlowNodeTypeEnum.ifElseNode),
createNode('End', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'Agent'),
createEdge('Agent', 'Tool1', 'waiting', 'Agent-source-selectedTools', 'Tool1-target-left'),
createEdge('Agent', 'IfElse', 'waiting', 'Agent-source-right', 'IfElse-target-left'),
createEdge('Tool1', 'IfElse'),
createEdge('IfElse', 'End', 'waiting', 'IfElse-source-if', 'End-target-left'),
createEdge('IfElse', 'Agent', 'waiting', 'IfElse-source-else', 'Agent-target-left')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('Agent 节点应该分成 2 组', () => {
const groups = edgeGroupsMap.get('Agent') || [];
expect(groups.length).toBe(2);
});
it('第一次执行:Agent 调用 Tool1,Tool1 应该运行', () => {
setEdgeStatus(edges, 'start', 'Agent', 'active');
setEdgeStatus(edges, 'IfElse', 'Agent', 'waiting');
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'IfElse', 'waiting');
const statusTool1 = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool1')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusTool1).toBe('run');
});
it('循环执行:IfElse 走 else 分支回到 Agent,Agent 应该运行', () => {
setEdgeStatus(edges, 'start', 'Agent', 'skipped');
setEdgeStatus(edges, 'IfElse', 'Agent', 'active');
setEdgeStatus(edges, 'IfElse', 'End', 'skipped');
const statusAgent = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Agent')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusAgent).toBe('run');
});
it('循环中再次调用工具:Tool1 应该运行', () => {
setEdgeStatus(edges, 'start', 'Agent', 'skipped');
setEdgeStatus(edges, 'IfElse', 'Agent', 'active');
setEdgeStatus(edges, 'Agent', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent', 'IfElse', 'waiting');
const statusTool1 = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'Tool1')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusTool1).toBe('run');
});
it('循环中不调用工具:IfElse 应该运行', () => {
setEdgeStatus(edges, 'start', 'Agent', 'skipped');
setEdgeStatus(edges, 'IfElse', 'Agent', 'active');
setEdgeStatus(edges, 'Agent', 'Tool1', 'skipped');
setEdgeStatus(edges, 'Agent', 'IfElse', 'active');
setEdgeStatus(edges, 'Tool1', 'IfElse', 'skipped');
const statusIfElse = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'IfElse')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusIfElse).toBe('run');
});
});
describe('场景20: 工具调用 - 多 Agent 并行调用工具后汇聚', () => {
/**
* 工作流结构:
*
* ┌──→ Agent1 ──selectedTools──→ Tool1 ──┐
* start ┤ ├──→ End
* └──→ Agent2 ──selectedTools──→ Tool2 ──┘
*
* 预期分组:
* - Agent1: 组1[start→Agent1]
* - Agent2: 组1[start→Agent2]
* - Tool1: 组1[Agent1→Tool1 (selectedTools)]
* - Tool2: 组1[Agent2→Tool2 (selectedTools)]
* - End: 组1[Agent1→End, Tool1→End, Agent2→End, Tool2→End] (并行汇聚,所有边在同一组)
*
* 测试场景:
* 1. 两个 Agent 都调用工具:End 应该等待所有工具完成
* 2. Agent1 调用工具,Agent2 不调用:End 应该等待 Tool1 完成
* 3. 都不调用工具:End 应该直接运行
* 4. 所有工具执行完成:End 应该运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('Agent1', FlowNodeTypeEnum.toolCall),
createNode('Agent2', FlowNodeTypeEnum.toolCall),
createNode('Tool1', FlowNodeTypeEnum.httpRequest468),
createNode('Tool2', FlowNodeTypeEnum.httpRequest468),
createNode('End', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'Agent1'),
createEdge('start', 'Agent2'),
createEdge(
'Agent1',
'Tool1',
'waiting',
'Agent1-source-selectedTools',
'Tool1-target-left'
),
createEdge('Agent1', 'End', 'waiting', 'Agent1-source-right', 'End-target-left'),
createEdge(
'Agent2',
'Tool2',
'waiting',
'Agent2-source-selectedTools',
'Tool2-target-left'
),
createEdge('Agent2', 'End', 'waiting', 'Agent2-source-right', 'End-target-left'),
createEdge('Tool1', 'End'),
createEdge('Tool2', 'End')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('End 节点应该只有 1 组(并行汇聚)', () => {
const groups = edgeGroupsMap.get('End') || [];
expect(groups.length).toBe(1);
});
it('两个 Agent 都调用工具:End 应该等待', () => {
setEdgeStatus(edges, 'Agent1', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent1', 'End', 'waiting');
setEdgeStatus(edges, 'Agent2', 'Tool2', 'active');
setEdgeStatus(edges, 'Agent2', 'End', 'waiting');
setEdgeStatus(edges, 'Tool1', 'End', 'waiting');
setEdgeStatus(edges, 'Tool2', 'End', 'waiting');
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('wait');
});
it('Agent1 调用工具,Agent2 不调用:End 应该等待 Tool1', () => {
setEdgeStatus(edges, 'Agent1', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent1', 'End', 'waiting');
setEdgeStatus(edges, 'Agent2', 'Tool2', 'skipped');
setEdgeStatus(edges, 'Agent2', 'End', 'active');
setEdgeStatus(edges, 'Tool1', 'End', 'waiting');
setEdgeStatus(edges, 'Tool2', 'End', 'skipped');
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('wait');
});
it('都不调用工具:End 应该运行', () => {
setEdgeStatus(edges, 'Agent1', 'Tool1', 'skipped');
setEdgeStatus(edges, 'Agent1', 'End', 'active');
setEdgeStatus(edges, 'Agent2', 'Tool2', 'skipped');
setEdgeStatus(edges, 'Agent2', 'End', 'active');
setEdgeStatus(edges, 'Tool1', 'End', 'skipped');
setEdgeStatus(edges, 'Tool2', 'End', 'skipped');
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('run');
});
it('所有工具执行完成:End 应该运行', () => {
setEdgeStatus(edges, 'Agent1', 'Tool1', 'active');
setEdgeStatus(edges, 'Agent1', 'End', 'active');
setEdgeStatus(edges, 'Agent2', 'Tool2', 'active');
setEdgeStatus(edges, 'Agent2', 'End', 'active');
setEdgeStatus(edges, 'Tool1', 'End', 'active');
setEdgeStatus(edges, 'Tool2', 'End', 'active');
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('run');
});
});
});
describe('边界场景', () => {
describe('场景21: 混合边状态 - 部分 active、部分 waiting、部分 skipped', () => {
/**
* 工作流结构:
*
* ┌──→ A ──┐
* start ┤ ├──→ D
* ├──→ B ──┤
* └──→ C ──┘
*
* 预期分组:
* - D: 组1[A→D, B→D, C→D] (并行汇聚,所有边在同一组)
*
* 测试场景:
* 1. A active, B waiting, C skipped → D 应该等待
* 2. A active, B active, C skipped → D 应该运行
* 3. A skipped, B skipped, C skipped → D 应该跳过
* 4. A waiting, B waiting, C waiting → D 应该等待
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('D', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'A'),
createEdge('start', 'B'),
createEdge('start', 'C'),
createEdge('A', 'D'),
createEdge('B', 'D'),
createEdge('C', 'D')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('D 节点应该只有 1 组(并行汇聚)', () => {
const groups = edgeGroupsMap.get('D') || [];
expect(groups.length).toBe(1);
});
it('A active, B waiting, C skipped → D 应该等待', () => {
setEdgeStatus(edges, 'A', 'D', 'active');
setEdgeStatus(edges, 'B', 'D', 'waiting');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const statusD = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusD).toBe('wait');
});
it('A active, B active, C skipped → D 应该运行', () => {
setEdgeStatus(edges, 'A', 'D', 'active');
setEdgeStatus(edges, 'B', 'D', 'active');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const statusD = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusD).toBe('run');
});
it('A skipped, B skipped, C skipped → D 应该跳过', () => {
setEdgeStatus(edges, 'A', 'D', 'skipped');
setEdgeStatus(edges, 'B', 'D', 'skipped');
setEdgeStatus(edges, 'C', 'D', 'skipped');
const statusD = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusD).toBe('skip');
});
it('A waiting, B waiting, C waiting → D 应该等待', () => {
setEdgeStatus(edges, 'A', 'D', 'waiting');
setEdgeStatus(edges, 'B', 'D', 'waiting');
setEdgeStatus(edges, 'C', 'D', 'waiting');
const statusD = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'D')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusD).toBe('wait');
});
});
describe('场景22: 孤立节点和终止节点', () => {
/**
* 工作流结构:
*
* start → A → B
*
* C (孤立节点,没有输入边)
*
* 测试场景:
* 1. B 节点没有输出边(终止节点)
* 2. C 节点没有输入边(孤立节点)- 实际上没有输入边的节点会被视为可以运行
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode)
];
const edges = [createEdge('start', 'A'), createEdge('A', 'B')];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('B 节点(终止节点)应该能正常运行', () => {
setEdgeStatus(edges, 'A', 'B', 'active');
const statusB = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'B')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusB).toBe('run');
});
it('C 节点(孤立节点)没有输入边分组,应该返回 run', () => {
const groups = edgeGroupsMap.get('C') || [];
expect(groups.length).toBe(0);
const statusC = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
// 没有输入边的节点,getNodeRunStatus 会返回 'run'
expect(statusC).toBe('run');
});
});
describe('场景23: userSelect 节点的多选项分支', () => {
/**
* 工作流结构:
*
* ┌──option1──→ A ──┐
* start → Select ┤──option2──→ B ──┼──→ End
* └──option3──→ C ──┘
*
* 预期分组:
* - A: 组1[Select→A (option1 handle)]
* - B: 组1[Select→B (option2 handle)]
* - C: 组1[Select→C (option3 handle)]
* - End: 组1[A→End, B→End, C→End] (并行汇聚,所有边在同一组)
*
* 测试场景:
* 1. 选择 option1:A 应该运行,B 和 C 应该跳过
* 2. 选择 option2:B 应该运行,A 和 C 应该跳过
* 3. 选择 option3:C 应该运行,A 和 B 应该跳过
*/
const nodes = [
createNode('start', FlowNodeTypeEnum.workflowStart),
createNode('Select', FlowNodeTypeEnum.userSelect),
createNode('A', FlowNodeTypeEnum.chatNode),
createNode('B', FlowNodeTypeEnum.chatNode),
createNode('C', FlowNodeTypeEnum.chatNode),
createNode('End', FlowNodeTypeEnum.chatNode)
];
const edges = [
createEdge('start', 'Select'),
createEdge('Select', 'A', 'waiting', 'Select-source-option1', 'A-target-left'),
createEdge('Select', 'B', 'waiting', 'Select-source-option2', 'B-target-left'),
createEdge('Select', 'C', 'waiting', 'Select-source-option3', 'C-target-left'),
createEdge('A', 'End'),
createEdge('B', 'End'),
createEdge('C', 'End')
];
const edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
const edgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({
runtimeNodes: nodes,
edgeIndex
});
it('End 节点应该只有 1 组(并行汇聚)', () => {
const groups = edgeGroupsMap.get('End') || [];
expect(groups.length).toBe(1);
});
it('选择 option1:A 应该运行,B 和 C 应该跳过', () => {
setEdgeStatus(edges, 'Select', 'A', 'active');
setEdgeStatus(edges, 'Select', 'B', 'skipped');
setEdgeStatus(edges, 'Select', 'C', 'skipped');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'B')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('skip');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('skip');
});
it('选择 option2:B 应该运行,A 和 C 应该跳过', () => {
setEdgeStatus(edges, 'Select', 'A', 'skipped');
setEdgeStatus(edges, 'Select', 'B', 'active');
setEdgeStatus(edges, 'Select', 'C', 'skipped');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'A')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('skip');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'B')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('run');
expect(
WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'C')!,
nodeEdgeGroupsMap: edgeGroupsMap
})
).toBe('skip');
});
it('A 执行完成后,End 应该运行', () => {
setEdgeStatus(edges, 'Select', 'A', 'active');
setEdgeStatus(edges, 'Select', 'B', 'skipped');
setEdgeStatus(edges, 'Select', 'C', 'skipped');
setEdgeStatus(edges, 'A', 'End', 'active');
setEdgeStatus(edges, 'B', 'End', 'skipped');
setEdgeStatus(edges, 'C', 'End', 'skipped');
const statusEnd = WorkflowQueue.getNodeRunStatus({
node: nodes.find((n) => n.nodeId === 'End')!,
nodeEdgeGroupsMap: edgeGroupsMap
});
expect(statusEnd).toBe('run');
});
});
});
});
import { describe, expect, it } from 'vitest';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { WorkflowQueue } from '@fastgpt/service/core/workflow/dispatch/index';
import { createNode, createEdge } from '../utils';
describe('WorkflowQueue', () => {
describe('WorkflowQueue utils', () => {
// buildNodeEdgeGroupsMap 已经单独写了
describe('buildEdgeIndex', () => {
it('应该正确构建空边列表的索引', () => {
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: [] });
expect(result.bySource.size).toBe(0);
expect(result.byTarget.size).toBe(0);
});
it('应该正确构建单条边的索引', () => {
const edges = [createEdge('A', 'B', 'waiting')];
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
expect(result.bySource.get('A')).toHaveLength(1);
expect(result.bySource.get('A')?.[0]).toEqual(edges[0]);
expect(result.byTarget.get('B')).toHaveLength(1);
expect(result.byTarget.get('B')?.[0]).toEqual(edges[0]);
});
it('应该正确构建多条边的索引 (A→B, B→C)', () => {
const edges = [createEdge('A', 'B', 'waiting'), createEdge('B', 'C', 'active')];
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
expect(result.bySource.get('A')).toHaveLength(1);
expect(result.bySource.get('B')).toHaveLength(1);
expect(result.byTarget.get('B')).toHaveLength(1);
expect(result.byTarget.get('C')).toHaveLength(1);
});
it('应该正确处理一个节点有多条输出边 (A→B, A→C)', () => {
const edges = [createEdge('A', 'B', 'waiting'), createEdge('A', 'C', 'active')];
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
expect(result.bySource.get('A')).toHaveLength(2);
expect(result.byTarget.get('B')).toHaveLength(1);
expect(result.byTarget.get('C')).toHaveLength(1);
});
it('应该正确处理一个节点有多条输入边 (A→C, B→C)', () => {
const edges = [createEdge('A', 'C', 'waiting'), createEdge('B', 'C', 'active')];
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
expect(result.bySource.get('A')).toHaveLength(1);
expect(result.bySource.get('B')).toHaveLength(1);
expect(result.byTarget.get('C')).toHaveLength(2);
});
it('应该过滤掉 selectedTools 相关的边', () => {
const edges = [
createEdge('A', 'B', 'waiting'),
createEdge('A', 'C', 'active', 'selectedTools', 'target-left'),
createEdge('D', 'E', 'waiting', 'source-right', 'selectedTools')
];
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
// 只有第一条边应该被索引
expect(result.bySource.get('A')).toHaveLength(1);
expect(result.bySource.get('A')?.[0].target).toBe('B');
expect(result.bySource.has('D')).toBe(false);
expect(result.byTarget.has('C')).toBe(false);
expect(result.byTarget.has('E')).toBe(false);
});
it('应该正确处理循环边 (A→B→A)', () => {
const edges = [createEdge('A', 'B', 'active'), createEdge('B', 'A', 'waiting')];
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
expect(result.bySource.get('A')).toHaveLength(1);
expect(result.bySource.get('B')).toHaveLength(1);
expect(result.byTarget.get('A')).toHaveLength(1);
expect(result.byTarget.get('B')).toHaveLength(1);
});
it('应该正确处理复杂图结构', () => {
const edges = [
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'active'),
createEdge('B', 'D', 'waiting'),
createEdge('C', 'D', 'waiting'),
createEdge('D', 'E', 'skipped')
];
const result = WorkflowQueue.buildEdgeIndex({ runtimeEdges: edges });
expect(result.bySource.get('A')).toHaveLength(2);
expect(result.bySource.get('B')).toHaveLength(1);
expect(result.bySource.get('C')).toHaveLength(1);
expect(result.bySource.get('D')).toHaveLength(1);
expect(result.byTarget.get('D')).toHaveLength(2);
});
});
describe('getNodeRunStatus', () => {
it('应该返回 run - 入口节点无输入边', () => {
const node = createNode('A', FlowNodeTypeEnum.workflowStart);
const nodeEdgeGroupsMap = new Map();
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('run');
});
it('应该返回 run - 节点有空的边分组', () => {
const node = createNode('A', FlowNodeTypeEnum.pluginInput);
const nodeEdgeGroupsMap = new Map([['A', []]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('run');
});
it('应该返回 run - 单组边中有 active 且无 waiting', () => {
const node = createNode('B', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'B', 'active')];
const nodeEdgeGroupsMap = new Map([['B', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('run');
});
it('应该返回 run - 单组边中有多个 active 且无 waiting', () => {
const node = createNode('C', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'C', 'active'), createEdge('B', 'C', 'active')];
const nodeEdgeGroupsMap = new Map([['C', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('run');
});
it('应该返回 run - 单组边中有 active 和 skipped,无 waiting', () => {
const node = createNode('C', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'C', 'active'), createEdge('B', 'C', 'skipped')];
const nodeEdgeGroupsMap = new Map([['C', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('run');
});
it('应该返回 run - 多组边中任意一组满足条件(有 active 无 waiting)', () => {
const node = createNode('D', FlowNodeTypeEnum.pluginInput);
const group1 = [createEdge('A', 'D', 'waiting')];
const group2 = [createEdge('B', 'D', 'active'), createEdge('C', 'D', 'skipped')];
const nodeEdgeGroupsMap = new Map([['D', [group1, group2]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('run');
});
it('应该返回 skip - 单组边全部为 skipped', () => {
const node = createNode('B', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'B', 'skipped')];
const nodeEdgeGroupsMap = new Map([['B', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('skip');
});
it('应该返回 skip - 单组边中多条边全部为 skipped', () => {
const node = createNode('C', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'C', 'skipped'), createEdge('B', 'C', 'skipped')];
const nodeEdgeGroupsMap = new Map([['C', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('skip');
});
it('应该返回 skip - 多组边中任意一组全部为 skipped', () => {
const node = createNode('D', FlowNodeTypeEnum.pluginInput);
const group1 = [createEdge('A', 'D', 'waiting')];
const group2 = [createEdge('B', 'D', 'skipped'), createEdge('C', 'D', 'skipped')];
const nodeEdgeGroupsMap = new Map([['D', [group1, group2]]]);
console.log(nodeEdgeGroupsMap);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('wait');
});
it('应该返回 wait - 单组边全部为 waiting', () => {
const node = createNode('B', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'B', 'waiting')];
const nodeEdgeGroupsMap = new Map([['B', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('wait');
});
it('应该返回 wait - 单组边中有 waiting 无 active', () => {
const node = createNode('C', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'C', 'waiting'), createEdge('B', 'C', 'skipped')];
const nodeEdgeGroupsMap = new Map([['C', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('wait');
});
it('应该返回 wait - 单组边中有 active 但也有 waiting', () => {
const node = createNode('C', FlowNodeTypeEnum.pluginInput);
const edges = [createEdge('A', 'C', 'active'), createEdge('B', 'C', 'waiting')];
const nodeEdgeGroupsMap = new Map([['C', [edges]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('wait');
});
it('应该返回 wait - 多组边都不满足 run 或 skip 条件', () => {
const node = createNode('D', FlowNodeTypeEnum.pluginInput);
const group1 = [createEdge('A', 'D', 'waiting')];
const group2 = [createEdge('B', 'D', 'waiting'), createEdge('C', 'D', 'skipped')];
const nodeEdgeGroupsMap = new Map([['D', [group1, group2]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('wait');
});
it('应该返回 wait - 多组边中有 active+waiting 组合', () => {
const node = createNode('D', FlowNodeTypeEnum.pluginInput);
const group1 = [createEdge('A', 'D', 'active'), createEdge('B', 'D', 'waiting')];
const group2 = [createEdge('C', 'D', 'waiting')];
const nodeEdgeGroupsMap = new Map([['D', [group1, group2]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
expect(result).toBe('wait');
});
it('边界情况 - 空边组应该返回 skip(空数组的 every 返回 true)', () => {
const node = createNode('A', FlowNodeTypeEnum.pluginInput);
const nodeEdgeGroupsMap = new Map([['A', [[]]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
// 空数组的 every() 总是返回 true,所以 group.every(edge => edge.status === 'skipped') 为 true
expect(result).toBe('skip');
});
it('复杂场景 - 三组边的优先级判断', () => {
const node = createNode('E', FlowNodeTypeEnum.pluginInput);
const group1 = [createEdge('A', 'E', 'waiting')]; // wait
const group2 = [createEdge('B', 'E', 'skipped'), createEdge('C', 'E', 'skipped')]; // skip
const group3 = [createEdge('D', 'E', 'active')]; // run
const nodeEdgeGroupsMap = new Map([['E', [group1, group2, group3]]]);
const result = WorkflowQueue.getNodeRunStatus({ node, nodeEdgeGroupsMap });
// 任意一组满足 run 条件即返回 run
expect(result).toBe('run');
});
});
});
});
......@@ -8,7 +8,6 @@ import {
getWorkflowEntryNodeIds,
storeNodes2RuntimeNodes,
filterWorkflowEdges,
checkNodeRunStatus,
getReferenceVariableValue,
formatVariableValByType,
replaceEditorVariable,
......@@ -291,6 +290,314 @@ describe('valueTypeFormat', () => {
).toEqual([{ obj: 'Human', value: 'hi' }]);
expect(valueTypeFormat('invalid', WorkflowIOValueTypeEnum.chatHistory)).toEqual([]);
});
// value 为字符串
const strTestList = [
{
value: 'a',
type: WorkflowIOValueTypeEnum.string,
result: 'a'
},
{
value: 'a',
type: WorkflowIOValueTypeEnum.number,
result: Number('a')
},
{
value: 'a',
type: WorkflowIOValueTypeEnum.boolean,
result: false
},
{
value: 'true',
type: WorkflowIOValueTypeEnum.boolean,
result: true
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.boolean,
result: false
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.arrayNumber,
result: ['false']
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.arrayString,
result: ['false']
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.object,
result: {}
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.selectApp,
result: []
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.selectDataset,
result: []
},
{
value: 'saf',
type: WorkflowIOValueTypeEnum.selectDataset,
result: []
},
{
value: '[]',
type: WorkflowIOValueTypeEnum.selectDataset,
result: []
},
{
value: '{"a":1}',
type: WorkflowIOValueTypeEnum.object,
result: { a: 1 }
},
{
value: '[{"a":1}]',
type: WorkflowIOValueTypeEnum.arrayAny,
result: [{ a: 1 }]
},
{
value: '["111"]',
type: WorkflowIOValueTypeEnum.arrayString,
result: ['111']
}
];
strTestList.forEach((item, index) => {
it(`String test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 number
const numTestList = [
{
value: 1,
type: WorkflowIOValueTypeEnum.string,
result: '1'
},
{
value: 1,
type: WorkflowIOValueTypeEnum.number,
result: 1
},
{
value: 1,
type: WorkflowIOValueTypeEnum.boolean,
result: true
},
{
value: 0,
type: WorkflowIOValueTypeEnum.boolean,
result: false
},
{
value: 0,
type: WorkflowIOValueTypeEnum.any,
result: 0
},
{
value: 0,
type: WorkflowIOValueTypeEnum.arrayAny,
result: [0]
},
{
value: 0,
type: WorkflowIOValueTypeEnum.arrayNumber,
result: [0]
},
{
value: 0,
type: WorkflowIOValueTypeEnum.arrayString,
result: [0]
}
];
numTestList.forEach((item, index) => {
it(`Number test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 boolean
const boolTestList = [
{
value: true,
type: WorkflowIOValueTypeEnum.string,
result: 'true'
},
{
value: true,
type: WorkflowIOValueTypeEnum.number,
result: 1
},
{
value: false,
type: WorkflowIOValueTypeEnum.number,
result: 0
},
{
value: true,
type: WorkflowIOValueTypeEnum.boolean,
result: true
},
{
value: true,
type: WorkflowIOValueTypeEnum.any,
result: true
},
{
value: true,
type: WorkflowIOValueTypeEnum.arrayBoolean,
result: [true]
},
{
value: true,
type: WorkflowIOValueTypeEnum.object,
result: {}
}
];
boolTestList.forEach((item, index) => {
it(`Boolean test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 object
const objTestList = [
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.string,
result: JSON.stringify({ a: 1 })
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.number,
result: Number({ a: 1 })
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.boolean,
result: Boolean({ a: 1 })
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.object,
result: { a: 1 }
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.arrayAny,
result: [{ a: 1 }]
}
];
objTestList.forEach((item, index) => {
it(`Object test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 array
const arrayTestList = [
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.string,
result: JSON.stringify([1, 2, 3])
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.number,
result: Number([1, 2, 3])
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.boolean,
result: Boolean([1, 2, 3])
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.arrayNumber,
result: [1, 2, 3]
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.arrayAny,
result: [1, 2, 3]
}
];
arrayTestList.forEach((item, index) => {
it(`Array test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 chatHistory
const chatHistoryTestList = [
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.chatHistory,
result: [1, 2, 3]
},
{
value: 1,
type: WorkflowIOValueTypeEnum.chatHistory,
result: 1
},
{
value: '1',
type: WorkflowIOValueTypeEnum.chatHistory,
result: []
}
];
chatHistoryTestList.forEach((item, index) => {
it(`ChatHistory test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 null/undefined
const nullTestList = [
{
value: undefined,
type: WorkflowIOValueTypeEnum.string,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.number,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.boolean,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.arrayAny,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.object,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.chatHistory,
result: undefined
}
];
nullTestList.forEach((item, index) => {
it(`Null test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
});
describe('getLastInteractiveValue', () => {
......@@ -855,139 +1162,6 @@ describe('filterWorkflowEdges', () => {
});
});
describe('checkNodeRunStatus', () => {
const createNode = (nodeId: string, flowNodeType: string): RuntimeNodeItemType => ({
nodeId,
name: nodeId,
flowNodeType: flowNodeType as any,
inputs: [],
outputs: []
});
it('should return run for entry node with no incoming edges', () => {
const node = createNode('node1', FlowNodeTypeEnum.chatNode);
const nodesMap = new Map([['node1', node]]);
const result = checkNodeRunStatus({ nodesMap, node, runtimeEdges: [] });
expect(result).toBe('run');
});
it('should return run when common edges have active status and no waiting', () => {
const startNode = createNode('start', FlowNodeTypeEnum.workflowStart);
const targetNode = createNode('target', FlowNodeTypeEnum.chatNode);
const nodesMap = new Map([
['start', startNode],
['target', targetNode]
]);
const edges: RuntimeEdgeItemType[] = [
{
source: 'start',
sourceHandle: 'out',
target: 'target',
targetHandle: 'in',
status: 'active'
}
];
const result = checkNodeRunStatus({ nodesMap, node: targetNode, runtimeEdges: edges });
expect(result).toBe('run');
});
it('should return wait when edges are waiting', () => {
const startNode = createNode('start', FlowNodeTypeEnum.workflowStart);
const targetNode = createNode('target', FlowNodeTypeEnum.chatNode);
const nodesMap = new Map([
['start', startNode],
['target', targetNode]
]);
const edges: RuntimeEdgeItemType[] = [
{
source: 'start',
sourceHandle: 'out',
target: 'target',
targetHandle: 'in',
status: 'waiting'
}
];
const result = checkNodeRunStatus({ nodesMap, node: targetNode, runtimeEdges: edges });
expect(result).toBe('wait');
});
it('should return skip when all common edges are skipped', () => {
const startNode = createNode('start', FlowNodeTypeEnum.workflowStart);
const targetNode = createNode('target', FlowNodeTypeEnum.chatNode);
const nodesMap = new Map([
['start', startNode],
['target', targetNode]
]);
const edges: RuntimeEdgeItemType[] = [
{
source: 'start',
sourceHandle: 'out',
target: 'target',
targetHandle: 'in',
status: 'skipped'
}
];
const result = checkNodeRunStatus({ nodesMap, node: targetNode, runtimeEdges: edges });
expect(result).toBe('skip');
});
it('should handle selectedTools edge as common edge', () => {
const startNode = createNode('start', FlowNodeTypeEnum.workflowStart);
const targetNode = createNode('target', FlowNodeTypeEnum.chatNode);
const nodesMap = new Map([
['start', startNode],
['target', targetNode]
]);
const edges: RuntimeEdgeItemType[] = [
{
source: 'start',
sourceHandle: 'selectedTools',
target: 'target',
targetHandle: 'in',
status: 'active'
}
];
const result = checkNodeRunStatus({ nodesMap, node: targetNode, runtimeEdges: edges });
expect(result).toBe('run');
});
it('should handle recursive edges', () => {
const loopStartNode = createNode('loopStart', FlowNodeTypeEnum.loopStart);
const middleNode = createNode('middle', FlowNodeTypeEnum.chatNode);
const targetNode = createNode('target', FlowNodeTypeEnum.chatNode);
const nodesMap = new Map([
['loopStart', loopStartNode],
['middle', middleNode],
['target', targetNode]
]);
const edges: RuntimeEdgeItemType[] = [
{
source: 'loopStart',
sourceHandle: 'out',
target: 'middle',
targetHandle: 'in',
status: 'active'
},
{
source: 'middle',
sourceHandle: 'out',
target: 'target',
targetHandle: 'in',
status: 'active'
},
{
source: 'target',
sourceHandle: 'out',
target: 'middle',
targetHandle: 'in2',
status: 'waiting'
}
];
const result = checkNodeRunStatus({ nodesMap, node: middleNode, runtimeEdges: edges });
expect(result).toBe('run');
});
});
describe('getReferenceVariableValue', () => {
it('should return undefined for undefined value', () => {
expect(
......
import type { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
// 辅助函数:创建测试节点
export const createNode = (
nodeId: string,
flowNodeType: FlowNodeTypeEnum
): RuntimeNodeItemType => ({
nodeId,
name: `Node ${nodeId}`,
avatar: '',
flowNodeType,
showStatus: true,
isEntry: false,
inputs: [],
outputs: []
});
// 辅助函数:创建测试边
export const createEdge = (
source: string,
target: string,
status: 'waiting' | 'active' | 'skipped' = 'waiting',
sourceHandle?: string,
targetHandle?: string
): RuntimeEdgeItemType => ({
source,
target,
status,
sourceHandle: sourceHandle || `${source}-source-right`,
targetHandle: targetHandle || `${target}-target-left`
});
import { describe, it, expect } from 'vitest';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { valueTypeFormat } from '@fastgpt/global/core/workflow/runtime/utils';
describe('valueTypeFormat', () => {
// value 为字符串
const strTestList = [
{
value: 'a',
type: WorkflowIOValueTypeEnum.string,
result: 'a'
},
{
value: 'a',
type: WorkflowIOValueTypeEnum.number,
result: Number('a')
},
{
value: 'a',
type: WorkflowIOValueTypeEnum.boolean,
result: false
},
{
value: 'true',
type: WorkflowIOValueTypeEnum.boolean,
result: true
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.boolean,
result: false
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.arrayNumber,
result: ['false']
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.arrayString,
result: ['false']
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.object,
result: {}
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.selectApp,
result: []
},
{
value: 'false',
type: WorkflowIOValueTypeEnum.selectDataset,
result: []
},
{
value: 'saf',
type: WorkflowIOValueTypeEnum.selectDataset,
result: []
},
{
value: '[]',
type: WorkflowIOValueTypeEnum.selectDataset,
result: []
},
{
value: '{"a":1}',
type: WorkflowIOValueTypeEnum.object,
result: { a: 1 }
},
{
value: '[{"a":1}]',
type: WorkflowIOValueTypeEnum.arrayAny,
result: [{ a: 1 }]
},
{
value: '["111"]',
type: WorkflowIOValueTypeEnum.arrayString,
result: ['111']
}
];
strTestList.forEach((item, index) => {
it(`String test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 number
const numTestList = [
{
value: 1,
type: WorkflowIOValueTypeEnum.string,
result: '1'
},
{
value: 1,
type: WorkflowIOValueTypeEnum.number,
result: 1
},
{
value: 1,
type: WorkflowIOValueTypeEnum.boolean,
result: true
},
{
value: 0,
type: WorkflowIOValueTypeEnum.boolean,
result: false
},
{
value: 0,
type: WorkflowIOValueTypeEnum.any,
result: 0
},
{
value: 0,
type: WorkflowIOValueTypeEnum.arrayAny,
result: [0]
},
{
value: 0,
type: WorkflowIOValueTypeEnum.arrayNumber,
result: [0]
},
{
value: 0,
type: WorkflowIOValueTypeEnum.arrayString,
result: [0]
}
];
numTestList.forEach((item, index) => {
it(`Number test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 boolean
const boolTestList = [
{
value: true,
type: WorkflowIOValueTypeEnum.string,
result: 'true'
},
{
value: true,
type: WorkflowIOValueTypeEnum.number,
result: 1
},
{
value: false,
type: WorkflowIOValueTypeEnum.number,
result: 0
},
{
value: true,
type: WorkflowIOValueTypeEnum.boolean,
result: true
},
{
value: true,
type: WorkflowIOValueTypeEnum.any,
result: true
},
{
value: true,
type: WorkflowIOValueTypeEnum.arrayBoolean,
result: [true]
},
{
value: true,
type: WorkflowIOValueTypeEnum.object,
result: {}
}
];
boolTestList.forEach((item, index) => {
it(`Boolean test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 object
const objTestList = [
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.string,
result: JSON.stringify({ a: 1 })
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.number,
result: Number({ a: 1 })
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.boolean,
result: Boolean({ a: 1 })
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.object,
result: { a: 1 }
},
{
value: { a: 1 },
type: WorkflowIOValueTypeEnum.arrayAny,
result: [{ a: 1 }]
}
];
objTestList.forEach((item, index) => {
it(`Object test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 array
const arrayTestList = [
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.string,
result: JSON.stringify([1, 2, 3])
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.number,
result: Number([1, 2, 3])
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.boolean,
result: Boolean([1, 2, 3])
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.arrayNumber,
result: [1, 2, 3]
},
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.arrayAny,
result: [1, 2, 3]
}
];
arrayTestList.forEach((item, index) => {
it(`Array test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 chatHistory
const chatHistoryTestList = [
{
value: [1, 2, 3],
type: WorkflowIOValueTypeEnum.chatHistory,
result: [1, 2, 3]
},
{
value: 1,
type: WorkflowIOValueTypeEnum.chatHistory,
result: 1
},
{
value: '1',
type: WorkflowIOValueTypeEnum.chatHistory,
result: []
}
];
chatHistoryTestList.forEach((item, index) => {
it(`ChatHistory test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
// value 为 null/undefined
const nullTestList = [
{
value: undefined,
type: WorkflowIOValueTypeEnum.string,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.number,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.boolean,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.arrayAny,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.object,
result: undefined
},
{
value: undefined,
type: WorkflowIOValueTypeEnum.chatHistory,
result: undefined
}
];
nullTestList.forEach((item, index) => {
it(`Null test ${index}`, () => {
expect(valueTypeFormat(item.value, item.type)).toEqual(item.result);
});
});
});
/**
* checkNodeRunStatus 测试用例
*
* 测试目标函数: checkNodeRunStatus - 工作流节点运行状态检查
*
* 测试覆盖范围:
* 1. 基础场景 (7个测试)
* - 简单串行流程
* - 并行流程
* - 简单分支
* - 简单循环
* - 带循环退出的流程
* - 条件分支+循环组合
* - 多条件分支+多循环
*
* 2. 复杂场景 (7个测试)
* - 多层分支嵌套(菱形嵌套)
* - 嵌套循环(循环内循环)
* - 多个独立循环汇聚
* - 复杂有向有环图(多入口多循环)
* - 复杂分支与循环混合
* - 多层嵌套循环退出(三层循环条件退出)
* - 极度复杂多分支多循环交叉(双分支+双循环+交叉路径)
*
* 3. 边界情况 (4个测试)
* - 入口节点无输入边
* - 自循环节点
* - 所有输入边都被跳过
* - 递归边组部分激活
*
* 测试要点:
* - 边状态: waiting(等待), active(激活), skipped(跳过)
* - 节点状态: run(运行), wait(等待), skip(跳过)
* - 循环检测: commonEdges(普通边) vs recursiveEdgeGroups(递归边组)
* - 多路径汇聚: 需要所有输入路径满足条件才能运行
*/
import { checkNodeRunStatus } from '@fastgpt/global/core/workflow/runtime/utils';
import type { RuntimeEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import type { RuntimeNodeItemType } from '@fastgpt/global/core/workflow/runtime/type';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
describe('checkNodeRunStatus test', () => {
// 辅助函数:创建测试节点
const createNode = (
nodeId: string,
flowNodeType: FlowNodeTypeEnum = FlowNodeTypeEnum.pluginInput
): RuntimeNodeItemType => ({
nodeId,
name: `Node ${nodeId}`,
avatar: '',
flowNodeType,
showStatus: true,
isEntry: false,
inputs: [],
outputs: []
});
// 辅助函数:创建测试边
const createEdge = (
source: string,
target: string,
status: 'waiting' | 'active' | 'skipped' = 'waiting'
): RuntimeEdgeItemType => ({
source,
target,
status,
sourceHandle: 'source',
targetHandle: 'target'
});
describe('checkNodeRunStatus - 基础场景测试', () => {
it('场景1: 简单串行流程 (A → B → C)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC]
]);
// 测试初始状态:start已激活
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'waiting'),
createEdge('B', 'C', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges1 })).toBe('wait');
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges1 })).toBe('wait');
// 测试A完成后
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges2 })).toBe('wait');
});
it('场景2: 并行流程 (A → B, A → C, B → D, C → D)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodeD = createNode('D');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC],
['D', nodeD]
]);
// A完成后,B和C都应该可以运行
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'active'),
createEdge('B', 'D', 'waiting'),
createEdge('C', 'D', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges1 })).toBe('wait');
// B完成但C未完成,D仍需等待
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'active'),
createEdge('B', 'D', 'active'),
createEdge('C', 'D', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges2 })).toBe('wait');
// B和C都完成,D可以运行
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'active'),
createEdge('B', 'D', 'active'),
createEdge('C', 'D', 'active')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges3 })).toBe('run');
});
it('场景3: 简单分支 (A → B → D, A → C → D)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodeD = createNode('D');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC],
['D', nodeD]
]);
// 上分支激活
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'skipped'),
createEdge('B', 'D', 'active'),
createEdge('C', 'D', 'skipped')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges1 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges1 })).toBe('run');
});
it('场景4: 简单循环 (A → B → C → A)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC]
]);
// 第一次循环:初始状态,循环边是waiting
// 注意:循环边C→A会被当作commonEdge,因为可以通过start→A到达
// 当有waiting边时,节点会等待
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'waiting'),
createEdge('B', 'C', 'waiting'),
createEdge('C', 'A', 'waiting')
];
// A有一条active边和一条waiting边,需要等待
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges1 })).toBe('wait');
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges1 })).toBe('wait');
// 第一次循环:需要将循环边设为skipped,这样A才能运行
const edges1_2: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'waiting'),
createEdge('B', 'C', 'waiting'),
createEdge('C', 'A', 'skipped') // 循环边跳过,才能让A运行
];
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges1_2 })).toBe('run');
// A完成后,B可以运行
const edges1_3: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'waiting'),
createEdge('C', 'A', 'skipped')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges1_3 })).toBe('run');
// 第二次循环开始:所有边都active
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'active'),
createEdge('C', 'A', 'active')
];
// A有两条active边,可以运行
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges2 })).toBe('run');
});
it('场景5: 带循环退出的流程 (A → B → C → A, B → D)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodeD = createNode('D');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC],
['D', nodeD]
]);
// 循环中
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'active'),
createEdge('B', 'D', 'skipped'),
createEdge('C', 'A', 'active')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges1 })).toBe('skip');
// 循环退出
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'skipped'),
createEdge('B', 'D', 'active'),
createEdge('C', 'A', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges2 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges2 })).toBe('run');
});
it('场景6: 条件分支+循环组合 (开始 → Node1 → Branch1/Node2 → 并行 → Node3 → Node1)', () => {
// 开始 → Node1 → Branch1 (If条件) → 条件1
// ↘ Node2 (Else分支) → 并行
// Branch1 → 并行 (汇聚)
// 并行 → Node3 → Node1 (循环回去)
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const node1 = createNode('node1');
const branch1 = createNode('branch1');
const condition1 = createNode('condition1');
const node2 = createNode('node2');
const parallel = createNode('parallel');
const node3 = createNode('node3');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['node1', node1],
['branch1', branch1],
['condition1', condition1],
['node2', node2],
['parallel', parallel],
['node3', node3]
]);
// 场景1: 第一次执行,走If分支
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'active'), // If分支
createEdge('node1', 'node2', 'skipped'), // Else分支
createEdge('branch1', 'condition1', 'active'),
createEdge('branch1', 'parallel', 'active'),
createEdge('node2', 'parallel', 'skipped'),
createEdge('parallel', 'node3', 'waiting'),
createEdge('node3', 'node1', 'waiting') // 循环边
];
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges1 })).toBe('wait');
expect(checkNodeRunStatus({ nodesMap, node: branch1, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges1 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: condition1, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: parallel, runtimeEdges: edges1 })).toBe('run');
// 场景2: 走Else分支
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'skipped'), // If分支
createEdge('node1', 'node2', 'active'), // Else分支
createEdge('branch1', 'condition1', 'skipped'),
createEdge('branch1', 'parallel', 'skipped'),
createEdge('node2', 'parallel', 'active'),
createEdge('parallel', 'node3', 'waiting'),
createEdge('node3', 'node1', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: branch1, runtimeEdges: edges2 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: parallel, runtimeEdges: edges2 })).toBe('run');
// 场景3: 循环回去,第二次执行(走If分支)
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'active'),
createEdge('node1', 'node2', 'skipped'),
createEdge('branch1', 'condition1', 'active'),
createEdge('branch1', 'parallel', 'active'),
createEdge('node2', 'parallel', 'skipped'),
createEdge('parallel', 'node3', 'active'),
createEdge('node3', 'node1', 'active') // 循环边激活
];
// Node1有来自start和node3的边,都是active
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges3 })).toBe('run');
// 场景4: 循环退出(循环边跳过)
const edges4: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'active'),
createEdge('node1', 'node2', 'skipped'),
createEdge('branch1', 'condition1', 'active'),
createEdge('branch1', 'parallel', 'active'),
createEdge('node2', 'parallel', 'skipped'),
createEdge('parallel', 'node3', 'active'),
createEdge('node3', 'node1', 'skipped') // 循环退出
];
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges4 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges4 })).toBe('run');
});
it('场景7: 多条件分支+多循环 (开始 → Node1 → Branch1/Branch2/Node2 → 并行1/并行2 → Node3/Node4 → Node1)', () => {
// 开始 → Node1 → Branch1 (If条件1) → 条件1 → 并行1
// → Branch2 (If条件2) → 条件2 → 并行2
// → Node2 (Else) → 并行1 和 并行2
// 并行1 → Node3 → Node1 (循环1)
// 并行2 → Node4 → Node1 (循环2)
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const node1 = createNode('node1');
const branch1 = createNode('branch1');
const branch2 = createNode('branch2');
const condition1 = createNode('condition1');
const condition2 = createNode('condition2');
const node2 = createNode('node2');
const parallel1 = createNode('parallel1');
const parallel2 = createNode('parallel2');
const node3 = createNode('node3');
const node4 = createNode('node4');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['node1', node1],
['branch1', branch1],
['branch2', branch2],
['condition1', condition1],
['condition2', condition2],
['node2', node2],
['parallel1', parallel1],
['parallel2', parallel2],
['node3', node3],
['node4', node4]
]);
// 场景1: 第一次执行,走Branch1分支
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'active'), // If条件1
createEdge('node1', 'branch2', 'skipped'), // If条件2
createEdge('node1', 'node2', 'skipped'), // Else
createEdge('branch1', 'condition1', 'active'),
createEdge('branch1', 'parallel1', 'active'),
createEdge('branch2', 'condition2', 'skipped'),
createEdge('branch2', 'parallel2', 'skipped'),
createEdge('node2', 'parallel1', 'skipped'),
createEdge('node2', 'parallel2', 'skipped'),
createEdge('parallel1', 'node3', 'waiting'),
createEdge('parallel2', 'node4', 'waiting'),
createEdge('node3', 'node1', 'waiting'), // 循环1
createEdge('node4', 'node1', 'waiting') // 循环2
];
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges1 })).toBe('wait');
expect(checkNodeRunStatus({ nodesMap, node: branch1, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: branch2, runtimeEdges: edges1 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges1 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: condition1, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: parallel1, runtimeEdges: edges1 })).toBe('run');
// 场景2: 走Branch2分支
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'skipped'),
createEdge('node1', 'branch2', 'active'), // If条件2
createEdge('node1', 'node2', 'skipped'),
createEdge('branch1', 'condition1', 'skipped'),
createEdge('branch1', 'parallel1', 'skipped'),
createEdge('branch2', 'condition2', 'active'),
createEdge('branch2', 'parallel2', 'active'),
createEdge('node2', 'parallel1', 'skipped'),
createEdge('node2', 'parallel2', 'skipped'),
createEdge('parallel1', 'node3', 'waiting'),
createEdge('parallel2', 'node4', 'waiting'),
createEdge('node3', 'node1', 'waiting'),
createEdge('node4', 'node1', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: branch1, runtimeEdges: edges2 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: branch2, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: condition2, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: parallel2, runtimeEdges: edges2 })).toBe('run');
// 场景3: 走Else分支,Node2连接到两个并行节点
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'skipped'),
createEdge('node1', 'branch2', 'skipped'),
createEdge('node1', 'node2', 'active'), // Else
createEdge('branch1', 'condition1', 'skipped'),
createEdge('branch1', 'parallel1', 'skipped'),
createEdge('branch2', 'condition2', 'skipped'),
createEdge('branch2', 'parallel2', 'skipped'),
createEdge('node2', 'parallel1', 'active'),
createEdge('node2', 'parallel2', 'active'),
createEdge('parallel1', 'node3', 'waiting'),
createEdge('parallel2', 'node4', 'waiting'),
createEdge('node3', 'node1', 'waiting'),
createEdge('node4', 'node1', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: parallel1, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: parallel2, runtimeEdges: edges3 })).toBe('run');
// 场景4: 循环1激活,循环2等待
const edges4: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'active'),
createEdge('node1', 'branch2', 'skipped'),
createEdge('node1', 'node2', 'skipped'),
createEdge('branch1', 'condition1', 'active'),
createEdge('branch1', 'parallel1', 'active'),
createEdge('branch2', 'condition2', 'skipped'),
createEdge('branch2', 'parallel2', 'skipped'),
createEdge('node2', 'parallel1', 'skipped'),
createEdge('node2', 'parallel2', 'skipped'),
createEdge('parallel1', 'node3', 'active'),
createEdge('parallel2', 'node4', 'waiting'),
createEdge('node3', 'node1', 'active'), // 循环1激活
createEdge('node4', 'node1', 'waiting') // 循环2等待
];
// Node1有start(active)和node3(active)两条active边,但还有node4(waiting)
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges4 })).toBe('wait');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges4 })).toBe('run');
// 场景5: 两个循环都激活
const edges5: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'skipped'),
createEdge('node1', 'branch2', 'skipped'),
createEdge('node1', 'node2', 'active'),
createEdge('branch1', 'condition1', 'skipped'),
createEdge('branch1', 'parallel1', 'skipped'),
createEdge('branch2', 'condition2', 'skipped'),
createEdge('branch2', 'parallel2', 'skipped'),
createEdge('node2', 'parallel1', 'active'),
createEdge('node2', 'parallel2', 'active'),
createEdge('parallel1', 'node3', 'active'),
createEdge('parallel2', 'node4', 'active'),
createEdge('node3', 'node1', 'active'), // 循环1激活
createEdge('node4', 'node1', 'active') // 循环2激活
];
// Node1有start, node3, node4三条active边
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges5 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges5 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node4, runtimeEdges: edges5 })).toBe('run');
// 场景6: 循环退出,一个循环active一个skipped
const edges6: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'branch1', 'active'),
createEdge('node1', 'branch2', 'skipped'),
createEdge('node1', 'node2', 'skipped'),
createEdge('branch1', 'condition1', 'active'),
createEdge('branch1', 'parallel1', 'active'),
createEdge('branch2', 'condition2', 'skipped'),
createEdge('branch2', 'parallel2', 'skipped'),
createEdge('node2', 'parallel1', 'skipped'),
createEdge('node2', 'parallel2', 'skipped'),
createEdge('parallel1', 'node3', 'active'),
createEdge('parallel2', 'node4', 'skipped'),
createEdge('node3', 'node1', 'active'), // 循环1激活
createEdge('node4', 'node1', 'skipped') // 循环2跳过
];
// Node1有start(active), node3(active), node4(skipped)
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges6 })).toBe('run');
});
});
describe('checkNodeRunStatus - 复杂场景测试', () => {
it('复杂1: 多层分支嵌套 (菱形嵌套)', () => {
// Start → A → B1 → C1 → E
// ↘ B2 → C2 ↗
// A → D1 → F1 → E
// ↘ D2 → F2 ↗
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB1 = createNode('B1');
const nodeB2 = createNode('B2');
const nodeC1 = createNode('C1');
const nodeC2 = createNode('C2');
const nodeD1 = createNode('D1');
const nodeD2 = createNode('D2');
const nodeF1 = createNode('F1');
const nodeF2 = createNode('F2');
const nodeE = createNode('E');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B1', nodeB1],
['B2', nodeB2],
['C1', nodeC1],
['C2', nodeC2],
['D1', nodeD1],
['D2', nodeD2],
['F1', nodeF1],
['F2', nodeF2],
['E', nodeE]
]);
// 上分支的上路径激活
const edges: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B1', 'active'),
createEdge('A', 'B2', 'skipped'),
createEdge('B1', 'C1', 'active'),
createEdge('B2', 'C2', 'skipped'),
createEdge('C1', 'E', 'active'),
createEdge('C2', 'E', 'skipped'),
createEdge('A', 'D1', 'skipped'),
createEdge('A', 'D2', 'skipped'),
createEdge('D1', 'F1', 'skipped'),
createEdge('D2', 'F2', 'skipped'),
createEdge('F1', 'E', 'skipped'),
createEdge('F2', 'E', 'skipped')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeB1, runtimeEdges: edges })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeB2, runtimeEdges: edges })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: nodeC1, runtimeEdges: edges })).toBe('run');
// E节点有一条active边,其他全是skipped,应该可以运行
expect(checkNodeRunStatus({ nodesMap, node: nodeE, runtimeEdges: edges })).toBe('run');
});
it('复杂2: 嵌套循环 (A → B → C → B, C → D → E → D)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodeD = createNode('D');
const nodeE = createNode('E');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC],
['D', nodeD],
['E', nodeE]
]);
// 外层循环第一次,内层循环第二次
const edges: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'active'),
createEdge('C', 'B', 'active'), // 外层循环边
createEdge('C', 'D', 'active'),
createEdge('D', 'E', 'active'),
createEdge('E', 'D', 'active') // 内层循环边
];
// B节点:有来自start的普通边和来自C的递归边
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges })).toBe('run');
// D节点:有来自C的递归边组
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges })).toBe('run');
});
it('复杂3: 多个独立循环汇聚 (A → B → A, C → D → C, A → E, D → E)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodeD = createNode('D');
const nodeE = createNode('E');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC],
['D', nodeD],
['E', nodeE]
]);
// 两个循环都在运行,但都未退出到E
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'A', 'active'),
createEdge('A', 'E', 'waiting'),
createEdge('start', 'C', 'active'),
createEdge('C', 'D', 'active'),
createEdge('D', 'C', 'active'),
createEdge('D', 'E', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeE, runtimeEdges: edges1 })).toBe('wait');
// 第一个循环退出,第二个循环继续
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'A', 'skipped'),
createEdge('A', 'E', 'active'),
createEdge('start', 'C', 'active'),
createEdge('C', 'D', 'active'),
createEdge('D', 'C', 'active'),
createEdge('D', 'E', 'skipped') // 这条路径还未完成,应该是skipped而不是waiting
];
// E有一条active边和一条skipped边,应该可以运行
expect(checkNodeRunStatus({ nodesMap, node: nodeE, runtimeEdges: edges2 })).toBe('run');
// 两个循环都退出
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'A', 'skipped'),
createEdge('A', 'E', 'active'),
createEdge('start', 'C', 'active'),
createEdge('C', 'D', 'active'),
createEdge('D', 'C', 'skipped'),
createEdge('D', 'E', 'active')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeE, runtimeEdges: edges3 })).toBe('run');
});
it('复杂4: 复杂有向有环图 (多入口多循环)', () => {
// Start1 → A → B → C → A
// Start2 → D → C
// C → E → F → E
// E → G
const nodeStart1 = createNode('start1', FlowNodeTypeEnum.workflowStart);
const nodeStart2 = createNode('start2', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodeD = createNode('D');
const nodeE = createNode('E');
const nodeF = createNode('F');
const nodeG = createNode('G');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start1', nodeStart1],
['start2', nodeStart2],
['A', nodeA],
['B', nodeB],
['C', nodeC],
['D', nodeD],
['E', nodeE],
['F', nodeF],
['G', nodeG]
]);
// 第一个循环在运行,第二条路径也激活
const edges1: RuntimeEdgeItemType[] = [
createEdge('start1', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'active'),
createEdge('C', 'A', 'active'), // 第一个循环
createEdge('start2', 'D', 'active'),
createEdge('D', 'C', 'active'),
createEdge('C', 'E', 'waiting'),
createEdge('E', 'F', 'waiting'),
createEdge('F', 'E', 'waiting'), // 第二个循环
createEdge('E', 'G', 'waiting')
];
// A有普通边(start1→A)和递归边(C→B→A)
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges1 })).toBe('run');
// C有来自两个路径的输入
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges1 })).toBe('run');
// 两个循环都退出,进入第二个循环
const edges2: RuntimeEdgeItemType[] = [
createEdge('start1', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'active'),
createEdge('C', 'A', 'skipped'),
createEdge('start2', 'D', 'active'),
createEdge('D', 'C', 'active'),
createEdge('C', 'E', 'active'),
createEdge('E', 'F', 'active'),
createEdge('F', 'E', 'active'), // 第二个循环
createEdge('E', 'G', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeE, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeG, runtimeEdges: edges2 })).toBe('wait');
// 第二个循环也退出
const edges3: RuntimeEdgeItemType[] = [
createEdge('start1', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'active'),
createEdge('C', 'A', 'skipped'),
createEdge('start2', 'D', 'active'),
createEdge('D', 'C', 'active'),
createEdge('C', 'E', 'active'),
createEdge('E', 'F', 'active'),
createEdge('F', 'E', 'skipped'),
createEdge('E', 'G', 'active')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeG, runtimeEdges: edges3 })).toBe('run');
});
it('复杂5: 复杂分支与循环混合 (条件分支+循环+汇聚)', () => {
// Start → A → B (条件分支)
// A → C (条件分支)
// B → D → E → D (循环)
// C → F → G → F (循环)
// E → H
// G → H
// H → I
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodeD = createNode('D');
const nodeE = createNode('E');
const nodeF = createNode('F');
const nodeG = createNode('G');
const nodeH = createNode('H');
const nodeI = createNode('I');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC],
['D', nodeD],
['E', nodeE],
['F', nodeF],
['G', nodeG],
['H', nodeH],
['I', nodeI]
]);
// 走B分支,D-E循环中
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'skipped'),
createEdge('B', 'D', 'active'),
createEdge('D', 'E', 'active'),
createEdge('E', 'D', 'active'), // 循环
createEdge('E', 'H', 'waiting'),
createEdge('C', 'F', 'skipped'),
createEdge('F', 'G', 'waiting'),
createEdge('G', 'F', 'waiting'), // 循环
createEdge('G', 'H', 'skipped'),
createEdge('H', 'I', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges1 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: nodeD, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeH, runtimeEdges: edges1 })).toBe('wait');
// B分支循环退出
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'skipped'),
createEdge('B', 'D', 'active'),
createEdge('D', 'E', 'active'),
createEdge('E', 'D', 'skipped'),
createEdge('E', 'H', 'active'),
createEdge('C', 'F', 'skipped'),
createEdge('F', 'G', 'waiting'),
createEdge('G', 'F', 'waiting'),
createEdge('G', 'H', 'skipped'),
createEdge('H', 'I', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeH, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeI, runtimeEdges: edges2 })).toBe('wait');
// H完成,I可以运行
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('A', 'C', 'skipped'),
createEdge('B', 'D', 'active'),
createEdge('D', 'E', 'active'),
createEdge('E', 'D', 'skipped'),
createEdge('E', 'H', 'active'),
createEdge('C', 'F', 'skipped'),
createEdge('F', 'G', 'waiting'),
createEdge('G', 'F', 'waiting'),
createEdge('G', 'H', 'skipped'),
createEdge('H', 'I', 'active')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeI, runtimeEdges: edges3 })).toBe('run');
// 走C分支场景
const edges4: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'skipped'),
createEdge('A', 'C', 'active'),
createEdge('B', 'D', 'skipped'),
createEdge('D', 'E', 'waiting'),
createEdge('E', 'D', 'waiting'),
createEdge('E', 'H', 'waiting'),
createEdge('C', 'F', 'active'),
createEdge('F', 'G', 'active'),
createEdge('G', 'F', 'active'), // 循环中
createEdge('G', 'H', 'waiting'),
createEdge('H', 'I', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeB, runtimeEdges: edges4 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges4 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeF, runtimeEdges: edges4 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeH, runtimeEdges: edges4 })).toBe('wait');
});
it('复杂6: 多层嵌套循环退出 (开始 → Node1 → Node2 → Node3 → 结束, 带三层条件退出)', () => {
// 开始 → Node1 → Node2 → Node3 → 结束
// 三个循环退出条件:
// - 条件3: Node3 → 结束 (退出到结束)
// - 条件1: Node3 → Node2 (退出到Node2)
// - 条件2: Node3 → Node1 (退出到Node1)
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const node1 = createNode('node1');
const node2 = createNode('node2');
const node3 = createNode('node3');
const nodeEnd = createNode('end');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['node1', node1],
['node2', node2],
['node3', node3],
['end', nodeEnd]
]);
// 场景1: 第一次执行,循环边都是skipped,只有到end的边waiting
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'node2', 'active'),
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'end', 'waiting'), // 条件3
createEdge('node3', 'node2', 'skipped'), // 条件1 (第一次未选择)
createEdge('node3', 'node1', 'skipped') // 条件2 (第一次未选择)
];
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeEnd, runtimeEdges: edges1 })).toBe('wait');
// 场景2: 选择条件1,循环到Node2
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'node2', 'active'),
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'end', 'skipped'), // 条件3未选择
createEdge('node3', 'node2', 'active'), // 条件1选择
createEdge('node3', 'node1', 'skipped') // 条件2未选择
];
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeEnd, runtimeEdges: edges2 })).toBe('skip');
// 场景3: 选择条件2,循环到Node1
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'node2', 'active'),
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'end', 'skipped'), // 条件3未选择
createEdge('node3', 'node2', 'skipped'), // 条件1未选择
createEdge('node3', 'node1', 'active') // 条件2选择
];
// Node1有来自start和node3的两条active边
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeEnd, runtimeEdges: edges3 })).toBe('skip');
// 场景4: 选择条件3,退出到结束
const edges4: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'node2', 'active'),
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'end', 'active'), // 条件3选择
createEdge('node3', 'node2', 'skipped'), // 条件1未选择
createEdge('node3', 'node1', 'skipped') // 条件2未选择
];
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges4 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeEnd, runtimeEdges: edges4 })).toBe('run');
});
it('复杂7: 极度复杂多分支多循环交叉 (开始 → Node1 → branch1/branch2 → 多层循环交叉)', () => {
// 主流程: 开始 → Node1 → (branch1→结束 / branch2→Node2) → Node3
// 下层循环: Node3 → Node5 → Node6
// 复杂循环路径:
// - Node6 → Node2 (branch2)
// - Node6 → Node3 (branch1)
// - Node5 → Node1 (branch2)
// - Node3 → Node1 (branch1)
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const node1 = createNode('node1');
const node2 = createNode('node2');
const node3 = createNode('node3');
const node5 = createNode('node5');
const node6 = createNode('node6');
const nodeEnd = createNode('end');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['node1', node1],
['node2', node2],
['node3', node3],
['node5', node5],
['node6', node6],
['end', nodeEnd]
]);
// 场景1: 第一次执行,选择branch1路径到结束,循环边都skipped
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'end', 'active'), // branch1 → 结束
createEdge('node1', 'node2', 'skipped'), // branch2
createEdge('node2', 'node3', 'skipped'),
createEdge('node3', 'node5', 'skipped'),
createEdge('node3', 'node1', 'skipped'), // 循环边 (branch1)
createEdge('node5', 'node6', 'skipped'),
createEdge('node5', 'node1', 'skipped'), // 循环边 (branch2)
createEdge('node6', 'node2', 'skipped'), // 循环边 (branch2)
createEdge('node6', 'node3', 'skipped') // 循环边 (branch1)
];
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: nodeEnd, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges1 })).toBe('skip');
// 场景2: 选择branch2路径,进入循环网络,第一次未选择循环
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'end', 'skipped'), // branch1
createEdge('node1', 'node2', 'active'), // branch2 → Node2
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'node5', 'active'),
createEdge('node3', 'node1', 'skipped'), // 循环边 (branch1) 第一次未选择
createEdge('node5', 'node6', 'active'),
createEdge('node5', 'node1', 'skipped'), // 循环边 (branch2) 第一次未选择
createEdge('node6', 'node2', 'skipped'), // 循环边 (branch2) 第一次未选择
createEdge('node6', 'node3', 'skipped') // 循环边 (branch1) 第一次未选择
];
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node5, runtimeEdges: edges2 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node6, runtimeEdges: edges2 })).toBe('run');
// 场景3: Node6选择branch1循环回Node3
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'end', 'skipped'), // branch1
createEdge('node1', 'node2', 'active'), // branch2
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'node5', 'active'),
createEdge('node3', 'node1', 'waiting'), // 循环边 (branch1)
createEdge('node5', 'node6', 'active'),
createEdge('node5', 'node1', 'skipped'), // 循环边 (branch2) 未选择
createEdge('node6', 'node2', 'skipped'), // 循环边 (branch2) 未选择
createEdge('node6', 'node3', 'active') // 循环边 (branch1) 选择
];
// Node3有来自node2和node6的两条active边
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node5, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node6, runtimeEdges: edges3 })).toBe('run');
// 场景4: Node6选择branch2循环回Node2
const edges4: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'end', 'skipped'), // branch1
createEdge('node1', 'node2', 'active'), // branch2
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'node5', 'active'),
createEdge('node3', 'node1', 'waiting'), // 循环边 (branch1)
createEdge('node5', 'node6', 'active'),
createEdge('node5', 'node1', 'skipped'), // 循环边 (branch2) 未选择
createEdge('node6', 'node2', 'active'), // 循环边 (branch2) 选择
createEdge('node6', 'node3', 'skipped') // 循环边 (branch1) 未选择
];
// Node2有来自node1和node6的两条active边
expect(checkNodeRunStatus({ nodesMap, node: node2, runtimeEdges: edges4 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges4 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node6, runtimeEdges: edges4 })).toBe('run');
// 场景5: Node5选择branch2循环回Node1
const edges5: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'end', 'skipped'), // branch1
createEdge('node1', 'node2', 'active'), // branch2
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'node5', 'active'),
createEdge('node3', 'node1', 'skipped'), // 循环边 (branch1) 未选择
createEdge('node5', 'node6', 'skipped'), // 不走node6
createEdge('node5', 'node1', 'active'), // 循环边 (branch2) 选择
createEdge('node6', 'node2', 'waiting'),
createEdge('node6', 'node3', 'waiting')
];
// Node1有来自start和node5的两条active边
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges5 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node5, runtimeEdges: edges5 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node6, runtimeEdges: edges5 })).toBe('skip');
// 场景6: Node3选择branch1循环回Node1
const edges6: RuntimeEdgeItemType[] = [
createEdge('start', 'node1', 'active'),
createEdge('node1', 'end', 'skipped'), // branch1
createEdge('node1', 'node2', 'active'), // branch2
createEdge('node2', 'node3', 'active'),
createEdge('node3', 'node5', 'skipped'), // 不走node5
createEdge('node3', 'node1', 'active'), // 循环边 (branch1) 选择
createEdge('node5', 'node6', 'skipped'), // node5被跳过,下游也跳过
createEdge('node5', 'node1', 'skipped'), // node5被跳过
createEdge('node6', 'node2', 'skipped'), // node6不会运行
createEdge('node6', 'node3', 'skipped') // node6不会运行
];
// Node1有来自start和node3的两条active边
expect(checkNodeRunStatus({ nodesMap, node: node1, runtimeEdges: edges6 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node3, runtimeEdges: edges6 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: node5, runtimeEdges: edges6 })).toBe('skip');
});
});
describe('checkNodeRunStatus - 边界情况测试', () => {
it('边界1: 入口节点无输入边', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodesMap = new Map<string, RuntimeNodeItemType>([['start', nodeStart]]);
const edges: RuntimeEdgeItemType[] = [];
expect(checkNodeRunStatus({ nodesMap, node: nodeStart, runtimeEdges: edges })).toBe('run');
});
it('边界2: 自循环节点 (A → A)', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA]
]);
const edges: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'A', 'active')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges })).toBe('run');
});
it('边界3: 所有输入边都被跳过', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC]
]);
const edges: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'skipped'),
createEdge('A', 'C', 'skipped'),
createEdge('B', 'C', 'skipped')
];
expect(checkNodeRunStatus({ nodesMap, node: nodeC, runtimeEdges: edges })).toBe('skip');
});
it('边界4: 递归边组部分激活', () => {
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodeA = createNode('A');
const nodeB = createNode('B');
const nodeC = createNode('C');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['A', nodeA],
['B', nodeB],
['C', nodeC]
]);
// C有两条来自循环的边,但只有一条active
const edges: RuntimeEdgeItemType[] = [
createEdge('start', 'A', 'active'),
createEdge('A', 'B', 'active'),
createEdge('B', 'C', 'active'),
createEdge('C', 'A', 'active'),
createEdge('C', 'B', 'waiting') // 另一条递归边还在waiting
];
// 只要有一组递归边满足条件即可运行
expect(checkNodeRunStatus({ nodesMap, node: nodeA, runtimeEdges: edges })).toBe('run');
});
});
describe('checkNodeRunStatus - 工具调用场景测试', () => {
it('工具调用1: Tool节点作为入口节点 (无workflowStart时)', () => {
// 场景:当工作流中没有 workflowStart/pluginInput 节点时,tool 节点可以作为入口节点
// Tool → Process → End
const toolNode = createNode('tool1', FlowNodeTypeEnum.tool);
const processNode = createNode('process');
const endNode = createNode('end');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['tool1', toolNode],
['process', processNode],
['end', endNode]
]);
// 场景1: Tool节点作为入口,无输入边
const edges1: RuntimeEdgeItemType[] = [
createEdge('tool1', 'process', 'waiting'),
createEdge('process', 'end', 'waiting')
];
// Tool节点作为入口节点应该可以运行
expect(checkNodeRunStatus({ nodesMap, node: toolNode, runtimeEdges: edges1 })).toBe('run');
// 注意:由于tool节点没有输入边(是入口),process节点也会没有可追溯到start的边
// 因此process节点在这个场景下也会返回'run'(因为commonEdges和recursiveEdgeGroups都为空)
expect(checkNodeRunStatus({ nodesMap, node: processNode, runtimeEdges: edges1 })).toBe('run');
// 场景2: Tool节点执行完成后,process可以运行但end仍需等待
const edges2: RuntimeEdgeItemType[] = [
createEdge('tool1', 'process', 'active'),
createEdge('process', 'end', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: processNode, runtimeEdges: edges2 })).toBe('run');
// end节点的输入边是waiting状态,需要等待process完成
expect(checkNodeRunStatus({ nodesMap, node: endNode, runtimeEdges: edges2 })).toBe('wait');
// 场景2.1: process完成后,end可以运行
const edges2_1: RuntimeEdgeItemType[] = [
createEdge('tool1', 'process', 'active'),
createEdge('process', 'end', 'active')
];
expect(checkNodeRunStatus({ nodesMap, node: endNode, runtimeEdges: edges2_1 })).toBe('run');
// 场景3: 有workflowStart时,tool节点不再是入口节点
const startNode = createNode('start', FlowNodeTypeEnum.workflowStart);
const nodesMapWithStart = new Map<string, RuntimeNodeItemType>([
['start', startNode],
['tool1', toolNode],
['process', processNode],
['end', endNode]
]);
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'tool1', 'active'),
createEdge('tool1', 'process', 'waiting'),
createEdge('process', 'end', 'waiting')
];
// 此时tool节点不再是入口节点,需要start激活才能运行
expect(
checkNodeRunStatus({ nodesMap: nodesMapWithStart, node: toolNode, runtimeEdges: edges3 })
).toBe('run');
expect(
checkNodeRunStatus({ nodesMap: nodesMapWithStart, node: processNode, runtimeEdges: edges3 })
).toBe('wait');
// Tool执行完成后,process可以运行
const edges4: RuntimeEdgeItemType[] = [
createEdge('start', 'tool1', 'active'),
createEdge('tool1', 'process', 'active'),
createEdge('process', 'end', 'waiting')
];
expect(
checkNodeRunStatus({ nodesMap: nodesMapWithStart, node: processNode, runtimeEdges: edges4 })
).toBe('run');
});
it('工具调用2: ToolSet节点与条件分支和循环组合 (Agent → ToolSet → Tool1/Tool2 → Result → Agent)', () => {
// 场景:Agent调用工具集,工具集根据条件选择不同工具执行,并支持循环调用
// Start → Agent → ToolSet → (Tool1 | Tool2) → Result → Agent (循环)
const nodeStart = createNode('start', FlowNodeTypeEnum.workflowStart);
const agentNode = createNode('agent', FlowNodeTypeEnum.agent);
const toolSetNode = createNode('toolSet', FlowNodeTypeEnum.toolSet);
const tool1Node = createNode('tool1', FlowNodeTypeEnum.tool);
const tool2Node = createNode('tool2', FlowNodeTypeEnum.tool);
const resultNode = createNode('result');
const nodesMap = new Map<string, RuntimeNodeItemType>([
['start', nodeStart],
['agent', agentNode],
['toolSet', toolSetNode],
['tool1', tool1Node],
['tool2', tool2Node],
['result', resultNode]
]);
// 场景1: 第一次执行,Agent选择Tool1
const edges1: RuntimeEdgeItemType[] = [
createEdge('start', 'agent', 'active'),
createEdge('agent', 'toolSet', 'active'),
createEdge('toolSet', 'tool1', 'active'), // 选择Tool1
createEdge('toolSet', 'tool2', 'skipped'), // Tool2未选择
createEdge('tool1', 'result', 'waiting'),
createEdge('tool2', 'result', 'skipped'),
createEdge('result', 'agent', 'waiting') // 循环边等待
];
expect(checkNodeRunStatus({ nodesMap, node: agentNode, runtimeEdges: edges1 })).toBe('wait');
expect(checkNodeRunStatus({ nodesMap, node: toolSetNode, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: tool1Node, runtimeEdges: edges1 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: tool2Node, runtimeEdges: edges1 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: resultNode, runtimeEdges: edges1 })).toBe('wait');
// 场景2: Tool1执行完成,Result处理结果
const edges2: RuntimeEdgeItemType[] = [
createEdge('start', 'agent', 'active'),
createEdge('agent', 'toolSet', 'active'),
createEdge('toolSet', 'tool1', 'active'),
createEdge('toolSet', 'tool2', 'skipped'),
createEdge('tool1', 'result', 'active'), // Tool1完成
createEdge('tool2', 'result', 'skipped'),
createEdge('result', 'agent', 'waiting')
];
expect(checkNodeRunStatus({ nodesMap, node: resultNode, runtimeEdges: edges2 })).toBe('run');
// 场景3: 循环回Agent,第二次调用选择Tool2
const edges3: RuntimeEdgeItemType[] = [
createEdge('start', 'agent', 'active'),
createEdge('agent', 'toolSet', 'active'),
createEdge('toolSet', 'tool1', 'skipped'), // Tool1未选择
createEdge('toolSet', 'tool2', 'active'), // 选择Tool2
createEdge('tool1', 'result', 'skipped'),
createEdge('tool2', 'result', 'active'), // Tool2完成
createEdge('result', 'agent', 'active') // 循环边激活
];
// Agent有来自start和result的两条active边
expect(checkNodeRunStatus({ nodesMap, node: agentNode, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: tool1Node, runtimeEdges: edges3 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: tool2Node, runtimeEdges: edges3 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: resultNode, runtimeEdges: edges3 })).toBe('run');
// 场景4: 循环退出,不再调用工具
const edges4: RuntimeEdgeItemType[] = [
createEdge('start', 'agent', 'active'),
createEdge('agent', 'toolSet', 'skipped'), // 不再调用工具集
createEdge('toolSet', 'tool1', 'skipped'),
createEdge('toolSet', 'tool2', 'skipped'),
createEdge('tool1', 'result', 'skipped'),
createEdge('tool2', 'result', 'skipped'),
createEdge('result', 'agent', 'skipped') // 循环退出
];
expect(checkNodeRunStatus({ nodesMap, node: agentNode, runtimeEdges: edges4 })).toBe('run');
expect(checkNodeRunStatus({ nodesMap, node: toolSetNode, runtimeEdges: edges4 })).toBe(
'skip'
);
expect(checkNodeRunStatus({ nodesMap, node: tool1Node, runtimeEdges: edges4 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: tool2Node, runtimeEdges: edges4 })).toBe('skip');
expect(checkNodeRunStatus({ nodesMap, node: resultNode, runtimeEdges: edges4 })).toBe('skip');
});
});
});
......@@ -12,7 +12,8 @@ export default defineConfig({
test: {
coverage: {
enabled: true,
reporter: ['text', 'text-summary', 'html', 'json-summary', 'json'],
reporter: ['html', 'json-summary', 'json'],
// reporter: ['text', 'text-summary', 'html', 'json-summary', 'json'],
reportOnFailure: true,
all: false, // 只包含被测试实际覆盖的文件,不包含空目录
include: ['projects/app/**/*.ts', 'packages/**/*.ts'],
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment