From bdb69a2250dd50bbd7dad43131a19e1e102835a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Mon, 3 Aug 2026 16:58:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E8=8A=82=E7=82=B9=20main=20=E5=91=BD=E5=90=8D=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 按节点参数顺序向 JavaScript main 传入独立参数值 - 保留历史单对象参数与显式 _result 输出兼容 - 补充参数模式解析和运行时回归测试 --- .../code/impl/JavascriptRuntimeEngine.java | 73 +++++++++++++++++-- .../easyagents/flow/core/node/CodeNode.java | 27 +++++++ .../flow/core/parser/impl/CodeNodeParser.java | 1 + .../test/JavascriptRuntimeEngineTest.java | 64 ++++++++++++++++ 4 files changed, 158 insertions(+), 7 deletions(-) diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java index 76a8a69..4931d48 100644 --- a/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/code/impl/JavascriptRuntimeEngine.java @@ -19,6 +19,7 @@ import com.alibaba.fastjson.JSONObject; import com.easyagents.flow.core.chain.Chain; import com.easyagents.flow.core.chain.ChainState; import com.easyagents.flow.core.chain.NodeState; +import com.easyagents.flow.core.chain.Parameter; import com.easyagents.flow.core.code.CodeRuntimeEngine; import com.easyagents.flow.core.node.CodeNode; import com.easyagents.flow.core.util.graalvm.JsInteropUtils; @@ -70,6 +71,10 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { .build(); private static final Source RESULT_INIT_SOURCE = Source.create("js", "var _result = {};"); + private static final Source STRICT_EQUAL_SOURCE = + Source.create( + "js", + "(left, right) => left === right;"); private static final List> SOURCE_CACHE_SEGMENTS = createSourceCacheSegments(); private static final ScheduledThreadPoolExecutor @@ -159,6 +164,7 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { // 在 JS 中创建 _result 对象 context.eval(RESULT_INIT_SOURCE); + Value initialResult = bindings.getMember("_result"); // 注入 _chain 和 _context bindings.putMember("_chain", chain); @@ -167,7 +173,18 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { // 执行用户脚本 context.eval(source(code, node)); - return resolveResult(context, bindings, parameterValues); + boolean legacyResultAssigned = + !context.eval(STRICT_EQUAL_SOURCE) + .execute( + initialResult, + bindings.getMember("_result")) + .asBoolean(); + return resolveResult( + context, + bindings, + node, + parameterValues, + legacyResultAssigned); } catch (PolyglotException e) { throw executionException(e, timedOut.get()); } catch (Exception e) { @@ -389,16 +406,20 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { * * @param context JavaScript 执行上下文 * @param bindings JavaScript 全局绑定 + * @param node 当前代码节点 * @param parameterValues 当前节点已解析的输入参数 + * @param legacyResultAssigned 用户代码是否重新赋值 _result * @return 代码节点输出 * @throws IllegalArgumentException main 返回值不是对象时抛出 */ private Map resolveResult(Context context, Value bindings, - Map parameterValues) { + CodeNode node, + Map parameterValues, + boolean legacyResultAssigned) { JSONObject legacyResult = GraalvmToFastJSONUtils.toJSONObject( bindings.getMember("_result")); - if (!legacyResult.isEmpty()) { + if (legacyResultAssigned || !legacyResult.isEmpty()) { return legacyResult; } @@ -407,11 +428,8 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { return legacyResult; } - Map mainInput = parameterValues == null - ? Collections.emptyMap() - : parameterValues; Value mainResult = mainFunction.execute( - JsInteropUtils.wrapJavaValueForJS(context, mainInput)); + resolveMainArguments(context, node, parameterValues)); Object convertedResult = GraalvmToFastJSONUtils.toFastJsonValue(mainResult); if (!(convertedResult instanceof JSONObject)) { @@ -421,4 +439,45 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { return (JSONObject) convertedResult; } + /** + * 根据节点契约构建 main 自动调用参数。 + * + * @param context JavaScript 执行上下文 + * @param node 当前代码节点 + * @param parameterValues 当前节点已解析的输入参数 + * @return GraalVM main 调用参数 + */ + private Object[] resolveMainArguments( + Context context, + CodeNode node, + Map parameterValues) { + if (node != null + && CodeNode.MAIN_ARGS_MODE_NAMED.equals( + node.getMainArgsMode())) { + List parameters = node.getParameters(); + if (parameters == null || parameters.isEmpty()) { + return new Object[0]; + } + Object[] arguments = new Object[parameters.size()]; + for (int index = 0; index < parameters.size(); index++) { + String name = parameters.get(index).getName(); + Object value = parameterValues == null + ? null + : parameterValues.get(name); + arguments[index] = + JsInteropUtils.wrapJavaValueForJS( + context, value); + } + return arguments; + } + + Map mainInput = parameterValues == null + ? Collections.emptyMap() + : parameterValues; + return new Object[]{ + JsInteropUtils.wrapJavaValueForJS( + context, mainInput) + }; + } + } diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java index e49d839..329aa07 100644 --- a/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/node/CodeNode.java @@ -29,8 +29,17 @@ import java.util.Map; public class CodeNode extends BaseNode { private static final long serialVersionUID = 1L; + /** + * main 按节点参数顺序接收独立参数。 + */ + public static final String MAIN_ARGS_MODE_NAMED = "named"; + protected String engine; protected String code; + /** + * main 自动调用参数模式;空值沿用历史单对象参数。 + */ + protected String mainArgsMode; public String getEngine() { return engine; @@ -48,6 +57,24 @@ public class CodeNode extends BaseNode { this.code = code; } + /** + * 获取 main 自动调用参数模式。 + * + * @return 参数模式;空值表示历史单对象参数模式 + */ + public String getMainArgsMode() { + return mainArgsMode; + } + + /** + * 设置 main 自动调用参数模式。 + * + * @param mainArgsMode 参数模式 + */ + public void setMainArgsMode(String mainArgsMode) { + this.mainArgsMode = mainArgsMode; + } + @Override public Map execute(Chain chain) { if (StringUtil.noText(code)) { diff --git a/easy-agents-flow/src/main/java/com/easyagents/flow/core/parser/impl/CodeNodeParser.java b/easy-agents-flow/src/main/java/com/easyagents/flow/core/parser/impl/CodeNodeParser.java index e2affb3..41eeb43 100644 --- a/easy-agents-flow/src/main/java/com/easyagents/flow/core/parser/impl/CodeNodeParser.java +++ b/easy-agents-flow/src/main/java/com/easyagents/flow/core/parser/impl/CodeNodeParser.java @@ -27,6 +27,7 @@ public class CodeNodeParser extends BaseNodeParser { CodeNode codeNode = new CodeNode(); codeNode.setEngine(engine); codeNode.setCode(data.getString("code")); + codeNode.setMainArgsMode(data.getString("mainArgsMode")); return codeNode; } } diff --git a/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java b/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java index 982b534..7514501 100644 --- a/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java +++ b/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java @@ -1,5 +1,6 @@ package com.easyagents.flow.core.test; +import com.alibaba.fastjson.JSONObject; import com.easyagents.flow.core.chain.Chain; import com.easyagents.flow.core.chain.ChainDefinition; import com.easyagents.flow.core.chain.ChainState; @@ -9,6 +10,7 @@ import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository; import com.easyagents.flow.core.code.impl.JavascriptExecutionException; import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine; import com.easyagents.flow.core.node.CodeNode; +import com.easyagents.flow.core.parser.impl.CodeNodeParser; import org.junit.Assert; import org.junit.Test; @@ -57,6 +59,46 @@ public class JavascriptRuntimeEngineTest { Assert.assertEquals(4L, result.get("totalLength")); } + /** + * 验证新节点按配置顺序把输入值传给 main 的独立形参。 + */ + @Test + public void shouldInvokeMainWithNamedArguments() { + CodeNode node = codeNode( + String.join("\n", + "function main(data1, data2) {", + " return { joined: data1 + data2 };", + "}"), + "data1", + "data2"); + node.setMainArgsMode(CodeNode.MAIN_ARGS_MODE_NAMED); + Chain chain = chain(Map.of( + "data1", "河北", + "data2", "分行")); + + Map result = node.execute(chain); + + Assert.assertEquals("河北分行", result.get("joined")); + } + + /** + * 验证解析器保留设计器声明的 main 参数模式。 + */ + @Test + public void shouldParseMainArgsMode() { + JSONObject data = new JSONObject(); + data.put("engine", "js"); + data.put("code", "function main(data) { return { data }; }"); + data.put("mainArgsMode", CodeNode.MAIN_ARGS_MODE_NAMED); + + CodeNode node = new CodeNodeParser().doParse( + new JSONObject(), data, new JSONObject()); + + Assert.assertEquals( + CodeNode.MAIN_ARGS_MODE_NAMED, + node.getMainArgsMode()); + } + /** * 验证已有 _result 输出优先,避免自动调用 main 改变历史流程。 */ @@ -75,6 +117,28 @@ public class JavascriptRuntimeEngineTest { Assert.assertEquals("legacy", result.get("answer")); } + /** + * 验证显式调用返回空对象时不会再次自动调用 main。 + */ + @Test + public void shouldKeepEmptyExplicitResultWithoutInvokingMainAgain() { + CodeNode node = codeNode( + String.join("\n", + "function main(data) {", + " if (typeof data !== 'string') {", + " throw new Error('main invoked twice');", + " }", + " return {};", + "}", + "_result = main(data);"), + "data"); + Chain chain = chain(Map.of("data", "hello")); + + Map result = node.execute(chain); + + Assert.assertTrue(result.isEmpty()); + } + /** * 验证 main 返回非对象时给出明确错误。 */