Commit 95f01662 by Archer Committed by GitHub

sandbox version (#6497)

* sandbox version

* add sandbox log

* update lock

* fix

* fix: sandbox

* doc

* add console

* i18n
parent 92848904
...@@ -34,7 +34,7 @@ async function main({data1, data2}){ ...@@ -34,7 +34,7 @@ async function main({data1, data2}){
} }
``` ```
**Python** — 在 main 函数参数中按变量名接收: **Python** — 在 main 函数参数中按变量名接收, 节点里输入的变量名一定要与`main`中的变量名一致,顺序可以任意
```python ```python
def main(data1, data2): def main(data1, data2):
......
...@@ -7,6 +7,7 @@ description: 'FastGPT V4.14.8 更新说明' ...@@ -7,6 +7,7 @@ description: 'FastGPT V4.14.8 更新说明'
## 🚀 新增内容 ## 🚀 新增内容
1. Next.js 版本升级到 15。 1. Next.js 版本升级到 15。
2. 重构代码沙盒。
## ⚙️ 优化 ## ⚙️ 优化
......
...@@ -80,7 +80,7 @@ ...@@ -80,7 +80,7 @@
"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.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/reply.mdx": "2025-07-23T21:35:03+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.en.mdx": "2026-02-28T12:36:59+08:00", "document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.en.mdx": "2026-02-28T12:36:59+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.mdx": "2026-02-28T12:36:59+08:00", "document/content/docs/introduction/guide/dashboard/workflow/sandbox-v2.mdx": "2026-03-03T23:31:51+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox.en.mdx": "2026-02-26T22:14:30+08:00", "document/content/docs/introduction/guide/dashboard/workflow/sandbox.en.mdx": "2026-02-26T22:14:30+08:00",
"document/content/docs/introduction/guide/dashboard/workflow/sandbox.mdx": "2026-02-28T12:36:59+08:00", "document/content/docs/introduction/guide/dashboard/workflow/sandbox.mdx": "2026-02-28T12:36:59+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.en.mdx": "2026-02-26T22:14:30+08:00",
...@@ -232,7 +232,7 @@ ...@@ -232,7 +232,7 @@
"document/content/docs/self-host/upgrading/4-14/4147.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4147.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4147.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4147.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4148.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4148.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/4-14/4148.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/4-14/4148.mdx": "2026-03-03T22:26:25+08:00",
"document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.en.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/40.mdx": "2026-03-03T17:39:47+08:00",
"document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00", "document/content/docs/self-host/upgrading/outdated/41.en.mdx": "2026-03-03T17:39:47+08:00",
......
...@@ -44,7 +44,8 @@ export const LogCategories = { ...@@ -44,7 +44,8 @@ export const LogCategories = {
INTERACTIVE: ['workflow', 'interactive'], INTERACTIVE: ['workflow', 'interactive'],
OPTIMIZE_CODE: ['workflow', 'optimize-code'], OPTIMIZE_CODE: ['workflow', 'optimize-code'],
STATUS: ['workflow', 'status'], STATUS: ['workflow', 'status'],
TOOLS: ['workflow', 'tools'] TOOLS: ['workflow', 'tools'],
CODE_SANDBOX: ['workflow', 'code-sandbox']
}), }),
APP: Object.assign(['app'], { APP: Object.assign(['app'], {
EVALUATION: ['app', 'evaluation'], EVALUATION: ['app', 'evaluation'],
......
import { SandboxCodeTypeEnum } from '@fastgpt/global/core/workflow/template/system/sandbox/constants'; import { SandboxCodeTypeEnum } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
import type { AxiosInstance } from 'axios'; import type { AxiosInstance } from 'axios';
import axios from 'axios'; import axios from 'axios';
import { getLogger, LogCategories } from '../../common/logger';
const logger = getLogger(LogCategories.MODULE.WORKFLOW.CODE_SANDBOX);
export type SanndboxPackagesResponse = { export type SanndboxPackagesResponse = {
js: string[]; js: string[];
...@@ -28,6 +30,7 @@ export class CodeSandbox { ...@@ -28,6 +30,7 @@ export class CodeSandbox {
(response) => { (response) => {
const data = response.data; const data = response.data;
if (!data.success) { if (!data.success) {
logger.warn('Request code sandbox failed', { data });
return Promise.reject(new Error(data.message || 'Request code sandbox failed')); return Promise.reject(new Error(data.message || 'Request code sandbox failed'));
} }
return response.data; return response.data;
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
"code_allow_packages": "Available dependencies", "code_allow_packages": "Available dependencies",
"code_allow_packages_func": "Available packages: {{modules}}\nGlobal functions: {{globals}}", "code_allow_packages_func": "Available packages: {{modules}}\nGlobal functions: {{globals}}",
"code_execution": "Code Sandbox", "code_execution": "Code Sandbox",
"code_sandbox_intro": "Execute a piece of script code, usually used for complex data processing.", "code_sandbox_intro": "Executing a script code in the sandbox can be used to perform complex data processing, but the syntax and available dependencies will be limited.",
"collection_metadata_filter": "Collection Metadata Filter", "collection_metadata_filter": "Collection Metadata Filter",
"complete_extraction_result": "Complete Extraction Result", "complete_extraction_result": "Complete Extraction Result",
"complete_extraction_result_description": "A JSON string, e.g., {\"name\":\"YY\",\"Time\":\"2023/7/2 18:00\"}", "complete_extraction_result_description": "A JSON string, e.g., {\"name\":\"YY\",\"Time\":\"2023/7/2 18:00\"}",
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
"code_allow_packages": "可用依赖", "code_allow_packages": "可用依赖",
"code_allow_packages_func": "可用依赖: {{modules}}\n全局函数: {{globals}}", "code_allow_packages_func": "可用依赖: {{modules}}\n全局函数: {{globals}}",
"code_execution": "代码运行", "code_execution": "代码运行",
"code_sandbox_intro": "执行一段脚本代码,通常用于进行复杂的数据处理。", "code_sandbox_intro": "在沙盒里执行一段脚本代码,可用于进行复杂的数据处理,语法和可用依赖会受到限制。",
"collection_metadata_filter": "集合元数据过滤", "collection_metadata_filter": "集合元数据过滤",
"complete_extraction_result": "完整提取结果", "complete_extraction_result": "完整提取结果",
"complete_extraction_result_description": "一个 JSON 字符串,例如:{\"name:\":\"YY\",\"Time\":\"2023/7/2 18:00\"}", "complete_extraction_result_description": "一个 JSON 字符串,例如:{\"name:\":\"YY\",\"Time\":\"2023/7/2 18:00\"}",
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
"code_allow_packages": "可用依賴", "code_allow_packages": "可用依賴",
"code_allow_packages_func": "可用依賴: {{modules}}\n全域函數: {{globals}}", "code_allow_packages_func": "可用依賴: {{modules}}\n全域函數: {{globals}}",
"code_execution": "程式碼執行", "code_execution": "程式碼執行",
"code_sandbox_intro": "執行一段腳本程式碼,通常用於進行複雜的資料處理。", "code_sandbox_intro": "在沙盒裡執行一段腳本程式碼,可用於進行複雜的資料處理,語法和可用依賴會受到限制。",
"collection_metadata_filter": "資料集詮釋資料篩選器", "collection_metadata_filter": "資料集詮釋資料篩選器",
"complete_extraction_result": "完整擷取結果", "complete_extraction_result": "完整擷取結果",
"complete_extraction_result_description": "一個 JSON 字串,例如:{\"name\":\"YY\",\"Time\":\"2023/7/2 18:00\"}", "complete_extraction_result_description": "一個 JSON 字串,例如:{\"name\":\"YY\",\"Time\":\"2023/7/2 18:00\"}",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
{ {
"name": "app", "name": "app",
"version": "4.14.7", "version": "4.14.8",
"private": false, "private": false,
"scripts": { "scripts": {
"dev": "NODE_OPTIONS='--max-old-space-size=8192' npm run build:workers && next dev", "dev": "NODE_OPTIONS='--max-old-space-size=8192' npm run build:workers && next dev",
......
...@@ -64,6 +64,9 @@ app.post('/sandbox/js', async (c) => { ...@@ -64,6 +64,9 @@ app.post('/sandbox/js', async (c) => {
); );
} }
const result = await jsPool.execute(parsed.data as ExecuteOptions); const result = await jsPool.execute(parsed.data as ExecuteOptions);
if (!result.success) {
console.log(`JS sandbox error: ${result.message}`);
}
return c.json(result); return c.json(result);
} catch (err: any) { } catch (err: any) {
console.log('JS sandbox error:', err); console.log('JS sandbox error:', err);
...@@ -89,6 +92,9 @@ app.post('/sandbox/python', async (c) => { ...@@ -89,6 +92,9 @@ app.post('/sandbox/python', async (c) => {
); );
} }
const result = await pythonPool.execute(parsed.data as ExecuteOptions); const result = await pythonPool.execute(parsed.data as ExecuteOptions);
if (!result.success) {
console.log(`Python sandbox error: ${result.message}`);
}
return c.json(result); return c.json(result);
} catch (err: any) { } catch (err: any) {
console.log('Python sandbox error:', err); console.log('Python sandbox error:', err);
...@@ -114,10 +120,9 @@ app.get('/sandbox/modules', (c) => { ...@@ -114,10 +120,9 @@ app.get('/sandbox/modules', (c) => {
/** 启动服务 */ /** 启动服务 */
console.log(`Sandbox server starting on port ${config.port}...`); console.log(`Sandbox server starting on port ${config.port}...`);
/** 导出 app 和 poolReady 供测试使用 */
export { app, poolReady };
export default { export default {
port: config.port, port: config.port,
fetch: app.fetch fetch: app.fetch
}; };
/** 导出 app 和 poolReady 供测试使用 */
export { app, poolReady };
...@@ -21,6 +21,7 @@ const RSS_POLL_INTERVAL = 500; ...@@ -21,6 +21,7 @@ const RSS_POLL_INTERVAL = 500;
export type PoolWorker = { export type PoolWorker = {
proc: ChildProcess; proc: ChildProcess;
rl: Interface; rl: Interface;
stderrRl: Interface;
busy: boolean; busy: boolean;
id: number; id: number;
stderrBuf: string[]; stderrBuf: string[];
...@@ -86,12 +87,12 @@ export abstract class BaseProcessPool { ...@@ -86,12 +87,12 @@ export abstract class BaseProcessPool {
for (const waiter of this.waitQueue) { for (const waiter of this.waitQueue) {
waiter.reject(new Error('Pool is shutting down')); waiter.reject(new Error('Pool is shutting down'));
} }
this.waitQueue = [];
for (const worker of this.workers) { for (const worker of this.workers) {
worker.proc.kill('SIGTERM'); this.cleanupWorker(worker);
} }
this.workers = []; this.workers = [];
this.idleWorkers = []; this.idleWorkers = [];
this.waitQueue = [];
} }
get stats() { get stats() {
...@@ -120,10 +121,10 @@ export abstract class BaseProcessPool { ...@@ -120,10 +121,10 @@ export abstract class BaseProcessPool {
}); });
const rl = createInterface({ input: proc.stdout!, terminal: false }); const rl = createInterface({ input: proc.stdout!, terminal: false });
const worker: PoolWorker = { proc, rl, busy: false, id, stderrBuf: [] }; const stderrRl = createInterface({ input: proc.stderr!, terminal: false });
const worker: PoolWorker = { proc, rl, stderrRl, busy: false, id, stderrBuf: [] };
// 收集 stderr 用于调试(保留最近 20 行) // 收集 stderr 用于调试(保留最近 20 行)
const stderrRl = createInterface({ input: proc.stderr!, terminal: false });
stderrRl.on('line', (line: string) => { stderrRl.on('line', (line: string) => {
worker.stderrBuf.push(line); worker.stderrBuf.push(line);
if (worker.stderrBuf.length > 20) worker.stderrBuf.shift(); if (worker.stderrBuf.length > 20) worker.stderrBuf.shift();
...@@ -219,6 +220,7 @@ export abstract class BaseProcessPool { ...@@ -219,6 +220,7 @@ export abstract class BaseProcessPool {
protected removeWorker(worker: PoolWorker): boolean { protected removeWorker(worker: PoolWorker): boolean {
const idx = this.workers.indexOf(worker); const idx = this.workers.indexOf(worker);
if (idx === -1) return false; if (idx === -1) return false;
this.cleanupWorker(worker);
this.workers.splice(idx, 1); this.workers.splice(idx, 1);
this.idleWorkers = this.idleWorkers.filter((w) => w !== worker); this.idleWorkers = this.idleWorkers.filter((w) => w !== worker);
return true; return true;
...@@ -447,7 +449,6 @@ export abstract class BaseProcessPool { ...@@ -447,7 +449,6 @@ export abstract class BaseProcessPool {
/** 从池中移除 worker,kill 进程,并在 ready 时 respawn */ /** 从池中移除 worker,kill 进程,并在 ready 时 respawn */
protected killAndRespawn(worker: PoolWorker): void { protected killAndRespawn(worker: PoolWorker): void {
this.removeWorker(worker); this.removeWorker(worker);
worker.proc.kill('SIGKILL');
if (this.ready) { if (this.ready) {
this.spawnWorker().catch((err) => { this.spawnWorker().catch((err) => {
console.error(`${this.tag}: failed to respawn worker ${worker.id}:`, err.message); console.error(`${this.tag}: failed to respawn worker ${worker.id}:`, err.message);
...@@ -455,6 +456,33 @@ export abstract class BaseProcessPool { ...@@ -455,6 +456,33 @@ export abstract class BaseProcessPool {
} }
} }
/**
* 彻底清理 worker 资源
* 关闭 readline 接口、移除事件监听器、清空缓冲区、kill 进程
*/
protected cleanupWorker(worker: PoolWorker): void {
try {
// 关闭 readline 接口
worker.rl.close();
worker.stderrRl.close();
// 移除所有事件监听器
worker.rl.removeAllListeners();
worker.stderrRl.removeAllListeners();
worker.proc.removeAllListeners();
// 清空缓冲区
worker.stderrBuf = [];
// Kill 进程
if (!worker.proc.killed) {
worker.proc.kill('SIGKILL');
}
} catch (err) {
// 忽略清理错误
}
}
/** 格式化 stderr 缓冲区用于错误信息 */ /** 格式化 stderr 缓冲区用于错误信息 */
protected formatStderr(worker: PoolWorker): string { protected formatStderr(worker: PoolWorker): string {
return worker.stderrBuf.length > 0 ? ` | stderr: ${worker.stderrBuf.join('\n')}` : ''; return worker.stderrBuf.length > 0 ? ` | stderr: ${worker.stderrBuf.join('\n')}` : '';
......
...@@ -605,7 +605,13 @@ def main_loop(): ...@@ -605,7 +605,13 @@ def main_loop():
if len(params) == 0: if len(params) == 0:
result = user_main() result = user_main()
elif len(params) == 1: elif len(params) == 1:
result = user_main(variables) p = params[0]
# If param name matches a variable key, pass that value directly.
# Otherwise fall back to passing the entire variables dict.
if p in variables:
result = user_main(variables[p])
else:
result = user_main(variables)
else: else:
call_args = [] call_args = []
for p in params: for p in params:
......
...@@ -353,17 +353,23 @@ rl.on('line', async (line: string) => { ...@@ -353,17 +353,23 @@ rl.on('line', async (line: string) => {
const logs: string[] = []; const logs: string[] = [];
let logSize = 0; let logSize = 0;
const MAX_LOG_SIZE = 1024 * 1024; // 1MB const MAX_LOG_SIZE = 1024 * 1024; // 1MB
const safeConsole = { const _consoleLog = (...args: any[]) => {
log(...args: any[]) { const line = args.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))).join(' ');
const line = args if (logSize + line.length <= MAX_LOG_SIZE) {
.map((a) => (typeof a === 'object' ? JSON.stringify(a) : String(a))) logs.push(line);
.join(' '); logSize += line.length;
if (logSize + line.length <= MAX_LOG_SIZE) {
logs.push(line);
logSize += line.length;
}
} }
}; };
const safeConsole = {
log: _consoleLog,
info: _consoleLog,
warn: _consoleLog,
error: _consoleLog,
debug: _consoleLog,
trace: _consoleLog,
dir: _consoleLog,
table: _consoleLog
};
let timer: ReturnType<typeof setTimeout> | undefined; let timer: ReturnType<typeof setTimeout> | undefined;
try { try {
......
...@@ -493,6 +493,24 @@ describe('Python 功能测试', () => { ...@@ -493,6 +493,24 @@ describe('Python 功能测试', () => {
code: `def main(name, age):\n return {'name': name, 'age': age}`, code: `def main(name, age):\n return {'name': name, 'age': age}`,
variables: { name: 'test', age: 25 }, variables: { name: 'test', age: 25 },
expect: { success: true, codeReturn: { name: 'test', age: 25 } } expect: { success: true, codeReturn: { name: 'test', age: 25 } }
},
{
name: 'main(content) 单参数按名取字符串值,可调用 split',
code: `def main(content):\n parts = content.split(',')\n return {'parts': parts, 'count': len(parts)}`,
variables: { content: 'a,b,c' },
expect: { success: true, codeReturn: { parts: ['a', 'b', 'c'], count: 3 } }
},
{
name: 'main(items) 单参数按名取列表值',
code: `def main(items):\n return {'sum': sum(items), 'len': len(items)}`,
variables: { items: [1, 2, 3, 4, 5] },
expect: { success: true, codeReturn: { sum: 15, len: 5 } }
},
{
name: 'main(v) 单参数名不在 variables 时回退传整个 dict',
code: `def main(v):\n return {'a': v['x'], 'b': v['y']}`,
variables: { x: 10, y: 20 },
expect: { success: true, codeReturn: { a: 10, b: 20 } }
} }
] ]
); );
......
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