feat: 完成工作流开始节点开场表单

- 增加开始节点 startFormMeta/startFormSchema 配置与运行参数解析

- 统一 Admin/UserCenter 开场表单渲染与文件集合输入

- 补充开始表单校验、引用迁移和前端工具测试
This commit is contained in:
2026-04-19 13:57:57 +08:00
parent 8546d927bc
commit 9feb889637
26 changed files with 3391 additions and 172 deletions

View File

@@ -45,6 +45,7 @@ public class WorkflowCheckService {
private static final String TYPE_LOOP = "loopNode";
private static final String TYPE_WORKFLOW = "workflow-node";
private static final String TYPE_PLUGIN = "plugin-node";
private static final String SYSTEM_START_PARAM_NAME = "user_input";
@Resource
private WorkflowService workflowService;
@@ -78,6 +79,10 @@ public class WorkflowCheckService {
Set<String> issueKeys = new LinkedHashSet<>();
ParsedWorkflow parsedWorkflow = parseAndCheckBase(content, issues, issueKeys);
if (parsedWorkflow != null) {
List<NodeView> startNodes = parsedWorkflow.nodes.stream()
.filter(node -> TYPE_START.equals(node.type))
.collect(Collectors.toList());
checkStartFormSchema(startNodes, issues, issueKeys);
checkPluginSchemaHashes(parsedWorkflow, issues, issueKeys);
}
@@ -436,6 +441,69 @@ public class WorkflowCheckService {
detectWorkflowReferenceCycle(currentWorkflowIdString, currentWorkflowIdString, currentContent, contentCache, issues, issueKeys);
}
/**
* 校验开始节点开场表单 Schema 的最小约束。
*
* @param startNodes 开始节点列表
* @param issues 问题收集
* @param issueKeys 去重键集合
*/
private void checkStartFormSchema(List<NodeView> startNodes,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
for (NodeView startNode : startNodes) {
if (startNode == null || startNode.data == null) {
continue;
}
JSONArray schema = startNode.data.getJSONArray("startFormSchema");
if (schema == null || schema.isEmpty()) {
continue;
}
JSONObject userInputField = null;
for (int i = 0; i < schema.size(); i++) {
JSONObject field = schema.getJSONObject(i);
if (field == null) {
continue;
}
String key = trimToNull(field.getString("key"));
if (!StringUtils.hasText(key)) {
addIssue(issues, issueKeys, "START_FORM_FIELD_KEY_EMPTY",
"开始表单字段缺少 key", startNode.id, null, startNode.name);
continue;
}
String type = trimToNull(field.getString("type"));
if (Arrays.asList("radio", "checkbox", "select").contains(type)) {
JSONArray options = field.getJSONArray("options");
if (options == null || options.isEmpty()) {
addIssue(issues, issueKeys, "START_FORM_OPTIONS_EMPTY",
"选择类开始表单字段必须配置至少一个选项", startNode.id, null, startNode.name);
}
}
if (SYSTEM_START_PARAM_NAME.equals(key)) {
userInputField = field;
}
}
if (userInputField == null) {
addIssue(issues, issueKeys, "START_FORM_USER_INPUT_MISSING",
"开始表单必须包含 user_input 主问题字段", startNode.id, null, startNode.name);
continue;
}
if (!Boolean.TRUE.equals(userInputField.getBoolean("required"))) {
addIssue(issues, issueKeys, "START_FORM_USER_INPUT_REQUIRED",
"开始表单的 user_input 字段必须为必填", startNode.id, null, startNode.name);
}
String userInputType = trimToNull(userInputField.getString("type"));
if (!Arrays.asList("text", "textarea").contains(userInputType)) {
addIssue(issues, issueKeys, "START_FORM_USER_INPUT_TYPE_INVALID",
"开始表单的 user_input 字段只能使用 text 或 textarea", startNode.id, null, startNode.name);
}
}
}
private void detectWorkflowReferenceCycle(String rootWorkflowId, String currentWorkflowId, String currentContent,
Map<String, String> contentCache,
List<WorkflowCheckIssue> issues, Set<String> issueKeys) {

View File

@@ -0,0 +1,446 @@
package tech.easyflow.ai.easyagentsflow.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.chain.ChainDefinition;
import com.easyagents.flow.core.chain.Parameter;
import com.easyagents.flow.core.parser.ChainParser;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.ai.entity.Workflow;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 工作流运行参数解析器。
*
* <p>负责统一构建运行页所需的开始节点参数、开场表单元信息和字段 Schema
* 避免 Admin / Public / UserCenter 三端各自维护一套解析逻辑。</p>
*/
@Service
public class WorkflowRunningParameterResolver {
private static final String START_NODE_TYPE = "startNode";
private static final String SYSTEM_START_PARAM_NAME = "user_input";
private static final String DEFAULT_START_FORM_TITLE = "开始问答";
private static final String DEFAULT_START_FORM_DESCRIPTION = "请先补充必要信息,再开始执行工作流。";
private static final String DEFAULT_START_FORM_SUBMIT_TEXT = "开始";
private static final int FILE_MAX_COUNT = 10;
private static final long FILE_MAX_SINGLE_SIZE = 5L * 1024 * 1024;
private static final long FILE_MAX_TOTAL_SIZE = 50L * 1024 * 1024;
@Resource
private ChainParser chainParser;
@Resource
private WorkflowDatacenterContentService workflowDatacenterContentService;
/**
* 构建工作流运行参数视图。
*
* @param workflow 工作流实体
* @return 运行参数视图;当流程内容无法解析时返回 {@code null}
*/
public Map<String, Object> buildRunningParametersView(Workflow workflow) {
if (workflow == null) {
return null;
}
List<Parameter> startParameters = resolveStartParameters(workflow.getContent());
if (startParameters == null) {
return null;
}
JSONObject startNodeData = findStartNodeData(workflow.getContent());
List<Map<String, Object>> startFormSchema = resolveStartFormSchema(startNodeData, startParameters);
Map<String, Object> result = new LinkedHashMap<>();
result.put("parameters", startParameters);
result.put("title", workflow.getTitle());
result.put("description", workflow.getDescription());
result.put("icon", workflow.getIcon());
result.put("startFormMeta", resolveStartFormMeta(startNodeData));
result.put("startFormSchema", startFormSchema);
return result;
}
/**
* 解析开始节点运行参数。
*
* @param content 工作流内容
* @return 开始节点参数;解析失败时返回 {@code null}
*/
public List<Parameter> resolveStartParameters(String content) {
try {
String preparedContent = workflowDatacenterContentService.prepareContent(content);
ChainDefinition definition = chainParser.parse(preparedContent);
return definition == null ? null : definition.getStartParameters();
} catch (Exception ignored) {
return null;
}
}
/**
* 归一化工作流运行时变量,确保文件参数统一为文件对象数组。
*
* @param content 工作流内容
* @param variables 原始运行变量
* @return 归一化后的变量副本
*/
public Map<String, Object> normalizeRuntimeVariables(String content, Map<String, Object> variables) {
Map<String, Object> normalized = new LinkedHashMap<>();
if (variables != null) {
normalized.putAll(variables);
}
List<Parameter> startParameters = resolveStartParameters(content);
if (startParameters == null || startParameters.isEmpty()) {
return normalized;
}
for (Parameter parameter : startParameters) {
if (!isFileParameter(parameter)) {
continue;
}
String name = trimToNull(parameter.getName());
if (!StringUtils.hasText(name) || !normalized.containsKey(name)) {
continue;
}
normalized.put(name, normalizeFileVariableValue(normalized.get(name), name));
}
return normalized;
}
private Map<String, Object> resolveStartFormMeta(JSONObject startNodeData) {
JSONObject rawMeta = startNodeData == null ? null : startNodeData.getJSONObject("startFormMeta");
Map<String, Object> meta = new LinkedHashMap<>();
meta.put("title", trimToDefault(rawMeta == null ? null : rawMeta.getString("title"), DEFAULT_START_FORM_TITLE));
meta.put("description", trimToDefault(rawMeta == null ? null : rawMeta.getString("description"), DEFAULT_START_FORM_DESCRIPTION));
meta.put("submitText", trimToDefault(rawMeta == null ? null : rawMeta.getString("submitText"), DEFAULT_START_FORM_SUBMIT_TEXT));
return meta;
}
private List<Map<String, Object>> resolveStartFormSchema(JSONObject startNodeData, List<Parameter> parameters) {
JSONArray rawSchema = startNodeData == null ? null : startNodeData.getJSONArray("startFormSchema");
List<Map<String, Object>> schema = new ArrayList<>();
Set<String> seenKeys = new LinkedHashSet<>();
if (rawSchema != null && !rawSchema.isEmpty()) {
for (int i = 0; i < rawSchema.size(); i++) {
JSONObject field = rawSchema.getJSONObject(i);
Map<String, Object> normalized = normalizeStartFormField(field, null);
if (normalized == null) {
continue;
}
String key = String.valueOf(normalized.get("key"));
if (!seenKeys.add(key)) {
continue;
}
if (SYSTEM_START_PARAM_NAME.equals(key)) {
schema.add(0, normalized);
} else {
schema.add(normalized);
}
}
}
if (schema.isEmpty()) {
for (Parameter parameter : parameters) {
Map<String, Object> normalized = normalizeStartFormField(null, parameter);
if (normalized == null) {
continue;
}
String key = String.valueOf(normalized.get("key"));
if (!seenKeys.add(key)) {
continue;
}
if (SYSTEM_START_PARAM_NAME.equals(key)) {
schema.add(0, normalized);
} else {
schema.add(normalized);
}
}
}
if (!seenKeys.contains(SYSTEM_START_PARAM_NAME)) {
schema.add(0, normalizeStartFormField(new JSONObject(), null));
}
return schema;
}
private Map<String, Object> normalizeStartFormField(JSONObject field, Parameter parameter) {
String parameterName = parameter == null ? null : trimToNull(parameter.getName());
String key = trimToNull(field == null ? null : field.getString("key"));
if (!StringUtils.hasText(key)) {
key = parameterName;
}
if (!StringUtils.hasText(key)) {
key = SYSTEM_START_PARAM_NAME;
}
boolean systemReserved = SYSTEM_START_PARAM_NAME.equals(key)
|| (field != null && Boolean.TRUE.equals(field.getBoolean("systemReserved")));
String type = resolveStartFormFieldType(field == null ? null : field.getString("type"), parameter, systemReserved);
List<String> options = resolveFieldOptions(field, parameter, type);
Map<String, Object> normalized = new LinkedHashMap<>();
normalized.put("key", key);
normalized.put("label", trimToDefault(
field == null ? null : field.getString("label"),
parameter == null ? null : parameter.getFormLabel(),
SYSTEM_START_PARAM_NAME.equals(key) ? "用户问题" : key
));
normalized.put("type", type);
normalized.put("required", systemReserved || (field != null && Boolean.TRUE.equals(field.getBoolean("required")))
|| (parameter != null && parameter.isRequired()));
normalized.put("placeholder", trimToDefault(
field == null ? null : field.getString("placeholder"),
parameter == null ? null : parameter.getFormPlaceholder(),
SYSTEM_START_PARAM_NAME.equals(key) ? "请输入用户问题" : ""
));
normalized.put("description", trimToDefault(
field == null ? null : field.getString("description"),
parameter == null ? null : parameter.getFormDescription(),
""
));
normalized.put("defaultValue", resolveDefaultValue(field, parameter, type));
normalized.put("options", options);
normalized.put("systemReserved", systemReserved);
return normalized;
}
private Object resolveDefaultValue(JSONObject field, Parameter parameter, String type) {
Object rawDefaultValue = field == null ? null : field.get("defaultValue");
if (rawDefaultValue != null) {
if ("checkbox".equals(type) && rawDefaultValue instanceof List<?>) {
return rawDefaultValue;
}
return rawDefaultValue;
}
return parameter == null ? "" : parameter.getDefaultValue();
}
private List<String> resolveFieldOptions(JSONObject field, Parameter parameter, String type) {
if (!"radio".equals(type) && !"checkbox".equals(type) && !"select".equals(type)) {
return new ArrayList<>();
}
List<String> options = new ArrayList<>();
if (field != null) {
JSONArray rawOptions = field.getJSONArray("options");
if (rawOptions != null) {
for (int i = 0; i < rawOptions.size(); i++) {
String option = trimToNull(rawOptions.getString(i));
if (StringUtils.hasText(option)) {
options.add(option);
}
}
}
}
if (!options.isEmpty()) {
return options;
}
if (parameter != null && parameter.getEnums() != null) {
for (Object option : parameter.getEnums()) {
String normalized = trimToNull(option == null ? null : String.valueOf(option));
if (StringUtils.hasText(normalized)) {
options.add(normalized);
}
}
}
return options;
}
private String resolveStartFormFieldType(String rawType, Parameter parameter, boolean systemReserved) {
String parameterType = parameterType(parameter);
String requested = trimToNull(rawType);
String normalized;
if (StringUtils.hasText(requested)) {
normalized = requested;
} else if (StringUtils.hasText(parameterType)) {
normalized = parameterType;
} else {
normalized = systemReserved ? "textarea" : "text";
}
if (systemReserved) {
return "text".equals(normalized) ? "text" : "textarea";
}
return switch (normalized) {
case "textarea", "radio", "checkbox", "select", "file" -> normalized;
default -> "text";
};
}
private String parameterType(Parameter parameter) {
if (parameter == null) {
return null;
}
if ("file".equals(trimToNull(parameter.getContentType()))
|| "file".equalsIgnoreCase(trimToNull(String.valueOf(parameter.getDataType())))) {
return "file";
}
String formType = trimToNull(parameter.getFormType());
if ("textarea".equals(formType)) {
return "textarea";
}
if ("radio".equals(formType)) {
return "radio";
}
if ("checkbox".equals(formType)) {
return "checkbox";
}
if ("select".equals(formType)) {
return "select";
}
return SYSTEM_START_PARAM_NAME.equals(trimToNull(parameter.getName())) ? "textarea" : "text";
}
/**
* 判断参数是否为文件输入参数。
*
* @param parameter 参数定义
* @return 是否文件参数
*/
private boolean isFileParameter(Parameter parameter) {
if (parameter == null) {
return false;
}
return "file".equals(trimToNull(parameter.getContentType()))
|| "file".equalsIgnoreCase(trimToNull(String.valueOf(parameter.getDataType())));
}
/**
* 将单文件或多文件运行值归一化为文件对象数组。
*
* @param value 原始变量值
* @param parameterName 参数名
* @return 归一化后的文件对象数组
*/
private List<Object> normalizeFileVariableValue(Object value, String parameterName) {
List<Object> candidates = new ArrayList<>();
collectFileValues(value, candidates);
if (candidates.isEmpty()) {
return new ArrayList<>();
}
List<Object> normalized = new ArrayList<>();
Set<String> seenFilePaths = new LinkedHashSet<>();
long totalSize = 0L;
for (Object candidate : candidates) {
if (!(candidate instanceof Map<?, ?> fileMap)) {
throw new BusinessException("文件参数 " + parameterName + " 的输入格式不正确,必须为文件对象或文件对象数组");
}
String fileName = trimObjectToNull(fileMap.get("fileName"));
String filePath = trimObjectToNull(fileMap.get("filePath"));
if (!StringUtils.hasText(fileName)) {
throw new BusinessException("文件参数 " + parameterName + " 缺少 fileName");
}
if (!StringUtils.hasText(filePath)) {
throw new BusinessException("文件参数 " + parameterName + " 缺少 filePath");
}
if (!seenFilePaths.add(filePath)) {
continue;
}
Long size = parseLong(fileMap.get("size"));
if (size != null && size > FILE_MAX_SINGLE_SIZE) {
throw new BusinessException("文件参数 " + parameterName + " 中单个文件不能超过 5MB");
}
if (size != null && size > 0) {
totalSize += size;
}
normalized.add(new LinkedHashMap<>(fileMap));
}
if (normalized.size() > FILE_MAX_COUNT) {
throw new BusinessException("文件参数 " + parameterName + " 最多上传 10 个文件");
}
if (totalSize > FILE_MAX_TOTAL_SIZE) {
throw new BusinessException("文件参数 " + parameterName + " 的文件总大小不能超过 50MB");
}
return normalized;
}
private void collectFileValues(Object value, List<Object> result) {
if (value == null) {
return;
}
if (value instanceof Collection<?> collection) {
for (Object item : collection) {
collectFileValues(item, result);
}
return;
}
result.add(value);
}
private String trimObjectToNull(Object value) {
return trimToNull(value == null ? null : String.valueOf(value));
}
private Long parseLong(Object value) {
if (value == null) {
return null;
}
if (value instanceof Number number) {
return number.longValue();
}
String text = trimObjectToNull(value);
if (!StringUtils.hasText(text)) {
return null;
}
try {
return Long.parseLong(text);
} catch (NumberFormatException e) {
throw new BusinessException("文件大小格式不正确: " + text);
}
}
private JSONObject findStartNodeData(String content) {
if (!StringUtils.hasText(content)) {
return null;
}
try {
Object parsed = JSON.parse(content);
if (!(parsed instanceof JSONObject root)) {
return null;
}
JSONArray nodes = root.getJSONArray("nodes");
if (nodes == null) {
return null;
}
for (int i = 0; i < nodes.size(); i++) {
JSONObject node = nodes.getJSONObject(i);
if (node == null) {
continue;
}
if (!START_NODE_TYPE.equals(trimToNull(node.getString("type")))) {
continue;
}
return node.getJSONObject("data");
}
return null;
} catch (Exception ignored) {
return null;
}
}
private String trimToDefault(String value, String fallback) {
String normalized = trimToNull(value);
return StringUtils.hasText(normalized) ? normalized : fallback;
}
private String trimToDefault(String primary, String secondary, String fallback) {
String normalizedPrimary = trimToNull(primary);
if (StringUtils.hasText(normalizedPrimary)) {
return normalizedPrimary;
}
String normalizedSecondary = trimToNull(secondary);
return StringUtils.hasText(normalizedSecondary) ? normalizedSecondary : fallback;
}
private String trimToNull(String value) {
if (!StringUtils.hasText(value)) {
return null;
}
return value.trim();
}
}

View File

@@ -236,6 +236,98 @@ public class WorkflowCheckServiceTest {
Assert.assertEquals(0, result.getIssueCount());
}
@Test
public void testPreExecuteShouldBlockStartFormWithoutUserInput() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject startData = data("开始");
JSONArray schema = new JSONArray();
JSONObject field = new JSONObject();
field.put("key", "scene");
field.put("type", "select");
field.put("required", true);
JSONArray options = new JSONArray();
options.add("售前");
field.put("options", options);
schema.add(field);
startData.put("startFormSchema", schema);
String content = workflowJson(
array(
node("s1", "startNode", null, startData),
node("e1", "endNode", null, data("结束"))
),
array(edge("e1", "s1", "e1"))
);
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "START_FORM_USER_INPUT_MISSING");
}
@Test
public void testPreExecuteShouldBlockInvalidStartFormUserInputTypeAndOptions() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject startData = data("开始");
JSONArray schema = new JSONArray();
JSONObject userInputField = new JSONObject();
userInputField.put("key", "user_input");
userInputField.put("type", "radio");
userInputField.put("required", false);
schema.add(userInputField);
JSONObject selectField = new JSONObject();
selectField.put("key", "scene");
selectField.put("type", "select");
selectField.put("required", true);
schema.add(selectField);
startData.put("startFormSchema", schema);
String content = workflowJson(
array(
node("s1", "startNode", null, startData),
node("e1", "endNode", null, data("结束"))
),
array(edge("e1", "s1", "e1"))
);
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "START_FORM_USER_INPUT_REQUIRED");
assertHasCode(result, "START_FORM_USER_INPUT_TYPE_INVALID");
assertHasCode(result, "START_FORM_OPTIONS_EMPTY");
}
@Test
public void testSaveShouldBlockInvalidStartFormSchema() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject startData = data("开始");
JSONArray schema = new JSONArray();
JSONObject userInputField = new JSONObject();
userInputField.put("key", "user_input");
userInputField.put("type", "radio");
userInputField.put("required", false);
schema.add(userInputField);
JSONObject checkboxField = new JSONObject();
checkboxField.put("key", "labels");
checkboxField.put("type", "checkbox");
checkboxField.put("required", true);
schema.add(checkboxField);
startData.put("startFormSchema", schema);
String content = workflowJson(
array(node("s1", "startNode", null, startData)),
new JSONArray()
);
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "START_FORM_USER_INPUT_REQUIRED");
assertHasCode(result, "START_FORM_USER_INPUT_TYPE_INVALID");
assertHasCode(result, "START_FORM_OPTIONS_EMPTY");
}
private static WorkflowCheckService newService(Map<String, String> workflowStore) throws Exception {
WorkflowCheckService service = new WorkflowCheckService();
ChainParser parser = ChainParser.builder()

View File

@@ -0,0 +1,267 @@
package tech.easyflow.ai.easyagentsflow.service;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.easyagents.flow.core.parser.ChainParser;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.node.SearchDatasetNodeParser;
import tech.easyflow.ai.node.WorkflowNodeParser;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* {@link WorkflowRunningParameterResolver} 测试。
*/
public class WorkflowRunningParameterResolverTest {
/**
* 应返回显式配置的开场表单元信息与字段 Schema。
*
* @throws Exception 反射注入失败
*/
@Test
public void testBuildRunningParametersViewShouldKeepStartFormSchema() throws Exception {
WorkflowRunningParameterResolver resolver = newResolver();
JSONObject startData = data("开始");
JSONArray schema = new JSONArray();
JSONObject userInputField = new JSONObject();
userInputField.put("key", "user_input");
userInputField.put("label", "主问题");
userInputField.put("type", "text");
userInputField.put("required", true);
schema.add(userInputField);
JSONObject fileField = new JSONObject();
fileField.put("key", "attachments");
fileField.put("label", "附件");
fileField.put("type", "file");
fileField.put("required", false);
schema.add(fileField);
JSONObject meta = new JSONObject();
meta.put("title", "问答入口");
meta.put("description", "请先填写信息");
meta.put("submitText", "立即开始");
startData.put("startFormMeta", meta);
startData.put("startFormSchema", schema);
startData.put("parameters", startParameters());
Workflow workflow = workflow(
workflowJson(
array(
node("s1", "startNode", null, startData),
node("e1", "endNode", null, data("结束"))
),
array(edge("e1", "s1", "e1"))
)
);
Map<String, Object> result = resolver.buildRunningParametersView(workflow);
Assert.assertNotNull(result);
Assert.assertEquals("问答入口", ((Map<?, ?>) result.get("startFormMeta")).get("title"));
List<Map<String, Object>> fields = (List<Map<String, Object>>) result.get("startFormSchema");
Assert.assertEquals(2, fields.size());
Assert.assertEquals("user_input", fields.get(0).get("key"));
Assert.assertEquals("text", fields.get(0).get("type"));
Assert.assertEquals("attachments", fields.get(1).get("key"));
Assert.assertEquals("file", fields.get(1).get("type"));
}
/**
* 旧工作流仅保留 parameters 时,也应补出最小开场表单 Schema。
*
* @throws Exception 反射注入失败
*/
@Test
public void testBuildRunningParametersViewShouldFallbackToParameters() throws Exception {
WorkflowRunningParameterResolver resolver = newResolver();
JSONObject startData = data("开始");
startData.put("parameters", startParameters());
Workflow workflow = workflow(
workflowJson(
array(
node("s1", "startNode", null, startData),
node("e1", "endNode", null, data("结束"))
),
array(edge("e1", "s1", "e1"))
)
);
Map<String, Object> result = resolver.buildRunningParametersView(workflow);
Assert.assertNotNull(result);
List<Map<String, Object>> fields = (List<Map<String, Object>>) result.get("startFormSchema");
Assert.assertEquals(2, fields.size());
Assert.assertEquals("user_input", fields.get(0).get("key"));
Assert.assertEquals("textarea", fields.get(0).get("type"));
Assert.assertEquals(Boolean.TRUE, fields.get(0).get("required"));
Assert.assertEquals("attachments", fields.get(1).get("key"));
Assert.assertEquals("file", fields.get(1).get("type"));
}
/**
* 文件参数运行值应统一归一化为数组并按 filePath 去重。
*
* @throws Exception 反射注入失败
*/
@Test
public void testNormalizeRuntimeVariablesShouldWrapFileValueIntoArray() throws Exception {
WorkflowRunningParameterResolver resolver = newResolver();
Map<String, Object> variables = new LinkedHashMap<>();
variables.put("attachments", fileValue("demo.pdf", "/files/demo.pdf", 1024L));
Map<String, Object> normalized = resolver.normalizeRuntimeVariables(workflowContentWithStartParameters(), variables);
Object attachments = normalized.get("attachments");
Assert.assertTrue(attachments instanceof List<?>);
Assert.assertEquals(1, ((List<?>) attachments).size());
Assert.assertTrue(((List<?>) attachments).get(0) instanceof Map<?, ?>);
}
/**
* 多文件参数应按 filePath 去重并保留已有非文件变量。
*
* @throws Exception 反射注入失败
*/
@Test
public void testNormalizeRuntimeVariablesShouldDedupeFileArray() throws Exception {
WorkflowRunningParameterResolver resolver = newResolver();
Map<String, Object> variables = new LinkedHashMap<>();
variables.put("user_input", "hello");
variables.put("attachments", List.of(
fileValue("a.pdf", "/files/a.pdf", 1024L),
fileValue("a-copy.pdf", "/files/a.pdf", 1024L),
fileValue("b.pdf", "/files/b.pdf", 1024L)
));
Map<String, Object> normalized = resolver.normalizeRuntimeVariables(workflowContentWithStartParameters(), variables);
Assert.assertEquals("hello", normalized.get("user_input"));
Object attachments = normalized.get("attachments");
Assert.assertTrue(attachments instanceof List<?>);
Assert.assertEquals(2, ((List<?>) attachments).size());
}
private static WorkflowRunningParameterResolver newResolver() throws Exception {
WorkflowRunningParameterResolver resolver = new WorkflowRunningParameterResolver();
ChainParser parser = ChainParser.builder()
.withDefaultParsers(true)
.build();
parser.addNodeParser("workflow-node", new WorkflowNodeParser());
parser.addNodeParser("search-dataset-node", new SearchDatasetNodeParser());
setField(resolver, "chainParser", parser);
setField(resolver, "workflowDatacenterContentService", new WorkflowDatacenterContentService());
return resolver;
}
private static Workflow workflow(String content) {
Workflow workflow = new Workflow();
workflow.setId(BigInteger.ONE);
workflow.setTitle("测试工作流");
workflow.setDescription("用于测试运行参数");
workflow.setIcon("icon.png");
workflow.setContent(content);
return workflow;
}
private static String workflowContentWithStartParameters() {
JSONObject startData = data("开始");
startData.put("parameters", startParameters());
return workflowJson(
array(
node("s1", "startNode", null, startData),
node("e1", "endNode", null, data("结束"))
),
array(edge("e1", "s1", "e1"))
);
}
private static JSONArray startParameters() {
JSONArray parameters = new JSONArray();
JSONObject userInput = new JSONObject();
userInput.put("name", "user_input");
userInput.put("dataType", "String");
userInput.put("refType", "input");
userInput.put("required", true);
userInput.put("contentType", "text");
userInput.put("formType", "textarea");
userInput.put("formLabel", "用户问题");
userInput.put("formPlaceholder", "请输入用户问题");
parameters.add(userInput);
JSONObject fileField = new JSONObject();
fileField.put("name", "attachments");
fileField.put("dataType", "File");
fileField.put("refType", "input");
fileField.put("required", false);
fileField.put("contentType", "file");
fileField.put("formType", "input");
fileField.put("formLabel", "附件");
parameters.add(fileField);
return parameters;
}
private static Map<String, Object> fileValue(String fileName, String filePath, Long size) {
Map<String, Object> value = new LinkedHashMap<>();
value.put("fileName", fileName);
value.put("filePath", filePath);
value.put("size", size);
value.put("contentType", "application/pdf");
value.put("url", filePath);
return value;
}
private static void setField(Object target, String fieldName, Object value) throws Exception {
Field field = WorkflowRunningParameterResolver.class.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
private static String workflowJson(JSONArray nodes, JSONArray edges) {
JSONObject root = new JSONObject();
root.put("nodes", nodes);
root.put("edges", edges);
return root.toJSONString();
}
private static JSONArray array(JSONObject... objects) {
JSONArray array = new JSONArray();
for (JSONObject object : objects) {
array.add(object);
}
return array;
}
private static JSONObject node(String id, String type, String parentId, JSONObject data) {
JSONObject node = new JSONObject();
node.put("id", id);
node.put("type", type);
if (parentId != null) {
node.put("parentId", parentId);
}
node.put("data", data);
return node;
}
private static JSONObject data(String title) {
JSONObject data = new JSONObject();
data.put("title", title);
return data;
}
private static JSONObject edge(String id, String source, String target) {
JSONObject edge = new JSONObject();
edge.put("id", id);
edge.put("source", source);
edge.put("target", target);
return edge;
}
}