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;
+ }
+}