feat: 完善循环节点配置与作用域输出
- 支持次数与数组独立或组合配置并补齐检查规则 - 统一循环体临时变量与下游正式输出候选 - 稳定知识库对象数组字段并补充前后端测试
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
"循环次数必须是 1~300 的整数",
|
||||
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",
|
||||
"循环次数必须是 1~300 的整数",
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user