feat: handle file schem show

This commit is contained in:
Joel 2025-08-27 14:46:33 +08:00
parent ff33d42c55
commit 4cb286c765
7 changed files with 212 additions and 106 deletions

View File

@ -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)
})
})

View File

@ -0,0 +1,42 @@
export type AnyObj = Record<string, any> | 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

View File

@ -44,7 +44,7 @@ const Field: FC<Props> = ({
/>
)}
<div className={cn('system-sm-medium ml-[7px] h-6 truncate leading-6 text-text-secondary', isRoot && rootClassName)}>{name}</div>
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{payload.schemaType && ` (${payload.schemaType})`}</div>
<div className='system-xs-regular ml-3 shrink-0 leading-6 text-text-tertiary'>{getFieldType(payload)}{(payload.schemaType && payload.schemaType !== 'file' && ` (${payload.schemaType})`)}</div>
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
</div>
{payload.description && (

View File

@ -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)
})
})

View File

@ -1,42 +1,12 @@
import { useSchemaTypeDefinitions } from '@/service/use-common'
type AnyObj = Record<string, any> | 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 {

View File

@ -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<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const { t } = useTranslation()
@ -49,6 +50,7 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
const pipelineId = useStore(s => s.pipelineId)
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
const { getMatchedSchemaType } = useMatchSchemaType()
return (
<div >
@ -139,7 +141,7 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
{outputItem.value?.type === 'object' ? (
<StructureOutputItem
rootClassName='code-sm-semibold text-text-secondary'
payload={wrapStructuredVarItem(outputItem)} />
payload={wrapStructuredVarItem(outputItem, getMatchedSchemaType(outputItem.value))} />
) : (
<VarItem
name={outputItem.name}

View File

@ -11,6 +11,7 @@ export const checkNodeValid = (_payload: LLMNodeType) => {
export const getFieldType = (field: Field) => {
const { type, items } = field
if(field.schemaType === 'file') return 'file'
if (type !== Type.array || !items)
return type