mirror of https://github.com/langgenius/dify.git
Merge 5be09d8df1 into 8b634a9bee
This commit is contained in:
commit
ab47d7d638
|
|
@ -19,6 +19,7 @@ import {
|
|||
toType,
|
||||
triggerEventParametersToFormSchemas,
|
||||
} from '@/app/components/tools/utils/to-form-schema'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
describe('Tool Data Processing Pipeline Integration', () => {
|
||||
describe('End-to-end: API schema → form schema → form value', () => {
|
||||
|
|
@ -184,8 +185,24 @@ describe('Tool Data Processing Pipeline Integration', () => {
|
|||
] as Parameters<typeof sortAgentSorts>[0]
|
||||
|
||||
const messageFiles = [
|
||||
{ id: 'f1', name: 'result.txt', type: 'document' },
|
||||
{ id: 'f2', name: 'summary.pdf', type: 'document' },
|
||||
{
|
||||
id: 'f1',
|
||||
name: 'result.txt',
|
||||
size: 0,
|
||||
type: 'text/plain',
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.remote_url,
|
||||
supportFileType: 'document',
|
||||
},
|
||||
{
|
||||
id: 'f2',
|
||||
name: 'summary.pdf',
|
||||
size: 0,
|
||||
type: 'application/pdf',
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.remote_url,
|
||||
supportFileType: 'document',
|
||||
},
|
||||
] as Parameters<typeof addFileInfos>[1]
|
||||
|
||||
const sorted = sortAgentSorts(thoughts)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ vi.mock('@/app/components/base/chat/chat/thought', () => ({
|
|||
),
|
||||
}))
|
||||
|
||||
// Mock FileList and Utils
|
||||
// Mock FileList
|
||||
vi.mock('@/app/components/base/file-uploader', () => ({
|
||||
FileList: ({ files }: { files: FileEntity[] }) => (
|
||||
<div data-testid="file-list-component">
|
||||
|
|
@ -32,10 +32,6 @@ vi.mock('@/app/components/base/file-uploader', () => ({
|
|||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/file-uploader/utils', () => ({
|
||||
getProcessedFilesFromResponse: (files: FileEntity[]) => files.map(f => ({ ...f, name: `processed-${f.id}` })),
|
||||
}))
|
||||
|
||||
describe('AgentContent', () => {
|
||||
const mockItem: ChatItem = {
|
||||
id: '1',
|
||||
|
|
@ -119,12 +115,15 @@ describe('AgentContent', () => {
|
|||
agent_thoughts: [
|
||||
{
|
||||
thought: 'T1',
|
||||
message_files: [{ id: 'file1' }, { id: 'file2' }],
|
||||
message_files: [
|
||||
{ id: 'file1', name: 'image-1.png' },
|
||||
{ id: 'file2', name: 'image-2.png' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
render(<AgentContent item={itemWithFiles as ChatItem} />)
|
||||
expect(screen.getByTestId('file-list-component')).toHaveTextContent('processed-file1, processed-file2')
|
||||
expect(screen.getByTestId('file-list-component')).toHaveTextContent('image-1.png, image-2.png')
|
||||
})
|
||||
|
||||
it('renders nothing if no annotation, content, or thoughts', () => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import type {
|
|||
import { memo } from 'react'
|
||||
import Thought from '@/app/components/base/chat/chat/thought'
|
||||
import { FileList } from '@/app/components/base/file-uploader'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { Markdown } from '@/app/components/base/markdown'
|
||||
|
||||
type AgentContentProps = {
|
||||
|
|
@ -40,7 +39,7 @@ const AgentContent: FC<AgentContentProps> = ({
|
|||
data-testid="agent-content-markdown"
|
||||
/>
|
||||
) : agent_thoughts?.map((thought, index) => (
|
||||
<div key={index} className="px-2 py-1" data-testid="agent-thought-item">
|
||||
<div key={thought.id || index} className="px-2 py-1" data-testid="agent-thought-item">
|
||||
{thought.thought && (
|
||||
<Markdown
|
||||
content={thought.thought}
|
||||
|
|
@ -59,7 +58,7 @@ const AgentContent: FC<AgentContentProps> = ({
|
|||
{
|
||||
!!thought.message_files?.length && (
|
||||
<FileList
|
||||
files={getProcessedFilesFromResponse(thought.message_files.map((item: any) => ({ ...item, related_id: item.id })))}
|
||||
files={thought.message_files}
|
||||
showDeleteAction={false}
|
||||
showDownloadAction={true}
|
||||
canPreview={true}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,29 @@
|
|||
import type { ThoughtItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { VisionFile } from '@/types/app'
|
||||
import type { FileResponse } from '@/types/workflow'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { addFileInfos, sortAgentSorts } from '../index'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import {
|
||||
addFileInfos,
|
||||
getVisionFileMimeType,
|
||||
getVisionFileName,
|
||||
getVisionFileSupportType,
|
||||
sortAgentSorts,
|
||||
} from '../index'
|
||||
|
||||
describe('tools/utils', () => {
|
||||
const createFileEntity = (overrides: Partial<FileEntity>): FileEntity => ({
|
||||
id: 'file-1',
|
||||
name: 'file.txt',
|
||||
size: 0,
|
||||
type: 'text/plain',
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.remote_url,
|
||||
supportFileType: 'document',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('sortAgentSorts', () => {
|
||||
it('returns null/undefined input as-is', () => {
|
||||
expect(sortAgentSorts(null as unknown as ThoughtItem[])).toBeNull()
|
||||
|
|
@ -40,6 +60,46 @@ describe('tools/utils', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('VisionFile helpers', () => {
|
||||
it.each([
|
||||
['image', 'image/png'],
|
||||
['video', 'video/mp4'],
|
||||
['audio', 'audio/mpeg'],
|
||||
['application/pdf', 'application/pdf'],
|
||||
['document', 'application/octet-stream'],
|
||||
['unknown', 'application/octet-stream'],
|
||||
])('returns %s mime type as %s', (fileType, expectedMimeType) => {
|
||||
expect(getVisionFileMimeType(fileType)).toBe(expectedMimeType)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['image', 'image'],
|
||||
['video', 'video'],
|
||||
['audio', 'audio'],
|
||||
['document', 'document'],
|
||||
['image/png', 'image'],
|
||||
['video/mp4', 'video'],
|
||||
['audio/mpeg', 'audio'],
|
||||
['application/pdf', 'document'],
|
||||
['application/json', 'document'],
|
||||
] as const)('returns %s support type as %s', (fileType, expectedSupportType) => {
|
||||
expect(getVisionFileSupportType(fileType)).toBe(expectedSupportType)
|
||||
})
|
||||
|
||||
it('extracts the file name from URLs with query params', () => {
|
||||
expect(getVisionFileName('https://example.com/generated.png?signature=1', 'image')).toBe('generated.png')
|
||||
})
|
||||
|
||||
it.each([
|
||||
['image', 'generated_image.png'],
|
||||
['video', 'generated_video.mp4'],
|
||||
['audio', 'generated_audio.mp3'],
|
||||
['document', 'generated_file.bin'],
|
||||
] as const)('returns a fallback file name for %s URLs without a file segment', (supportFileType, expectedFileName) => {
|
||||
expect(getVisionFileName('https://example.com/', supportFileType)).toBe(expectedFileName)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addFileInfos', () => {
|
||||
it('returns null/undefined input as-is', () => {
|
||||
expect(addFileInfos(null as unknown as ThoughtItem[], [])).toBeNull()
|
||||
|
|
@ -52,8 +112,8 @@ describe('tools/utils', () => {
|
|||
})
|
||||
|
||||
it('adds message_files by matching file IDs', () => {
|
||||
const file1 = { id: 'file-1', name: 'doc.pdf' } as FileEntity
|
||||
const file2 = { id: 'file-2', name: 'img.png' } as FileEntity
|
||||
const file1 = createFileEntity({ id: 'file-1', name: 'doc.pdf', type: 'application/pdf' })
|
||||
const file2 = createFileEntity({ id: 'file-2', name: 'img.png', type: 'image/png', supportFileType: 'image' })
|
||||
const items = [
|
||||
{ id: '1', files: ['file-1', 'file-2'] },
|
||||
{ id: '2', files: [] },
|
||||
|
|
@ -63,6 +123,84 @@ describe('tools/utils', () => {
|
|||
expect((result[0] as ThoughtItem & { message_files: FileEntity[] }).message_files).toEqual([file1, file2])
|
||||
})
|
||||
|
||||
it('normalizes backend agent file payloads into FileEntity objects', () => {
|
||||
const rawFile = {
|
||||
id: 'file-1',
|
||||
related_id: 'tool-file-1',
|
||||
extension: '.png',
|
||||
filename: 'generated.png',
|
||||
size: 128,
|
||||
mime_type: 'image/png',
|
||||
transfer_method: TransferMethod.remote_url,
|
||||
type: 'image',
|
||||
url: 'https://example.com/generated.png',
|
||||
upload_file_id: 'tool-file-1',
|
||||
remote_url: '',
|
||||
} satisfies FileResponse & { id: string }
|
||||
|
||||
const items = [{ id: '1', files: ['file-1'] }] as unknown as ThoughtItem[]
|
||||
|
||||
const result = addFileInfos(items, [rawFile])
|
||||
const messageFiles = (result[0] as ThoughtItem & { message_files: FileEntity[] }).message_files
|
||||
|
||||
expect(messageFiles).toHaveLength(1)
|
||||
expect(messageFiles[0]).toEqual(expect.objectContaining({
|
||||
id: 'file-1',
|
||||
name: 'generated.png',
|
||||
type: 'image/png',
|
||||
supportFileType: 'image',
|
||||
}))
|
||||
})
|
||||
|
||||
it('normalizes VisionFile payloads into FileEntity objects', () => {
|
||||
const visionFile: VisionFile = {
|
||||
id: 'file-vision-1',
|
||||
type: 'image',
|
||||
transfer_method: TransferMethod.remote_url,
|
||||
url: 'https://example.com/generated.png?signature=1',
|
||||
upload_file_id: 'upload-vision-1',
|
||||
}
|
||||
|
||||
const items = [{ id: '1', files: ['file-vision-1'] }] as unknown as ThoughtItem[]
|
||||
|
||||
const result = addFileInfos(items, [visionFile])
|
||||
const messageFiles = (result[0] as ThoughtItem & { message_files: FileEntity[] }).message_files
|
||||
|
||||
expect(messageFiles).toHaveLength(1)
|
||||
expect(messageFiles[0]).toEqual(expect.objectContaining({
|
||||
id: 'file-vision-1',
|
||||
name: 'generated.png',
|
||||
type: 'image/png',
|
||||
transferMethod: TransferMethod.remote_url,
|
||||
supportFileType: 'image',
|
||||
uploadedId: 'upload-vision-1',
|
||||
}))
|
||||
})
|
||||
|
||||
it('matches VisionFile payloads by upload_file_id when id is missing', () => {
|
||||
const visionFile: VisionFile = {
|
||||
type: 'document',
|
||||
transfer_method: TransferMethod.remote_url,
|
||||
url: 'https://example.com/',
|
||||
upload_file_id: 'upload-vision-fallback',
|
||||
}
|
||||
|
||||
const items = [{ id: '1', files: ['upload-vision-fallback'] }] as unknown as ThoughtItem[]
|
||||
|
||||
const result = addFileInfos(items, [visionFile])
|
||||
const messageFiles = (result[0] as ThoughtItem & { message_files: FileEntity[] }).message_files
|
||||
|
||||
expect(messageFiles).toHaveLength(1)
|
||||
expect(messageFiles[0]).toEqual(expect.objectContaining({
|
||||
id: 'upload-vision-fallback',
|
||||
name: 'generated_file.bin',
|
||||
type: 'application/octet-stream',
|
||||
transferMethod: TransferMethod.remote_url,
|
||||
supportFileType: 'document',
|
||||
uploadedId: 'upload-vision-fallback',
|
||||
}))
|
||||
})
|
||||
|
||||
it('returns items without files unchanged', () => {
|
||||
const items = [
|
||||
{ id: '1' },
|
||||
|
|
@ -73,7 +211,7 @@ describe('tools/utils', () => {
|
|||
})
|
||||
|
||||
it('does not mutate original items', () => {
|
||||
const file1 = { id: 'file-1', name: 'doc.pdf' } as FileEntity
|
||||
const file1 = createFileEntity({ id: 'file-1', name: 'doc.pdf', type: 'application/pdf' })
|
||||
const items = [{ id: '1', files: ['file-1'] }] as unknown as ThoughtItem[]
|
||||
const result = addFileInfos(items, [file1])
|
||||
expect(result[0]).not.toBe(items[0])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { ThoughtItem } from '@/app/components/base/chat/chat/type'
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { VisionFile } from '@/types/app'
|
||||
import type { FileResponse } from '@/types/workflow'
|
||||
import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
|
||||
import { ToolTypeEnum } from '../../workflow/block-selector/types'
|
||||
|
||||
export const getToolType = (type: string) => {
|
||||
|
|
@ -28,14 +30,107 @@ export const sortAgentSorts = (list: ThoughtItem[]) => {
|
|||
return temp
|
||||
}
|
||||
|
||||
export const addFileInfos = (list: ThoughtItem[], messageFiles: (FileEntity | VisionFile)[]) => {
|
||||
type AgentThoughtHistoryFile = FileResponse & { id: string }
|
||||
type AgentThoughtMessageFile = FileEntity | VisionFile | AgentThoughtHistoryFile
|
||||
type VisionFileSupportType = 'image' | 'video' | 'audio' | 'document'
|
||||
|
||||
const isFileEntity = (file: AgentThoughtMessageFile): file is FileEntity => {
|
||||
return 'transferMethod' in file
|
||||
}
|
||||
|
||||
const isAgentThoughtHistoryFile = (file: AgentThoughtMessageFile): file is AgentThoughtHistoryFile => {
|
||||
return 'filename' in file && 'mime_type' in file
|
||||
}
|
||||
|
||||
export const getVisionFileMimeType = (fileType: string) => {
|
||||
if (fileType.includes('/'))
|
||||
return fileType
|
||||
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
return 'image/png'
|
||||
case 'video':
|
||||
return 'video/mp4'
|
||||
case 'audio':
|
||||
return 'audio/mpeg'
|
||||
default:
|
||||
return 'application/octet-stream'
|
||||
}
|
||||
}
|
||||
|
||||
export const getVisionFileSupportType = (fileType: string): VisionFileSupportType => {
|
||||
if (fileType.includes('/')) {
|
||||
const [mainType] = fileType.split('/')
|
||||
if (['image', 'video', 'audio'].includes(mainType))
|
||||
return mainType as 'image' | 'video' | 'audio'
|
||||
return 'document'
|
||||
}
|
||||
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
case 'video':
|
||||
case 'audio':
|
||||
return fileType
|
||||
default:
|
||||
return 'document'
|
||||
}
|
||||
}
|
||||
|
||||
export const getVisionFileName = (url: string, supportFileType: VisionFileSupportType) => {
|
||||
const fileName = url.split('/').pop()?.split('?')[0]
|
||||
if (fileName)
|
||||
return fileName
|
||||
|
||||
switch (supportFileType) {
|
||||
case 'image':
|
||||
return 'generated_image.png'
|
||||
case 'video':
|
||||
return 'generated_video.mp4'
|
||||
case 'audio':
|
||||
return 'generated_audio.mp3'
|
||||
default:
|
||||
return 'generated_file.bin'
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeAgentThoughtMessageFile = (file: AgentThoughtMessageFile): FileEntity => {
|
||||
if (isFileEntity(file))
|
||||
return file
|
||||
|
||||
if (!isAgentThoughtHistoryFile(file)) {
|
||||
const supportFileType = getVisionFileSupportType(file.type)
|
||||
return {
|
||||
id: file.id || file.upload_file_id,
|
||||
name: getVisionFileName(file.url, supportFileType),
|
||||
size: 0,
|
||||
type: getVisionFileMimeType(file.type),
|
||||
progress: 100,
|
||||
transferMethod: file.transfer_method,
|
||||
supportFileType,
|
||||
uploadedId: file.upload_file_id,
|
||||
url: file.url,
|
||||
}
|
||||
}
|
||||
|
||||
return getProcessedFilesFromResponse([{
|
||||
...file,
|
||||
related_id: file.id,
|
||||
}])[0]
|
||||
}
|
||||
|
||||
export const addFileInfos = (list: ThoughtItem[], messageFiles: AgentThoughtMessageFile[]) => {
|
||||
if (!list || !messageFiles)
|
||||
return list
|
||||
return list.map((item) => {
|
||||
if (item.files && item.files?.length > 0) {
|
||||
const matchedFiles = item.files
|
||||
.map(fileId => messageFiles.find(file => (file.id || ('upload_file_id' in file ? file.upload_file_id : '')) === fileId))
|
||||
.filter((file): file is AgentThoughtMessageFile => Boolean(file))
|
||||
.map(normalizeAgentThoughtMessageFile)
|
||||
|
||||
return {
|
||||
...item,
|
||||
message_files: item.files.map(fileId => messageFiles.find(file => file.id === fileId)) as FileEntity[],
|
||||
message_files: matchedFiles,
|
||||
}
|
||||
}
|
||||
return item
|
||||
|
|
|
|||
|
|
@ -1735,9 +1735,6 @@
|
|||
"app/components/base/chat/chat/answer/agent-content.tsx": {
|
||||
"style/multiline-ternary": {
|
||||
"count": 2
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/chat/chat/answer/human-input-content/utils.ts": {
|
||||
|
|
|
|||
Loading…
Reference in New Issue