diff --git a/packages/global/core/app/constants.ts b/packages/global/core/app/constants.ts index 6d586326f507..b3589e3e6fc2 100644 --- a/packages/global/core/app/constants.ts +++ b/packages/global/core/app/constants.ts @@ -123,3 +123,33 @@ export const getUploadFileType = ({ } return types.join(', '); }; + +export const isFileTypeAllowedByAccept = (file: Pick, accept: string) => { + const acceptTypes = accept + .split(',') + .map((item) => item.trim().toLowerCase()) + .filter(Boolean); + + if (acceptTypes.length === 0) return true; + + const filename = file.name.toLowerCase(); + const mimeType = file.type.toLowerCase(); + + return acceptTypes.some((acceptType) => { + if (acceptType === '*' || acceptType === '*/*') return true; + + if (acceptType.startsWith('.')) { + return filename.endsWith(acceptType); + } + + if (acceptType.endsWith('/*')) { + return mimeType.startsWith(acceptType.slice(0, -1)); + } + + if (acceptType.includes('/')) { + return mimeType === acceptType; + } + + return filename.endsWith(`.${acceptType}`); + }); +}; diff --git a/packages/global/test/core/app/constants.test.ts b/packages/global/test/core/app/constants.test.ts new file mode 100644 index 000000000000..fddc54590e5e --- /dev/null +++ b/packages/global/test/core/app/constants.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import { isFileTypeAllowedByAccept } from '@fastgpt/global/core/app/constants'; + +const createFile = ({ name, type = '' }: { name: string; type?: string }) => ({ name, type }); + +describe('isFileTypeAllowedByAccept', () => { + it('accepts extension matches without relying on browser MIME type', () => { + expect(isFileTypeAllowedByAccept(createFile({ name: 'report.DOCX' }), '.pdf, .docx')).toBe( + true + ); + }); + + it('accepts MIME wildcard matches', () => { + expect( + isFileTypeAllowedByAccept(createFile({ name: 'photo', type: 'image/png' }), 'image/*') + ).toBe(true); + }); + + it('accepts custom extensions without a leading dot', () => { + expect(isFileTypeAllowedByAccept(createFile({ name: 'data.jsonl' }), 'jsonl')).toBe(true); + }); + + it('rejects unmatched files', () => { + expect( + isFileTypeAllowedByAccept( + createFile({ name: 'movie.mp4', type: 'video/mp4' }), + '.pdf, image/*' + ) + ).toBe(false); + }); +}); diff --git a/pro b/pro index 46972180ebc8..a10974c0a73c 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 46972180ebc8de2300a24826cc1b41caae8bfa42 +Subproject commit a10974c0a73ca45f4e041738e5adb227923e289c diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx index 53f923479ab2..1ee35cc049f3 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Input/ChatInput.tsx @@ -13,7 +13,7 @@ import dynamic from 'next/dynamic'; import { useContextSelector } from 'use-context-selector'; import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import { documentFileType } from '@fastgpt/global/common/file/constants'; +import { isFileTypeAllowedByAccept } from '@fastgpt/global/core/app/constants'; import FilePreview from '../../components/FilePreview'; import { useFileUpload } from '../hooks/useFileUpload'; import ComplianceTip from '@/components/common/ComplianceTip/index'; @@ -25,13 +25,6 @@ import type { WorkflowInteractiveResponseType } from '@fastgpt/global/core/workf const InputGuideBox = dynamic(() => import('./InputGuideBox')); -const fileTypeFilter = (file: File) => { - return ( - file.type.includes('image') || - documentFileType.split(',').some((type) => file.name.endsWith(type.trim())) - ); -}; - const ChatInput = ({ lastInteractive, onSendMessage, @@ -84,6 +77,7 @@ const ChatInput = ({ uploadFiles, selectFileIcon, selectFileLabel, + fileType, showSelectFile, showSelectImg, showSelectVideo, @@ -107,6 +101,10 @@ const ChatInput = ({ showSelectVideo || showSelectAudio || showSelectCustomFileExtension; + const fileTypeFilter = useCallback( + (file: File) => isFileTypeAllowedByAccept(file, fileType), + [fileType] + ); // Upload files useRequest(uploadFiles, { @@ -263,6 +261,7 @@ const ChatInput = ({ setValue, handleSend, canUploadFile, + fileTypeFilter, onSelectFile ] ); diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx index 8d98f0f617af..3469437ab33f 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/hooks/useFileUpload.tsx @@ -266,6 +266,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => { uploadFiles, selectFileIcon, selectFileLabel, + fileType, showSelectFile, showSelectImg, showSelectVideo, diff --git a/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx b/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx index 18418e434a71..9412987b243b 100644 --- a/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx +++ b/projects/app/src/components/core/chat/HelperBot/Chatinput.tsx @@ -16,7 +16,7 @@ import dynamic from 'next/dynamic'; import { useContextSelector } from 'use-context-selector'; import { WorkflowRuntimeContext } from '../ChatContainer/context/workflowRuntimeContext'; import { useSystem } from '@fastgpt/web/hooks/useSystem'; -import { documentFileType } from '@fastgpt/global/common/file/constants'; +import { isFileTypeAllowedByAccept } from '@fastgpt/global/core/app/constants'; import FilePreview from '../ChatContainer/components/FilePreview'; import { useFileUpload } from './hooks/useFileUpload'; import ComplianceTip from '@/components/common/ComplianceTip/index'; @@ -24,13 +24,6 @@ import { useToast } from '@fastgpt/web/hooks/useToast'; import { HelperBotContext } from './context'; import type { onSendMessageFnType } from './type'; -const fileTypeFilter = (file: File) => { - return ( - file.type.includes('image') || - documentFileType.split(',').some((type) => file.name.endsWith(type.trim())) - ); -}; - const ChatInput = ({ chatId, onSendMessage, @@ -69,6 +62,7 @@ const ChatInput = ({ onSelectFile, selectFileIcon, selectFileLabel, + fileType, showSelectFile, showSelectImg, showSelectVideo, @@ -91,6 +85,10 @@ const ChatInput = ({ showSelectVideo || showSelectAudio || showSelectCustomFileExtension; + const fileTypeFilter = useCallback( + (file: File) => isFileTypeAllowedByAccept(file, fileType), + [fileType] + ); /* on send */ const handleSend = useCallback( @@ -224,6 +222,7 @@ const ChatInput = ({ setValue, handleSend, canUploadFile, + fileTypeFilter, onSelectFile ] ); diff --git a/projects/app/src/components/core/chat/HelperBot/hooks/useFileUpload.tsx b/projects/app/src/components/core/chat/HelperBot/hooks/useFileUpload.tsx index 076c85dfbcb0..813bea77912e 100644 --- a/projects/app/src/components/core/chat/HelperBot/hooks/useFileUpload.tsx +++ b/projects/app/src/components/core/chat/HelperBot/hooks/useFileUpload.tsx @@ -249,6 +249,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => { uploadFiles, selectFileIcon, selectFileLabel, + fileType, showSelectFile, showSelectImg, showSelectVideo, diff --git a/projects/app/src/pageComponents/dashboard/skill/detail/preview/PreviewInput.tsx b/projects/app/src/pageComponents/dashboard/skill/detail/preview/PreviewInput.tsx index f3e78bbe7a58..e3db598c9238 100644 --- a/projects/app/src/pageComponents/dashboard/skill/detail/preview/PreviewInput.tsx +++ b/projects/app/src/pageComponents/dashboard/skill/detail/preview/PreviewInput.tsx @@ -7,11 +7,12 @@ import MyBox from '@fastgpt/web/components/common/MyBox'; import { useForm, useFieldArray } from 'react-hook-form'; import { useRequest } from '@fastgpt/web/hooks/useRequest'; import { useToast } from '@fastgpt/web/hooks/useToast'; -import { documentFileType } from '@fastgpt/global/common/file/constants'; +import { isFileTypeAllowedByAccept } from '@fastgpt/global/core/app/constants'; import type { PreviewInputFormType, UserInputFileItemType } from './type'; import { textareaMinH } from './constants'; import { usePreviewFileUpload } from './usePreviewFileUpload'; import FilePreview from './FilePreview'; +import { ACCEPTED_FILE_TYPE } from './usePreviewFileUpload'; type PreviewInputProps = { appId: string; @@ -20,9 +21,7 @@ type PreviewInputProps = { onStop: () => void; }; -const fileTypeFilter = (file: File) => - file.type.includes('image') || - documentFileType.split(',').some((type) => file.name.endsWith(type.trim())); +const fileTypeFilter = (file: File) => isFileTypeAllowedByAccept(file, ACCEPTED_FILE_TYPE); const PreviewInput = ({ appId, isChatting, onSend, onStop }: PreviewInputProps) => { const { t } = useTranslation(); diff --git a/projects/app/src/pageComponents/dashboard/skill/detail/preview/usePreviewFileUpload.tsx b/projects/app/src/pageComponents/dashboard/skill/detail/preview/usePreviewFileUpload.tsx index 56c2b5c6d67d..764181be047a 100644 --- a/projects/app/src/pageComponents/dashboard/skill/detail/preview/usePreviewFileUpload.tsx +++ b/projects/app/src/pageComponents/dashboard/skill/detail/preview/usePreviewFileUpload.tsx @@ -16,7 +16,7 @@ import { putFileToS3 } from '@fastgpt/web/common/file/utils'; import type { PreviewInputFormType, UserInputFileItemType } from './type'; // 支持图片 + 文档,固定配置 -const ACCEPTED_FILE_TYPE = 'image/*, .txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx'; +export const ACCEPTED_FILE_TYPE = 'image/*, .txt, .docx, .csv, .xlsx, .pdf, .md, .html, .pptx'; type UsePreviewFileUploadOptions = { fileCtrl: UseFieldArrayReturn;