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 040087e..6496981 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 @@ -15,6 +15,7 @@ */ package com.easyagents.flow.core.code.impl; +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; @@ -25,8 +26,16 @@ import org.graalvm.polyglot.Context; import org.graalvm.polyglot.HostAccess; import org.graalvm.polyglot.Value; +import java.util.Collections; import java.util.Map; +/** + * 基于 GraalVM 的 JavaScript 代码节点执行器。 + * + *

优先保留历史 {@code _result} 输出约定;当脚本未写入 + * {@code _result} 且声明了 {@code main} 函数时,自动传入节点参数并使用 + * {@code main} 返回的对象作为节点输出。

+ */ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { // 使用 Context.Builder 构建上下文,线程安全 @@ -37,6 +46,15 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { .option("js.ecmascript-version", "2021"); // 使用较新的 ECMAScript 版本 + /** + * 执行 JavaScript 代码并返回节点输出。 + * + * @param code 用户代码 + * @param node 当前代码节点 + * @param chain 当前工作流 + * @return 代码节点输出 + * @throws RuntimeException JavaScript 执行失败或 main 返回值不是对象时抛出 + */ @Override public Map execute(String code, CodeNode node, Chain chain) { try (Context context = CONTEXT_BUILDER.build()) { @@ -73,13 +91,48 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine { // 执行用户脚本 context.eval("js", code); - Value resultValue = bindings.getMember("_result"); - - return GraalvmToFastJSONUtils.toJSONObject(resultValue); + return resolveResult(context, bindings, parameterValues); } catch (Exception e) { throw new RuntimeException("Polyglot JS 脚本执行失败: " + e.getMessage(), e); } } + /** + * 解析脚本输出,兼容历史 _result 写法并自动调用 main 函数。 + * + * @param context JavaScript 执行上下文 + * @param bindings JavaScript 全局绑定 + * @param parameterValues 当前节点已解析的输入参数 + * @return 代码节点输出 + * @throws IllegalArgumentException main 返回值不是对象时抛出 + */ + private Map resolveResult(Context context, + Value bindings, + Map parameterValues) { + JSONObject legacyResult = GraalvmToFastJSONUtils.toJSONObject( + bindings.getMember("_result")); + if (!legacyResult.isEmpty()) { + return legacyResult; + } + + Value mainFunction = bindings.getMember("main"); + if (mainFunction == null || !mainFunction.canExecute()) { + return legacyResult; + } + + Map mainInput = parameterValues == null + ? Collections.emptyMap() + : parameterValues; + Value mainResult = mainFunction.execute( + JsInteropUtils.wrapJavaValueForJS(context, mainInput)); + Object convertedResult = + GraalvmToFastJSONUtils.toFastJsonValue(mainResult); + if (!(convertedResult instanceof JSONObject)) { + throw new IllegalArgumentException( + "JavaScript main 函数必须返回对象,例如:return { result: value }"); + } + return (JSONObject) convertedResult; + } + } 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 new file mode 100644 index 0000000..cf675e4 --- /dev/null +++ b/easy-agents-flow/src/test/java/com/easyagents/flow/core/test/JavascriptRuntimeEngineTest.java @@ -0,0 +1,122 @@ +package com.easyagents.flow.core.test; + +import com.easyagents.flow.core.chain.Chain; +import com.easyagents.flow.core.chain.ChainDefinition; +import com.easyagents.flow.core.chain.ChainState; +import com.easyagents.flow.core.chain.Parameter; +import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository; +import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository; +import com.easyagents.flow.core.node.CodeNode; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * JavaScript 代码节点输出约定回归测试。 + */ +public class JavascriptRuntimeEngineTest { + + /** + * 验证运行时自动调用 main,并把节点输入作为对象参数传入。 + */ + @Test + public void shouldInvokeMainAndUseReturnedObject() { + CodeNode node = codeNode( + String.join("\n", + "function main({ data1, data2 }) {", + " return {", + " joined: data1 + data2,", + " totalLength: data1.length + data2.length", + " };", + "}"), + "data1", + "data2"); + Chain chain = chain(Map.of( + "data1", "河北", + "data2", "分行")); + + Map result = node.execute(chain); + + Assert.assertEquals("河北分行", result.get("joined")); + Assert.assertEquals(4L, result.get("totalLength")); + } + + /** + * 验证已有 _result 输出优先,避免自动调用 main 改变历史流程。 + */ + @Test + public void shouldKeepLegacyResultWithoutInvokingMain() { + CodeNode node = codeNode( + String.join("\n", + "_result.answer = 'legacy';", + "function main() {", + " throw new Error('main should not be invoked');", + "}")); + Chain chain = chain(Collections.emptyMap()); + + Map result = node.execute(chain); + + Assert.assertEquals("legacy", result.get("answer")); + } + + /** + * 验证 main 返回非对象时给出明确错误。 + */ + @Test + public void shouldRejectNonObjectMainResult() { + CodeNode node = codeNode( + "function main() { return 'invalid'; }"); + Chain chain = chain(Collections.emptyMap()); + + try { + node.execute(chain); + Assert.fail("main 返回非对象时应执行失败"); + } catch (RuntimeException exception) { + Assert.assertTrue(exception.getMessage().contains( + "JavaScript main 函数必须返回对象")); + } + } + + /** + * 创建 JavaScript 代码节点。 + * + * @param code 用户代码 + * @param parameterNames 输入参数名 + * @return 代码节点 + */ + private CodeNode codeNode(String code, String... parameterNames) { + CodeNode node = new CodeNode(); + node.setId("code-node"); + node.setName("代码节点"); + node.setEngine("js"); + node.setCode(code); + node.setParameters(Arrays.stream(parameterNames) + .map(Parameter::new) + .collect(Collectors.toList())); + return node; + } + + /** + * 创建带初始化状态和输入变量的工作流。 + * + * @param inputs 工作流输入 + * @return 工作流 + */ + private Chain chain(Map inputs) { + Chain chain = new Chain( + new ChainDefinition(), + "javascript-runtime-" + UUID.randomUUID()); + chain.setChainStateRepository( + new InMemoryChainStateRepository()); + chain.setNodeStateRepository( + new InMemoryNodeStateRepository()); + ChainState state = chain.initializeState(); + state.getMemory().putAll(inputs); + return chain; + } +}