发布 v1.10 #5

Merged
czm merged 147 commits from develop into main 2026-08-20 11:36:27 +08:00
14 changed files with 504 additions and 59 deletions
Showing only changes of commit fb08424cef - Show all commits

View File

@@ -241,7 +241,9 @@ public class WorkflowExecutionAuditConsumer implements MQConsumerHandler {
"iterationCount"))
.intValue(),
String.valueOf(
map.get("outputName")));
map.get("outputName")),
Boolean.TRUE.equals(
map.get("flattenAggregation")));
}
Map<Object, Object> restored =
new LinkedHashMap<>();

View File

@@ -248,6 +248,7 @@ public class WorkflowCheckService {
for (NodeView node : nodes) {
checkConfiguredLoopCount(node, issues, issueKeys);
checkExplicitLoopInputs(node, issues, issueKeys);
checkLoopOutputAggregations(node, issues, issueKeys);
if (StringUtils.hasText(node.parentId)) {
NodeView parent = nodeMap.get(node.parentId);
if (parent != null && !TYPE_LOOP.equals(parent.type)) {
@@ -605,6 +606,51 @@ public class WorkflowCheckService {
true, 0, "Array<", 0, "Array<".length()));
}
/**
* 校验循环输出的扁平聚合只作用于数组引用。
*
* @param node 循环节点
* @param issues 问题列表
* @param issueKeys 问题去重键
*/
private void checkLoopOutputAggregations(
NodeView node,
List<WorkflowCheckIssue> issues,
Set<String> issueKeys) {
if (!TYPE_LOOP.equals(node.type) || node.data == null) {
return;
}
JSONArray outputDefs = node.data.getJSONArray("outputDefs");
if (outputDefs == null || outputDefs.isEmpty()) {
return;
}
for (int index = 0; index < outputDefs.size(); index++) {
JSONObject outputDef = outputDefs.getJSONObject(index);
if (outputDef == null
|| !outputDef.getBooleanValue("flattenAggregation")) {
continue;
}
String refType = trimToNull(outputDef.getString("refType"));
String ref = trimToNull(outputDef.getString("ref"));
String dataType = trimToNull(outputDef.getString("dataType"));
if ("ref".equals(refType)
&& StringUtils.hasText(ref)
&& isArrayDataType(dataType)) {
continue;
}
String outputName = trimToNull(outputDef.getString("name"));
addIssue(
issues,
issueKeys,
"LOOP_OUTPUT_FLATTEN_TYPE_INVALID",
"循环输出参数[" + safe(outputName)
+ "]启用扁平聚合时必须引用数组变量",
node.id,
null,
node.name);
}
}
/**
* 校验 index 和 loopItem 仅在所属循环体内引用。
*

View File

@@ -627,11 +627,11 @@ public class WorkflowExecutionAuditConsumerTest {
loopRepository.append(
resultId,
0,
Map.of("answer", "first"));
Map.of("answer", List.of("first", "second")));
loopRepository.append(
resultId,
1,
Map.of("answer", "second"));
Map.of("answer", List.of("third")));
WorkflowExecutionAuditConsumer consumer =
new WorkflowExecutionAuditConsumer(
resultService,
@@ -650,7 +650,8 @@ public class WorkflowExecutionAuditConsumerTest {
new LoopResultReference(
resultId,
2,
"answer"));
"answer",
true));
WorkflowExecStep incomingStep =
new WorkflowExecStep();
incomingStep.setExecKey("step-loop");
@@ -700,10 +701,10 @@ public class WorkflowExecutionAuditConsumerTest {
resultCaptor.getValue().getOutput(),
Map.class);
Assert.assertEquals(
List.of("first", "second"),
List.of("first", "second", "third"),
stepOutput.get("answers"));
Assert.assertEquals(
List.of("first", "second"),
List.of("first", "second", "third"),
resultOutput.get("answers"));
}

View File

@@ -221,6 +221,55 @@ public class WorkflowCheckServiceTest {
assertHasCode(result, "EXPLICIT_LOOP_ITEMS_TYPE_INVALID");
}
/**
* 验证循环数组输出允许启用扁平聚合。
*/
@Test
public void testSaveShouldPassArrayLoopOutputFlattenAggregation() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject loopData = loopData(
fixedParameter("count", "2", "Number"), null);
JSONObject output = refParameter(
"res",
"knowledge.documents.content",
"Array<String>");
output.put("flattenAggregation", true);
loopData.put("outputDefs", array(output));
String content = workflowJson(
array(node("loop-1", "loopNode", null, loopData)),
new JSONArray());
WorkflowCheckResult result = service.checkContent(
content, WorkflowCheckStage.SAVE, null);
Assert.assertTrue(result.isPassed());
}
/**
* 验证循环标量输出不能启用扁平聚合。
*/
@Test
public void testSaveShouldBlockScalarLoopOutputFlattenAggregation() throws Exception {
WorkflowCheckService service = newService(new HashMap<>());
JSONObject loopData = loopData(
fixedParameter("count", "2", "Number"), null);
JSONObject output = refParameter(
"res",
"child.output",
"String");
output.put("flattenAggregation", true);
loopData.put("outputDefs", array(output));
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_OUTPUT_FLATTEN_TYPE_INVALID");
}
/**
* 验证新旧循环输入不能同时提交。
*/

View File

@@ -11,6 +11,7 @@ import FloatingTrigger from './floating-trigger.svelte';
import Heading from './heading.svelte';
import MenuButton from './menu-button.svelte';
import MixedInput from './mixed-input.svelte';
import InfoTooltip from './info-tooltip.svelte';
export {
Button,
@@ -26,4 +27,5 @@ export {
Heading,
MenuButton,
MixedInput,
InfoTooltip,
};

View File

@@ -0,0 +1,95 @@
<script lang="ts">
const {
text,
label = '查看说明',
}: {
text: string;
label?: string;
} = $props();
</script>
<button
type="button"
class="tf-info-tooltip nopan nodrag"
aria-label={label}
data-help={text}
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="9" />
<path d="M12 10.75V16" />
<circle cx="12" cy="7.75" r="0.75" class="tf-info-tooltip-dot" />
</svg>
</button>
<style lang="less">
.tf-info-tooltip {
position: relative;
display: inline-flex;
flex: 0 0 16px;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
padding: 0;
color: var(--tf-text-secondary);
cursor: help;
background: transparent;
border: 0;
border-radius: 50%;
svg {
width: 15px;
height: 15px;
overflow: visible;
fill: none;
stroke: currentcolor;
stroke-width: 1.8;
stroke-linecap: round;
}
.tf-info-tooltip-dot {
fill: currentcolor;
stroke: none;
}
&:hover,
&:focus {
color: var(--tf-primary-color);
background: var(--tf-primary-soft-bg);
outline: none;
}
&:focus {
box-shadow: 0 0 0 2px var(--tf-primary-soft-border);
}
&::after {
position: absolute;
bottom: calc(100% + 8px);
left: 0;
z-index: 120;
width: 280px;
max-width: calc(100vw - 32px);
padding: 10px 12px;
font-size: 12px;
font-weight: 400;
line-height: 1.5;
color: var(--tf-tip-text);
text-align: left;
white-space: normal;
pointer-events: none;
visibility: hidden;
content: attr(data-help);
background: var(--tf-tip-bg);
border-radius: 8px;
box-shadow: var(--tf-shadow-medium);
opacity: 0;
}
&:hover::after,
&:focus::after {
visibility: visible;
opacity: 1;
}
}
</style>

View File

@@ -14,6 +14,7 @@
placeholder,
variant = 'default',
showSelectedType = true,
selectedType,
...rest
}: {
items: SelectItem[],
@@ -24,6 +25,7 @@
placeholder?: string
variant?: 'default' | 'reference' | 'model'
showSelectedType?: boolean
selectedType?: string
[key: string]: any
} = $props();
@@ -184,10 +186,10 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polyline points="6 9 12 15 18 9"></polyline></svg>
</span>
{/if}
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
<span class="tf-parameter-name" title={String(item.displayLabel || item.label)}>{item.displayLabel || item.label}</span>
</div>
{#if item.dataType}
<span class="tf-parameter-type">{item.dataType}</span>
<span class="tf-parameter-type" title={item.dataType}>{item.dataType}</span>
{/if}
{#if item.itemTypeLabel}
<span class="tf-parameter-meta">{item.itemTypeLabel}</span>
@@ -230,9 +232,9 @@
{/if}
</span>
{/if}
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
{#if variant === 'reference' && showSelectedType && item.dataType}
<span class="tf-parameter-type">{item.dataType}</span>
<span class="tf-parameter-name" title={String(item.displayLabel || item.label)}>{item.displayLabel || item.label}</span>
{#if variant === 'reference' && showSelectedType && (selectedType ?? item.dataType)}
<span class="tf-parameter-type" title={selectedType ?? item.dataType}>{selectedType ?? item.dataType}</span>
{/if}
</div>
{/if}
@@ -251,9 +253,9 @@
{/if}
</span>
{/if}
<span class="tf-parameter-name">{item.displayLabel || item.label}</span>
{#if variant === 'reference' && showSelectedType && item.dataType}
<span class="tf-parameter-type">{item.dataType}</span>
<span class="tf-parameter-name" title={String(item.displayLabel || item.label)}>{item.displayLabel || item.label}</span>
{#if variant === 'reference' && showSelectedType && (selectedType ?? item.dataType)}
<span class="tf-parameter-type" title={selectedType ?? item.dataType}>{selectedType ?? item.dataType}</span>
{/if}
</div>
{#if index < activeItemsState.length - 1}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import {Input, MenuButton, Textarea} from '../base';
import {Checkbox, InfoTooltip, Input, MenuButton, Textarea} from '../base';
import {Button, FloatingTrigger, Select} from '../base/index.js';
import {getCurrentNodeId} from '#components/utils/NodeUtils';
import {useNodesData, useSvelteFlow} from '@xyflow/svelte';
@@ -7,6 +7,10 @@
import {useRefOptions} from '../utils/useRefOptions.svelte';
import {onMount} from 'svelte';
import type {Parameter} from '#types';
import {
isArrayDataType,
resolveLoopOutputDataType,
} from '../../utils/loopScope';
onMount(() => {
if (!param.refType) {
@@ -21,7 +25,8 @@
useChildrenOnly,
showContentType = false,
fixedNumberMin,
fixedNumberMax
fixedNumberMax,
loopOutputAggregation = false
}: {
parameter: Parameter,
index: number,
@@ -30,6 +35,7 @@
showContentType?: boolean,
fixedNumberMin?: number,
fixedNumberMax?: number,
loopOutputAggregation?: boolean,
} = $props();
@@ -44,12 +50,12 @@
const { updateNodeData } = useSvelteFlow();
const updateParam = (key: string, value: any) => {
const updateParams = (patch: Partial<Parameter>) => {
updateNodeData(currentNodeId, (node) => {
let parameters = node.data?.[dataKeyName] as Array<Parameter>;
parameters[index] = {
...parameters[index],
[key]: value
...patch
};
return {
[dataKeyName]: [...parameters]
@@ -57,6 +63,9 @@
});
};
const updateParam = (key: string, value: any) => {
updateParams({[key]: value});
};
const updateParamByEvent = (name: string, event: Event) => {
const newValue = (event.target as any).value;
@@ -99,13 +108,35 @@
const updateRef = (item: any) => {
const newValue = item.value;
if (!loopOutputAggregation) {
updateParam('ref', newValue);
return;
}
const dataType = item.dataType || 'String';
updateParams({
ref: newValue,
dataType,
flattenAggregation:
isArrayDataType(dataType)
? Boolean(param.flattenAggregation)
: false
});
};
const updateRefType = (item: any) => {
const newValue = item.value;
if (!loopOutputAggregation) {
updateParam('refType', newValue);
return;
}
updateParams({
refType: newValue,
flattenAggregation:
newValue !== 'ref'
? false
: Boolean(param.flattenAggregation)
});
};
const updateContentType = (item: any) => {
@@ -125,7 +156,29 @@
});
triggerObject?.hide();
};
let selectItems = useRefOptions(() => useChildrenOnly === true);
let selectItems = useRefOptions(
() => useChildrenOnly === true,
() => param.ref || ''
);
let sourceDataType = $derived.by(() => {
return selectItems.selected?.dataType || param.dataType || 'String';
});
let canFlattenAggregation = $derived.by(() => {
return isArrayDataType(sourceDataType);
});
let loopOutputDataType = $derived.by(() => {
return resolveLoopOutputDataType(
sourceDataType,
canFlattenAggregation && Boolean(param.flattenAggregation)
);
});
const updateFlattenAggregation = (event: Event) => {
const checked = (event.target as HTMLInputElement).checked;
updateParams({
dataType: sourceDataType,
flattenAggregation: canFlattenAggregation && checked
});
};
</script>
@@ -153,11 +206,25 @@
</div>
{:else if (param.refType !== 'input')}
<Select items={selectItems.current} style="width: 100%" defaultValue={["ref"]} value={[param.ref]} variant="reference"
selectedType={loopOutputAggregation ? loopOutputDataType : undefined}
expandAll
onSelect={updateRef} />
{/if}
</div>
<div class="input-item">
<div class="input-item input-actions">
{#if loopOutputAggregation && param.refType === 'ref' && param.ref && canFlattenAggregation}
<label class="flatten-aggregation-control">
<Checkbox
checked={Boolean(param.flattenAggregation)}
onchange={updateFlattenAggregation}
/>
<span>扁平聚合</span>
</label>
<InfoTooltip
label="查看扁平聚合说明"
text="关闭时按轮次保留数组;开启后按循环顺序合并,只扁平一层。例如 [[A, B], [C]] 会变为 [A, B, C],轮次分组信息将丢失。"
/>
{/if}
<FloatingTrigger placement="bottom" bind:this={triggerObject}>
<MenuButton />
{#snippet floating()}
@@ -209,6 +276,12 @@
min-width: 0;
}
.input-actions {
gap: 5px;
justify-content: flex-end;
white-space: nowrap;
}
.fixed-value {
display: flex;
flex-direction: column;
@@ -243,4 +316,19 @@
}
}
.flatten-aggregation-control {
display: inline-flex;
gap: 5px;
align-items: center;
color: var(--tf-text-primary);
cursor: pointer;
font-size: 11px;
white-space: nowrap;
:global(.tf-checkbox:focus-visible) {
outline: none;
box-shadow: 0 0 0 2px var(--tf-primary-soft-border);
}
}
</style>

View File

@@ -9,7 +9,8 @@
useChildrenOnly,
showContentType = false,
fixedNumberMin,
fixedNumberMax
fixedNumberMax,
loopOutputAggregation = false
}: {
noneParameterText?: string;
dataKeyName?: string;
@@ -17,6 +18,7 @@
showContentType?: boolean,
fixedNumberMin?: number,
fixedNumberMax?: number,
loopOutputAggregation?: boolean,
} = $props();
let currentNodeId = getCurrentNodeId();
@@ -28,7 +30,7 @@
</script>
<div class="input-container">
<div class:loop-output-aggregation={loopOutputAggregation} class="input-container">
{#if (parameters.length !== 0)}
<div class="input-header">参数名称</div>
<div class="input-header">参数值</div>
@@ -43,6 +45,7 @@
{showContentType}
{fixedNumberMin}
{fixedNumberMax}
{loopOutputAggregation}
/>
{:else }
<div class="none-params">{noneParameterText}</div>
@@ -61,6 +64,10 @@
min-width: 0;
box-sizing: border-box;
&.loop-output-aggregation {
grid-template-columns: 124px minmax(0, 1fr) auto;
}
.none-params {
font-size: 12px;
background: var(--tf-bg-muted);

View File

@@ -50,7 +50,12 @@
</svg>
</Button>
</div>
<RefParameterList noneParameterText="无输出参数" dataKeyName="outputDefs" useChildrenOnly={true} />
<RefParameterList
noneParameterText="无输出参数"
dataKeyName="outputDefs"
useChildrenOnly={true}
loopOutputAggregation={true}
/>
</NodeWrapper>

View File

@@ -7,6 +7,7 @@ import {
buildLoopReferenceParameters,
buildLoopScopeParameters,
isArrayDataType,
projectParameterDataType,
} from '../../utils/loopScope';
const fillRefNodeIds = (
@@ -25,9 +26,9 @@ const fillRefNodeIds = (
const getChildren = (
params: any,
parentId: string,
nodeIsChildren: boolean,
nodeType: string,
parentPathLabel = '',
ancestorCollectionDepth = 0,
parentIsCollection = false,
) => {
if (!params || params.length === 0) return [];
@@ -43,25 +44,29 @@ const getChildren = (
: parentIsCollection
? `${parentPathLabel}.[].${normalizedChildLabel}`
: `${parentPathLabel}.${normalizedChildLabel}`;
const dataType = nodeIsChildren
? `Array<${param.dataType || 'String'}>`
: param.dataType || 'String';
const dataType = projectParameterDataType(
param,
ancestorCollectionDepth,
);
const nextCollectionDepth =
ancestorCollectionDepth + (isCollection ? 1 : 0);
return {
label: pathLabel,
dataType: dataType,
dataType,
value: parentId + '.' + param.name,
selectable: true,
nodeType: nodeType,
displayLabel: pathLabel,
pathLabel,
itemTypeLabel: parentIsCollection ? '数组项字段' : undefined,
itemTypeLabel:
ancestorCollectionDepth > 0 ? '数组项字段' : undefined,
isCollection,
children: getChildren(
param.children,
parentId + '.' + param.name,
nodeIsChildren,
nodeType,
pathLabel,
nextCollectionDepth,
isCollection,
),
};
@@ -70,7 +75,6 @@ const getChildren = (
const nodeToOptions = (
node: Node,
nodeIsChildren: boolean,
currentNode: Node,
nodes: Node[],
) => {
@@ -94,13 +98,10 @@ const nodeToOptions = (
const children = [];
if (parameters)
for (const parameter of parameters) {
const dataType = nodeIsChildren
? `Array<${parameter.dataType || 'String'}>`
: parameter.dataType || 'String';
const label = getStartNodeParameterLabel(parameter);
children.push({
label,
dataType: dataType,
dataType: projectParameterDataType(parameter),
value: node.id + '.' + parameter.name,
selectable: true,
nodeType: nodeType,
@@ -132,7 +133,6 @@ const nodeToOptions = (
children: getChildren(
referenceParameters,
node.id,
false,
nodeType,
),
};
@@ -145,7 +145,7 @@ const nodeToOptions = (
value: node.id,
selectable: false,
nodeType: nodeType,
children: getChildren(outputDefs, node.id, nodeIsChildren, nodeType),
children: getChildren(outputDefs, node.id, nodeType),
};
}
}
@@ -153,6 +153,7 @@ const nodeToOptions = (
export const useRefOptions: any = (
useChildrenOnly: boolean | (() => boolean) = false,
currentRef: string | (() => string) = '',
) => {
const currentNodeId = getCurrentNodeId();
const currentNode = useNodesData(currentNodeId);
@@ -161,11 +162,16 @@ export const useRefOptions: any = (
typeof useChildrenOnly === 'function'
? useChildrenOnly()
: useChildrenOnly;
const getCurrentRef = () =>
typeof currentRef === 'function' ? currentRef() : currentRef;
let selectItems = $derived.by(() => {
const resultOptions = [];
if (!currentNode.current) {
return [];
return {
items: [],
selected: undefined,
};
}
//通过 nodeLookup.get 才会得到有 parentId 的 node
@@ -173,11 +179,9 @@ export const useRefOptions: any = (
if (isChildrenOnly()) {
for (const node of nodes) {
const nodeIsChildren = node.parentId === currentNode.current.id;
if (nodeIsChildren) {
if (node.parentId === currentNode.current.id) {
const nodeOptions = nodeToOptions(
node,
nodeIsChildren,
cNode,
nodes,
);
@@ -193,10 +197,8 @@ export const useRefOptions: any = (
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,
nodes,
);
@@ -205,12 +207,30 @@ export const useRefOptions: any = (
}
}
return resultOptions;
const items = resultOptions;
const stack = [...items];
let selected;
const currentValue = getCurrentRef();
// 单次深度优先扫描定位当前引用,避免为每个参数额外构建全量索引。
while (stack.length > 0) {
const item = stack.pop();
if (item?.value === currentValue) {
selected = item;
break;
}
if (item?.children?.length) {
stack.push(...item.children);
}
}
return { items, selected };
});
return {
get current() {
return selectItems;
return selectItems.items;
},
get selected() {
return selectItems.selected;
},
};
};

View File

@@ -170,4 +170,5 @@ export type Parameter = {
requiredDisabled?: boolean;
systemReserved?: boolean;
autoManaged?: boolean;
flattenAggregation?: boolean;
};

View File

@@ -5,6 +5,8 @@ import {
buildLoopItemParameter,
buildLoopReferenceParameters,
buildLoopScopeParameters,
findReferenceParameter,
resolveLoopOutputDataType,
resolveLoopInputs,
} from './loopScope';
@@ -227,7 +229,7 @@ describe('loopScope', () => {
'records',
]);
expect(downstreamParameters[0]?.dataType).toBe('Array<String>');
expect(downstreamParameters[1]?.dataType).toBe('Array');
expect(downstreamParameters[1]?.dataType).toBe('Array<Object>');
expect(downstreamParameters[1]?.children?.[0]?.name).toBe('summary');
downstreamParameters[1]!.children![0]!.name = 'changed';
@@ -235,4 +237,64 @@ describe('loopScope', () => {
(childNode.data.outputDefs as Array<any>)[1].children[0].name,
).toBe('summary');
});
it('集合子字段按实际层级推导循环输出类型', () => {
const loopNode = {
id: 'loop',
type: 'loopNode',
position: { x: 0, y: 0 },
data: {
loopInputs: {
count: {
name: 'count',
refType: 'fixed',
value: '2',
dataType: 'Number',
},
},
outputDefs: [
{
name: 'grouped',
refType: 'ref',
ref: 'knowledge.documents.content',
dataType: 'Array<String>',
},
{
name: 'flattened',
refType: 'ref',
ref: 'knowledge.documents.content',
dataType: 'Array<String>',
flattenAggregation: true,
},
],
},
} satisfies Node;
const downstreamNode = {
id: 'downstream',
type: 'endNode',
position: { x: 0, y: 0 },
data: {},
} satisfies Node;
const nodes = [knowledgeNode, loopNode, downstreamNode];
expect(
findReferenceParameter(
nodes,
'knowledge.documents.content',
)?.dataType,
).toBe('Array<String>');
expect(
buildLoopReferenceParameters(loopNode, downstreamNode, nodes)
.map((parameter) => parameter.dataType),
).toEqual(['Array<Array<String>>', 'Array<String>']);
});
it('扁平聚合只移除循环新增的一层数组', () => {
expect(resolveLoopOutputDataType('String', false))
.toBe('Array<String>');
expect(resolveLoopOutputDataType('Array<String>', false))
.toBe('Array<Array<String>>');
expect(resolveLoopOutputDataType('Array<String>', true))
.toBe('Array<String>');
});
});

View File

@@ -24,6 +24,53 @@ export function isArrayDataType(dataType?: string | null) {
return normalized === 'array' || normalized.startsWith('array<');
}
function normalizeDataType(
dataType?: string | null,
hasChildren = false,
) {
const normalized = asString(dataType);
if (!normalized) {
return hasChildren ? 'Object' : 'String';
}
if (normalized.toLowerCase() === 'array') {
return `Array<${hasChildren ? 'Object' : 'Any'}>`;
}
return normalized;
}
/**
* 计算参数穿过上级集合后的实际引用类型。
*/
export function projectParameterDataType(
parameter: Parameter,
ancestorCollectionDepth = 0,
) {
const dataType = normalizeDataType(
parameter.dataType,
Boolean(parameter.children?.length),
);
if (ancestorCollectionDepth <= 0) {
return dataType;
}
const prefix = 'Array<'.repeat(ancestorCollectionDepth);
const suffix = '>'.repeat(ancestorCollectionDepth);
return `${prefix}${dataType}${suffix}`;
}
/**
* 计算循环完成后对外暴露的输出类型。
*/
export function resolveLoopOutputDataType(
sourceDataType?: string | null,
flattenAggregation = false,
) {
const normalized = normalizeDataType(sourceDataType);
if (flattenAggregation && isArrayDataType(normalized)) {
return normalized;
}
return `Array<${normalized}>`;
}
function getNodeParameters(node: Node): Parameter[] {
if (node.type === 'startNode') {
return Array.isArray(node.data?.parameters)
@@ -52,16 +99,32 @@ export function findReferenceParameter(
const path = normalizedReference.slice(node.id.length + 1).split('.');
let parameters = getNodeParameters(node);
let current: Parameter | undefined;
for (const segment of path) {
let ancestorCollectionDepth = 0;
for (let index = 0; index < path.length; index++) {
const segment = path[index];
current = parameters.find(
(parameter) => asString(parameter.name) === segment,
);
if (!current) {
return undefined;
}
if (
index < path.length - 1 &&
isArrayDataType(current.dataType)
) {
ancestorCollectionDepth++;
}
parameters = current.children || [];
}
return current ? cloneParameter(current) : undefined;
if (!current) {
return undefined;
}
const projected = cloneParameter(current);
projected.dataType = projectParameterDataType(
current,
ancestorCollectionDepth,
);
return projected;
}
export function resolveLoopInputs(
@@ -219,17 +282,19 @@ export function buildLoopReferenceParameters(
nodes,
outputParameter.ref,
);
if (!sourceParameter) {
return outputParameter;
}
const sourceDataType = asString(sourceParameter.dataType) || 'String';
const sourceChildren = sourceParameter.children?.map(cloneParameter);
const sourceDataType =
asString(
sourceParameter?.dataType || outputParameter.dataType,
) || 'String';
const sourceChildren = (
sourceParameter?.children || outputParameter.children
)?.map(cloneParameter);
return {
...outputParameter,
dataType:
sourceChildren?.length && !isArrayDataType(sourceDataType)
? 'Array'
: `Array<${sourceDataType}>`,
dataType: resolveLoopOutputDataType(
sourceDataType,
Boolean(outputParameter.flattenAggregation),
),
children: sourceChildren,
};
})