发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
7 changed files with 232 additions and 51 deletions
Showing only changes of commit a75ae9a843 - Show all commits

View File

@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.chain.Chain; import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.chain.ChainState; import com.easyagents.flow.core.chain.ChainState;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.code.CodeRuntimeEngine; import com.easyagents.flow.core.code.CodeRuntimeEngine;
import com.easyagents.flow.core.node.CodeNode; import com.easyagents.flow.core.node.CodeNode;
import com.easyagents.flow.core.util.StringUtil; import com.easyagents.flow.core.util.StringUtil;
@@ -17,6 +18,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -91,6 +93,7 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
"", "",
" local_ctx = dict(context)", " local_ctx = dict(context)",
" local_ctx['_result'] = {}", " local_ctx['_result'] = {}",
" initial_result = local_ctx['_result']",
" local_ctx['__builtins__'] = __builtins__", " local_ctx['__builtins__'] = __builtins__",
"", "",
" captured_stdout = _BoundedTextWriter(max_output_bytes)", " captured_stdout = _BoundedTextWriter(max_output_bytes)",
@@ -101,12 +104,17 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
" with contextlib.redirect_stdout(captured_stdout), contextlib.redirect_stderr(captured_stderr):", " with contextlib.redirect_stdout(captured_stdout), contextlib.redirect_stderr(captured_stderr):",
" exec(code, local_ctx, local_ctx)", " exec(code, local_ctx, local_ctx)",
" result = local_ctx.get('_result')", " result = local_ctx.get('_result')",
" if isinstance(result, dict) and not result:", " legacy_result_assigned = result is not initial_result",
" if isinstance(result, dict) and not result and not legacy_result_assigned:",
" main_func = local_ctx.get('main')", " main_func = local_ctx.get('main')",
" if callable(main_func):", " if callable(main_func):",
" main_invoked = True", " main_invoked = True",
" main_input = {name: context.get(name) for name in parameter_names if isinstance(name, str)}", " if payload.get('mainArgsMode') == 'named':",
" result = main_func(main_input)", " main_args = [context.get(name) for name in parameter_names]",
" result = main_func(*main_args)",
" else:",
" main_input = {name: context.get(name) for name in parameter_names if isinstance(name, str)}",
" result = main_func(main_input)",
" except Exception:", " except Exception:",
" sys.stderr.write(traceback.format_exc())", " sys.stderr.write(traceback.format_exc())",
" sys.exit(2)", " sys.exit(2)",
@@ -253,9 +261,8 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
payload.put("context", context); payload.put("context", context);
payload.put( payload.put(
"parameterNames", "parameterNames",
parameterValues == null resolveParameterNames(node, parameterValues));
? Collections.emptyList() payload.put("mainArgsMode", node.getMainArgsMode());
: new ArrayList<>(parameterValues.keySet()));
payload.put("maxOutputBytes", maxOutputBytes); payload.put("maxOutputBytes", maxOutputBytes);
String payloadText = JSON.toJSONString(payload); String payloadText = JSON.toJSONString(payload);
@@ -312,6 +319,30 @@ public class PythonRuntimeEngine implements CodeRuntimeEngine {
} }
} }
/**
* 按节点配置顺序解析 main 自动调用参数名。
*
* @param node 当前代码节点
* @param parameterValues 已解析的节点参数
* @return 有序参数名
*/
private List<String> resolveParameterNames(
CodeNode node, Map<String, Object> parameterValues) {
if (node != null
&& node.getParameters() != null
&& !node.getParameters().isEmpty()) {
List<String> names =
new ArrayList<>(node.getParameters().size());
for (Parameter parameter : node.getParameters()) {
names.add(parameter.getName());
}
return names;
}
return parameterValues == null
? Collections.emptyList()
: new ArrayList<>(parameterValues.keySet());
}
/** /**
* 构建传入 Python 子进程的上下文。 * 构建传入 Python 子进程的上下文。
* *

View File

@@ -2,12 +2,14 @@ package tech.easyflow.ai.easyagentsflow.code;
import com.easyagents.flow.core.chain.Chain; import com.easyagents.flow.core.chain.Chain;
import com.easyagents.flow.core.chain.ChainDefinition; import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository; import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
import com.easyagents.flow.core.node.CodeNode; import com.easyagents.flow.core.node.CodeNode;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Assume; import org.junit.Assume;
import org.junit.Test; import org.junit.Test;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@@ -64,6 +66,38 @@ public class PythonRuntimeEngineTest {
Assert.assertEquals(Boolean.FALSE, result.get("has_ambient")); Assert.assertEquals(Boolean.FALSE, result.get("has_ambient"));
} }
/**
* 验证新节点按配置顺序把输入值传给 main 的独立形参。
*/
@Test
public void testExecuteMainWithNamedArguments() {
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
Chain chain = createChain();
CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test");
node.setParameters(Arrays.asList(
new Parameter("data"),
new Parameter("suffix")));
node.setMainArgsMode(CodeNode.MAIN_ARGS_MODE_NAMED);
PythonRuntimeEngine engine = new PythonRuntimeEngine(
PYTHON_COMMAND,
3000L,
65536,
System.getProperty("java.io.tmpdir"));
Map<String, Object> result = engine.execute(
String.join("\n",
"def main(data, suffix):",
" return {'answer': data + suffix}"),
node,
chain,
Map.of(
"suffix", "111",
"data", "hello"));
Assert.assertEquals("hello111", result.get("answer"));
}
@Test @Test
public void testLegacyResultTakesPrecedenceOverMain() { public void testLegacyResultTakesPrecedenceOverMain() {
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable()); Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
@@ -88,6 +122,35 @@ public class PythonRuntimeEngineTest {
Assert.assertEquals("legacy", result.get("source")); Assert.assertEquals("legacy", result.get("source"));
} }
/**
* 验证显式调用返回空字典时不会再次自动调用 main。
*/
@Test
public void testEmptyExplicitResultDoesNotInvokeMainAgain() {
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());
Chain chain = createChain();
CodeNode node = (CodeNode) chain.getDefinition().getNodeById("code-test");
PythonRuntimeEngine engine = new PythonRuntimeEngine(
PYTHON_COMMAND,
3000L,
65536,
System.getProperty("java.io.tmpdir"));
Map<String, Object> result = engine.execute(
String.join("\n",
"def main(data):",
" if not isinstance(data, str):",
" raise RuntimeError('main invoked twice')",
" return {}",
"_result = main(data)"),
node,
chain,
Map.of("data", "hello"));
Assert.assertTrue(result.isEmpty());
}
@Test @Test
public void testMainMustReturnDict() { public void testMainMustReturnDict() {
Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable()); Assume.assumeTrue(PythonRuntimeEngine.probe(PYTHON_COMMAND, 1500L).isAvailable());

View File

@@ -19,6 +19,7 @@
import {genShortId} from '../utils/IdGen'; import {genShortId} from '../utils/IdGen';
import {createCodeNodeScaffold} from '../utils/codeNodeScaffold'; import {createCodeNodeScaffold} from '../utils/codeNodeScaffold';
import { import {
analyzeCodeNodeMainArgsMode,
analyzeCodeNodeOutputs, analyzeCodeNodeOutputs,
reconcileInferredCodeNodeOutputs, reconcileInferredCodeNodeOutputs,
syncCodeNodeInputParameters, syncCodeNodeInputParameters,
@@ -39,7 +40,7 @@
const codeNodeHelp = `代码如何返回结果 const codeNodeHelp = `代码如何返回结果
- 输入参数会按名称传入 main例如输入参数 data 对应 main(data)。 - 输入参数会按名称传入 main例如输入参数 data 对应 main(data)。
- JavaScript 的 main 返回对象Python 的 main 返回 dict。 - JavaScript 的 main 返回对象Python 的 main 返回 dict。
- 保留 _result = main(...),运行时会读取 _result 作为节点结果 - 历史代码中的 _result = main(...) 写法仍然兼容
输出参数如何配置 输出参数如何配置
- return 中的静态字段会自动补全到“输出参数”,并推断 String、Number、Boolean、File、Object、Array。 - return 中的静态字段会自动补全到“输出参数”,并推断 String、Number、Boolean、File、Object、Array。
@@ -50,12 +51,10 @@ JavaScript 示例
function main(data) { function main(data) {
return { answer: data, score: 95 }; return { answer: data, score: 95 };
} }
_result = main(data);
Python 示例 Python 示例
def main(data): def main(data):
return {'answer': data, 'score': 95} return {'answer': data, 'score': 95}
_result = main(data)
- 输出参数配置answer(String)、score(Number) - 输出参数配置answer(String)、score(Number)
- 结束节点输出参数可引用代码节点ID.answer、代码节点ID.score`; - 结束节点输出参数可引用代码节点ID.answer、代码节点ID.score`;
@@ -77,9 +76,9 @@ Python 示例
const codePlaceholder = $derived.by(() => { const codePlaceholder = $derived.by(() => {
const engine = String(nodeData.engine || defaultEngine).trim().toLowerCase(); const engine = String(nodeData.engine || defaultEngine).trim().toLowerCase();
if (engine === 'python' || engine === 'py') { 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 = ( const reconcileOutputs = (
@@ -113,6 +112,9 @@ Python 示例
syncHint = syncResult.synced ? '' : (syncResult.reason || ''); syncHint = syncResult.synced ? '' : (syncResult.reason || '');
return { return {
code: syncResult.code, code: syncResult.code,
mainArgsMode: syncResult.synced
? 'named'
: currentData.mainArgsMode,
parameters: syncResult.parameters, parameters: syncResult.parameters,
outputDefs: reconcileOutputs( outputDefs: reconcileOutputs(
syncResult.code, syncResult.code,
@@ -157,13 +159,23 @@ Python 示例
(node.data.parameters as Parameter[]) || [], (node.data.parameters as Parameter[]) || [],
(node.data.outputDefs as Parameter[]) || [], (node.data.outputDefs as Parameter[]) || [],
); );
const mainArgsMode = analyzeCodeNodeMainArgsMode(
code,
String(node.data.engine || defaultEngine),
(node.data.parameters as Parameter[]) || [],
);
if ( if (
JSON.stringify(outputDefs) === JSON.stringify(outputDefs) ===
JSON.stringify(node.data.outputDefs || []) JSON.stringify(node.data.outputDefs || []) &&
(!mainArgsMode ||
mainArgsMode === node.data.mainArgsMode)
) { ) {
return {}; return {};
} }
return { outputDefs }; return {
outputDefs,
...(mainArgsMode ? { mainArgsMode } : {}),
};
}); });
}, 300); }, 300);
}; };
@@ -178,6 +190,8 @@ Python 示例
(node.data.parameters as Parameter[]) || [], (node.data.parameters as Parameter[]) || [],
(node.data.outputDefs as Parameter[]) || [], (node.data.outputDefs as Parameter[]) || [],
); );
patch.codeScaffoldVersion = 2;
patch.mainArgsMode = 'named';
} }
return patch; return patch;
}); });

View File

@@ -146,8 +146,6 @@ export function createCodeNodeScaffold(
' return {', ' return {',
...outputLines, ...outputLines,
' }', ' }',
'',
`_result = main(${argumentsText})`,
].join('\n'); ].join('\n');
} }
@@ -160,8 +158,6 @@ export function createCodeNodeScaffold(
...outputLines, ...outputLines,
' };', ' };',
'}', '}',
'',
`_result = main(${argumentsText});`,
].join('\n'); ].join('\n');
} }

View File

@@ -5,6 +5,7 @@ import type { Parameter } from '#types';
import { createCodeNodeScaffold } from './codeNodeScaffold'; import { createCodeNodeScaffold } from './codeNodeScaffold';
import { getAvailableNodes } from './nodePalette'; import { getAvailableNodes } from './nodePalette';
import { import {
analyzeCodeNodeMainArgsMode,
analyzeCodeNodeOutputs, analyzeCodeNodeOutputs,
reconcileInferredCodeNodeOutputs, reconcileInferredCodeNodeOutputs,
syncCodeNodeInputParameters, syncCodeNodeInputParameters,
@@ -20,12 +21,13 @@ describe('code node scaffold', () => {
expect(codeNode?.extra).toMatchObject({ expect(codeNode?.extra).toMatchObject({
code: createCodeNodeScaffold('js'), code: createCodeNodeScaffold('js'),
codeScaffoldManaged: true, codeScaffoldManaged: true,
codeScaffoldVersion: 1, codeScaffoldVersion: 2,
engine: 'js', engine: 'js',
mainArgsMode: 'named',
}); });
}); });
it('creates an explicit javascript main call', () => { it('creates a javascript main scaffold without an explicit call', () => {
expect( expect(
createCodeNodeScaffold('js', [{ name: 'data' }], [{ name: 'result' }]), createCodeNodeScaffold('js', [{ name: 'data' }], [{ name: 'result' }]),
).toBe( ).toBe(
@@ -35,13 +37,11 @@ describe('code node scaffold', () => {
' "result": null,', ' "result": null,',
' };', ' };',
'}', '}',
'',
'_result = main(data);',
].join('\n'), ].join('\n'),
); );
}); });
it('creates an explicit python main call', () => { it('creates a python main scaffold without an explicit call', () => {
expect( expect(
createCodeNodeScaffold( createCodeNodeScaffold(
'python', 'python',
@@ -54,8 +54,6 @@ describe('code node scaffold', () => {
' return {', ' return {',
' "result": None,', ' "result": None,',
' }', ' }',
'',
'_result = main(data)',
].join('\n'), ].join('\n'),
); );
}); });
@@ -68,7 +66,7 @@ describe('code node scaffold', () => {
]); ]);
expect(code).toContain('def main(data):'); 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', engine: 'js',
emptyCode: createCodeNodeScaffold('js'), emptyCode: createCodeNodeScaffold('js'),
mainWithData: 'function main(data)', mainWithData: 'function main(data)',
callWithData: '_result = main(data);',
mainWithQuery: 'function main(query)', mainWithQuery: 'function main(query)',
callWithQuery: '_result = main(query);', legacyCallWithData: '_result = main(data);',
legacyCallWithQuery: '_result = main(query);',
}, },
{ {
engine: 'python', engine: 'python',
emptyCode: createCodeNodeScaffold('python'), emptyCode: createCodeNodeScaffold('python'),
mainWithData: 'def main(data):', mainWithData: 'def main(data):',
callWithData: '_result = main(data)',
mainWithQuery: 'def main(query):', mainWithQuery: 'def main(query):',
callWithQuery: '_result = main(query)', legacyCallWithData: '_result = main(data)',
legacyCallWithQuery: '_result = main(query)',
}, },
])('code node input sync for $engine', (fixture) => { ])('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 = { const blankParameter: Parameter = {
id: 'input-1', id: 'input-1',
name: '', name: '',
@@ -115,7 +113,7 @@ describe.each([
expect(namedResult.synced).toBe(true); expect(namedResult.synced).toBe(true);
expect(namedResult.code).toContain(fixture.mainWithData); 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'); expect(namedResult.parameters[0].codeSyncName).toBe('data');
}); });
@@ -143,7 +141,7 @@ describe.each([
expect(cleared.parameters[0].codeSyncName).toBe('data'); expect(cleared.parameters[0].codeSyncName).toBe('data');
expect(renamed.synced).toBe(true); expect(renamed.synced).toBe(true);
expect(renamed.code).toContain(fixture.mainWithQuery); 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', () => { it('does not overwrite a user-managed main signature', () => {
@@ -170,6 +168,26 @@ describe.each([
expect(result.code).toBe(manualCode); 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', () => { it('does not partially update multiple explicit main calls', () => {
const initial = syncCodeNodeInputParameters( const initial = syncCodeNodeInputParameters(
fixture.emptyCode, fixture.emptyCode,
@@ -179,9 +197,8 @@ describe.each([
); );
const duplicatedCall = [ const duplicatedCall = [
initial.code, initial.code,
fixture.engine === 'python' fixture.legacyCallWithData,
? '_result = main(data)' fixture.legacyCallWithData,
: '_result = main(data);',
].join('\n'); ].join('\n');
const result = syncCodeNodeInputParameters( const result = syncCodeNodeInputParameters(
duplicatedCall, 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([ describe.each([
{ {
engine: 'js', engine: 'js',

View File

@@ -27,6 +27,8 @@ export type CodeNodeSyncResult = {
synced: boolean; synced: boolean;
}; };
export type CodeNodeMainArgsMode = 'named' | 'object';
export type InferredCodeNodeOutput = { export type InferredCodeNodeOutput = {
dataType: CodeNodeDataType; dataType: CodeNodeDataType;
name: string; name: string;
@@ -192,7 +194,7 @@ function readSimpleNames(
return children.map((child) => nodeText(child, source)); return children.map((child) => nodeText(child, source));
} }
function findExplicitMainCall(document: ParsedDocument) { function findExplicitMainCalls(document: ParsedDocument) {
const matches: SyntaxNode[] = []; const matches: SyntaxNode[] = [];
for (const statement of childNodes(document.topNode)) { for (const statement of childNodes(document.topNode)) {
if (statement.name === 'FunctionDefinition') { if (statement.name === 'FunctionDefinition') {
@@ -235,7 +237,7 @@ function findExplicitMainCall(document: ParsedDocument) {
matches.push(argList); matches.push(argList);
} }
} }
return matches.length === 1 ? matches[0] : null; return matches;
} }
function arraysEqual(left: string[], right: string[]) { function arraysEqual(left: string[], right: string[]) {
@@ -313,7 +315,7 @@ function withPreviousSyncNames(previous: Parameter[], next: Parameter[]) {
} }
/** /**
* 在 main 签名和显式调用仍受控时,同步代码节点输入参数。 * 在 main 签名和可选显式调用仍受控时,同步代码节点输入参数。
*/ */
export function syncCodeNodeInputParameters( export function syncCodeNodeInputParameters(
source: string, source: string,
@@ -345,12 +347,12 @@ export function syncCodeNodeInputParameters(
); );
const document = parseDocument(source, engine); const document = parseDocument(source, engine);
const mainFunction = document && findMainFunction(document); const mainFunction = document && findMainFunction(document);
const explicitCall = document && findExplicitMainCall(document); const explicitCalls = document ? findExplicitMainCalls(document) : [];
if (!document || !mainFunction || !explicitCall) { if (!document || !mainFunction || explicitCalls.length > 1) {
return { return {
code: source, code: source,
parameters: nextWithSyncNames, parameters: nextWithSyncNames,
reason: '未找到可安全同步的 main 函数和显式调用', reason: '未找到可安全同步的唯一 main 函数',
synced: false, synced: false,
}; };
} }
@@ -360,11 +362,10 @@ export function syncCodeNodeInputParameters(
source, source,
new Set(['VariableDefinition', 'VariableName']), new Set(['VariableDefinition', 'VariableName']),
); );
const currentArguments = readSimpleNames( const explicitCall = explicitCalls[0];
explicitCall, const currentArguments = explicitCall
source, ? readSimpleNames(explicitCall, source, new Set(['VariableName']))
new Set(['VariableName']), : previousNames;
);
if ( if (
!currentParams || !currentParams ||
!currentArguments || !currentArguments ||
@@ -381,18 +382,21 @@ export function syncCodeNodeInputParameters(
} }
const replacement = nextNames.join(', '); const replacement = nextNames.join(', ');
const code = applySourceEdits(source, [ const edits: SourceEdit[] = [
{ {
from: mainFunction.paramList.from + 1, from: mainFunction.paramList.from + 1,
to: mainFunction.paramList.to - 1, to: mainFunction.paramList.to - 1,
insert: replacement, insert: replacement,
}, },
{ ];
if (explicitCall) {
edits.push({
from: explicitCall.from + 1, from: explicitCall.from + 1,
to: explicitCall.to - 1, to: explicitCall.to - 1,
insert: replacement, insert: replacement,
}, });
]); }
const code = applySourceEdits(source, edits);
return { return {
code, code,
parameters: nextWithSyncNames.map((parameter) => ({ 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) { function parseQuotedString(rawValue: string) {
const raw = rawValue.trim(); const raw = rawValue.trim();
if (raw.length < 2) { if (raw.length < 2) {

View File

@@ -81,8 +81,9 @@ const BUILT_IN_NODES: NodePaletteItem[] = [
extra: { extra: {
code: DEFAULT_CODE_NODE_JAVASCRIPT, code: DEFAULT_CODE_NODE_JAVASCRIPT,
codeScaffoldManaged: true, codeScaffoldManaged: true,
codeScaffoldVersion: 1, codeScaffoldVersion: 2,
engine: 'js', engine: 'js',
mainArgsMode: 'named',
}, },
}, },
{ {