mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 01:26:33 +08:00
feat: handle file schem show
This commit is contained in:
parent
ff33d42c55
commit
4cb286c765
@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -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
|
||||||
@ -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={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>}
|
{required && <div className='system-2xs-medium-uppercase ml-3 leading-6 text-text-warning'>{t('app.structOutput.required')}</div>}
|
||||||
</div>
|
</div>
|
||||||
{payload.description && (
|
{payload.description && (
|
||||||
|
|||||||
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,42 +1,12 @@
|
|||||||
import { useSchemaTypeDefinitions } from '@/service/use-common'
|
import { useSchemaTypeDefinitions } from '@/service/use-common'
|
||||||
|
import type { AnyObj } from './match-schema-type'
|
||||||
type AnyObj = Record<string, any> | null
|
import matchTheSchemaType from './match-schema-type'
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
const useMatchSchemaType = () => {
|
const useMatchSchemaType = () => {
|
||||||
const { data: schemaTypeDefinitions } = useSchemaTypeDefinitions()
|
const { data: schemaTypeDefinitions } = useSchemaTypeDefinitions()
|
||||||
const getMatchedSchemaType = (obj: AnyObj): string => {
|
const getMatchedSchemaType = (obj: AnyObj): string => {
|
||||||
if(!schemaTypeDefinitions) return ''
|
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 matched ? matched.name : ''
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { useStore } from '@/app/components/workflow/store'
|
|||||||
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
import { toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
|
||||||
import ToolForm from '../tool/components/tool-form'
|
import ToolForm from '../tool/components/tool-form'
|
||||||
import { wrapStructuredVarItem } from '@/app/components/workflow/utils/tool'
|
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 Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -49,6 +50,7 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
|||||||
|
|
||||||
const pipelineId = useStore(s => s.pipelineId)
|
const pipelineId = useStore(s => s.pipelineId)
|
||||||
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
const setShowInputFieldPanel = useStore(s => s.setShowInputFieldPanel)
|
||||||
|
const { getMatchedSchemaType } = useMatchSchemaType()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div >
|
<div >
|
||||||
@ -139,7 +141,7 @@ const Panel: FC<NodePanelProps<DataSourceNodeType>> = ({ id, data }) => {
|
|||||||
{outputItem.value?.type === 'object' ? (
|
{outputItem.value?.type === 'object' ? (
|
||||||
<StructureOutputItem
|
<StructureOutputItem
|
||||||
rootClassName='code-sm-semibold text-text-secondary'
|
rootClassName='code-sm-semibold text-text-secondary'
|
||||||
payload={wrapStructuredVarItem(outputItem)} />
|
payload={wrapStructuredVarItem(outputItem, getMatchedSchemaType(outputItem.value))} />
|
||||||
) : (
|
) : (
|
||||||
<VarItem
|
<VarItem
|
||||||
name={outputItem.name}
|
name={outputItem.name}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export const checkNodeValid = (_payload: LLMNodeType) => {
|
|||||||
|
|
||||||
export const getFieldType = (field: Field) => {
|
export const getFieldType = (field: Field) => {
|
||||||
const { type, items } = field
|
const { type, items } = field
|
||||||
|
if(field.schemaType === 'file') return 'file'
|
||||||
if (type !== Type.array || !items)
|
if (type !== Type.array || !items)
|
||||||
return type
|
return type
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user