Commit 03dd9c00 by Archer Committed by GitHub

perf: runtime performance (#6665)

* perf: runtime performance

* add stringify trace

* remove trace val

* remove trace val

* remove logger

* remove logger

* add test

* add log
parent 22348594
# 工作流 CPU 阻塞模块分析
> 从 `packages/service/core/workflow/dispatch/index.ts` 入口出发,排查所有**同步占用 CPU、阻塞整个进程**的模块。
Node.js 单线程模型下,CPU 阻塞指:在当前调用栈未让出事件循环(无 `await`)的情况下执行大量计算,导致其他请求无法被处理。
---
## 一、`WorkflowQueue` 构造函数——图算法批量同步执行
**文件**: `packages/service/core/workflow/dispatch/index.ts:348`
每次创建工作流实例时,构造函数**同步**依次执行:
```ts
constructor(...) {
// 1. O(E) 构建边索引
this.edgeIndex = WorkflowQueue.buildEdgeIndex({ runtimeEdges });
// 2. O(N+E) DFS 边分类 ← 递归,全同步
// 3. O(N+E) Tarjan SCC ← 递归,全同步
// 4. O(N²) BFS per node ← 每个节点一次 BFS 回溯
this.nodeEdgeGroupsMap = WorkflowQueue.buildNodeEdgeGroupsMap({ ... });
}
```
三个算法全部是纯同步的 CPU 密集计算,无任何 `await` 让出点。
---
## 二、Tarjan SCC 算法——递归 DFS,无让出
**文件**: `packages/service/core/workflow/utils/tarjan.ts:31`
```ts
function tarjan(nodeId: string) {
// ...
for (const edge of outEdges) {
if (!discoveryTime.has(targetId)) {
tarjan(targetId); // ⚠️ 同步递归,无 await
}
}
}
for (const node of runtimeNodes) {
tarjan(node.nodeId); // 对每个未访问节点启动递归
}
```
**问题**
- 纯同步递归,执行期间完全占用 Event Loop。
- 节点数 N 较大(如 100+ 节点)时,递归深度 = 工作流拓扑深度,调用栈可能很深。
- 同文件 `classifyEdgesByDFS` 也是完全相同的递归 DFS 结构,与 Tarjan 串行执行,等于一次工作流启动 **做两遍图遍历**
---
## 三、`findBranchHandle`——每节点一次 BFS,合计 O(N²)
**文件**: `packages/service/core/workflow/dispatch/index.ts:543`
```ts
private static buildNodeEdgeGroupsMap(...) {
runtimeNodes.forEach((targetNode) => {
// 对每个节点的每条边,调用 findBranchHandle
const branchGroups = this.groupEdgesByBranch(nonBackEdges, ...);
});
}
private static findBranchHandle(edge, ...) {
const queue = [{ nodeId: edge.source, ... }];
while (queue.length > 0) {
// BFS 向上回溯,最坏遍历所有节点 ← 纯同步
const inEdges = edgeIndex.byTarget.get(nodeId) || [];
for (const inEdge of inEdges) {
queue.push({ nodeId: inEdge.source, ... });
}
}
}
```
**问题**
- `buildNodeEdgeGroupsMap` 在构造函数中对每个节点调用,每次调用又做一次 BFS。
- 最坏复杂度 O(N × (N + E)),对于 100 节点、200 边的工作流约为 30000 次循环迭代,全同步。
---
## 四、`replaceEditorVariable`——每节点每输入做正则+递归,全同步
**文件**: `packages/global/core/workflow/runtime/utils.ts:372`
每次节点运行前,`getNodeRunParams` 对每个 input 都调用:
```ts
node.inputs.forEach((input) => {
// 每个 input 都调用一次 replaceEditorVariable
let value = replaceEditorVariable({
text: input.value,
nodes: this.data.runtimeNodes, // 传入所有节点
variables: this.data.variables
});
value = getReferenceVariableValue({ value, nodes, variables });
});
```
`replaceEditorVariable` 内部:
```ts
// 1. 全局正则匹配,提取所有变量引用
const matches = [...text.matchAll(variablePattern)];
for (const match of matches) {
// 2. nodes.find() O(N) 线性扫描
const node = nodes.find((node) => node.nodeId === nodeId);
// 3. 每个变量编译一次新 RegExp ← 正则编译有 CPU 开销
replacements.push({ pattern: `\\{\\{\\$${escapedNodeId}...`, replacement: formatVal });
}
// 4. 如果有嵌套变量,递归调用自身(最多 depth=10)
if (hasReplacements && /\{\{\$[^.]+\.[^$]+\$\}\}/.test(result)) {
result = replaceEditorVariable({ text: result, nodes, variables, depth: depth + 1 });
}
```
**问题**
- **每次 `nodes.find()`** 是 O(N) 线性扫描,完全没有缓存。一个节点有 10 个 input、每个 input 引用 5 个变量、工作流有 50 个节点 → **2500 次 O(N) 扫描**
- **每个变量引用** `new RegExp(pattern)` 一次,正则编译有 CPU 成本。
- **最多 10 层递归**,每层都重复上述过程。
- 整个函数链(`replaceEditorVariable` + `getReferenceVariableValue`)全同步,**每个节点运行前都会触发,且节点越多调用越频繁**
---
## 五、`getReferenceVariableValue`——O(N) 数组扫描,无缓存
**文件**: `packages/global/core/workflow/runtime/utils.ts:297`
```ts
const node = nodes.find((node) => node.nodeId === sourceNodeId); // O(N)
return node.outputs.find((output) => output.id === outputId)?.value; // O(outputs)
```
**问题**
- 每次调用都线性扫描整个 `nodes` 数组。
-`replaceEditorVariable` 频繁调用(每个变量引用一次)。
- `nodes` 数组在运行时不会变化(节点结构固定),却没有预建索引,每次都从头扫。
---
## 汇总
| 位置 | 函数 | 复杂度 | 触发时机 | 是否有让出点 |
|------|------|--------|---------|------------|
| `dispatch/index.ts` 构造函数 | `buildEdgeIndex` | O(E) | 每次工作流启动 | ❌ 无 |
| `utils/tarjan.ts` | `classifyEdgesByDFS` | O(N+E) 递归 | 每次工作流启动 | ❌ 无 |
| `utils/tarjan.ts` | `findSCCs (tarjan)` | O(N+E) 递归 | 每次工作流启动 | ❌ 无 |
| `dispatch/index.ts` | `buildNodeEdgeGroupsMap` + `findBranchHandle` | O(N²) | 每次工作流启动 | ❌ 无 |
| `runtime/utils.ts` | `replaceEditorVariable` | O(N × inputs × depth) | 每节点运行前 | ❌ 无 |
| `runtime/utils.ts` | `getReferenceVariableValue` | O(N) per call | 每 input 一次 | ❌ 无 |
**最严重的场景**:大型工作流(100+ 节点)并发启动时,构造函数中的图算法(第一~四项)全部同步执行,每个请求都会独占 Event Loop 若干毫秒,并发时互相堆叠,导致明显卡顿。
**最高频的场景**:Agent 节点有大量工具调用时,每轮工具调用后节点重新 resolve 触发下游节点的参数注入,`replaceEditorVariable` 被高频调用,且每次都对所有节点做线性扫描。
...@@ -6,6 +6,7 @@ description: 'FastGPT V4.14.10 更新说明' ...@@ -6,6 +6,7 @@ description: 'FastGPT V4.14.10 更新说明'
## 注意 ## 注意
1. 代码沙盒镜像名变更: `{{hub}}/fastgpt-sandbox` -> `{{hub}}/fastgpt-code-sandbox` 1. 代码沙盒镜像名变更: `{{hub}}/fastgpt-sandbox` -> `{{hub}}/fastgpt-code-sandbox`
2. 系统工具部分头像,移除了 icon,都转用图片链接,如果丢失了头像,可以重新更新一次系统工具(卸载再安装,或者直接导入 pkg 覆盖)
## 🚀 新增内容 ## 🚀 新增内容
...@@ -15,6 +16,8 @@ description: 'FastGPT V4.14.10 更新说明' ...@@ -15,6 +16,8 @@ description: 'FastGPT V4.14.10 更新说明'
## ⚙️ 优化 ## ⚙️ 优化
1. 工作流 runtime,减少计算复杂。
2. 增加一些对于大变量的计算限制,避免计算复杂度过高导致线程阻塞。
## 🐛 修复 ## 🐛 修复
......
...@@ -220,7 +220,7 @@ ...@@ -220,7 +220,7 @@
"document/content/docs/self-host/upgrading/4-14/4140.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4140.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4141.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4141.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4141.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-26T16:35:07+08:00", "document/content/docs/self-host/upgrading/4-14/41410.mdx": "2026-03-27T12:01:02+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4142.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4143.en.mdx": "2026-03-03T17:39:47+08:00",
......
import crypto from 'crypto'; import crypto from 'crypto';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import path from 'path'; import path from 'path';
import { getErrText } from '../error/utils';
export const checkStrOversize = (str: string, size = 1e8) => {
if (str.length > size) {
return true;
}
return false;
};
/* check string is a web link */ /* check string is a web link */
export function strIsLink(str?: string) { export function strIsLink(str?: string) {
...@@ -30,7 +38,25 @@ export const valToStr = (val: any) => { ...@@ -30,7 +38,25 @@ export const valToStr = (val: any) => {
if (val === undefined) return ''; if (val === undefined) return '';
if (val === null) return 'null'; if (val === null) return 'null';
if (typeof val === 'object') return JSON.stringify(val); if (typeof val === 'object') {
try {
const start = Date.now();
const res = JSON.stringify(val);
if (Date.now() - start > 1000) {
console.warn('Slow JSON.stringify', {
duration: Date.now() - start,
valLength: res.length
});
}
return res;
} catch (error) {
console.error('Failed to stringify value', { error });
return `Failed to stringify value: ${getErrText(error)}`;
}
}
return String(val); return String(val);
}; };
...@@ -41,6 +67,9 @@ export function replaceVariable( ...@@ -41,6 +67,9 @@ export function replaceVariable(
depth = 0 depth = 0
) { ) {
if (typeof text !== 'string') return text; if (typeof text !== 'string') return text;
if (checkStrOversize(text)) {
throw new Error('Text length exceeds 100,000,000 characters.');
}
const MAX_REPLACEMENT_DEPTH = 10; const MAX_REPLACEMENT_DEPTH = 10;
const processedVariables = new Set<string>(); const processedVariables = new Set<string>();
......
...@@ -99,6 +99,7 @@ export type ChatDispatchProps = { ...@@ -99,6 +99,7 @@ export type ChatDispatchProps = {
export type ModuleDispatchProps<T> = ChatDispatchProps & { export type ModuleDispatchProps<T> = ChatDispatchProps & {
node: RuntimeNodeItemType; node: RuntimeNodeItemType;
runtimeNodes: RuntimeNodeItemType[]; runtimeNodes: RuntimeNodeItemType[];
runtimeNodesMap: Map<string, RuntimeNodeItemType>;
runtimeEdges: RuntimeEdgeItemType[]; runtimeEdges: RuntimeEdgeItemType[];
params: T; params: T;
......
import json5 from 'json5'; import json5 from 'json5';
import { replaceVariable, valToStr } from '../../../common/string/tools'; import { checkStrOversize, replaceVariable, valToStr } from '../../../common/string/tools';
import { ChatRoleEnum } from '../../../core/chat/constants'; import { ChatRoleEnum } from '../../../core/chat/constants';
import type { ChatItemType } from '../../../core/chat/type'; import type { ChatItemType } from '../../../core/chat/type';
import type { NodeOutputItemType } from './type'; import type { NodeOutputItemType } from './type';
...@@ -296,17 +296,16 @@ export const filterWorkflowEdges = (edges: RuntimeEdgeItemType[]) => { ...@@ -296,17 +296,16 @@ export const filterWorkflowEdges = (edges: RuntimeEdgeItemType[]) => {
*/ */
export const getReferenceVariableValue = ({ export const getReferenceVariableValue = ({
value, value,
nodes, nodesMap,
variables variables
}: { }: {
value?: ReferenceValueType; value?: ReferenceValueType;
nodes: RuntimeNodeItemType[]; nodesMap: Record<string, RuntimeNodeItemType> | Map<string, RuntimeNodeItemType>;
variables: Record<string, any>; variables: Record<string, any>;
}) => { }) => {
if (!value) return value; if (!value) return value;
// handle single reference value const resoleValue = (value: [string, string | undefined]) => {
if (isValidReferenceValueFormat(value)) {
const sourceNodeId = value[0]; const sourceNodeId = value[0];
const outputId = value[1]; const outputId = value[1];
...@@ -316,12 +315,17 @@ export const getReferenceVariableValue = ({ ...@@ -316,12 +315,17 @@ export const getReferenceVariableValue = ({
} }
// 避免 value 刚好就是二个元素的字符串数组 // 避免 value 刚好就是二个元素的字符串数组
const node = nodes.find((node) => node.nodeId === sourceNodeId); const node = nodesMap instanceof Map ? nodesMap.get(sourceNodeId) : nodesMap[sourceNodeId];
if (!node) { if (!node) {
return value; return value;
} }
return node.outputs.find((output) => output.id === outputId)?.value; return node.outputs.find((output) => output.id === outputId)?.value;
};
// handle single reference value
if (isValidReferenceValueFormat(value)) {
return resoleValue(value as [string, string | undefined]);
} }
// handle reference array // handle reference array
...@@ -330,15 +334,12 @@ export const getReferenceVariableValue = ({ ...@@ -330,15 +334,12 @@ export const getReferenceVariableValue = ({
value.length > 0 && value.length > 0 &&
value.every((item) => isValidReferenceValueFormat(item)) value.every((item) => isValidReferenceValueFormat(item))
) { ) {
const result = value.map<any>((val) => { return value
return getReferenceVariableValue({ .map<any>((val) => {
value: val, return resoleValue(val as [string, string | undefined]);
nodes, })
variables .flat()
}); .filter((item) => item !== undefined);
});
return result.flat().filter((item) => item !== undefined);
} }
return value; return value;
...@@ -368,20 +369,42 @@ export const formatVariableValByType = (val: any, valueType?: WorkflowIOValueTyp ...@@ -368,20 +369,42 @@ export const formatVariableValByType = (val: any, valueType?: WorkflowIOValueTyp
return val; return val;
}; };
// 模块级 RegExp 缓存,避免每次变量替换都重新编译正则
const _replaceRegexCache = new Map<string, RegExp>();
const _MAX_REGEX_CACHE_SIZE = 5000;
const _getCachedRegex = (pattern: string): RegExp => {
let re = _replaceRegexCache.get(pattern);
if (!re) {
if (_replaceRegexCache.size >= _MAX_REGEX_CACHE_SIZE) {
_replaceRegexCache.clear();
}
re = new RegExp(pattern, 'g');
_replaceRegexCache.set(pattern, re);
}
return re;
};
// replace {{$xx.xx$}} variables for text // replace {{$xx.xx$}} variables for text
export function replaceEditorVariable({ export function replaceEditorVariable({
text, text,
nodes, nodesMap,
variables, variables,
depth = 0 depth = 0
}: { }: {
text: any; text: any;
nodes: RuntimeNodeItemType[]; nodesMap: Record<string, RuntimeNodeItemType> | Map<string, RuntimeNodeItemType>;
variables: Record<string, any>; // global variables variables: Record<string, any>; // global variables
depth?: number; depth?: number;
}) { }) {
const getNode = (nodeId: string) => {
return nodesMap instanceof Map ? nodesMap.get(nodeId) : nodesMap[nodeId];
};
if (typeof text !== 'string') return text; if (typeof text !== 'string') return text;
if (text === '') return text; if (text === '') return text;
if (checkStrOversize(text)) {
throw new Error('Text length exceeds 100,000,000 characters.');
}
const MAX_REPLACEMENT_DEPTH = 10; const MAX_REPLACEMENT_DEPTH = 10;
const processedVariables = new Set<string>(); const processedVariables = new Set<string>();
...@@ -398,10 +421,10 @@ export function replaceEditorVariable({ ...@@ -398,10 +421,10 @@ export function replaceEditorVariable({
if (typeof value !== 'string') return false; if (typeof value !== 'string') return false;
// Check if the value contains the target variable pattern (direct self-reference) // Check if the value contains the target variable pattern (direct self-reference)
const selfRefPattern = new RegExp( const selfRefPattern = _getCachedRegex(
`\\{\\{\\$${targetKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\$\\}\\}`, `\\{\\{\\$${targetKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\$\\}\\}`
'g'
); );
selfRefPattern.lastIndex = 0;
return selfRefPattern.test(value); return selfRefPattern.test(value);
}; };
...@@ -431,7 +454,7 @@ export function replaceEditorVariable({ ...@@ -431,7 +454,7 @@ export function replaceEditorVariable({
return variables[id]; return variables[id];
} }
// Find upstream node input/output // Find upstream node input/output
const node = nodes.find((node) => node.nodeId === nodeId); const node = getNode(nodeId);
if (!node) return; if (!node) return;
const output = node.outputs.find((output) => output.id === id); const output = node.outputs.find((output) => output.id === id);
...@@ -439,7 +462,13 @@ export function replaceEditorVariable({ ...@@ -439,7 +462,13 @@ export function replaceEditorVariable({
// Use the node's input as the variable value(Example: HTTP data will reference its own dynamic input) // Use the node's input as the variable value(Example: HTTP data will reference its own dynamic input)
const input = node.inputs.find((input) => input.key === id); const input = node.inputs.find((input) => input.key === id);
if (input) return getReferenceVariableValue({ value: input.value, nodes, variables }); if (input) {
return getReferenceVariableValue({
value: input.value,
nodesMap,
variables
});
}
})(); })();
// Check for direct circular reference // Check for direct circular reference
...@@ -461,13 +490,20 @@ export function replaceEditorVariable({ ...@@ -461,13 +490,20 @@ export function replaceEditorVariable({
} }
// Apply all replacements // Apply all replacements
replacements.forEach(({ pattern, replacement }) => { for (const { pattern, replacement } of replacements) {
result = result.replace(new RegExp(pattern, 'g'), replacement); if (checkStrOversize(result)) {
}); console.warn('Text length exceeds 100,000,000 characters.');
break;
}
const re = _getCachedRegex(pattern);
re.lastIndex = 0;
result = result.replace(re, () => replacement);
}
// If we made replacements and there might be nested variables, recursively process // If we made replacements and there might be nested variables, recursively process
if (hasReplacements && /\{\{\$[^.]+\.[^$]+\$\}\}/.test(result)) { if (hasReplacements && /\{\{\$[^.]+\.[^$]+\$\}\}/.test(result)) {
result = replaceEditorVariable({ text: result, nodes, variables, depth: depth + 1 }); result = replaceEditorVariable({ text: result, nodesMap, variables, depth: depth + 1 });
} }
return result || ''; return result || '';
......
...@@ -37,7 +37,7 @@ const buildHttpRequest = ({ ...@@ -37,7 +37,7 @@ const buildHttpRequest = ({
const replaceVariables = (text: string) => { const replaceVariables = (text: string) => {
return replaceEditorVariable({ return replaceEditorVariable({
text, text,
nodes: [], nodesMap: new Map(),
variables: params variables: params
}); });
}; };
......
...@@ -749,7 +749,7 @@ export class WorkflowQueue { ...@@ -749,7 +749,7 @@ export class WorkflowQueue {
if (runningNodePromises.size > 0) { if (runningNodePromises.size > 0) {
// 当上一个节点运行结束时,立即运行下一轮 // 当上一个节点运行结束时,立即运行下一轮
await Promise.race(runningNodePromises).catch((error) => { await Promise.race(runningNodePromises).catch((error) => {
logger.error('Workflow race error', { error }); logger.error('Workflow race error', { chatId: this.data.chatId, error });
}); });
} else { } else {
// 理论上不应出现此情况,防御性退回到让出进程 // 理论上不应出现此情况,防御性退回到让出进程
...@@ -852,14 +852,14 @@ export class WorkflowQueue { ...@@ -852,14 +852,14 @@ export class WorkflowQueue {
// replace {{$xx.xx$}} and {{xx}} variables // replace {{$xx.xx$}} and {{xx}} variables
let value = replaceEditorVariable({ let value = replaceEditorVariable({
text: input.value, text: input.value,
nodes: this.data.runtimeNodes, nodesMap: this.runtimeNodesMap,
variables: this.data.variables variables: this.data.variables
}); });
// replace reference variables // replace reference variables
value = getReferenceVariableValue({ value = getReferenceVariableValue({
value, value,
nodes: this.data.runtimeNodes, nodesMap: this.runtimeNodesMap,
variables: this.data.variables variables: this.data.variables
}); });
...@@ -899,6 +899,7 @@ export class WorkflowQueue { ...@@ -899,6 +899,7 @@ export class WorkflowQueue {
retainDatasetCite: this.data.retainDatasetCite, retainDatasetCite: this.data.retainDatasetCite,
node, node,
runtimeNodes: this.data.runtimeNodes, runtimeNodes: this.data.runtimeNodes,
runtimeNodesMap: this.runtimeNodesMap,
runtimeEdges: this.data.runtimeEdges, runtimeEdges: this.data.runtimeEdges,
params, params,
mode mode
......
...@@ -69,7 +69,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -69,7 +69,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
responseChatItemId, responseChatItemId,
variables, variables,
node, node,
runtimeNodes, runtimeNodesMap,
histories, histories,
params: { params: {
system_httpMethod: httpMethod = 'POST', system_httpMethod: httpMethod = 'POST',
...@@ -110,7 +110,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -110,7 +110,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
const replaceStringVariables = (text: string) => { const replaceStringVariables = (text: string) => {
return replaceEditorVariable({ return replaceEditorVariable({
text, text,
nodes: runtimeNodes, nodesMap: runtimeNodesMap,
variables: allVariables variables: allVariables
}); });
}; };
...@@ -178,7 +178,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H ...@@ -178,7 +178,7 @@ export const dispatchHttp468Request = async (props: HttpRequestProps): Promise<H
if (httpContentType === ContentTypes.json) { if (httpContentType === ContentTypes.json) {
httpJsonBody = replaceJsonBodyString( httpJsonBody = replaceJsonBodyString(
{ text: httpJsonBody }, { text: httpJsonBody },
{ variables, allVariables, runtimeNodes } { variables, allVariables, runtimeNodesMap }
); );
return json5.parse(httpJsonBody); return json5.parse(httpJsonBody);
} }
...@@ -306,10 +306,10 @@ export const replaceJsonBodyString = ( ...@@ -306,10 +306,10 @@ export const replaceJsonBodyString = (
props: { props: {
variables: Record<string, any>; variables: Record<string, any>;
allVariables: Record<string, any>; allVariables: Record<string, any>;
runtimeNodes: RuntimeNodeItemType[]; runtimeNodesMap: Map<string, RuntimeNodeItemType>;
} }
) => { ) => {
const { variables, allVariables, runtimeNodes } = props; const { variables, allVariables, runtimeNodesMap } = props;
const MAX_REPLACEMENT_DEPTH = 10; const MAX_REPLACEMENT_DEPTH = 10;
const processedVariables = new Set<string>(); const processedVariables = new Set<string>();
...@@ -405,15 +405,20 @@ export const replaceJsonBodyString = ( ...@@ -405,15 +405,20 @@ export const replaceJsonBodyString = (
return variables[id]; return variables[id];
} }
// Find upstream node input/output // Find upstream node input/output
const node = runtimeNodes.find((node) => node.nodeId === nodeId); const node = runtimeNodesMap.get(nodeId);
if (!node) return; if (!node) return;
const output = node.outputs.find((output) => output.id === id); const output = node.outputs.find((output) => output.id === id);
if (output) return formatVariableValByType(output.value, output.valueType); if (output) return formatVariableValByType(output.value, output.valueType);
const input = node.inputs.find((input) => input.key === id); const input = node.inputs.find((input) => input.key === id);
if (input) if (input) {
return getReferenceVariableValue({ value: input.value, nodes: runtimeNodes, variables }); return getReferenceVariableValue({
value: input.value,
nodesMap: runtimeNodesMap,
variables
});
}
})(); })();
const formatVal = valToStr(variableVal, isInQuotes); const formatVal = valToStr(variableVal, isInQuotes);
......
...@@ -105,7 +105,7 @@ function getResult( ...@@ -105,7 +105,7 @@ function getResult(
condition: IfElseConditionType, condition: IfElseConditionType,
list: ConditionListItemType[], list: ConditionListItemType[],
variables: Record<string, any>, variables: Record<string, any>,
runtimeNodes: RuntimeNodeItemType[] runtimeNodesMap: Map<string, RuntimeNodeItemType>
) { ) {
const listResult = list.map((item) => { const listResult = list.map((item) => {
const { variable, condition: variableCondition, value, valueType } = item; const { variable, condition: variableCondition, value, valueType } = item;
...@@ -114,7 +114,7 @@ function getResult( ...@@ -114,7 +114,7 @@ function getResult(
const conditionLeftValue = getReferenceVariableValue({ const conditionLeftValue = getReferenceVariableValue({
value: variable, value: variable,
variables, variables,
nodes: runtimeNodes nodesMap: runtimeNodesMap
}); });
const conditionRightValue = const conditionRightValue =
...@@ -122,7 +122,7 @@ function getResult( ...@@ -122,7 +122,7 @@ function getResult(
? getReferenceVariableValue({ ? getReferenceVariableValue({
value: value as ReferenceItemValueType, value: value as ReferenceItemValueType,
variables, variables,
nodes: runtimeNodes nodesMap: runtimeNodesMap
}) })
: value; : value;
...@@ -135,7 +135,7 @@ function getResult( ...@@ -135,7 +135,7 @@ function getResult(
export const dispatchIfElse = async (props: Props): Promise<Response> => { export const dispatchIfElse = async (props: Props): Promise<Response> => {
const { const {
params, params,
runtimeNodes, runtimeNodesMap,
variables, variables,
node: { nodeId } node: { nodeId }
} = props; } = props;
...@@ -144,7 +144,7 @@ export const dispatchIfElse = async (props: Props): Promise<Response> => { ...@@ -144,7 +144,7 @@ export const dispatchIfElse = async (props: Props): Promise<Response> => {
let res = IfElseResultEnum.ELSE as string; let res = IfElseResultEnum.ELSE as string;
for (let i = 0; i < ifElseList.length; i++) { for (let i = 0; i < ifElseList.length; i++) {
const item = ifElseList[i]; const item = ifElseList[i];
const result = getResult(item.condition, item.list, variables, runtimeNodes); const result = getResult(item.condition, item.list, variables, runtimeNodesMap);
if (result) { if (result) {
res = getElseIFLabel(i); res = getElseIFLabel(i);
break; break;
......
...@@ -25,14 +25,14 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> => ...@@ -25,14 +25,14 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
chatConfig, chatConfig,
params, params,
variables, variables,
runtimeNodes, runtimeNodesMap,
workflowStreamResponse, workflowStreamResponse,
externalProvider, externalProvider,
runningAppInfo runningAppInfo
} = props; } = props;
const { updateList } = params; const { updateList } = params;
const nodeIds = runtimeNodes.map((node) => node.nodeId); const nodeIds = Array.from(runtimeNodesMap.keys());
const result = updateList.map((item) => { const result = updateList.map((item) => {
const variable = item.variable; const variable = item.variable;
...@@ -55,7 +55,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> => ...@@ -55,7 +55,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
typeof item.value?.[1] === 'string' typeof item.value?.[1] === 'string'
? replaceEditorVariable({ ? replaceEditorVariable({
text: item.value?.[1], text: item.value?.[1],
nodes: runtimeNodes, nodesMap: runtimeNodesMap,
variables variables
}) })
: item.value?.[1]; : item.value?.[1];
...@@ -65,7 +65,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> => ...@@ -65,7 +65,7 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
return getReferenceVariableValue({ return getReferenceVariableValue({
value: item.value, value: item.value,
variables, variables,
nodes: runtimeNodes nodesMap: runtimeNodesMap
}); });
} }
})(); })();
...@@ -75,10 +75,9 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> => ...@@ -75,10 +75,9 @@ export const dispatchUpdateVariable = async (props: Props): Promise<Response> =>
if (varNodeId === VARIABLE_NODE_ID) { if (varNodeId === VARIABLE_NODE_ID) {
variables[varKey] = value; variables[varKey] = value;
} else { } else {
const node = runtimeNodesMap.get(varNodeId);
// Other nodes // Other nodes
runtimeNodes node?.outputs?.find((output) => {
.find((node) => node.nodeId === varNodeId)
?.outputs?.find((output) => {
if (output.id === varKey) { if (output.id === varKey) {
output.value = value; output.value = value;
return true; return true;
......
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next'; import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import type { import type { ApiKeyHealthResponseType } from '@fastgpt/global/openapi/support/openapi/api';
ApiKeyHealthResponseType import { ApiKeyHealthParamsSchema } from '@fastgpt/global/openapi/support/openapi/api';
} from '@fastgpt/global/openapi/support/openapi/api';
import {
ApiKeyHealthParamsSchema
} from '@fastgpt/global/openapi/support/openapi/api';
import { MongoOpenApi } from '@fastgpt/service/support/openapi/schema'; import { MongoOpenApi } from '@fastgpt/service/support/openapi/schema';
import { useIPFrequencyLimit } from '../../../../../../../packages/service/common/middle/reqFrequencyLimit'; import { useIPFrequencyLimit } from '../../../../../../../packages/service/common/middle/reqFrequencyLimit';
......
...@@ -41,7 +41,7 @@ describe('replaceJsonBodyString', () => { ...@@ -41,7 +41,7 @@ describe('replaceJsonBodyString', () => {
const mockProps = { const mockProps = {
variables: mockVariables, variables: mockVariables,
allVariables: mockAllVariables, allVariables: mockAllVariables,
runtimeNodes: mockRuntimeNodes runtimeNodesMap: new Map(mockRuntimeNodes.map((node) => [node.nodeId, node]))
}; };
describe('Basic variable replacement functionality', () => { describe('Basic variable replacement functionality', () => {
......
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