发布 v1.10 #5
@@ -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 子进程的上下文。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user