feat: 支持代码节点自动调用 main
- 将节点输入作为对象参数传入 main,并映射对象返回值 - 保留 _result 兼容并补充异常与回归测试
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package com.easyagents.flow.core.code.impl;
|
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.Chain;
|
||||||
import com.easyagents.flow.core.chain.ChainState;
|
import com.easyagents.flow.core.chain.ChainState;
|
||||||
import com.easyagents.flow.core.chain.NodeState;
|
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.HostAccess;
|
||||||
import org.graalvm.polyglot.Value;
|
import org.graalvm.polyglot.Value;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 GraalVM 的 JavaScript 代码节点执行器。
|
||||||
|
*
|
||||||
|
* <p>优先保留历史 {@code _result} 输出约定;当脚本未写入
|
||||||
|
* {@code _result} 且声明了 {@code main} 函数时,自动传入节点参数并使用
|
||||||
|
* {@code main} 返回的对象作为节点输出。</p>
|
||||||
|
*/
|
||||||
public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||||
|
|
||||||
// 使用 Context.Builder 构建上下文,线程安全
|
// 使用 Context.Builder 构建上下文,线程安全
|
||||||
@@ -37,6 +46,15 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
.option("js.ecmascript-version", "2021"); // 使用较新的 ECMAScript 版本
|
.option("js.ecmascript-version", "2021"); // 使用较新的 ECMAScript 版本
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行 JavaScript 代码并返回节点输出。
|
||||||
|
*
|
||||||
|
* @param code 用户代码
|
||||||
|
* @param node 当前代码节点
|
||||||
|
* @param chain 当前工作流
|
||||||
|
* @return 代码节点输出
|
||||||
|
* @throws RuntimeException JavaScript 执行失败或 main 返回值不是对象时抛出
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> execute(String code, CodeNode node, Chain chain) {
|
public Map<String, Object> execute(String code, CodeNode node, Chain chain) {
|
||||||
try (Context context = CONTEXT_BUILDER.build()) {
|
try (Context context = CONTEXT_BUILDER.build()) {
|
||||||
@@ -73,13 +91,48 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
// 执行用户脚本
|
// 执行用户脚本
|
||||||
context.eval("js", code);
|
context.eval("js", code);
|
||||||
|
|
||||||
Value resultValue = bindings.getMember("_result");
|
return resolveResult(context, bindings, parameterValues);
|
||||||
|
|
||||||
return GraalvmToFastJSONUtils.toJSONObject(resultValue);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Polyglot JS 脚本执行失败: " + e.getMessage(), 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<String, Object> resolveResult(Context context,
|
||||||
|
Value bindings,
|
||||||
|
Map<String, Object> 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<String, Object> 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user