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,54 @@
import { describe, expect, it } from 'vitest';
import { filterRefOptionsByDataType } from './refOptionFilter';
describe('filterRefOptionsByDataType', () => {
const options = [
{
label: '知识库',
selectable: false,
value: 'knowledge',
children: [
{
label: '文档',
selectable: true,
value: 'knowledge.documents',
dataType: 'Array<Object>',
},
{
label: '数量',
selectable: true,
value: 'knowledge.count',
dataType: 'Number',
},
{
label: '标题',
selectable: true,
value: 'knowledge.title',
dataType: 'String',
},
],
},
];
it('数组输入接受 Array 及 Array 泛型', () => {
const filtered = filterRefOptionsByDataType(options, ['Array']);
expect(filtered[0]?.children.map((item: any) => item.value)).toEqual([
'knowledge.documents',
]);
});
it('次数输入只展示 Number并保留当前旧引用', () => {
const filtered = filterRefOptionsByDataType(
options,
['Number'],
'knowledge.title',
);
expect(filtered[0]?.children.map((item: any) => item.value)).toEqual([
'knowledge.count',
'knowledge.title',
]);
});
});

View File

@@ -0,0 +1,48 @@
function isAcceptedDataType(dataType: string, acceptedDataTypes: string[]) {
const normalized = dataType.trim().toLowerCase();
return acceptedDataTypes.some((acceptedDataType) => {
const accepted = acceptedDataType.trim().toLowerCase();
return accepted === 'array'
? normalized === 'array' || normalized.startsWith('array<')
: normalized === accepted;
});
}
/**
* 按数据类型过滤引用选项,并保留当前已选择的旧引用。
*/
export function filterRefOptionsByDataType(
options: any[],
acceptedDataTypes: string[],
currentRef = '',
): any[] {
if (!acceptedDataTypes.length) {
return options;
}
return options
.map((option) => {
const children = Array.isArray(option.children)
? filterRefOptionsByDataType(
option.children,
acceptedDataTypes,
currentRef,
)
: [];
const selectable =
option.selectable === true &&
(isAcceptedDataType(
String(option.dataType || ''),
acceptedDataTypes,
) ||
String(option.value || '') === currentRef);
if (!selectable && children.length === 0) {
return undefined;
}
return {
...option,
selectable,
children,
};
})
.filter(Boolean);
}

View File

@@ -3,6 +3,11 @@ import type { Parameter } from '#types';
import { getCurrentNodeId, getOptions } from '#components/utils/NodeUtils';
import { getStartNodeParameterLabel } from '#components/utils/startNodeParameterLabel';
import { nodeIcons } from '../../consts';
import {
buildLoopReferenceParameters,
buildLoopScopeParameters,
isArrayDataType,
} from '../../utils/loopScope';
const fillRefNodeIds = (
refNodeIds: string[],
@@ -27,7 +32,10 @@ const getChildren = (
) => {
if (!params || params.length === 0) return [];
return params.map((param: any) => {
const isCollection = param.dataType === 'Array' && param.children && param.children.length > 0;
const isCollection =
isArrayDataType(param.dataType) &&
param.children &&
param.children.length > 0;
const childBaseLabel = param.formLabel || param.displayName || param.name;
const normalizedChildLabel = String(childBaseLabel || '').trim();
const pathLabel = !parentPathLabel
@@ -64,6 +72,7 @@ const nodeToOptions = (
node: Node,
nodeIsChildren: boolean,
currentNode: Node,
nodes: Node[],
) => {
const options = getOptions();
const nodeType = node.type || '';
@@ -105,29 +114,27 @@ const nodeToOptions = (
nodeType: nodeType,
children,
};
} else if (nodeType === 'loopNode' && currentNode.parentId) {
} else if (nodeType === 'loopNode') {
const referenceParameters = buildLoopReferenceParameters(
node,
currentNode,
nodes,
);
if (!referenceParameters.length) {
return undefined;
}
return {
label: title,
icon: icon,
value: node.id,
selectable: false,
nodeType: nodeType,
children: [
{
label: 'loopItem',
dataType: 'Any',
value: node.id + '.loopItem',
selectable: true,
nodeType: nodeType,
},
{
label: 'index',
dataType: 'Number',
value: node.id + '.index',
selectable: true,
nodeType: nodeType,
},
],
children: getChildren(
referenceParameters,
node.id,
false,
nodeType,
),
};
} else {
const outputDefs = node.data.outputDefs;
@@ -168,7 +175,12 @@ export const useRefOptions: any = (
for (const node of nodes) {
const nodeIsChildren = node.parentId === currentNode.current.id;
if (nodeIsChildren) {
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
const nodeOptions = nodeToOptions(
node,
nodeIsChildren,
cNode,
nodes,
);
nodeOptions && resultOptions.push(nodeOptions);
}
}
@@ -177,9 +189,17 @@ export const useRefOptions: any = (
fillRefNodeIds(refNodeIds, currentNodeId, edges);
for (const node of nodes) {
if (refNodeIds.includes(node.id)) {
const isScopedLoop =
node.type === 'loopNode' &&
buildLoopScopeParameters(node, cNode, nodes).length > 0;
if (refNodeIds.includes(node.id) || isScopedLoop) {
const nodeIsChildren = node.parentId === currentNode.current.id;
const nodeOptions = nodeToOptions(node, nodeIsChildren, cNode);
const nodeOptions = nodeToOptions(
node,
nodeIsChildren,
cNode,
nodes,
);
nodeOptions && resultOptions.push(nodeOptions);
}
}