mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-22 20:37:48 +00:00
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 <37035781+dogfar@users.noreply.github.com> Co-authored-by: gggaaallleee <91131304+gggaaallleee@users.noreply.github.com> Co-authored-by: gggaaallleee <1293587368@qq.com>
This commit is contained in:
@@ -1,7 +1,23 @@
|
||||
export const JS_TEMPLATE = `function main({data1, data2}){
|
||||
|
||||
return {
|
||||
result: data1,
|
||||
data2
|
||||
}
|
||||
return {
|
||||
result: data1,
|
||||
data2
|
||||
}
|
||||
}`;
|
||||
|
||||
export const PY_TEMPLATE = `def main(data1, data2):
|
||||
return {
|
||||
"result": data1,
|
||||
"data2": data2
|
||||
}
|
||||
`;
|
||||
|
||||
export enum SandboxCodeTypeEnum {
|
||||
js = 'js',
|
||||
py = 'py'
|
||||
}
|
||||
export const SNADBOX_CODE_TEMPLATE = {
|
||||
[SandboxCodeTypeEnum.js]: JS_TEMPLATE,
|
||||
[SandboxCodeTypeEnum.py]: PY_TEMPLATE
|
||||
};
|
||||
|
@@ -68,12 +68,14 @@ export const CodeNode: FlowNodeTemplateType = {
|
||||
key: NodeInputKeyEnum.codeType,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.hidden],
|
||||
label: '',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
value: 'js'
|
||||
},
|
||||
{
|
||||
key: NodeInputKeyEnum.code,
|
||||
renderTypeList: [FlowNodeInputTypeEnum.custom],
|
||||
label: '',
|
||||
valueType: WorkflowIOValueTypeEnum.string,
|
||||
value: JS_TEMPLATE
|
||||
}
|
||||
],
|
||||
|
@@ -4,9 +4,10 @@ import { DispatchNodeResultType } from '@fastgpt/global/core/workflow/runtime/ty
|
||||
import axios from 'axios';
|
||||
import { formatHttpError } from '../utils';
|
||||
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
|
||||
import { SandboxCodeTypeEnum } from '@fastgpt/global/core/workflow/template/system/sandbox/constants';
|
||||
|
||||
type RunCodeType = ModuleDispatchProps<{
|
||||
[NodeInputKeyEnum.codeType]: 'js';
|
||||
[NodeInputKeyEnum.codeType]: string;
|
||||
[NodeInputKeyEnum.code]: string;
|
||||
[NodeInputKeyEnum.addInputParam]: Record<string, any>;
|
||||
}>;
|
||||
@@ -16,6 +17,14 @@ type RunCodeResponse = DispatchNodeResultType<{
|
||||
[key: string]: any;
|
||||
}>;
|
||||
|
||||
function getURL(codeType: string): string {
|
||||
if (codeType == SandboxCodeTypeEnum.py) {
|
||||
return `${process.env.SANDBOX_URL}/sandbox/python`;
|
||||
} else {
|
||||
return `${process.env.SANDBOX_URL}/sandbox/js`;
|
||||
}
|
||||
}
|
||||
|
||||
export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeResponse> => {
|
||||
const {
|
||||
params: { codeType, code, [NodeInputKeyEnum.addInputParam]: customVariables }
|
||||
@@ -27,7 +36,7 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
|
||||
};
|
||||
}
|
||||
|
||||
const sandBoxRequestUrl = `${process.env.SANDBOX_URL}/sandbox/js`;
|
||||
const sandBoxRequestUrl = getURL(codeType);
|
||||
try {
|
||||
const { data: runResult } = await axios.post<{
|
||||
success: boolean;
|
||||
@@ -40,6 +49,8 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
|
||||
variables: customVariables
|
||||
});
|
||||
|
||||
console.log(runResult);
|
||||
|
||||
if (runResult.success) {
|
||||
return {
|
||||
[NodeOutputKeyEnum.rawResponse]: runResult.data.codeReturn,
|
||||
@@ -52,7 +63,7 @@ export const dispatchRunCode = async (props: RunCodeType): Promise<RunCodeRespon
|
||||
...runResult.data.codeReturn
|
||||
};
|
||||
} else {
|
||||
throw new Error('Run code failed');
|
||||
return Promise.reject('Run code failed');
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
@@ -106,6 +106,7 @@ export const getHistories = (history?: ChatItemType[] | number, histories: ChatI
|
||||
/* value type format */
|
||||
export const valueTypeFormat = (value: any, type?: WorkflowIOValueTypeEnum) => {
|
||||
if (value === undefined) return;
|
||||
if (!type) return value;
|
||||
|
||||
if (type === 'string') {
|
||||
if (typeof value !== 'object') return String(value);
|
||||
|
@@ -13,6 +13,7 @@ export const readDocsFile = async ({ buffer }: ReadRawTextByBuffer): Promise<Rea
|
||||
buffer
|
||||
},
|
||||
{
|
||||
ignoreEmptyParagraphs: false,
|
||||
convertImage: images.imgElement(async (image) => {
|
||||
const imageBase64 = await image.readAsBase64String();
|
||||
const uuid = crypto.randomUUID();
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState, useEffect } from 'react';
|
||||
import Editor, { Monaco, loader } from '@monaco-editor/react';
|
||||
import { Box, BoxProps } from '@chakra-ui/react';
|
||||
import MyIcon from '../../Icon';
|
||||
import { getWebReqUrl } from '../../../../common/system/utils';
|
||||
|
||||
import usePythonCompletion from './usePythonCompletion';
|
||||
loader.config({
|
||||
paths: { vs: getWebReqUrl('/js/monaco-editor.0.45.0/vs') }
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export type Props = Omit<BoxProps, 'resize' | 'onChange'> & {
|
||||
onOpenModal?: () => void;
|
||||
variables?: EditorVariablePickerType[];
|
||||
defaultHeight?: number;
|
||||
language?: string;
|
||||
};
|
||||
|
||||
const options = {
|
||||
@@ -53,11 +54,14 @@ const MyEditor = ({
|
||||
variables = [],
|
||||
defaultHeight = 200,
|
||||
onOpenModal,
|
||||
language = 'typescript',
|
||||
...props
|
||||
}: Props) => {
|
||||
const [height, setHeight] = useState(defaultHeight);
|
||||
const initialY = useRef(0);
|
||||
|
||||
const registerPythonCompletion = usePythonCompletion();
|
||||
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
initialY.current = e.clientY;
|
||||
|
||||
@@ -76,35 +80,47 @@ const MyEditor = ({
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
}, []);
|
||||
|
||||
const beforeMount = useCallback((monaco: Monaco) => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: false,
|
||||
allowComments: false,
|
||||
schemas: [
|
||||
{
|
||||
uri: 'http://myserver/foo-schema.json', // 一个假设的 URI
|
||||
fileMatch: ['*'], // 匹配所有文件
|
||||
schema: {} // 空的 Schema
|
||||
}
|
||||
]
|
||||
});
|
||||
const editorRef = useRef<any>(null);
|
||||
const monacoRef = useRef<Monaco | null>(null);
|
||||
|
||||
monaco.editor.defineTheme('JSONEditorTheme', {
|
||||
base: 'vs', // 可以基于已有的主题进行定制
|
||||
inherit: true, // 继承基础主题的设置
|
||||
rules: [{ token: 'variable', foreground: '2B5FD9' }],
|
||||
colors: {
|
||||
'editor.background': '#ffffff00',
|
||||
'editorLineNumber.foreground': '#aaa',
|
||||
'editorOverviewRuler.border': '#ffffff00',
|
||||
'editor.lineHighlightBackground': '#F7F8FA',
|
||||
'scrollbarSlider.background': '#E8EAEC',
|
||||
'editorIndentGuide.activeBackground': '#ddd',
|
||||
'editorIndentGuide.background': '#eee'
|
||||
}
|
||||
});
|
||||
const handleEditorDidMount = useCallback((editor: any, monaco: Monaco) => {
|
||||
editorRef.current = editor;
|
||||
monacoRef.current = monaco;
|
||||
}, []);
|
||||
|
||||
const beforeMount = useCallback(
|
||||
(monaco: Monaco) => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: false,
|
||||
allowComments: false,
|
||||
schemas: [
|
||||
{
|
||||
uri: 'http://myserver/foo-schema.json', // 一个假设的 URI
|
||||
fileMatch: ['*'], // 匹配所有文件
|
||||
schema: {} // 空的 Schema
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
monaco.editor.defineTheme('JSONEditorTheme', {
|
||||
base: 'vs', // 可以基于已有的主题进行定制
|
||||
inherit: true, // 继承基础主题的设置
|
||||
rules: [{ token: 'variable', foreground: '2B5FD9' }],
|
||||
colors: {
|
||||
'editor.background': '#ffffff00',
|
||||
'editorLineNumber.foreground': '#aaa',
|
||||
'editorOverviewRuler.border': '#ffffff00',
|
||||
'editor.lineHighlightBackground': '#F7F8FA',
|
||||
'scrollbarSlider.background': '#E8EAEC',
|
||||
'editorIndentGuide.activeBackground': '#ddd',
|
||||
'editorIndentGuide.background': '#eee'
|
||||
}
|
||||
});
|
||||
registerPythonCompletion(monaco);
|
||||
},
|
||||
[registerPythonCompletion]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderWidth={'1px'}
|
||||
@@ -118,7 +134,7 @@ const MyEditor = ({
|
||||
>
|
||||
<Editor
|
||||
height={'100%'}
|
||||
defaultLanguage="typescript"
|
||||
language={language}
|
||||
options={options as any}
|
||||
theme="JSONEditorTheme"
|
||||
beforeMount={beforeMount}
|
||||
@@ -127,6 +143,7 @@ const MyEditor = ({
|
||||
onChange={(e) => {
|
||||
onChange?.(e || '');
|
||||
}}
|
||||
onMount={handleEditorDidMount}
|
||||
/>
|
||||
{resize && (
|
||||
<Box
|
||||
|
@@ -4,15 +4,31 @@ import { Button, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react'
|
||||
import MyModal from '../../MyModal';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
type Props = Omit<EditorProps, 'resize'> & {};
|
||||
type Props = Omit<EditorProps, 'resize'> & { language?: string };
|
||||
function getLanguage(language: string | undefined): string {
|
||||
let fullName: string;
|
||||
switch (language) {
|
||||
case 'py':
|
||||
fullName = 'python';
|
||||
break;
|
||||
case 'js':
|
||||
fullName = 'typescript';
|
||||
break;
|
||||
default:
|
||||
fullName = `typescript`;
|
||||
break;
|
||||
}
|
||||
return fullName;
|
||||
}
|
||||
|
||||
const CodeEditor = (props: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
const { language, ...otherProps } = props;
|
||||
const fullName = getLanguage(language);
|
||||
return (
|
||||
<>
|
||||
<MyEditor {...props} resize onOpenModal={onOpen} />
|
||||
<MyEditor {...props} resize onOpenModal={onOpen} language={fullName} />
|
||||
<MyModal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
@@ -23,7 +39,7 @@ const CodeEditor = (props: Props) => {
|
||||
isCentered
|
||||
>
|
||||
<ModalBody flex={'1 0 0'} overflow={'auto'}>
|
||||
<MyEditor {...props} bg={'myGray.50'} height={'100%'} />
|
||||
<MyEditor {...props} bg={'myGray.50'} height={'100%'} language={fullName} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button mr={2} onClick={onClose} px={6}>
|
||||
|
@@ -0,0 +1,83 @@
|
||||
import { Monaco } from '@monaco-editor/react';
|
||||
import { useCallback } from 'react';
|
||||
let monacoInstance: Monaco | null = null;
|
||||
const usePythonCompletion = () => {
|
||||
return useCallback((monaco: Monaco) => {
|
||||
if (monacoInstance === monaco) return;
|
||||
monacoInstance = monaco;
|
||||
|
||||
monaco.languages.registerCompletionItemProvider('python', {
|
||||
provideCompletionItems: (model, position) => {
|
||||
const wordInfo = model.getWordUntilPosition(position);
|
||||
const currentWordPrefix = wordInfo.word;
|
||||
|
||||
const lineContent = model.getLineContent(position.lineNumber);
|
||||
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: wordInfo.startColumn,
|
||||
endColumn: wordInfo.endColumn
|
||||
};
|
||||
|
||||
const baseSuggestions = [
|
||||
{
|
||||
label: 'len',
|
||||
kind: monaco.languages.CompletionItemKind.Function,
|
||||
insertText: 'len()',
|
||||
documentation: 'get length of object',
|
||||
range,
|
||||
sortText: 'a'
|
||||
}
|
||||
];
|
||||
|
||||
const filtered = baseSuggestions.filter((item) =>
|
||||
item.label.toLowerCase().startsWith(currentWordPrefix.toLowerCase())
|
||||
);
|
||||
|
||||
if (lineContent.startsWith('import')) {
|
||||
const importLength = 'import'.length;
|
||||
const afterImport = lineContent.slice(importLength);
|
||||
const spaceMatch = afterImport.match(/^\s*/);
|
||||
const spaceLength = spaceMatch ? spaceMatch[0].length : 0;
|
||||
|
||||
const startReplaceCol = importLength + spaceLength + 1;
|
||||
const currentCol = position.column;
|
||||
|
||||
const replaceRange = new monaco.Range(
|
||||
position.lineNumber,
|
||||
startReplaceCol,
|
||||
position.lineNumber,
|
||||
currentCol
|
||||
);
|
||||
|
||||
const needsSpace = spaceLength === 0;
|
||||
return {
|
||||
suggestions: [
|
||||
{
|
||||
label: 'numpy',
|
||||
kind: monaco.languages.CompletionItemKind.Module,
|
||||
insertText: `${needsSpace ? ' ' : ''}numpy as np`,
|
||||
documentation: 'numerical computing library',
|
||||
range: replaceRange,
|
||||
sortText: 'a'
|
||||
},
|
||||
{
|
||||
label: 'pandas',
|
||||
kind: monaco.languages.CompletionItemKind.Module,
|
||||
insertText: `${needsSpace ? ' ' : ''}pandas as pd`,
|
||||
documentation: 'data analysis library',
|
||||
range: replaceRange
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
return { suggestions: filtered };
|
||||
},
|
||||
triggerCharacters: ['.', '_']
|
||||
});
|
||||
}, []);
|
||||
};
|
||||
|
||||
export default usePythonCompletion;
|
@@ -20,6 +20,7 @@
|
||||
"classification_result": "Classification Result",
|
||||
"code.Reset template": "Reset Template",
|
||||
"code.Reset template confirm": "Confirm reset code template? This will reset all inputs and outputs to template values. Please save your current code.",
|
||||
"code.Switch language confirm": "Switching the language will reset the code, will it continue?",
|
||||
"code_execution": "Code Sandbox",
|
||||
"collection_metadata_filter": "Collection Metadata Filter",
|
||||
"complete_extraction_result": "Complete Extraction Result",
|
||||
@@ -153,6 +154,7 @@
|
||||
"select_another_application_to_call": "You can choose another application to call",
|
||||
"special_array_format": "Special array format, returns an empty array when the search result is empty.",
|
||||
"start_with": "Starts With",
|
||||
"support_code_language": "Support import list: pandas,numpy",
|
||||
"target_fields_description": "A target field consists of 'description' and 'key'. Multiple target fields can be extracted.",
|
||||
"template.ai_chat": "AI Chat",
|
||||
"template.ai_chat_intro": "AI Large Model Chat",
|
||||
|
@@ -20,6 +20,7 @@
|
||||
"classification_result": "分类结果",
|
||||
"code.Reset template": "还原模板",
|
||||
"code.Reset template confirm": "确认还原代码模板?将会重置所有输入和输出至模板值,请注意保存当前代码。",
|
||||
"code.Switch language confirm": "切换语言将重置代码,是否继续?",
|
||||
"code_execution": "代码运行",
|
||||
"collection_metadata_filter": "集合元数据过滤",
|
||||
"complete_extraction_result": "完整提取结果",
|
||||
@@ -153,6 +154,7 @@
|
||||
"select_another_application_to_call": "可以选择一个其他应用进行调用",
|
||||
"special_array_format": "特殊数组格式,搜索结果为空时,返回空数组。",
|
||||
"start_with": "开始为",
|
||||
"support_code_language": "支持import列表:pandas,numpy",
|
||||
"target_fields_description": "由 '描述' 和 'key' 组成一个目标字段,可提取多个目标字段",
|
||||
"template.ai_chat": "AI 对话",
|
||||
"template.ai_chat_intro": "AI 大模型对话",
|
||||
|
@@ -20,6 +20,7 @@
|
||||
"classification_result": "分類結果",
|
||||
"code.Reset template": "重設範本",
|
||||
"code.Reset template confirm": "確定要重設程式碼範本嗎?這將會把所有輸入和輸出重設為範本值。請儲存您目前的程式碼。",
|
||||
"code.Switch language confirm": "切換語言將重置代碼,是否繼續?",
|
||||
"code_execution": "程式碼執行",
|
||||
"collection_metadata_filter": "資料集詮釋資料篩選器",
|
||||
"complete_extraction_result": "完整擷取結果",
|
||||
@@ -153,6 +154,7 @@
|
||||
"select_another_application_to_call": "可以選擇另一個應用程式來呼叫",
|
||||
"special_array_format": "特殊陣列格式,搜尋結果為空時,回傳空陣列。",
|
||||
"start_with": "開頭為",
|
||||
"support_code_language": "支持import列表:pandas,numpy",
|
||||
"target_fields_description": "由「描述」和「鍵值」組成一個目標欄位,可以擷取多個目標欄位",
|
||||
"template.ai_chat": "AI 對話",
|
||||
"template.ai_chat_intro": "AI 大型語言模型對話",
|
||||
|
Reference in New Issue
Block a user