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

@@ -17,6 +17,9 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 为工作流知识库节点提供检索能力。
*/
@Component
public class KnowledgeProviderImpl implements KnowledgeProvider {
@@ -24,14 +27,23 @@ public class KnowledgeProviderImpl implements KnowledgeProvider {
private DocumentCollectionService documentCollectionService;
/**
* 获取知识库
* @param id 知识库id
* 获取知识库检索器。
*
* @param id 知识库 ID
* @return 知识库检索器
*/
@Override
public Knowledge getKnowledge(Object id) {
return new Knowledge() {
/**
* {@inheritDoc}
*/
@Override
public List<Map<String, Object>> search(String keyword, int limit, KnowledgeNode knowledgeNode, Chain chain) {
public List<Map<String, Object>> search(
String keyword,
int limit,
KnowledgeNode knowledgeNode,
Chain chain) {
KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest();
request.setKnowledgeId(new BigInteger(id.toString()));
request.setQuery(keyword);
@@ -45,10 +57,29 @@ public class KnowledgeProviderImpl implements KnowledgeProvider {
}
List<Map<String, Object>> res = new ArrayList<>();
for (Document document : documents) {
res.add(JSONObject.from(document));
res.add(toWorkflowDocument(document, id));
}
return res;
}
};
}
/**
* 将检索文档转换为工作流稳定对象,并保留旧序列化字段。
*
* @param document 检索文档
* @param knowledgeId 知识库 ID
* @return 工作流文档对象
*/
private Map<String, Object> toWorkflowDocument(
Document document, Object knowledgeId) {
JSONObject result = JSONObject.from(document);
result.put("title", document.getTitle());
result.put("content", document.getContent());
result.put(
"documentId",
document.getMetadata("documentId", document.getId()));
result.put("knowledgeId", knowledgeId);
return result;
}
}

View File

@@ -40,6 +40,9 @@ import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 校验工作流结构、节点配置和预执行约束。
*/
@Service
public class WorkflowCheckService {
private static final String LEVEL_ERROR = "ERROR";
@@ -240,7 +243,7 @@ public class WorkflowCheckService {
Set<String> issueKeys) {
for (NodeView node : nodes) {
checkConfiguredLoopCount(node, issues, issueKeys);
checkFixedExplicitLoopCount(node, issues, issueKeys);
checkExplicitLoopInputs(node, issues, issueKeys);
if (StringUtils.hasText(node.parentId)) {
NodeView parent = nodeMap.get(node.parentId);
if (parent != null && !TYPE_LOOP.equals(parent.type)) {
@@ -255,6 +258,7 @@ public class WorkflowCheckService {
}
}
checkLoopParentCycle(node, nodeMap, issues, issueKeys);
checkLoopVariableScope(node, nodeMap, issues, issueKeys);
}
}
@@ -281,36 +285,293 @@ public class WorkflowCheckService {
}
/**
* 校验显式循环节点使用固定数值时的次数范围
* 校验显式循环节点的新旧输入结构
*
* @param node 节点
* @param issues 问题列表
* @param issueKeys 问题去重键
*/
private void checkFixedExplicitLoopCount(
private void checkExplicitLoopInputs(
NodeView node,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
if (!TYPE_LOOP.equals(node.type) || node.data == null) {
return;
}
JSONObject loopInputs = node.data.getJSONObject("loopInputs");
JSONArray loopVars = node.data.getJSONArray("loopVars");
if (loopVars == null || loopVars.isEmpty()) {
if (loopInputs != null) {
if (loopVars != null && !loopVars.isEmpty()) {
addIssue(
issues,
issueKeys,
"LOOP_INPUT_SCHEMA_CONFLICT",
"循环输入配置存在冲突,请重新保存循环节点",
node.id,
null,
node.name);
}
JSONObject count = loopInputs.getJSONObject("count");
JSONObject items = loopInputs.getJSONObject("items");
if (count == null && items == null) {
addIssue(
issues,
issueKeys,
"LOOP_INPUT_REQUIRED",
"请至少配置循环次数或输入数组",
node.id,
null,
node.name);
return;
}
checkExplicitCountParameter(count, node, issues, issueKeys);
checkExplicitItemsParameter(items, node, issues, issueKeys);
return;
}
if (loopVars == null || loopVars.isEmpty()) {
addIssue(
issues,
issueKeys,
"LOOP_INPUT_REQUIRED",
"请至少配置循环次数或输入数组",
node.id,
null,
node.name);
return;
}
JSONObject loopVar = loopVars.getJSONObject(0);
if (loopVar == null || !"fixed".equals(loopVar.getString("refType"))) {
if (loopVar == null) {
addIssue(
issues,
issueKeys,
"LOOP_INPUT_REQUIRED",
"请至少配置循环次数或输入数组",
node.id,
null,
node.name);
return;
}
if (!"fixed".equals(loopVar.getString("refType"))) {
return;
}
Object value = loopVar.get("value");
if (value != null && StringUtils.hasText(String.valueOf(value))) {
if (value == null || !StringUtils.hasText(String.valueOf(value))) {
addIssue(
issues,
issueKeys,
"EXPLICIT_LOOP_COUNT_INVALID",
"循环次数必须是 1300 的整数",
node.id,
null,
node.name);
return;
}
addLoopCountIssueIfInvalid(
value,
"EXPLICIT_LOOP_COUNT_INVALID",
node,
issues,
issueKeys);
}
/**
* 校验显式循环次数参数。
*
* @param count 次数参数
* @param node 节点
* @param issues 问题列表
* @param issueKeys 问题去重键
*/
private void checkExplicitCountParameter(
JSONObject count,
NodeView node,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
if (count == null) {
return;
}
String refType = trimToNull(count.getString("refType"));
if ("fixed".equals(refType)) {
Object value = count.get("value");
if (value == null || !StringUtils.hasText(String.valueOf(value))) {
addIssue(
issues,
issueKeys,
"EXPLICIT_LOOP_COUNT_INVALID",
"循环次数必须是 1300 的整数",
node.id,
null,
node.name);
return;
}
addLoopCountIssueIfInvalid(
value,
"EXPLICIT_LOOP_COUNT_INVALID",
node,
issues,
issueKeys);
return;
}
if (!"ref".equals(refType)
|| !StringUtils.hasText(trimToNull(count.getString("ref")))
|| !"Number".equalsIgnoreCase(
safe(count.getString("dataType")))) {
addIssue(
issues,
issueKeys,
"EXPLICIT_LOOP_COUNT_INVALID",
"循环次数必须引用数值变量",
node.id,
null,
node.name);
}
}
/**
* 校验显式循环数组参数。
*
* @param items 数组参数
* @param node 节点
* @param issues 问题列表
* @param issueKeys 问题去重键
*/
private void checkExplicitItemsParameter(
JSONObject items,
NodeView node,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
if (items == null) {
return;
}
String refType = trimToNull(items.getString("refType"));
String ref = trimToNull(items.getString("ref"));
String dataType = trimToNull(items.getString("dataType"));
if (!"ref".equals(refType)
|| !StringUtils.hasText(ref)
|| !isArrayDataType(dataType)) {
addIssue(
issues,
issueKeys,
"EXPLICIT_LOOP_ITEMS_TYPE_INVALID",
"输入数组必须引用数组变量",
node.id,
null,
node.name);
}
}
/**
* 判断参数类型是否为数组。
*
* @param dataType 参数类型
* @return 数组类型返回 {@code true}
*/
private boolean isArrayDataType(String dataType) {
return StringUtils.hasText(dataType)
&& ("Array".equalsIgnoreCase(dataType)
|| dataType.regionMatches(
true, 0, "Array<", 0, "Array<".length()));
}
/**
* 校验 index 和 loopItem 仅在所属循环体内引用。
*
* @param node 当前节点
* @param nodeMap 节点索引
* @param issues 问题列表
* @param issueKeys 问题去重键
*/
private void checkLoopVariableScope(
NodeView node,
Map<String, NodeView> nodeMap,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
if (node.data == null) {
return;
}
Set<String> references = new LinkedHashSet<>();
collectParameterReferences(node.data, references);
for (String reference : references) {
int separator = reference.indexOf('.');
if (separator <= 0) {
continue;
}
String loopNodeId = reference.substring(0, separator);
String variablePath = reference.substring(separator + 1);
if (!("index".equals(variablePath)
|| variablePath.startsWith("index.")
|| "loopItem".equals(variablePath)
|| variablePath.startsWith("loopItem."))) {
continue;
}
NodeView loopNode = nodeMap.get(loopNodeId);
if (loopNode == null || !TYPE_LOOP.equals(loopNode.type)) {
continue;
}
if (!isDescendantOfLoop(node, loopNodeId, nodeMap)) {
addIssue(
issues,
issueKeys,
"LOOP_SCOPE_REFERENCE_INVALID",
"循环变量只能在对应循环体内使用",
node.id,
null,
node.name);
}
}
}
/**
* 递归收集节点数据中的参数引用。
*
* @param value 待遍历值
* @param references 引用结果
*/
private void collectParameterReferences(
Object value, Set<String> references) {
if (value instanceof JSONObject object) {
String reference = trimToNull(object.getString("ref"));
if (StringUtils.hasText(reference)) {
references.add(reference);
}
for (Object child : object.values()) {
collectParameterReferences(child, references);
}
return;
}
if (value instanceof JSONArray array) {
for (Object child : array) {
collectParameterReferences(child, references);
}
}
}
/**
* 判断节点是否位于指定循环节点内部。
*
* @param node 当前节点
* @param loopNodeId 循环节点 ID
* @param nodeMap 节点索引
* @return 位于循环体内返回 {@code true}
*/
private boolean isDescendantOfLoop(
NodeView node,
String loopNodeId,
Map<String, NodeView> nodeMap) {
Set<String> visited = new HashSet<>();
NodeView current = node;
while (current != null
&& StringUtils.hasText(current.parentId)
&& visited.add(current.id)) {
if (loopNodeId.equals(current.parentId)) {
return true;
}
current = nodeMap.get(current.parentId);
}
return false;
}
/**