From 4cb286c765b8569744a6957f2274056ede444b46 Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 27 Aug 2025 14:46:33 +0800 Subject: [PATCH] feat: handle file schem show --- .../variable/match-schema-type.spec.ts | 162 ++++++++++++++++++ .../components/variable/match-schema-type.ts | 42 +++++ .../object-child-tree-panel/show/field.tsx | 2 +- .../variable/use-match-schema-type.spec.ts | 71 -------- .../variable/use-match-schema-type.ts | 36 +--- .../workflow/nodes/data-source/panel.tsx | 4 +- .../components/workflow/nodes/llm/utils.ts | 1 + 7 files changed, 212 insertions(+), 106 deletions(-) create mode 100644 web/app/components/workflow/nodes/_base/components/variable/match-schema-type.spec.ts create mode 100644 web/app/components/workflow/nodes/_base/components/variable/match-schema-type.ts delete mode 100644 web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.spec.ts diff --git a/web/app/components/workflow/nodes/_base/components/variable/match-schema-type.spec.ts b/web/app/components/workflow/nodes/_base/components/variable/match-schema-type.spec.ts new file mode 100644 index 0000000000..c56d7cc46c --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/match-schema-type.spec.ts @@ -0,0 +1,162 @@ +import matchTheSchemaType from './match-schema-type' + +describe('match the schema type', () => { + it('should return true for identical primitive types', () => { + expect(matchTheSchemaType({ type: 'string' }, { type: 'string' })).toBe(true) + expect(matchTheSchemaType({ type: 'number' }, { type: 'number' })).toBe(true) + }) + + it('should return false for different primitive types', () => { + expect(matchTheSchemaType({ type: 'string' }, { type: 'number' })).toBe(false) + }) + + it('should ignore values and only compare types', () => { + expect(matchTheSchemaType({ type: 'string', value: 'hello' }, { type: 'string', value: 'world' })).toBe(true) + expect(matchTheSchemaType({ type: 'number', value: 42 }, { type: 'number', value: 100 })).toBe(true) + }) + + it('should return true for structural differences but no types', () => { + expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string', other: 'xxx' })).toBe(true) + expect(matchTheSchemaType({ type: 'string', other: { b: 'xxx' } }, { type: 'string' })).toBe(true) + }) + + it('should handle nested objects with same structure and types', () => { + const obj1 = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + address: { + type: 'object', + properties: { + street: { type: 'string' }, + city: { type: 'string' }, + }, + }, + }, + } + const obj2 = { + type: 'object', + properties: { + name: { type: 'string', value: 'Alice' }, + age: { type: 'number', value: 30 }, + address: { + type: 'object', + properties: { + street: { type: 'string', value: '123 Main St' }, + city: { type: 'string', value: 'Wonderland' }, + }, + }, + }, + } + expect(matchTheSchemaType(obj1, obj2)).toBe(true) + }) + it('should return false for nested objects with different structures', () => { + const obj1 = { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'number' }, + }, + } + const obj2 = { + type: 'object', + properties: { + name: { type: 'string' }, + address: { type: 'string' }, + }, + } + expect(matchTheSchemaType(obj1, obj2)).toBe(false) + }) + + it('file struct should match file type', () => { + const fileSchema = { + $id: 'https://dify.ai/schemas/v1/file.json', + $schema: 'http://json-schema.org/draft-07/schema#', + version: '1.0.0', + type: 'object', + title: 'File Schema', + description: 'Schema for file objects (v1)', + properties: { + name: { + type: 'string', + description: 'file name', + }, + size: { + type: 'number', + description: 'file size', + }, + extension: { + type: 'string', + description: 'file extension', + }, + type: { + type: 'string', + description: 'file type', + }, + mime_type: { + type: 'string', + description: 'file mime type', + }, + transfer_method: { + type: 'string', + description: 'file transfer method', + }, + url: { + type: 'string', + description: 'file url', + }, + related_id: { + type: 'string', + description: 'file related id', + }, + }, + required: [ + 'name', + ], + } + const file = { + type: 'object', + title: 'File', + description: 'Schema for file objects (v1)', + properties: { + name: { + type: 'string', + description: 'file name', + }, + size: { + type: 'number', + description: 'file size', + }, + extension: { + type: 'string', + description: 'file extension', + }, + type: { + type: 'string', + description: 'file type', + }, + mime_type: { + type: 'string', + description: 'file mime type', + }, + transfer_method: { + type: 'string', + description: 'file transfer method', + }, + url: { + type: 'string', + description: 'file url', + }, + related_id: { + type: 'string', + description: 'file related id', + }, + }, + required: [ + 'name', + ], + } + expect(matchTheSchemaType(fileSchema, file)).toBe(true) + }) +}) diff --git a/web/app/components/workflow/nodes/_base/components/variable/match-schema-type.ts b/web/app/components/workflow/nodes/_base/components/variable/match-schema-type.ts new file mode 100644 index 0000000000..9ca4981065 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/variable/match-schema-type.ts @@ -0,0 +1,42 @@ +export type AnyObj = Record | null + +const isObj = (x: any): x is object => x !== null && typeof x === 'object' + +// only compare type in object +function matchTheSchemaType(scheme: AnyObj, target: AnyObj): boolean { + const isMatch = (schema: AnyObj, t: AnyObj): boolean => { + const oSchema = isObj(schema) + const oT = isObj(t) + if(!oSchema) + return true + if (!oT) { // ignore the object without type + // deep find oSchema has type + for (const key in schema) { + if (key === 'type') + return false + if (isObj((schema as any)[key]) && !isMatch((schema as any)[key], null)) + return false + } + return true + } + // check current `type` + const tx = (schema as any).type + const ty = (t as any).type + const isTypeValueObj = isObj(tx) + + if(!isTypeValueObj) // caution: type can be object, so that it would not be compare by value + if (tx !== ty) return false + + // recurse into all keys + const keys = new Set([...Object.keys(schema as object), ...Object.keys(t as object)]) + for (const k of keys) { + if (k === 'type' && !isTypeValueObj) continue // already checked + if (!isMatch((schema as any)[k], (t as any)[k])) return false + } + return true + } + + return isMatch(scheme, target) +} + +export default matchTheSchemaType diff --git a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx index f95c2353f3..7862dc824c 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show/field.tsx @@ -44,7 +44,7 @@ const Field: FC = ({ /> )}
{name}
-
{getFieldType(payload)}{payload.schemaType && ` (${payload.schemaType})`}
+
{getFieldType(payload)}{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}
{required &&
{t('app.structOutput.required')}
} {payload.description && ( diff --git a/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.spec.ts b/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.spec.ts deleted file mode 100644 index f5bdbe6e19..0000000000 --- a/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { deepEqualByType } from './use-match-schema-type' - -describe('deepEqualByType', () => { - it('should return true for identical primitive types', () => { - expect(deepEqualByType({ type: 'string' }, { type: 'string' })).toBe(true) - expect(deepEqualByType({ type: 'number' }, { type: 'number' })).toBe(true) - }) - - it('should return false for different primitive types', () => { - expect(deepEqualByType({ type: 'string' }, { type: 'number' })).toBe(false) - }) - - it('should ignore values and only compare types', () => { - expect(deepEqualByType({ type: 'string', value: 'hello' }, { type: 'string', value: 'world' })).toBe(true) - expect(deepEqualByType({ type: 'number', value: 42 }, { type: 'number', value: 100 })).toBe(true) - }) - - it('should return true for structural differences but no types', () => { - expect(deepEqualByType({ type: 'string', other: { b: 'xxx' } }, { type: 'string', other: 'xxx' })).toBe(true) - expect(deepEqualByType({ type: 'string', other: { b: 'xxx' } }, { type: 'string' })).toBe(true) - }) - - it('should handle nested objects with same structure and types', () => { - const obj1 = { - type: 'object', - properties: { - name: { type: 'string' }, - age: { type: 'number' }, - address: { - type: 'object', - properties: { - street: { type: 'string' }, - city: { type: 'string' }, - }, - }, - }, - } - const obj2 = { - type: 'object', - properties: { - name: { type: 'string', value: 'Alice' }, - age: { type: 'number', value: 30 }, - address: { - type: 'object', - properties: { - street: { type: 'string', value: '123 Main St' }, - city: { type: 'string', value: 'Wonderland' }, - }, - }, - }, - } - expect(deepEqualByType(obj1, obj2)).toBe(true) - }) - it('should return false for nested objects with different structures', () => { - const obj1 = { - type: 'object', - properties: { - name: { type: 'string' }, - age: { type: 'number' }, - }, - } - const obj2 = { - type: 'object', - properties: { - name: { type: 'string' }, - address: { type: 'string' }, - }, - } - expect(deepEqualByType(obj1, obj2)).toBe(false) - }) -}) diff --git a/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts b/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts index 6d29a77e57..e13e84a90d 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts +++ b/web/app/components/workflow/nodes/_base/components/variable/use-match-schema-type.ts @@ -1,42 +1,12 @@ import { useSchemaTypeDefinitions } from '@/service/use-common' - -type AnyObj = Record | null - -// only compare type in object -export function deepEqualByType(a: AnyObj, b: AnyObj): boolean { - const isObj = (x: any): x is object => x !== null && typeof x === 'object' - - const cmp = (x: AnyObj, y: AnyObj): boolean => { - const ox = isObj(x) - const oy = isObj(y) - if (!ox && !oy) return true // both primitives → ignore values - if (ox !== oy) { // ignore the object without type - if(ox && !('type' in (x as object))) - return true - return !!(oy && !('type' in (y as object))) - } - // check current `type` - const tx = (x as any).type - const ty = (y as any).type - if (tx !== ty) return false - - // recurse into all keys - const keys = new Set([...Object.keys(x as object), ...Object.keys(y as object)]) - for (const k of keys) { - if (k === 'type') continue // already checked - if (!cmp((x as any)[k], (y as any)[k])) return false - } - return true - } - - return cmp(a, b) -} +import type { AnyObj } from './match-schema-type' +import matchTheSchemaType from './match-schema-type' const useMatchSchemaType = () => { const { data: schemaTypeDefinitions } = useSchemaTypeDefinitions() const getMatchedSchemaType = (obj: AnyObj): string => { if(!schemaTypeDefinitions) return '' - const matched = schemaTypeDefinitions.find(def => deepEqualByType(obj, def.schema)) + const matched = schemaTypeDefinitions.find(def => matchTheSchemaType(obj, def.schema)) return matched ? matched.name : '' } return { diff --git a/web/app/components/workflow/nodes/data-source/panel.tsx b/web/app/components/workflow/nodes/data-source/panel.tsx index c2b389aaf5..95db573e4d 100644 --- a/web/app/components/workflow/nodes/data-source/panel.tsx +++ b/web/app/components/workflow/nodes/data-source/panel.tsx @@ -23,6 +23,7 @@ import { useStore } from '@/app/components/workflow/store' import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import ToolForm from '../tool/components/tool-form' import { wrapStructuredVarItem } from '@/app/components/workflow/utils/tool' +import useMatchSchemaType from '../_base/components/variable/use-match-schema-type' const Panel: FC> = ({ id, data }) => { const { t } = useTranslation() @@ -49,6 +50,7 @@ const Panel: FC> = ({ id, data }) => { const pipelineId = useStore(s => s.pipelineId) const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel) + const { getMatchedSchemaType } = useMatchSchemaType() return (
@@ -139,7 +141,7 @@ const Panel: FC> = ({ id, data }) => { {outputItem.value?.type === 'object' ? ( + payload={wrapStructuredVarItem(outputItem, getMatchedSchemaType(outputItem.value))} /> ) : ( { export const getFieldType = (field: Field) => { const { type, items } = field + if(field.schemaType === 'file') return 'file' if (type !== Type.array || !items) return type