feat: 默认省略代码节点显式 main 调用
- 新节点仅生成 main 与 return 并同步输入形参 - Python 运行时按配置顺序传入独立参数值 - 保留历史显式调用和单对象参数写法
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
import {genShortId} from '../utils/IdGen';
|
||||
import {createCodeNodeScaffold} from '../utils/codeNodeScaffold';
|
||||
import {
|
||||
analyzeCodeNodeMainArgsMode,
|
||||
analyzeCodeNodeOutputs,
|
||||
reconcileInferredCodeNodeOutputs,
|
||||
syncCodeNodeInputParameters,
|
||||
@@ -39,7 +40,7 @@
|
||||
const codeNodeHelp = `代码如何返回结果
|
||||
- 输入参数会按名称传入 main,例如输入参数 data 对应 main(data)。
|
||||
- JavaScript 的 main 返回对象;Python 的 main 返回 dict。
|
||||
- 保留 _result = main(...),运行时会读取 _result 作为节点结果。
|
||||
- 历史代码中的 _result = main(...) 写法仍然兼容。
|
||||
|
||||
输出参数如何配置
|
||||
- return 中的静态字段会自动补全到“输出参数”,并推断 String、Number、Boolean、File、Object、Array。
|
||||
@@ -50,12 +51,10 @@ JavaScript 示例
|
||||
function main(data) {
|
||||
return { answer: data, score: 95 };
|
||||
}
|
||||
_result = main(data);
|
||||
|
||||
Python 示例
|
||||
def main(data):
|
||||
return {'answer': data, 'score': 95}
|
||||
_result = main(data)
|
||||
|
||||
- 输出参数配置:answer(String)、score(Number)
|
||||
- 结束节点输出参数可引用:代码节点ID.answer、代码节点ID.score`;
|
||||
@@ -77,9 +76,9 @@ Python 示例
|
||||
const codePlaceholder = $derived.by(() => {
|
||||
const engine = String(nodeData.engine || defaultEngine).trim().toLowerCase();
|
||||
if (engine === 'python' || engine === 'py') {
|
||||
return "请输入代码,例如:def main(data):\n return {'output': data}\n\n_result = main(data)";
|
||||
return "请输入代码,例如:def main(data):\n return {'output': data}";
|
||||
}
|
||||
return '请输入代码,例如:function main(data) { return { output: data }; }\n_result = main(data);';
|
||||
return '请输入代码,例如:function main(data) { return { output: data }; }';
|
||||
});
|
||||
|
||||
const reconcileOutputs = (
|
||||
@@ -113,6 +112,9 @@ Python 示例
|
||||
syncHint = syncResult.synced ? '' : (syncResult.reason || '');
|
||||
return {
|
||||
code: syncResult.code,
|
||||
mainArgsMode: syncResult.synced
|
||||
? 'named'
|
||||
: currentData.mainArgsMode,
|
||||
parameters: syncResult.parameters,
|
||||
outputDefs: reconcileOutputs(
|
||||
syncResult.code,
|
||||
@@ -157,13 +159,23 @@ Python 示例
|
||||
(node.data.parameters as Parameter[]) || [],
|
||||
(node.data.outputDefs as Parameter[]) || [],
|
||||
);
|
||||
const mainArgsMode = analyzeCodeNodeMainArgsMode(
|
||||
code,
|
||||
String(node.data.engine || defaultEngine),
|
||||
(node.data.parameters as Parameter[]) || [],
|
||||
);
|
||||
if (
|
||||
JSON.stringify(outputDefs) ===
|
||||
JSON.stringify(node.data.outputDefs || [])
|
||||
JSON.stringify(node.data.outputDefs || []) &&
|
||||
(!mainArgsMode ||
|
||||
mainArgsMode === node.data.mainArgsMode)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
return { outputDefs };
|
||||
return {
|
||||
outputDefs,
|
||||
...(mainArgsMode ? { mainArgsMode } : {}),
|
||||
};
|
||||
});
|
||||
}, 300);
|
||||
};
|
||||
@@ -178,6 +190,8 @@ Python 示例
|
||||
(node.data.parameters as Parameter[]) || [],
|
||||
(node.data.outputDefs as Parameter[]) || [],
|
||||
);
|
||||
patch.codeScaffoldVersion = 2;
|
||||
patch.mainArgsMode = 'named';
|
||||
}
|
||||
return patch;
|
||||
});
|
||||
|
||||
@@ -146,8 +146,6 @@ export function createCodeNodeScaffold(
|
||||
' return {',
|
||||
...outputLines,
|
||||
' }',
|
||||
'',
|
||||
`_result = main(${argumentsText})`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
@@ -160,8 +158,6 @@ export function createCodeNodeScaffold(
|
||||
...outputLines,
|
||||
' };',
|
||||
'}',
|
||||
'',
|
||||
`_result = main(${argumentsText});`,
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Parameter } from '#types';
|
||||
import { createCodeNodeScaffold } from './codeNodeScaffold';
|
||||
import { getAvailableNodes } from './nodePalette';
|
||||
import {
|
||||
analyzeCodeNodeMainArgsMode,
|
||||
analyzeCodeNodeOutputs,
|
||||
reconcileInferredCodeNodeOutputs,
|
||||
syncCodeNodeInputParameters,
|
||||
@@ -20,12 +21,13 @@ describe('code node scaffold', () => {
|
||||
expect(codeNode?.extra).toMatchObject({
|
||||
code: createCodeNodeScaffold('js'),
|
||||
codeScaffoldManaged: true,
|
||||
codeScaffoldVersion: 1,
|
||||
codeScaffoldVersion: 2,
|
||||
engine: 'js',
|
||||
mainArgsMode: 'named',
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an explicit javascript main call', () => {
|
||||
it('creates a javascript main scaffold without an explicit call', () => {
|
||||
expect(
|
||||
createCodeNodeScaffold('js', [{ name: 'data' }], [{ name: 'result' }]),
|
||||
).toBe(
|
||||
@@ -35,13 +37,11 @@ describe('code node scaffold', () => {
|
||||
' "result": null,',
|
||||
' };',
|
||||
'}',
|
||||
'',
|
||||
'_result = main(data);',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
|
||||
it('creates an explicit python main call', () => {
|
||||
it('creates a python main scaffold without an explicit call', () => {
|
||||
expect(
|
||||
createCodeNodeScaffold(
|
||||
'python',
|
||||
@@ -54,8 +54,6 @@ describe('code node scaffold', () => {
|
||||
' return {',
|
||||
' "result": None,',
|
||||
' }',
|
||||
'',
|
||||
'_result = main(data)',
|
||||
].join('\n'),
|
||||
);
|
||||
});
|
||||
@@ -68,7 +66,7 @@ describe('code node scaffold', () => {
|
||||
]);
|
||||
|
||||
expect(code).toContain('def main(data):');
|
||||
expect(code).toContain('_result = main(data)');
|
||||
expect(code).not.toContain('_result');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,20 +75,20 @@ describe.each([
|
||||
engine: 'js',
|
||||
emptyCode: createCodeNodeScaffold('js'),
|
||||
mainWithData: 'function main(data)',
|
||||
callWithData: '_result = main(data);',
|
||||
mainWithQuery: 'function main(query)',
|
||||
callWithQuery: '_result = main(query);',
|
||||
legacyCallWithData: '_result = main(data);',
|
||||
legacyCallWithQuery: '_result = main(query);',
|
||||
},
|
||||
{
|
||||
engine: 'python',
|
||||
emptyCode: createCodeNodeScaffold('python'),
|
||||
mainWithData: 'def main(data):',
|
||||
callWithData: '_result = main(data)',
|
||||
mainWithQuery: 'def main(query):',
|
||||
callWithQuery: '_result = main(query)',
|
||||
legacyCallWithData: '_result = main(data)',
|
||||
legacyCallWithQuery: '_result = main(query)',
|
||||
},
|
||||
])('code node input sync for $engine', (fixture) => {
|
||||
it('syncs a newly configured input name to main and its call', () => {
|
||||
it('syncs a newly configured input name to main', () => {
|
||||
const blankParameter: Parameter = {
|
||||
id: 'input-1',
|
||||
name: '',
|
||||
@@ -115,7 +113,7 @@ describe.each([
|
||||
|
||||
expect(namedResult.synced).toBe(true);
|
||||
expect(namedResult.code).toContain(fixture.mainWithData);
|
||||
expect(namedResult.code).toContain(fixture.callWithData);
|
||||
expect(namedResult.code).not.toContain('_result');
|
||||
expect(namedResult.parameters[0].codeSyncName).toBe('data');
|
||||
});
|
||||
|
||||
@@ -143,7 +141,7 @@ describe.each([
|
||||
expect(cleared.parameters[0].codeSyncName).toBe('data');
|
||||
expect(renamed.synced).toBe(true);
|
||||
expect(renamed.code).toContain(fixture.mainWithQuery);
|
||||
expect(renamed.code).toContain(fixture.callWithQuery);
|
||||
expect(renamed.code).not.toContain('_result');
|
||||
});
|
||||
|
||||
it('does not overwrite a user-managed main signature', () => {
|
||||
@@ -170,6 +168,26 @@ describe.each([
|
||||
expect(result.code).toBe(manualCode);
|
||||
});
|
||||
|
||||
it('keeps a compatible legacy explicit main call synchronized', () => {
|
||||
const initial = syncCodeNodeInputParameters(
|
||||
fixture.emptyCode,
|
||||
fixture.engine,
|
||||
[],
|
||||
[{ id: 'input-1', name: 'data' }],
|
||||
);
|
||||
const legacyCode = [initial.code, fixture.legacyCallWithData].join('\n');
|
||||
const result = syncCodeNodeInputParameters(
|
||||
legacyCode,
|
||||
fixture.engine,
|
||||
initial.parameters,
|
||||
[{ ...initial.parameters[0], name: 'query' }],
|
||||
);
|
||||
|
||||
expect(result.synced).toBe(true);
|
||||
expect(result.code).toContain(fixture.mainWithQuery);
|
||||
expect(result.code).toContain(fixture.legacyCallWithQuery);
|
||||
});
|
||||
|
||||
it('does not partially update multiple explicit main calls', () => {
|
||||
const initial = syncCodeNodeInputParameters(
|
||||
fixture.emptyCode,
|
||||
@@ -179,9 +197,8 @@ describe.each([
|
||||
);
|
||||
const duplicatedCall = [
|
||||
initial.code,
|
||||
fixture.engine === 'python'
|
||||
? '_result = main(data)'
|
||||
: '_result = main(data);',
|
||||
fixture.legacyCallWithData,
|
||||
fixture.legacyCallWithData,
|
||||
].join('\n');
|
||||
const result = syncCodeNodeInputParameters(
|
||||
duplicatedCall,
|
||||
@@ -223,6 +240,33 @@ describe.each([
|
||||
});
|
||||
});
|
||||
|
||||
describe('code node main argument mode', () => {
|
||||
it.each([
|
||||
{
|
||||
engine: 'js',
|
||||
namedCode: 'function main(data) { return { data }; }',
|
||||
objectCode: 'function main(inputs) { return { data: inputs.data }; }',
|
||||
},
|
||||
{
|
||||
engine: 'python',
|
||||
namedCode: 'def main(data):\n return {"data": data}',
|
||||
objectCode: 'def main(inputs):\n return {"data": inputs.get("data")}',
|
||||
},
|
||||
])(
|
||||
'recognizes named and legacy object contracts for $engine',
|
||||
({ engine, namedCode, objectCode }) => {
|
||||
const parameters = [{ name: 'data', dataType: 'String' }];
|
||||
|
||||
expect(analyzeCodeNodeMainArgsMode(namedCode, engine, parameters)).toBe(
|
||||
'named',
|
||||
);
|
||||
expect(analyzeCodeNodeMainArgsMode(objectCode, engine, parameters)).toBe(
|
||||
'object',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{
|
||||
engine: 'js',
|
||||
|
||||
@@ -27,6 +27,8 @@ export type CodeNodeSyncResult = {
|
||||
synced: boolean;
|
||||
};
|
||||
|
||||
export type CodeNodeMainArgsMode = 'named' | 'object';
|
||||
|
||||
export type InferredCodeNodeOutput = {
|
||||
dataType: CodeNodeDataType;
|
||||
name: string;
|
||||
@@ -192,7 +194,7 @@ function readSimpleNames(
|
||||
return children.map((child) => nodeText(child, source));
|
||||
}
|
||||
|
||||
function findExplicitMainCall(document: ParsedDocument) {
|
||||
function findExplicitMainCalls(document: ParsedDocument) {
|
||||
const matches: SyntaxNode[] = [];
|
||||
for (const statement of childNodes(document.topNode)) {
|
||||
if (statement.name === 'FunctionDefinition') {
|
||||
@@ -235,7 +237,7 @@ function findExplicitMainCall(document: ParsedDocument) {
|
||||
matches.push(argList);
|
||||
}
|
||||
}
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
return matches;
|
||||
}
|
||||
|
||||
function arraysEqual(left: string[], right: string[]) {
|
||||
@@ -313,7 +315,7 @@ function withPreviousSyncNames(previous: Parameter[], next: Parameter[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 main 签名和显式调用仍受控时,同步代码节点输入参数。
|
||||
* 在 main 签名和可选显式调用仍受控时,同步代码节点输入参数。
|
||||
*/
|
||||
export function syncCodeNodeInputParameters(
|
||||
source: string,
|
||||
@@ -345,12 +347,12 @@ export function syncCodeNodeInputParameters(
|
||||
);
|
||||
const document = parseDocument(source, engine);
|
||||
const mainFunction = document && findMainFunction(document);
|
||||
const explicitCall = document && findExplicitMainCall(document);
|
||||
if (!document || !mainFunction || !explicitCall) {
|
||||
const explicitCalls = document ? findExplicitMainCalls(document) : [];
|
||||
if (!document || !mainFunction || explicitCalls.length > 1) {
|
||||
return {
|
||||
code: source,
|
||||
parameters: nextWithSyncNames,
|
||||
reason: '未找到可安全同步的 main 函数和显式调用',
|
||||
reason: '未找到可安全同步的唯一 main 函数',
|
||||
synced: false,
|
||||
};
|
||||
}
|
||||
@@ -360,11 +362,10 @@ export function syncCodeNodeInputParameters(
|
||||
source,
|
||||
new Set(['VariableDefinition', 'VariableName']),
|
||||
);
|
||||
const currentArguments = readSimpleNames(
|
||||
explicitCall,
|
||||
source,
|
||||
new Set(['VariableName']),
|
||||
);
|
||||
const explicitCall = explicitCalls[0];
|
||||
const currentArguments = explicitCall
|
||||
? readSimpleNames(explicitCall, source, new Set(['VariableName']))
|
||||
: previousNames;
|
||||
if (
|
||||
!currentParams ||
|
||||
!currentArguments ||
|
||||
@@ -381,18 +382,21 @@ export function syncCodeNodeInputParameters(
|
||||
}
|
||||
|
||||
const replacement = nextNames.join(', ');
|
||||
const code = applySourceEdits(source, [
|
||||
const edits: SourceEdit[] = [
|
||||
{
|
||||
from: mainFunction.paramList.from + 1,
|
||||
to: mainFunction.paramList.to - 1,
|
||||
insert: replacement,
|
||||
},
|
||||
{
|
||||
];
|
||||
if (explicitCall) {
|
||||
edits.push({
|
||||
from: explicitCall.from + 1,
|
||||
to: explicitCall.to - 1,
|
||||
insert: replacement,
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
const code = applySourceEdits(source, edits);
|
||||
return {
|
||||
code,
|
||||
parameters: nextWithSyncNames.map((parameter) => ({
|
||||
@@ -403,6 +407,34 @@ export function syncCodeNodeInputParameters(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 main 简单形参与节点输入名称判断运行时调用方式。
|
||||
*/
|
||||
export function analyzeCodeNodeMainArgsMode(
|
||||
source: string,
|
||||
rawEngine: string | undefined,
|
||||
parameters: Parameter[],
|
||||
): CodeNodeMainArgsMode | null {
|
||||
const engine = normalizeCodeNodeEngine(rawEngine);
|
||||
const document = parseDocument(source, engine);
|
||||
const mainFunction = document && findMainFunction(document);
|
||||
if (!document || !mainFunction) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configuredNames = parameterNames(parameters, engine, 'name');
|
||||
const currentParams = readSimpleNames(
|
||||
mainFunction.paramList,
|
||||
source,
|
||||
new Set(['VariableDefinition', 'VariableName']),
|
||||
);
|
||||
return configuredNames &&
|
||||
currentParams &&
|
||||
arraysEqual(currentParams, configuredNames)
|
||||
? 'named'
|
||||
: 'object';
|
||||
}
|
||||
|
||||
function parseQuotedString(rawValue: string) {
|
||||
const raw = rawValue.trim();
|
||||
if (raw.length < 2) {
|
||||
|
||||
@@ -81,8 +81,9 @@ const BUILT_IN_NODES: NodePaletteItem[] = [
|
||||
extra: {
|
||||
code: DEFAULT_CODE_NODE_JAVASCRIPT,
|
||||
codeScaffoldManaged: true,
|
||||
codeScaffoldVersion: 1,
|
||||
codeScaffoldVersion: 2,
|
||||
engine: 'js',
|
||||
mainArgsMode: 'named',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user