fix: 兼容工作流开始节点字段化参数解析

- 为开始节点输入补齐裸参数名与节点作用域别名映射

- 文本模板优先解析扁平节点路径并补充回归测试
This commit is contained in:
2026-04-12 20:31:02 +08:00
parent f57544daa2
commit 090eca5df5
6 changed files with 143 additions and 1 deletions

View File

@@ -62,6 +62,12 @@
<version>77.1</version> <version>77.1</version>
</dependency> </dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@@ -220,6 +220,7 @@ public class Chain {
if (variables != null && !variables.isEmpty()) { if (variables != null && !variables.isEmpty()) {
state.getMemory().putAll(variables); state.getMemory().putAll(variables);
applyStartParameterAliases(state.getMemory(), variables);
fields.add(ChainStateField.MEMORY); fields.add(ChainStateField.MEMORY);
} }
@@ -245,6 +246,48 @@ public class Chain {
} }
} }
/**
* 为开始节点输入参数补齐 `nodeId.paramName` 与 `paramName` 双向别名。
* 这样既兼容运行表单仅提交裸参数名,也兼容设计器内部统一保存完整引用路径。
*
* @param memory 流程内存
* @param variables 本次注入的变量
*/
public void applyStartParameterAliases(Map<String, Object> memory, Map<String, Object> variables) {
if (memory == null || variables == null || variables.isEmpty()) {
return;
}
List<Node> startNodes = definition == null ? Collections.emptyList() : definition.getStartNodes();
if (startNodes == null || startNodes.isEmpty()) {
return;
}
for (Node startNode : startNodes) {
if (startNode == null || StringUtil.noText(startNode.getId())) {
continue;
}
List<Parameter> parameters = startNode.getParameters();
if (parameters == null || parameters.isEmpty()) {
continue;
}
for (Parameter parameter : parameters) {
if (parameter == null || parameter.getRefType() != RefType.INPUT || StringUtil.noText(parameter.getName())) {
continue;
}
String parameterName = parameter.getName().trim();
String scopedName = startNode.getId() + "." + parameterName;
Object plainValue = variables.get(parameterName);
Object scopedValue = variables.get(scopedName);
if (plainValue != null && !memory.containsKey(scopedName)) {
memory.put(scopedName, plainValue);
}
if (scopedValue != null && !memory.containsKey(parameterName)) {
memory.put(parameterName, scopedValue);
}
}
}
}
public void executeNode(Node node, Trigger trigger) { public void executeNode(Node node, Trigger trigger) {
try { try {
EXECUTION_THREAD_LOCAL.set(this); EXECUTION_THREAD_LOCAL.set(this);

View File

@@ -156,6 +156,7 @@ public class ChainExecutor {
if (variables != null && !variables.isEmpty()) { if (variables != null && !variables.isEmpty()) {
temp.updateStateSafely(s -> { temp.updateStateSafely(s -> {
s.getMemory().putAll(variables); s.getMemory().putAll(variables);
temp.applyStartParameterAliases(s.getMemory(), variables);
return EnumSet.of(ChainStateField.MEMORY); return EnumSet.of(ChainStateField.MEMORY);
}); });
} }

View File

@@ -226,6 +226,14 @@ public class TextTemplate {
*/ */
private Object getValueByJsonPath(Map<String, Object> root, String path, boolean escapeForJsonOutput) { private Object getValueByJsonPath(Map<String, Object> root, String path, boolean escapeForJsonOutput) {
try { try {
Object directValue = MapUtil.getByPath(root, path);
if (directValue != null) {
if (escapeForJsonOutput && directValue instanceof String) {
return escapeJsonString((String) directValue);
}
return directValue;
}
String fullPath = path.startsWith("$") ? path : "$." + path; String fullPath = path.startsWith("$") ? path : "$." + path;
JSONPath compiled = MapUtil.computeIfAbsent(JSONPATH_CACHE, fullPath, JSONPath::compile); JSONPath compiled = MapUtil.computeIfAbsent(JSONPATH_CACHE, fullPath, JSONPath::compile);
Object value = compiled.eval(root); Object value = compiled.eval(root);

View File

@@ -0,0 +1,60 @@
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.Parameter;
import com.easyagents.flow.core.chain.RefType;
import com.easyagents.flow.core.node.StartNode;
import org.junit.Assert;
import org.junit.Test;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* 验证开始节点输入参数在运行时会同时写入裸参数名与 `nodeId.paramName` 别名。
*/
public class ChainStartParameterAliasTest {
@Test
public void shouldCreateScopedAliasFromPlainStartInput() {
Chain chain = createChain();
Map<String, Object> memory = new HashMap<>();
Map<String, Object> variables = new HashMap<>();
variables.put("user_input", "hello");
chain.applyStartParameterAliases(memory, variables);
Assert.assertEquals("hello", memory.get("start_1.user_input"));
Assert.assertNull(memory.get("user_input"));
}
@Test
public void shouldCreatePlainAliasFromScopedStartInput() {
Chain chain = createChain();
Map<String, Object> memory = new HashMap<>();
Map<String, Object> variables = new HashMap<>();
variables.put("start_1.user_input", "hello");
chain.applyStartParameterAliases(memory, variables);
Assert.assertEquals("hello", memory.get("user_input"));
Assert.assertNull(memory.get("start_1.user_input"));
}
private Chain createChain() {
ChainDefinition definition = new ChainDefinition();
StartNode startNode = new StartNode();
startNode.setId("start_1");
Parameter parameter = new Parameter();
parameter.setName("user_input");
parameter.setRefType(RefType.INPUT);
startNode.setParameters(java.util.Collections.singletonList(parameter));
definition.addNode(startNode);
definition.setEdges(Collections.emptyList());
return new Chain(definition, "state_1");
}
}

View File

@@ -0,0 +1,24 @@
package com.easyagents.flow.core.test;
import com.easyagents.flow.core.util.TextTemplate;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
/**
* 验证文本模板能解析带点号的扁平 key例如 `nodeId.paramName`。
*/
public class TextTemplatePathTest {
@Test
public void shouldResolveFlatScopedKeyBeforeJsonPathFallback() {
Map<String, Object> parameters = new HashMap<>();
parameters.put("node_1.user_input", "你好啊");
String result = TextTemplate.of("{{node_1.user_input}}").formatToString(parameters);
Assert.assertEquals("你好啊", result);
}
}