feat: 工作流适配数据中枢查询节点

- 新增查询数据与写入数据节点并移除旧数据中心节点入口

- 将查询数据节点切换为连接服务加 SQL 的执行模型

- 同步更新工作流校验、提示词上下文与设计器交互
This commit is contained in:
2026-04-02 18:56:34 +08:00
parent 798effbd5b
commit 1ecc28e498
40 changed files with 1973 additions and 692 deletions

View File

@@ -8,6 +8,7 @@ import org.junit.Test;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckResult;
import tech.easyflow.ai.easyagentsflow.entity.WorkflowCheckStage;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.ai.node.SearchDatasetNodeParser;
import tech.easyflow.ai.node.WorkflowNodeParser;
import tech.easyflow.ai.service.WorkflowService;
@@ -69,6 +70,24 @@ public class WorkflowCheckServiceTest {
assertHasCode(result, "EDGE_TARGET_NOT_FOUND");
}
@Test
public void testSaveShouldBlockSearchDatasetWithoutSql() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject searchData = data("查询数据");
JSONObject datasetRef = new JSONObject();
datasetRef.put("sourceId", "1001");
datasetRef.put("tableId", "2001");
searchData.put("datasetRef", datasetRef);
String content = workflowJson(
array(node("search-1", "search-dataset-node", null, searchData)),
new JSONArray()
);
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.SAVE, null);
Assert.assertFalse(result.isPassed());
assertHasCode(result, "SEARCH_DATASET_INVALID");
}
@Test
public void testPreExecuteShouldBlockMissingStartOrEnd() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
@@ -86,6 +105,26 @@ public class WorkflowCheckServiceTest {
assertHasCode(result, "END_NODE_MISSING");
}
@Test
public void testPreExecuteShouldPassForSourceOnlySearchDatasetNode() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
String content = workflowJson(
array(
node("s1", "startNode", null, data("开始")),
searchDatasetNode("q1", null, "1001"),
node("e1", "endNode", null, data("结束"))
),
array(
edge("e1", "s1", "q1"),
edge("e2", "q1", "e1")
)
);
WorkflowCheckResult result = service.checkContent(content, WorkflowCheckStage.PRE_EXECUTE, BigInteger.ONE);
Assert.assertTrue(result.isPassed());
Assert.assertEquals(0, result.getIssueCount());
}
@Test
public void testPreExecuteShouldBlockRootEntryNotStart() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
@@ -203,8 +242,10 @@ public class WorkflowCheckServiceTest {
.withDefaultParsers(true)
.build();
parser.addNodeParser("workflow-node", new WorkflowNodeParser());
parser.addNodeParser("search-dataset-node", new SearchDatasetNodeParser());
setField(service, "chainParser", parser);
setField(service, "workflowService", mockWorkflowService(workflowStore));
setField(service, "workflowDatacenterContentService", new WorkflowDatacenterContentService());
return service;
}
@@ -294,6 +335,15 @@ public class WorkflowCheckServiceTest {
return node(id, "workflow-node", parentId, data);
}
private static JSONObject searchDatasetNode(String id, String parentId, String sourceId) {
JSONObject data = data("查询数据");
JSONObject datasetRef = new JSONObject();
datasetRef.put("sourceId", sourceId);
data.put("datasetRef", datasetRef);
data.put("querySql", "SELECT 1");
return node(id, "search-dataset-node", parentId, data);
}
private static JSONObject data(String title) {
JSONObject data = new JSONObject();
data.put("title", title);

View File

@@ -0,0 +1,73 @@
package tech.easyflow.ai.node;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.datacenter.execution.model.DatasetRef;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.Map;
public class SearchDatasetNodeTest {
@Test
public void testResolveQuerySqlShouldUseNodeQuerySqlAndResolveTemplate() throws Exception {
DatasetRef datasetRef = new DatasetRef();
datasetRef.setSourceId(BigInteger.valueOf(1001L));
SearchDatasetNode node = new SearchDatasetNode(datasetRef, """
SELECT id, name
FROM orders_{{biz}}
WHERE name LIKE '%{{keyword}}%'
ORDER BY created_at {{direction}}
""");
Map<String, Object> params = new HashMap<>();
params.put("biz", "prod");
params.put("keyword", "vip");
params.put("direction", "DESC");
String sql = invokeResolveQuerySql(node, params);
Assert.assertEquals("""
SELECT id, name
FROM orders_prod
WHERE name LIKE '%vip%'
ORDER BY created_at DESC
""".trim(), sql);
}
@Test
public void testResolveQuerySqlShouldUseNodeQuerySqlWhenParamsDoNotContainSql() throws Exception {
DatasetRef datasetRef = new DatasetRef();
datasetRef.setSourceId(BigInteger.valueOf(2002L));
SearchDatasetNode node = new SearchDatasetNode(datasetRef, "SELECT * FROM fallback_table");
Map<String, Object> params = new HashMap<>();
String sql = invokeResolveQuerySql(node, params);
Assert.assertEquals("SELECT * FROM fallback_table", sql);
}
@Test
public void testResolveQuerySqlShouldRejectBlankSql() throws Exception {
DatasetRef datasetRef = new DatasetRef();
datasetRef.setSourceId(BigInteger.valueOf(3003L));
SearchDatasetNode node = new SearchDatasetNode(datasetRef, " ");
try {
invokeResolveQuerySql(node, new HashMap<>());
Assert.fail("expected BusinessException");
} catch (Exception e) {
Throwable cause = e.getCause() == null ? e : e.getCause();
Assert.assertTrue(cause instanceof BusinessException);
Assert.assertEquals("查询数据节点未设置 SQL", cause.getMessage());
}
}
private String invokeResolveQuerySql(SearchDatasetNode node, Map<String, Object> params) throws Exception {
Method method = SearchDatasetNode.class.getDeclaredMethod("resolveQuerySql", Map.class);
method.setAccessible(true);
return (String) method.invoke(node, params);
}
}