feat: 完善循环节点配置与作用域输出

- 支持次数与数组独立或组合配置并补齐检查规则

- 统一循环体临时变量与下游正式输出候选

- 稳定知识库对象数组字段并补充前后端测试
This commit is contained in:
2026-07-29 18:10:23 +08:00
parent 19c7b60a65
commit 766554bf63
13 changed files with 1737 additions and 62 deletions

View File

@@ -0,0 +1,83 @@
package tech.easyflow.ai.easyagentsflow.knowledge;
import com.easyagents.core.document.Document;
import com.easyagents.flow.core.knowledge.Knowledge;
import com.easyagents.flow.core.node.KnowledgeNode;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
import tech.easyflow.ai.service.DocumentCollectionService;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* {@link KnowledgeProviderImpl} 的工作流文档契约测试。
*/
public class KnowledgeProviderImplTest {
/**
* 验证对象数组同时提供稳定顶层字段和旧版字段。
*
* @throws Exception 注入测试依赖失败时抛出
*/
@Test
public void shouldExposeStableDocumentFieldsAndKeepLegacyFields()
throws Exception {
Document document = new Document();
document.setId(BigInteger.valueOf(42));
document.setTitle("文档标题");
document.setContent("文档内容");
document.addMetadata("documentId", BigInteger.valueOf(420));
document.addMetadata("legacyKey", "legacy-value");
DocumentCollectionService service =
mock(DocumentCollectionService.class);
when(service.search(any(KnowledgeRetrievalRequest.class)))
.thenReturn(Collections.singletonList(document));
KnowledgeProviderImpl provider = new KnowledgeProviderImpl();
setField(provider, "documentCollectionService", service);
KnowledgeNode knowledgeNode = new KnowledgeNode();
knowledgeNode.setId("knowledge-node");
Knowledge knowledge =
provider.getKnowledge(BigInteger.valueOf(88));
List<Map<String, Object>> result =
knowledge.search("问题", 10, knowledgeNode, null);
Assert.assertEquals(1, result.size());
Map<String, Object> item = result.get(0);
Assert.assertEquals("文档标题", item.get("title"));
Assert.assertEquals("文档内容", item.get("content"));
Assert.assertEquals(BigInteger.valueOf(420), item.get("documentId"));
Assert.assertEquals(BigInteger.valueOf(88), item.get("knowledgeId"));
Assert.assertEquals(42L, ((Number) item.get("id")).longValue());
Assert.assertTrue(item.containsKey("metadataMap"));
Assert.assertEquals(
"legacy-value",
((Map<?, ?>) item.get("metadataMap")).get("legacyKey"));
}
/**
* 注入测试依赖。
*
* @param target 目标对象
* @param fieldName 字段名称
* @param value 字段值
* @throws Exception 字段不存在或不可访问时抛出
*/
private static void setField(
Object target, String fieldName, Object value) throws Exception {
Field field = KnowledgeProviderImpl.class
.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
}
}

View File

@@ -64,6 +64,134 @@ public class WorkflowCheckServiceTest {
assertHasCode(result, "EXPLICIT_LOOP_COUNT_INVALID");
}
/**
* 验证循环次数和输入数组可分别存在,也可同时存在。
*/
@Test
public void testSaveShouldPassForSupportedExplicitLoopInputs() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject countOnly = loopData(
fixedParameter("count", "3", "Number"), null);
JSONObject itemsOnly = loopData(
null, refParameter("items", "start.items", "Array<Object>"));
JSONObject both = loopData(
refParameter("count", "start.count", "Number"),
refParameter("items", "knowledge.documents", "Array"));
String content = workflowJson(
array(
node("loop-count", "loopNode", null, countOnly),
node("loop-items", "loopNode", null, itemsOnly),
node("loop-both", "loopNode", null, both)
),
new JSONArray());
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertTrue(result.isPassed());
}
/**
* 验证显式循环至少需要一个输入。
*/
@Test
public void testSaveShouldBlockEmptyExplicitLoopInputs() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject loopData = data("循环");
loopData.put("loopInputs", new JSONObject());
String content = workflowJson(
array(node("loop-1", "loopNode", null, loopData)),
new JSONArray());
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "LOOP_INPUT_REQUIRED");
}
/**
* 验证输入数组只接受数组变量引用。
*/
@Test
public void testSaveShouldBlockFixedOrNonArrayLoopItems() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject fixedItems = loopData(
null, fixedParameter("items", "[]", "Array"));
JSONObject stringItems = loopData(
null, refParameter("items", "start.value", "String"));
String content = workflowJson(
array(
node("loop-fixed", "loopNode", null, fixedItems),
node("loop-string", "loopNode", null, stringItems)
),
new JSONArray());
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "EXPLICIT_LOOP_ITEMS_TYPE_INVALID");
}
/**
* 验证新旧循环输入不能同时提交。
*/
@Test
public void testSaveShouldBlockConflictingLoopInputSchemas() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject loopData = loopData(
fixedParameter("count", "2", "Number"), null);
loopData.put("loopVars", array(
fixedParameter("loopVar", "2", "Number")));
String content = workflowJson(
array(node("loop-1", "loopNode", null, loopData)),
new JSONArray());
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "LOOP_INPUT_SCHEMA_CONFLICT");
}
/**
* 验证循环变量只能由对应循环体中的节点引用。
*/
@Test
public void testSaveShouldBlockLoopVariableReferenceOutsideScope() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject childData = data("循环内节点");
childData.put("inputDefs", array(
refParameter("item", "loop-1.loopItem.content", "String")));
JSONObject outsideData = data("循环外节点");
outsideData.put("inputDefs", array(
refParameter("index", "loop-1.index", "Number")));
String content = workflowJson(
array(
node(
"loop-1",
"loopNode",
null,
loopData(fixedParameter(
"count", "2", "Number"), null)),
node("inside", "codeNode", "loop-1", childData),
node("outside", "codeNode", null, outsideData)
),
new JSONArray());
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
Assert.assertEquals(
1,
result.getIssues().stream()
.filter(issue -> "LOOP_SCOPE_REFERENCE_INVALID"
.equals(issue.getCode()))
.count());
}
/**
* 验证嵌套节点只能挂在显式循环节点下。
*/
@@ -552,6 +680,62 @@ public class WorkflowCheckServiceTest {
return node(id, "search-dataset-node", parentId, data);
}
/**
* 创建显式循环节点数据。
*
* @param count 次数参数
* @param items 数组参数
* @return 循环节点数据
*/
private static JSONObject loopData(JSONObject count, JSONObject items) {
JSONObject data = data("循环");
JSONObject inputs = new JSONObject();
if (count != null) {
inputs.put("count", count);
}
if (items != null) {
inputs.put("items", items);
}
data.put("loopInputs", inputs);
return data;
}
/**
* 创建固定值参数。
*
* @param name 参数名称
* @param value 参数值
* @param dataType 参数类型
* @return 参数对象
*/
private static JSONObject fixedParameter(
String name, String value, String dataType) {
JSONObject parameter = new JSONObject();
parameter.put("name", name);
parameter.put("refType", "fixed");
parameter.put("value", value);
parameter.put("dataType", dataType);
return parameter;
}
/**
* 创建变量引用参数。
*
* @param name 参数名称
* @param ref 引用路径
* @param dataType 参数类型
* @return 参数对象
*/
private static JSONObject refParameter(
String name, String ref, String dataType) {
JSONObject parameter = new JSONObject();
parameter.put("name", name);
parameter.put("refType", "ref");
parameter.put("ref", ref);
parameter.put("dataType", dataType);
return parameter;
}
private static JSONObject data(String title) {
JSONObject data = new JSONObject();
data.put("title", title);