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
......@@ -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",
......@@ -416,4 +416,4 @@
"document/content/docs/use-cases/external-integration/wecom.mdx": "2025-12-10T20:07:05+08:00",
"document/content/docs/use-cases/index.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/use-cases/index.mdx": "2025-07-24T14:23:04+08:00"
}
\ No newline at end of file
}
......@@ -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 }
}
}
);
......
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';
});
......
......@@ -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 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`
});
......@@ -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