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.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;
|
||||||
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
import com.easyagents.flow.core.code.CodeRuntimeEngine;
|
import com.easyagents.flow.core.code.CodeRuntimeEngine;
|
||||||
import com.easyagents.flow.core.node.CodeNode;
|
import com.easyagents.flow.core.node.CodeNode;
|
||||||
import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
|
import com.easyagents.flow.core.util.graalvm.JsInteropUtils;
|
||||||
@@ -70,6 +71,10 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
.build();
|
.build();
|
||||||
private static final Source RESULT_INIT_SOURCE =
|
private static final Source RESULT_INIT_SOURCE =
|
||||||
Source.create("js", "var _result = {};");
|
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 =
|
private static final List<Map<String, Source>> SOURCE_CACHE_SEGMENTS =
|
||||||
createSourceCacheSegments();
|
createSourceCacheSegments();
|
||||||
private static final ScheduledThreadPoolExecutor
|
private static final ScheduledThreadPoolExecutor
|
||||||
@@ -159,6 +164,7 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
|
|
||||||
// 在 JS 中创建 _result 对象
|
// 在 JS 中创建 _result 对象
|
||||||
context.eval(RESULT_INIT_SOURCE);
|
context.eval(RESULT_INIT_SOURCE);
|
||||||
|
Value initialResult = bindings.getMember("_result");
|
||||||
|
|
||||||
// 注入 _chain 和 _context
|
// 注入 _chain 和 _context
|
||||||
bindings.putMember("_chain", chain);
|
bindings.putMember("_chain", chain);
|
||||||
@@ -167,7 +173,18 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
// 执行用户脚本
|
// 执行用户脚本
|
||||||
context.eval(source(code, node));
|
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) {
|
} catch (PolyglotException e) {
|
||||||
throw executionException(e, timedOut.get());
|
throw executionException(e, timedOut.get());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -389,16 +406,20 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
*
|
*
|
||||||
* @param context JavaScript 执行上下文
|
* @param context JavaScript 执行上下文
|
||||||
* @param bindings JavaScript 全局绑定
|
* @param bindings JavaScript 全局绑定
|
||||||
|
* @param node 当前代码节点
|
||||||
* @param parameterValues 当前节点已解析的输入参数
|
* @param parameterValues 当前节点已解析的输入参数
|
||||||
|
* @param legacyResultAssigned 用户代码是否重新赋值 _result
|
||||||
* @return 代码节点输出
|
* @return 代码节点输出
|
||||||
* @throws IllegalArgumentException main 返回值不是对象时抛出
|
* @throws IllegalArgumentException main 返回值不是对象时抛出
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> resolveResult(Context context,
|
private Map<String, Object> resolveResult(Context context,
|
||||||
Value bindings,
|
Value bindings,
|
||||||
Map<String, Object> parameterValues) {
|
CodeNode node,
|
||||||
|
Map<String, Object> parameterValues,
|
||||||
|
boolean legacyResultAssigned) {
|
||||||
JSONObject legacyResult = GraalvmToFastJSONUtils.toJSONObject(
|
JSONObject legacyResult = GraalvmToFastJSONUtils.toJSONObject(
|
||||||
bindings.getMember("_result"));
|
bindings.getMember("_result"));
|
||||||
if (!legacyResult.isEmpty()) {
|
if (legacyResultAssigned || !legacyResult.isEmpty()) {
|
||||||
return legacyResult;
|
return legacyResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,11 +428,8 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
return legacyResult;
|
return legacyResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> mainInput = parameterValues == null
|
|
||||||
? Collections.emptyMap()
|
|
||||||
: parameterValues;
|
|
||||||
Value mainResult = mainFunction.execute(
|
Value mainResult = mainFunction.execute(
|
||||||
JsInteropUtils.wrapJavaValueForJS(context, mainInput));
|
resolveMainArguments(context, node, parameterValues));
|
||||||
Object convertedResult =
|
Object convertedResult =
|
||||||
GraalvmToFastJSONUtils.toFastJsonValue(mainResult);
|
GraalvmToFastJSONUtils.toFastJsonValue(mainResult);
|
||||||
if (!(convertedResult instanceof JSONObject)) {
|
if (!(convertedResult instanceof JSONObject)) {
|
||||||
@@ -421,4 +439,45 @@ public class JavascriptRuntimeEngine implements CodeRuntimeEngine {
|
|||||||
return (JSONObject) convertedResult;
|
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 {
|
public class CodeNode extends BaseNode {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* main 按节点参数顺序接收独立参数。
|
||||||
|
*/
|
||||||
|
public static final String MAIN_ARGS_MODE_NAMED = "named";
|
||||||
|
|
||||||
protected String engine;
|
protected String engine;
|
||||||
protected String code;
|
protected String code;
|
||||||
|
/**
|
||||||
|
* main 自动调用参数模式;空值沿用历史单对象参数。
|
||||||
|
*/
|
||||||
|
protected String mainArgsMode;
|
||||||
|
|
||||||
public String getEngine() {
|
public String getEngine() {
|
||||||
return engine;
|
return engine;
|
||||||
@@ -48,6 +57,24 @@ public class CodeNode extends BaseNode {
|
|||||||
this.code = code;
|
this.code = code;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 main 自动调用参数模式。
|
||||||
|
*
|
||||||
|
* @return 参数模式;空值表示历史单对象参数模式
|
||||||
|
*/
|
||||||
|
public String getMainArgsMode() {
|
||||||
|
return mainArgsMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 main 自动调用参数模式。
|
||||||
|
*
|
||||||
|
* @param mainArgsMode 参数模式
|
||||||
|
*/
|
||||||
|
public void setMainArgsMode(String mainArgsMode) {
|
||||||
|
this.mainArgsMode = mainArgsMode;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> execute(Chain chain) {
|
public Map<String, Object> execute(Chain chain) {
|
||||||
if (StringUtil.noText(code)) {
|
if (StringUtil.noText(code)) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public class CodeNodeParser extends BaseNodeParser<CodeNode> {
|
|||||||
CodeNode codeNode = new CodeNode();
|
CodeNode codeNode = new CodeNode();
|
||||||
codeNode.setEngine(engine);
|
codeNode.setEngine(engine);
|
||||||
codeNode.setCode(data.getString("code"));
|
codeNode.setCode(data.getString("code"));
|
||||||
|
codeNode.setMainArgsMode(data.getString("mainArgsMode"));
|
||||||
return codeNode;
|
return codeNode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.easyagents.flow.core.test;
|
package com.easyagents.flow.core.test;
|
||||||
|
|
||||||
|
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.ChainDefinition;
|
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||||
import com.easyagents.flow.core.chain.ChainState;
|
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.JavascriptExecutionException;
|
||||||
import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine;
|
import com.easyagents.flow.core.code.impl.JavascriptRuntimeEngine;
|
||||||
import com.easyagents.flow.core.node.CodeNode;
|
import com.easyagents.flow.core.node.CodeNode;
|
||||||
|
import com.easyagents.flow.core.parser.impl.CodeNodeParser;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
@@ -57,6 +59,46 @@ public class JavascriptRuntimeEngineTest {
|
|||||||
Assert.assertEquals(4L, result.get("totalLength"));
|
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 改变历史流程。
|
* 验证已有 _result 输出优先,避免自动调用 main 改变历史流程。
|
||||||
*/
|
*/
|
||||||
@@ -75,6 +117,28 @@ public class JavascriptRuntimeEngineTest {
|
|||||||
Assert.assertEquals("legacy", result.get("answer"));
|
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 返回非对象时给出明确错误。
|
* 验证 main 返回非对象时给出明确错误。
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user