feat: 支持 Python 代码节点 main 返回输出

- 自动传入节点输入并映射 main 返回字典,兼容历史 _result

- 限制脚本输出缓冲并补充运行时与编辑器测试

- 同步 Python 帮助、占位提示和独立可执行补全片段
This commit is contained in:
2026-08-03 11:16:03 +08:00
parent 51dbfd41b6
commit 1bf755f6c4
5 changed files with 267 additions and 17 deletions

View File

@@ -22,8 +22,8 @@
let currentNode = useNodesData(currentNodeId);
const { addParameter } = useAddParameter();
const { updateNodeData } = useSvelteFlow();
const codeNodeHelp = `JavaScript 如何返回结果
- 定义 main 函数并返回对象,系统会自动执行。
const codeNodeHelp = `代码如何返回结果
- JavaScript 定义 main 函数并返回对象Python 定义 main 函数并返回 dict,系统会自动执行。
- main 接收由输入参数组成的对象。
输出参数如何配置
@@ -31,11 +31,15 @@
- 字段名与 main 返回对象的 key 保持一致。
- 下游节点可引用代码节点ID.输出参数名。
示例
- 代码里写:
JavaScript 示例
function main({ input }) {
return { answer: input, score: 95 };
}
Python 示例
def main(inputs):
return {'answer': inputs.get('input'), 'score': 95}
- 输出参数配置answer(String)、score(Number)
- 结束节点输出参数可引用代码节点ID.answer、代码节点ID.score
@@ -53,6 +57,13 @@
const firstAvailable = engines.find((item) => item.selectable !== false);
return firstAvailable?.value || 'js';
});
const codePlaceholder = $derived.by(() => {
const engine = String(data.engine || defaultEngine).trim().toLowerCase();
if (engine === 'python' || engine === 'py') {
return "请输入代码例如def main(inputs):\n return {'output': inputs.get('input')}";
}
return '请输入代码例如function main({ input }) { return { output: input }; }';
});
onMount(async () => {
const codeEngines = await options.provider?.codeEngine?.();
@@ -118,7 +129,7 @@
<CodeScriptEditor
mode="textarea"
rows={10}
placeholder={'请输入代码例如function main({ input }) { return { output: input }; }'}
placeholder={codePlaceholder}
style="width: 100%"
engine={(data.engine as string) || (defaultEngine as string)}
parameters={editorParameters}

View File

@@ -71,6 +71,36 @@ describe('codeCompletion utils', () => {
);
});
it('should prefer main return snippets for python output', () => {
const completions = createBusinessCompletions('python', []);
const mainCompletion = completions.find((item) => item.label === 'main');
const resultCompletion = completions.find(
(item) => item.label === 'result-object',
);
expect(mainCompletion).toMatchObject({
type: 'snippet',
detail: '代码节点入口',
boost: 1000,
});
expect(String(mainCompletion?.apply)).toContain('def main(inputs):');
expect(String(mainCompletion?.apply)).toContain('return {');
expect(String(resultCompletion?.apply)).toContain('def main(inputs):');
expect(String(resultCompletion?.apply)).toContain(
"return {'message': 'ok', 'data': inputs.get('data')}",
);
});
it('should provide standalone python snippets', () => {
const completions = createBusinessCompletions('python', []);
const snippets = completions.filter((item) => item.type === 'snippet');
expect(snippets).toHaveLength(4);
for (const snippet of snippets) {
expect(String(snippet.apply)).toMatch(/^def main\(inputs\):\n /);
}
});
it('should detect blocked nodes for comment/string/property contexts', () => {
expect(shouldSkipBusinessCompletion('Comment')).toBe(true);
expect(shouldSkipBusinessCompletion('LineComment')).toBe(true);

View File

@@ -60,21 +60,29 @@ const PYTHON_SNIPPETS: Array<{
insert: string;
detail: string;
}> = [
{
label: 'main',
detail: '代码节点入口',
insert:
"def main(inputs):\n return {\n 'output': inputs.get('input'),\n }",
},
{
label: 'if-else',
detail: '条件分支',
insert:
"if condition:\n _result['value'] = value\nelse:\n _result['value'] = None",
"def main(inputs):\n condition = inputs.get('condition')\n value = inputs.get('value')\n if condition:\n return {'value': value}\n return {'value': None}",
},
{
label: 'for-loop',
detail: '遍历数组',
insert: "for item in items:\n # TODO\n pass\n_result['done'] = True",
insert:
"def main(inputs):\n result = []\n for item in inputs.get('items', []):\n result.append(item)\n return {'result': result}",
},
{
label: 'result-object',
detail: '返回对象',
insert: "_result['message'] = 'ok'\n_result['data'] = data",
insert:
"def main(inputs):\n return {'message': 'ok', 'data': inputs.get('data')}",
},
];