feat: 完成工作流开始节点开场表单
- 增加开始节点 startFormMeta/startFormSchema 配置与运行参数解析 - 统一 Admin/UserCenter 开场表单渲染与文件集合输入 - 补充开始表单校验、引用迁移和前端工具测试
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user