发布 v1.1.0 #2

Merged
czm merged 34 commits from develop into main 2026-08-20 11:35:40 +08:00
2 changed files with 178 additions and 3 deletions
Showing only changes of commit e74d229de2 - Show all commits

View File

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

View File

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