+
+
+
+
diff --git a/easyflow-ui-usercenter/app/src/views/ai/workflow/components/__tests__/workflowImageValue.test.ts b/easyflow-ui-usercenter/app/src/views/ai/workflow/components/__tests__/workflowImageValue.test.ts
new file mode 100644
index 00000000..2174a7cc
--- /dev/null
+++ b/easyflow-ui-usercenter/app/src/views/ai/workflow/components/__tests__/workflowImageValue.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from 'vitest';
+
+import {
+ buildWorkflowImageValueFromResource,
+ buildWorkflowImageValueFromUpload,
+ buildWorkflowImageValueFromUrl,
+ hasWorkflowImageValue,
+ normalizeWorkflowImageValue,
+ validateWorkflowImageFile,
+} from '../workflowImageValue';
+
+describe('workflowImageValue', () => {
+ it('支持 URL、上传和素材图片值', () => {
+ expect(buildWorkflowImageValueFromUrl('https://example.com/a.png')).toEqual({
+ sourceType: 'url',
+ url: 'https://example.com/a.png',
+ });
+ expect(
+ buildWorkflowImageValueFromUpload(
+ new File(['image'], 'a.png', { type: 'image/png' }),
+ '/files/a.png',
+ ),
+ ).toMatchObject({ sourceType: 'upload', fileName: 'a.png' });
+ expect(
+ buildWorkflowImageValueFromResource({
+ resourceName: 'asset',
+ resourceType: 0,
+ resourceUrl: '/files/asset.webp',
+ suffix: 'webp',
+ }),
+ ).toMatchObject({ sourceType: 'resource', fileName: 'asset.webp' });
+ });
+
+ it('兼容旧值并校验必填', () => {
+ expect(normalizeWorkflowImageValue('https://example.com/a.png')).toEqual({
+ sourceType: 'url',
+ url: 'https://example.com/a.png',
+ });
+ expect(
+ hasWorkflowImageValue({
+ fileName: 'legacy.jpg',
+ filePath: '/files/legacy.jpg',
+ }),
+ ).toBe(true);
+ expect(hasWorkflowImageValue(undefined)).toBe(false);
+ });
+
+ it('拒绝超限和非图片文件', () => {
+ expect(() =>
+ validateWorkflowImageFile(
+ new File(
+ [new Uint8Array(10 * 1024 * 1024 + 1)],
+ 'oversized.png',
+ { type: 'image/png' },
+ ),
+ ),
+ ).toThrow('单张图片不能超过 10 MiB');
+ expect(() =>
+ validateWorkflowImageFile(
+ new File(['text'], 'note.txt', { type: 'text/plain' }),
+ ),
+ ).toThrow('仅支持 PNG、JPEG、WebP、GIF、BMP 图片');
+ });
+});
diff --git a/easyflow-ui-usercenter/app/src/views/ai/workflow/components/workflowFormParameters.ts b/easyflow-ui-usercenter/app/src/views/ai/workflow/components/workflowFormParameters.ts
new file mode 100644
index 00000000..577f5466
--- /dev/null
+++ b/easyflow-ui-usercenter/app/src/views/ai/workflow/components/workflowFormParameters.ts
@@ -0,0 +1,54 @@
+const SUPPORTED_CONTENT_TYPES = ['text', 'image', 'video', 'audio', 'other'];
+
+export function resolveWorkflowFormParameters(workflowParams: any) {
+ const schema = Array.isArray(workflowParams?.startFormSchema)
+ ? workflowParams.startFormSchema
+ : [];
+ if (schema.length === 0) {
+ return workflowParams?.parameters || [];
+ }
+ const parameterMap = new Map(
+ (Array.isArray(workflowParams?.parameters)
+ ? workflowParams.parameters
+ : []
+ ).map((parameter: any) => [
+ String(parameter?.name || '').trim(),
+ parameter,
+ ]),
+ );
+ return schema.map((field: any) => {
+ const type = String(field.type || '').trim() || 'text';
+ const rawContentType = String(field.contentType || '').trim();
+ const parameter = parameterMap.get(String(field.key || '').trim()) as
+ | any
+ | undefined;
+ const parameterContentType = String(parameter?.contentType || '').trim();
+ const contentType =
+ type === 'file'
+ ? 'file'
+ : parameterContentType === 'image'
+ ? 'image'
+ : SUPPORTED_CONTENT_TYPES.includes(rawContentType)
+ ? rawContentType
+ : 'text';
+ return {
+ name: field.key,
+ formLabel: field.label || field.key,
+ formDescription: field.description || '',
+ formPlaceholder: field.placeholder || '',
+ required: Boolean(field.required),
+ defaultValue: field.defaultValue,
+ enums: Array.isArray(field.options) ? field.options : [],
+ contentType,
+ formType: type === 'text' || contentType === 'file' ? 'input' : type,
+ dataType:
+ type === 'checkbox'
+ ? 'Array'
+ : contentType === 'file'
+ ? 'File'
+ : contentType === 'image'
+ ? 'Object'
+ : 'String',
+ };
+ });
+}
diff --git a/easyflow-ui-usercenter/app/src/views/ai/workflow/components/workflowImageValue.ts b/easyflow-ui-usercenter/app/src/views/ai/workflow/components/workflowImageValue.ts
new file mode 100644
index 00000000..fa0da6eb
--- /dev/null
+++ b/easyflow-ui-usercenter/app/src/views/ai/workflow/components/workflowImageValue.ts
@@ -0,0 +1,213 @@
+export type WorkflowImageSourceType = 'resource' | 'upload' | 'url';
+
+export type WorkflowImageValue =
+ | {
+ sourceType: 'url';
+ url: string;
+ }
+ | {
+ sourceType: 'resource' | 'upload';
+ fileName: string;
+ filePath: string;
+ contentType?: string;
+ size?: number;
+ url?: string;
+ };
+
+export interface WorkflowImageResourceLike {
+ fileSize?: number | string;
+ resourceName?: string;
+ resourceType?: number | string;
+ resourceUrl?: string;
+ suffix?: string;
+}
+
+export const WORKFLOW_IMAGE_LIMITS = {
+ maxSize: 10 * 1024 * 1024,
+ accept: '.png,.jpg,.jpeg,.webp,.gif,.bmp',
+} as const;
+
+const ALLOWED_MIME_TYPES = new Set([
+ 'image/bmp',
+ 'image/gif',
+ 'image/jpeg',
+ 'image/png',
+ 'image/webp',
+]);
+const ALLOWED_EXTENSIONS = new Set(['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp']);
+
+export function buildWorkflowImageValueFromUpload(
+ file: File,
+ path: string,
+): WorkflowImageValue {
+ validateWorkflowImageFile(file);
+ const filePath = String(path || '').trim();
+ if (!filePath) {
+ throw new Error('上传结果缺少图片路径');
+ }
+ return {
+ sourceType: 'upload',
+ fileName: file.name,
+ filePath,
+ contentType: file.type || undefined,
+ size: file.size,
+ url: filePath,
+ };
+}
+
+export function buildWorkflowImageValueFromResource(
+ resource: WorkflowImageResourceLike,
+): WorkflowImageValue {
+ const filePath = String(resource?.resourceUrl || '').trim();
+ if (!filePath) {
+ throw new Error('图片素材缺少 resourceUrl');
+ }
+ if (
+ resource.resourceType !== undefined &&
+ Number(resource.resourceType) !== 0
+ ) {
+ throw new Error('请选择图片素材');
+ }
+ const suffix = String(resource?.suffix || '').trim().toLowerCase();
+ if (suffix && !ALLOWED_EXTENSIONS.has(suffix)) {
+ throw new Error('仅支持 PNG、JPEG、WebP、GIF、BMP 图片');
+ }
+ const size = toNumber(resource?.fileSize);
+ validateWorkflowImageSize(size);
+ const resourceName = String(resource?.resourceName || '').trim();
+ const fallbackName = filePath.split('/').pop()?.split('?')[0] || 'image';
+ return {
+ sourceType: 'resource',
+ fileName:
+ resourceName && suffix
+ ? `${resourceName}.${suffix}`
+ : resourceName || fallbackName,
+ filePath,
+ contentType: suffixToMimeType(suffix),
+ size,
+ url: filePath,
+ };
+}
+
+export function buildWorkflowImageValueFromUrl(url: string): WorkflowImageValue {
+ const normalized = String(url || '').trim();
+ let parsed: URL;
+ try {
+ parsed = new URL(normalized);
+ } catch {
+ throw new Error('请输入有效的图片 URL');
+ }
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
+ throw new Error('图片 URL 仅支持 HTTP/HTTPS');
+ }
+ return {
+ sourceType: 'url',
+ url: normalized,
+ };
+}
+
+export function normalizeWorkflowImageValue(
+ value: unknown,
+): WorkflowImageValue | undefined {
+ if (typeof value === 'string' && value.trim()) {
+ try {
+ return buildWorkflowImageValueFromUrl(value);
+ } catch {
+ return undefined;
+ }
+ }
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
+ return undefined;
+ }
+ const candidate = value as Record