feat: 支持代码节点 main 命名参数调用
- 按节点参数顺序向 JavaScript main 传入独立参数值 - 保留历史单对象参数与显式 _result 输出兼容 - 补充参数模式解析和运行时回归测试
This commit is contained in:
@@ -19,6 +19,7 @@ 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;
|
||||
import com.easyagents.flow.core.chain.Parameter;
|
||||
import com.easyagents.flow.core.code.CodeRuntimeEngine;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
|
||||
@@ -70,6 +71,10 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
.build();
|
||||
private static final Source RESULT_INIT_SOURCE =
|
||||
Source.create("js", "var _result = {};");
|
||||
private static final Source STRICT_EQUAL_SOURCE =
|
||||
Source.create(
|
||||
"js",
|
||||
"(left, right) => left === right;");
|
||||
private static final List<Map<String, Source>> SOURCE_CACHE_SEGMENTS =
|
||||
createSourceCacheSegments();
|
||||
private static final ScheduledThreadPoolExecutor
|
||||
@@ -159,6 +164,7 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
|
||||
// 在 JS 中创建 _result 对象
|
||||
context.eval(RESULT_INIT_SOURCE);
|
||||
Value initialResult = bindings.getMember("_result");
|
||||
|
||||
// 注入 _chain 和 _context
|
||||
bindings.putMember("_chain", chain);
|
||||
@@ -167,7 +173,18 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
// 执行用户脚本
|
||||
context.eval(source(code, node));
|
||||
|
||||
return resolveResult(context, bindings, parameterValues);
|
||||
boolean legacyResultAssigned =
|
||||
!context.eval(STRICT_EQUAL_SOURCE)
|
||||
.execute(
|
||||
initialResult,
|
||||
bindings.getMember("_result"))
|
||||
.asBoolean();
|
||||
return resolveResult(
|
||||
context,
|
||||
bindings,
|
||||
node,
|
||||
parameterValues,
|
||||
legacyResultAssigned);
|
||||
} catch (PolyglotException e) {
|
||||
throw executionException(e, timedOut.get());
|
||||
} catch (Exception e) {
|
||||
@@ -389,16 +406,20 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
*
|
||||
* @param context JavaScript 执行上下文
|
||||
* @param bindings JavaScript 全局绑定
|
||||
* @param node 当前代码节点
|
||||
* @param parameterValues 当前节点已解析的输入参数
|
||||
* @param legacyResultAssigned 用户代码是否重新赋值 _result
|
||||
* @return 代码节点输出
|
||||
* @throws IllegalArgumentException main 返回值不是对象时抛出
|
||||
*/
|
||||
private Map<String, Object> resolveResult(Context context,
|
||||
Value bindings,
|
||||
Map<String, Object> parameterValues) {
|
||||
CodeNode node,
|
||||
Map<String, Object> parameterValues,
|
||||
boolean legacyResultAssigned) {
|
||||
JSONObject legacyResult = GraalvmToFastJSONUtils.toJSONObject(
|
||||
bindings.getMember("_result"));
|
||||
if (!legacyResult.isEmpty()) {
|
||||
if (legacyResultAssigned || !legacyResult.isEmpty()) {
|
||||
return legacyResult;
|
||||
}
|
||||
|
||||
@@ -407,11 +428,8 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
return legacyResult;
|
||||
}
|
||||
|
||||
Map<String, Object> mainInput = parameterValues == null
|
||||
? Collections.emptyMap()
|
||||
: parameterValues;
|
||||
Value mainResult = mainFunction.execute(
|
||||
JsInteropUtils.wrapJavaValueForJS(context, mainInput));
|
||||
resolveMainArguments(context, node, parameterValues));
|
||||
Object convertedResult =
|
||||
GraalvmToFastJSONUtils.toFastJsonValue(mainResult);
|
||||
if (!(convertedResult instanceof JSONObject)) {
|
||||
@@ -421,4 +439,45 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
||||
return (JSONObject) convertedResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据节点契约构建 main 自动调用参数。
|
||||
*
|
||||
* @param context JavaScript 执行上下文
|
||||
* @param node 当前代码节点
|
||||
* @param parameterValues 当前节点已解析的输入参数
|
||||
* @return GraalVM main 调用参数
|
||||
*/
|
||||
private Object[] resolveMainArguments(
|
||||
Context context,
|
||||
CodeNode node,
|
||||
Map<String, Object> parameterValues) {
|
||||
if (node != null
|
||||
&& CodeNode.MAIN_ARGS_MODE_NAMED.equals(
|
||||
node.getMainArgsMode())) {
|
||||
List<Parameter> parameters = node.getParameters();
|
||||
if (parameters == null || parameters.isEmpty()) {
|
||||
return new Object[0];
|
||||
}
|
||||
Object[] arguments = new Object[parameters.size()];
|
||||
for (int index = 0; index < parameters.size(); index++) {
|
||||
String name = parameters.get(index).getName();
|
||||
Object value = parameterValues == null
|
||||
? null
|
||||
: parameterValues.get(name);
|
||||
arguments[index] =
|
||||
JsInteropUtils.wrapJavaValueForJS(
|
||||
context, value);
|
||||
}
|
||||
return arguments;
|
||||
}
|
||||
|
||||
Map<String, Object> mainInput = parameterValues == null
|
||||
? Collections.emptyMap()
|
||||
: parameterValues;
|
||||
return new Object[]{
|
||||
JsInteropUtils.wrapJavaValueForJS(
|
||||
context, mainInput)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -29,8 +29,17 @@ import java.util.Map;
|
||||
public class CodeNode extends BaseNode {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* main 按节点参数顺序接收独立参数。
|
||||
*/
|
||||
public static final String MAIN_ARGS_MODE_NAMED = "named";
|
||||
|
||||
protected String engine;
|
||||
protected String code;
|
||||
/**
|
||||
* main 自动调用参数模式;空值沿用历史单对象参数。
|
||||
*/
|
||||
protected String mainArgsMode;
|
||||
|
||||
public String getEngine() {
|
||||
return engine;
|
||||
@@ -48,6 +57,24 @@ public class CodeNode extends BaseNode {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 main 自动调用参数模式。
|
||||
*
|
||||
* @return 参数模式;空值表示历史单对象参数模式
|
||||
*/
|
||||
public String getMainArgsMode() {
|
||||
return mainArgsMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 main 自动调用参数模式。
|
||||
*
|
||||
* @param mainArgsMode 参数模式
|
||||
*/
|
||||
public void setMainArgsMode(String mainArgsMode) {
|
||||
this.mainArgsMode = mainArgsMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> execute(Chain chain) {
|
||||
if (StringUtil.noText(code)) {
|
||||
|
||||
@@ -27,6 +27,7 @@ public class CodeNodeParser extends BaseNodeParser<CodeNode> {
|
||||
CodeNode codeNode = new CodeNode();
|
||||
codeNode.setEngine(engine);
|
||||
codeNode.setCode(data.getString("code"));
|
||||
codeNode.setMainArgsMode(data.getString("mainArgsMode"));
|
||||
return codeNode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.easyagents.flow.core.test;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.easyagents.flow.core.chain.Chain;
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.ChainState;
|
||||
@@ -9,6 +10,7 @@ import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
|
||||
import com.easyagents.flow.core.code.impl.JavascriptExecutionException;
|
||||
import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine;
|
||||
import com.easyagents.flow.core.node.CodeNode;
|
||||
import com.easyagents.flow.core.parser.impl.CodeNodeParser;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -57,6 +59,46 @@ public class JavascriptRuntimeEngineTest {
|
||||
Assert.assertEquals(4L, result.get("totalLength"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证新节点按配置顺序把输入值传给 main 的独立形参。
|
||||
*/
|
||||
@Test
|
||||
public void shouldInvokeMainWithNamedArguments() {
|
||||
CodeNode node = codeNode(
|
||||
String.join("\n",
|
||||
"function main(data1, data2) {",
|
||||
" return { joined: data1 + data2 };",
|
||||
"}"),
|
||||
"data1",
|
||||
"data2");
|
||||
node.setMainArgsMode(CodeNode.MAIN_ARGS_MODE_NAMED);
|
||||
Chain chain = chain(Map.of(
|
||||
"data1", "河北",
|
||||
"data2", "分行"));
|
||||
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
|
||||
Assert.assertEquals("河北分行", result.get("joined"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证解析器保留设计器声明的 main 参数模式。
|
||||
*/
|
||||
@Test
|
||||
public void shouldParseMainArgsMode() {
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("engine", "js");
|
||||
data.put("code", "function main(data) { return { data }; }");
|
||||
data.put("mainArgsMode", CodeNode.MAIN_ARGS_MODE_NAMED);
|
||||
|
||||
CodeNode node = new CodeNodeParser().doParse(
|
||||
new JSONObject(), data, new JSONObject());
|
||||
|
||||
Assert.assertEquals(
|
||||
CodeNode.MAIN_ARGS_MODE_NAMED,
|
||||
node.getMainArgsMode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证已有 _result 输出优先,避免自动调用 main 改变历史流程。
|
||||
*/
|
||||
@@ -75,6 +117,28 @@ public class JavascriptRuntimeEngineTest {
|
||||
Assert.assertEquals("legacy", result.get("answer"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证显式调用返回空对象时不会再次自动调用 main。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepEmptyExplicitResultWithoutInvokingMainAgain() {
|
||||
CodeNode node = codeNode(
|
||||
String.join("\n",
|
||||
"function main(data) {",
|
||||
" if (typeof data !== 'string') {",
|
||||
" throw new Error('main invoked twice');",
|
||||
" }",
|
||||
" return {};",
|
||||
"}",
|
||||
"_result = main(data);"),
|
||||
"data");
|
||||
Chain chain = chain(Map.of("data", "hello"));
|
||||
|
||||
Map<String, Object> result = node.execute(chain);
|
||||
|
||||
Assert.assertTrue(result.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 main 返回非对象时给出明确错误。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user