Skip to content

Commit 565a966

Browse files
c121914yudogfargggaaallleee
authored
Python Sandbox (#4380)
* Python3 Sandbox (#3944) * update python box (#4251) * update python box * Adjust the height of the NodeCode border. * update python sandbox and add test systemcall bash * update sandbox * add VERSION_RELEASE (#4376) * save empty docx * fix pythonbox log error * fix: js template --------- Co-authored-by: dogfar <[email protected]> Co-authored-by: gggaaallleee <[email protected]> Co-authored-by: gggaaallleee <[email protected]>
1 parent 8323c2d commit 565a966

File tree

23 files changed

+778
-93
lines changed

23 files changed

+778
-93
lines changed
Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
export const JS_TEMPLATE = `function main({data1, data2}){
22
3+
return {
4+
result: data1,
5+
data2
6+
}
7+
}`;
8+
9+
export const PY_TEMPLATE = `def main(data1, data2):
310
return {
4-
result: data1,
5-
data2
11+
"result": data1,
12+
"data2": data2
613
}
7-
}`;
14+
`;
15+
16+
export enum SandboxCodeTypeEnum {
17+
js = 'js',
18+
py = 'py'
19+
}
20+
export const SNADBOX_CODE_TEMPLATE = {
21+
[SandboxCodeTypeEnum.js]: JS_TEMPLATE,
22+
[SandboxCodeTypeEnum.py]: PY_TEMPLATE
23+
};

packages/global/core/workflow/template/system/sandbox/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,14 @@ export const CodeNode: FlowNodeTemplateType = {
6868
key: NodeInputKeyEnum.codeType,
6969
renderTypeList: [FlowNodeInputTypeEnum.hidden],
7070
label: '',
71+
valueType: WorkflowIOValueTypeEnum.string,
7172
value: 'js'
7273
},
7374
{
7475
key: NodeInputKeyEnum.code,
7576
renderTypeList: [FlowNodeInputTypeEnum.custom],
7677
label: '',
78+
valueType: WorkflowIOValueTypeEnum.string,
7779
value: JS_TEMPLATE
7880
}
7981
],

packages/service/core/workflow/dispatch/code/run.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/ty
44
import axios from 'axios';
55
import { formatHttpError } from '../utils';
66
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
7+
import { SandboxCodeTypeEnum } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
78

89
type RunCodeType = ModuleDispatchProps<{
9-
[NodeInputKeyEnum.codeType]: 'js';
10+
[NodeInputKeyEnum.codeType]: string;
1011
[NodeInputKeyEnum.code]: string;
1112
[NodeInputKeyEnum.addInputParam]: Record<string, any>;
1213
}>;
@@ -16,6 +17,14 @@ type RunCodeResponse = DispatchNodeResultType<{
1617
[key: string]: any;
1718
}>;
1819

20+
function getURL(codeType: string): string {
21+
if (codeType == SandboxCodeTypeEnum.py) {
22+
return `${process.env.SANDBOX_URL}/sandbox/python`;
23+
} else {
24+
return `${process.env.SANDBOX_URL}/sandbox/js`;
25+
}
26+
}
27+
1928
export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeResponse> => {
2029
const {
2130
params: { codeType, code, [NodeInputKeyEnum.addInputParam]: customVariables }
@@ -27,7 +36,7 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
2736
};
2837
}
2938

30-
const sandBoxRequestUrl = `${process.env.SANDBOX_URL}/sandbox/js`;
39+
const sandBoxRequestUrl = getURL(codeType);
3140
try {
3241
const { data: runResult } = await axios.post<{
3342
success: boolean;
@@ -40,6 +49,8 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
4049
variables: customVariables
4150
});
4251

52+
console.log(runResult);
53+
4354
if (runResult.success) {
4455
return {
4556
[NodeOutputKeyEnum.rawResponse]: runResult.data.codeReturn,
@@ -52,7 +63,7 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
5263
...runResult.data.codeReturn
5364
};
5465
} else {
55-
throw new Error('Run code failed');
66+
return Promise.reject('Run code failed');
5667
}
5768
} catch (error) {
5869
return {

packages/service/core/workflow/dispatch/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI
106106
/* value type format */
107107
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
108108
if (value === undefined) return;
109+
if (!type) return value;
109110

110111
if (type === 'string') {
111112
if (typeof value !== 'object') return String(value);

packages/service/worker/readFile/extension/docx.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export const readDocsFile = async ({ buffer }: ReadRawTextByBuffer): Promise<Rea
1313
buffer
1414
},
1515
{
16+
ignoreEmptyParagraphs: false,
1617
convertImage: images.imgElement(async (image) => {
1718
const imageBase64 = await image.readAsBase64String();
1819
const uuid = crypto.randomUUID();

packages/web/components/common/Textarea/CodeEditor/Editor.tsx

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React, { useCallback, useRef, useState } from 'react';
1+
import React, { useCallback, useRef, useState, useEffect } from 'react';
22
import Editor, { Monaco, loader } from '@monaco-editor/react';
33
import { Box, BoxProps } from '@chakra-ui/react';
44
import MyIcon from '../../Icon';
55
import { getWebReqUrl } from '../../../../common/system/utils';
6-
6+
import usePythonCompletion from './usePythonCompletion';
77
loader.config({
88
paths: { vs: getWebReqUrl('/js/monaco-editor.0.45.0/vs') }
99
});
@@ -21,6 +21,7 @@ export type Props = Omit<BoxProps, 'resize' | 'onChange'> & {
2121
onOpenModal?: () => void;
2222
variables?: EditorVariablePickerType[];
2323
defaultHeight?: number;
24+
language?: string;
2425
};
2526

2627
const options = {
@@ -53,11 +54,14 @@ const MyEditor = ({
5354
variables = [],
5455
defaultHeight = 200,
5556
onOpenModal,
57+
language = 'typescript',
5658
...props
5759
}: Props) => {
5860
const [height, setHeight] = useState(defaultHeight);
5961
const initialY = useRef(0);
6062

63+
const registerPythonCompletion = usePythonCompletion();
64+
6165
const handleMouseDown = useCallback((e: React.MouseEvent) => {
6266
initialY.current = e.clientY;
6367

@@ -76,35 +80,47 @@ const MyEditor = ({
7680
document.addEventListener('mouseup', handleMouseUp);
7781
}, []);
7882

79-
const beforeMount = useCallback((monaco: Monaco) => {
80-
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
81-
validate: false,
82-
allowComments: false,
83-
schemas: [
84-
{
85-
uri: 'http://myserver/foo-schema.json', // 一个假设的 URI
86-
fileMatch: ['*'], // 匹配所有文件
87-
schema: {} // 空的 Schema
88-
}
89-
]
90-
});
83+
const editorRef = useRef<any>(null);
84+
const monacoRef = useRef<Monaco | null>(null);
9185

92-
monaco.editor.defineTheme('JSONEditorTheme', {
93-
base: 'vs', // 可以基于已有的主题进行定制
94-
inherit: true, // 继承基础主题的设置
95-
rules: [{ token: 'variable', foreground: '2B5FD9' }],
96-
colors: {
97-
'editor.background': '#ffffff00',
98-
'editorLineNumber.foreground': '#aaa',
99-
'editorOverviewRuler.border': '#ffffff00',
100-
'editor.lineHighlightBackground': '#F7F8FA',
101-
'scrollbarSlider.background': '#E8EAEC',
102-
'editorIndentGuide.activeBackground': '#ddd',
103-
'editorIndentGuide.background': '#eee'
104-
}
105-
});
86+
const handleEditorDidMount = useCallback((editor: any, monaco: Monaco) => {
87+
editorRef.current = editor;
88+
monacoRef.current = monaco;
10689
}, []);
10790

91+
const beforeMount = useCallback(
92+
(monaco: Monaco) => {
93+
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
94+
validate: false,
95+
allowComments: false,
96+
schemas: [
97+
{
98+
uri: 'http://myserver/foo-schema.json', // 一个假设的 URI
99+
fileMatch: ['*'], // 匹配所有文件
100+
schema: {} // 空的 Schema
101+
}
102+
]
103+
});
104+
105+
monaco.editor.defineTheme('JSONEditorTheme', {
106+
base: 'vs', // 可以基于已有的主题进行定制
107+
inherit: true, // 继承基础主题的设置
108+
rules: [{ token: 'variable', foreground: '2B5FD9' }],
109+
colors: {
110+
'editor.background': '#ffffff00',
111+
'editorLineNumber.foreground': '#aaa',
112+
'editorOverviewRuler.border': '#ffffff00',
113+
'editor.lineHighlightBackground': '#F7F8FA',
114+
'scrollbarSlider.background': '#E8EAEC',
115+
'editorIndentGuide.activeBackground': '#ddd',
116+
'editorIndentGuide.background': '#eee'
117+
}
118+
});
119+
registerPythonCompletion(monaco);
120+
},
121+
[registerPythonCompletion]
122+
);
123+
108124
return (
109125
<Box
110126
borderWidth={'1px'}
@@ -118,7 +134,7 @@ const MyEditor = ({
118134
>
119135
<Editor
120136
height={'100%'}
121-
defaultLanguage="typescript"
137+
language={language}
122138
options={options as any}
123139
theme="JSONEditorTheme"
124140
beforeMount={beforeMount}
@@ -127,6 +143,7 @@ const MyEditor = ({
127143
onChange={(e) => {
128144
onChange?.(e || '');
129145
}}
146+
onMount={handleEditorDidMount}
130147
/>
131148
{resize && (
132149
<Box

packages/web/components/common/Textarea/CodeEditor/index.tsx

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,31 @@ import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react'
44
import MyModal from '../../MyModal';
55
import { useTranslation } from 'next-i18next';
66

7-
type Props = Omit<EditorProps, 'resize'> & {};
7+
type Props = Omit<EditorProps, 'resize'> & { language?: string };
8+
function getLanguage(language: string | undefined): string {
9+
let fullName: string;
10+
switch (language) {
11+
case 'py':
12+
fullName = 'python';
13+
break;
14+
case 'js':
15+
fullName = 'typescript';
16+
break;
17+
default:
18+
fullName = `typescript`;
19+
break;
20+
}
21+
return fullName;
22+
}
823

924
const CodeEditor = (props: Props) => {
1025
const { t } = useTranslation();
1126
const { isOpen, onOpen, onClose } = useDisclosure();
12-
27+
const { language, ...otherProps } = props;
28+
const fullName = getLanguage(language);
1329
return (
1430
<>
15-
<MyEditor {...props} resize onOpenModal={onOpen} />
31+
<MyEditor {...props} resize onOpenModal={onOpen} language={fullName} />
1632
<MyModal
1733
isOpen={isOpen}
1834
onClose={onClose}
@@ -23,7 +39,7 @@ const CodeEditor = (props: Props) => {
2339
isCentered
2440
>
2541
<ModalBody flex={'1 0 0'} overflow={'auto'}>
26-
<MyEditor {...props} bg={'myGray.50'} height={'100%'} />
42+
<MyEditor {...props} bg={'myGray.50'} height={'100%'} language={fullName} />
2743
</ModalBody>
2844
<ModalFooter>
2945
<Button mr={2} onClick={onClose} px={6}>
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { Monaco } from '@monaco-editor/react';
2+
import { useCallback } from 'react';
3+
let monacoInstance: Monaco | null = null;
4+
const usePythonCompletion = () => {
5+
return useCallback((monaco: Monaco) => {
6+
if (monacoInstance === monaco) return;
7+
monacoInstance = monaco;
8+
9+
monaco.languages.registerCompletionItemProvider('python', {
10+
provideCompletionItems: (model, position) => {
11+
const wordInfo = model.getWordUntilPosition(position);
12+
const currentWordPrefix = wordInfo.word;
13+
14+
const lineContent = model.getLineContent(position.lineNumber);
15+
16+
const range = {
17+
startLineNumber: position.lineNumber,
18+
endLineNumber: position.lineNumber,
19+
startColumn: wordInfo.startColumn,
20+
endColumn: wordInfo.endColumn
21+
};
22+
23+
const baseSuggestions = [
24+
{
25+
label: 'len',
26+
kind: monaco.languages.CompletionItemKind.Function,
27+
insertText: 'len()',
28+
documentation: 'get length of object',
29+
range,
30+
sortText: 'a'
31+
}
32+
];
33+
34+
const filtered = baseSuggestions.filter((item) =>
35+
item.label.toLowerCase().startsWith(currentWordPrefix.toLowerCase())
36+
);
37+
38+
if (lineContent.startsWith('import')) {
39+
const importLength = 'import'.length;
40+
const afterImport = lineContent.slice(importLength);
41+
const spaceMatch = afterImport.match(/^\s*/);
42+
const spaceLength = spaceMatch ? spaceMatch[0].length : 0;
43+
44+
const startReplaceCol = importLength + spaceLength + 1;
45+
const currentCol = position.column;
46+
47+
const replaceRange = new monaco.Range(
48+
position.lineNumber,
49+
startReplaceCol,
50+
position.lineNumber,
51+
currentCol
52+
);
53+
54+
const needsSpace = spaceLength === 0;
55+
return {
56+
suggestions: [
57+
{
58+
label: 'numpy',
59+
kind: monaco.languages.CompletionItemKind.Module,
60+
insertText: `${needsSpace ? ' ' : ''}numpy as np`,
61+
documentation: 'numerical computing library',
62+
range: replaceRange,
63+
sortText: 'a'
64+
},
65+
{
66+
label: 'pandas',
67+
kind: monaco.languages.CompletionItemKind.Module,
68+
insertText: `${needsSpace ? ' ' : ''}pandas as pd`,
69+
documentation: 'data analysis library',
70+
range: replaceRange
71+
}
72+
]
73+
};
74+
}
75+
76+
return { suggestions: filtered };
77+
},
78+
triggerCharacters: ['.', '_']
79+
});
80+
}, []);
81+
};
82+
83+
export default usePythonCompletion;

packages/web/i18n/en/workflow.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"classification_result": "Classification Result",
2121
"code.Reset template": "Reset Template",
2222
"code.Reset template confirm": "Confirm reset code template? This will reset all inputs and outputs to template values. Please save your current code.",
23+
"code.Switch language confirm": "Switching the language will reset the code, will it continue?",
2324
"code_execution": "Code Sandbox",
2425
"collection_metadata_filter": "Collection Metadata Filter",
2526
"complete_extraction_result": "Complete Extraction Result",
@@ -153,6 +154,7 @@
153154
"select_another_application_to_call": "You can choose another application to call",
154155
"special_array_format": "Special array format, returns an empty array when the search result is empty.",
155156
"start_with": "Starts With",
157+
"support_code_language": "Support import list: pandas,numpy",
156158
"target_fields_description": "A target field consists of 'description' and 'key'. Multiple target fields can be extracted.",
157159
"template.ai_chat": "AI Chat",
158160
"template.ai_chat_intro": "AI Large Model Chat",

packages/web/i18n/zh-CN/workflow.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"classification_result": "分类结果",
2121
"code.Reset template": "还原模板",
2222
"code.Reset template confirm": "确认还原代码模板?将会重置所有输入和输出至模板值,请注意保存当前代码。",
23+
"code.Switch language confirm": "切换语言将重置代码,是否继续?",
2324
"code_execution": "代码运行",
2425
"collection_metadata_filter": "集合元数据过滤",
2526
"complete_extraction_result": "完整提取结果",
@@ -153,6 +154,7 @@
153154
"select_another_application_to_call": "可以选择一个其他应用进行调用",
154155
"special_array_format": "特殊数组格式,搜索结果为空时,返回空数组。",
155156
"start_with": "开始为",
157+
"support_code_language": "支持import列表:pandas,numpy",
156158
"target_fields_description": "由 '描述' 和 'key' 组成一个目标字段,可提取多个目标字段",
157159
"template.ai_chat": "AI 对话",
158160
"template.ai_chat_intro": "AI 大模型对话",

0 commit comments

Comments
 (0)