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