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

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