fix: 完善工作流图片参数流转

- 图片参数使用 Object 类型并过滤上游图片引用

- 试运行支持上传、URL 和素材库图片输入

- 将模型图片能力映射到运行配置
This commit is contained in:
2026-07-31 14:51:07 +08:00
parent fb08424cef
commit 4a0efe8879
18 changed files with 924 additions and 50 deletions

View File

@@ -36,6 +36,9 @@
let isStartNodeInputParam = $derived.by(() => {
return node?.current?.type === START_NODE_TYPE && param.refType === 'input';
});
let isImageStartParam = $derived.by(() => {
return isStartNodeInputParam && param.contentType === 'image';
});
let availableFormTypes = $derived.by(() => {
if (isSystemStartParam) {
return startFormTypes.filter((item) => item.value === 'input' || item.value === 'textarea');
@@ -117,7 +120,15 @@
} else if (key === 'required') {
patch = { required: Boolean(value) };
} else if (key === 'formType') {
patch = { type: toStartFormFieldType(value) };
const type = toStartFormFieldType(value);
patch = {
type,
...(type === 'file'
? { contentType: 'file' }
: param.contentType === 'file'
? { contentType: 'text' }
: {})
};
} else if (key === 'formLabel') {
patch = { label: value };
} else if (key === 'formDescription') {
@@ -127,7 +138,16 @@
} else if (key === 'enums') {
patch = { options: Array.isArray(value) ? value : [] };
} else if (key === 'contentType') {
patch = { type: value === 'file' ? 'file' : 'text' };
patch = {
contentType: value,
...(value === 'file'
? { type: 'file' }
: value === 'image'
? { type: 'text' }
: param.contentType === 'file' || param.formType === 'file'
? { type: 'text' }
: {})
};
}
if (Object.keys(patch).length === 0) {
return;
@@ -249,17 +269,19 @@
数据内容:
<Select items={contentTypes} style="width: 100%" defaultValue={["text"]}
value={param.contentType ? [param.contentType] : []}
disabled={param.systemReserved === true || isStartNodeInputParam}
disabled={param.systemReserved === true}
onSelect={updateContentType}
/>
</div>
<div class="input-more-item">
输入方式:
<Select items={availableFormTypes} style="width: 100%" defaultValue={["input"]}
value={displayFormTypeValue}
onSelect={updateFormType}
/>
</div>
{#if !isImageStartParam}
<div class="input-more-item">
输入方式:
<Select items={availableFormTypes} style="width: 100%" defaultValue={["input"]}
value={displayFormTypeValue}
onSelect={updateFormType}
/>
</div>
{/if}
{#if param.formType === "radio" || param.formType === "checkbox" || param.formType === "select" }
<div class="input-more-item">

View File

@@ -26,6 +26,7 @@
showContentType = false,
fixedNumberMin,
fixedNumberMax,
acceptedContentTypes = [],
loopOutputAggregation = false
}: {
parameter: Parameter,
@@ -35,6 +36,7 @@
showContentType?: boolean,
fixedNumberMin?: number,
fixedNumberMax?: number,
acceptedContentTypes?: string[],
loopOutputAggregation?: boolean,
} = $props();
@@ -158,6 +160,7 @@
};
let selectItems = useRefOptions(
() => useChildrenOnly === true,
() => acceptedContentTypes,
() => param.ref || ''
);
let sourceDataType = $derived.by(() => {

View File

@@ -10,6 +10,7 @@
showContentType = false,
fixedNumberMin,
fixedNumberMax,
acceptedContentTypes = [],
loopOutputAggregation = false
}: {
noneParameterText?: string;
@@ -18,6 +19,7 @@
showContentType?: boolean,
fixedNumberMin?: number,
fixedNumberMax?: number,
acceptedContentTypes?: string[],
loopOutputAggregation?: boolean,
} = $props();
@@ -45,6 +47,7 @@
{showContentType}
{fixedNumberMin}
{fixedNumberMax}
{acceptedContentTypes}
{loopOutputAggregation}
/>
{:else }

View File

@@ -1,6 +1,53 @@
import { describe, expect, it } from 'vitest';
import { filterRefOptionsByDataType } from './refOptionFilter';
import {
filterRefOptionsByContentType,
filterRefOptionsByDataType,
} from './refOptionFilter';
describe('filterRefOptionsByContentType', () => {
const options = [
{
label: '开始节点',
selectable: false,
value: 'start',
children: [
{
label: '图片',
selectable: true,
value: 'start.image',
contentType: 'image',
},
{
label: '问题',
selectable: true,
value: 'start.user_input',
contentType: 'text',
},
],
},
];
it('图片输入仅展示图片类型引用', () => {
const filtered = filterRefOptionsByContentType(options, ['image']);
expect(filtered[0]?.children).toHaveLength(1);
expect(filtered[0]?.children[0]?.value).toBe('start.image');
});
it('保留当前已选择的旧版非图片引用', () => {
const filtered = filterRefOptionsByContentType(
options,
['image'],
'start.user_input',
);
expect(filtered[0]?.children.map((item: any) => item.value)).toEqual([
'start.image',
'start.user_input',
]);
});
});
describe('filterRefOptionsByDataType', () => {
const options = [

View File

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

View File

@@ -9,6 +9,7 @@ import {
isArrayDataType,
projectParameterDataType,
} from '../../utils/loopScope';
import { filterRefOptionsByContentType } from './refOptionFilter';
const fillRefNodeIds = (
refNodeIds: string[],
@@ -53,6 +54,7 @@ const getChildren = (
return {
label: pathLabel,
dataType,
contentType: param.contentType,
value: parentId + '.' + param.name,
selectable: true,
nodeType: nodeType,
@@ -102,6 +104,7 @@ const nodeToOptions = (
children.push({
label,
dataType: projectParameterDataType(parameter),
contentType: parameter.contentType,
value: node.id + '.' + parameter.name,
selectable: true,
nodeType: nodeType,
@@ -153,6 +156,7 @@ const nodeToOptions = (
export const useRefOptions: any = (
useChildrenOnly: boolean | (() => boolean) = false,
acceptedContentTypes: string[] | (() => string[]) = [],
currentRef: string | (() => string) = '',
) => {
const currentNodeId = getCurrentNodeId();
@@ -162,6 +166,10 @@ export const useRefOptions: any = (
typeof useChildrenOnly === 'function'
? useChildrenOnly()
: useChildrenOnly;
const getAcceptedContentTypes = () =>
typeof acceptedContentTypes === 'function'
? acceptedContentTypes()
: acceptedContentTypes;
const getCurrentRef = () =>
typeof currentRef === 'function' ? currentRef() : currentRef;
@@ -207,7 +215,11 @@ export const useRefOptions: any = (
}
}
const items = resultOptions;
const items = filterRefOptionsByContentType(
resultOptions,
getAcceptedContentTypes(),
getCurrentRef(),
);
const stack = [...items];
let selected;
const currentValue = getCurrentRef();