feat: 支持代码节点 main 命名参数调用

- 按节点参数顺序向 JavaScript main 传入独立参数值

- 保留历史单对象参数与显式 _result 输出兼容

- 补充参数模式解析和运行时回归测试
This commit is contained in:
2026-08-03 16:58:11 +08:00
parent 5fd4d845af
commit bdb69a2250
4 changed files with 158 additions and 7 deletions

View File

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

View File

@@ -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)) {

View File

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

View File

@@ -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 返回非对象时给出明确错误。
*/ */