发布 v1.1.0 #2
@@ -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 代码节点执行器。
|
||||
*
|
||||
* <p>优先保留历史 {@code _result} 输出约定;当脚本未写入
|
||||
* {@code _result} 且声明了 {@code main} 函数时,自动传入节点参数并使用
|
||||
* {@code main} 返回的对象作为节点输出。</p>
|
||||
*/
|
||||
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<String, Object> 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<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