From 74e16204e36f07f1f83d21a3092f6346e68ff997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=8D=E9=97=B2=E7=8A=AC?= Date: Fri, 7 Nov 2025 16:48:10 +0800 Subject: [PATCH] fix: plugin file selector (#5871) * fix: plugin file selector * fix: render * fix: upload * fix: file selector auth --------- Co-authored-by: archer <545436317@qq.com> --- .../service/common/s3/sources/chat/type.ts | 4 +- .../core/app/formRender/FileSelector.tsx | 114 ++++++++++++------ .../components/core/app/formRender/index.tsx | 1 + .../PluginRunBox/components/RenderInput.tsx | 1 + .../nodes/NodePluginIO/InputEditModal.tsx | 5 +- 5 files changed, 87 insertions(+), 38 deletions(-) diff --git a/packages/service/common/s3/sources/chat/type.ts b/packages/service/common/s3/sources/chat/type.ts index a306eb172..bd245d018 100644 --- a/packages/service/common/s3/sources/chat/type.ts +++ b/packages/service/common/s3/sources/chat/type.ts @@ -3,7 +3,7 @@ import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo'; export const ChatFileUploadSchema = z.object({ appId: ObjectIdSchema, - chatId: z.string().length(24), + chatId: z.string().nonempty(), uId: z.string().nonempty(), filename: z.string().nonempty() }); @@ -11,7 +11,7 @@ export type CheckChatFileKeys = z.infer; export const DelChatFileByPrefixSchema = z.object({ appId: ObjectIdSchema, - chatId: z.string().length(24).optional(), + chatId: z.string().nonempty().optional(), uId: z.string().nonempty().optional() }); export type DelChatFileByPrefixParams = z.infer; diff --git a/projects/app/src/components/core/app/formRender/FileSelector.tsx b/projects/app/src/components/core/app/formRender/FileSelector.tsx index 1d1f16037..3492efa5a 100644 --- a/projects/app/src/components/core/app/formRender/FileSelector.tsx +++ b/projects/app/src/components/core/app/formRender/FileSelector.tsx @@ -1,5 +1,5 @@ import type { DragEvent } from 'react'; -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import type { UserInputFileItemType } from '../../chat/ChatContainer/ChatBox/type'; import { Box, @@ -31,8 +31,9 @@ import { ChatBoxContext } from '../../chat/ChatContainer/ChatBox/Provider'; import { POST } from '@/web/common/api/request'; import { getErrText } from '@fastgpt/global/common/error/utils'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; -import { useDebounceEffect } from 'ahooks'; import { formatFileSize, parseUrlToFileType } from '@fastgpt/global/common/file/tools'; +import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; +import { PluginRunContext } from '../../chat/ChatContainer/PluginRunBox/context'; const FileSelector = ({ fileUrls, @@ -45,49 +46,86 @@ const FileSelector = ({ canSelectCustomFileExtension, customFileExtensionList, canLocalUpload, - canUrlUpload + canUrlUpload, + isDisabled = false }: AppFileSelectConfigType & { - fileUrls: string[]; - onChange: (e: string[]) => void; + fileUrls: string[] | any[]; // Can be string[] or file object[] + onChange: (e: any[]) => void; canLocalUpload?: boolean; canUrlUpload?: boolean; + isDisabled?: boolean; }) => { const { feConfigs } = useSystemStore(); const { toast } = useToast(); const { t } = useTranslation(); - const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData); - const appId = useContextSelector(ChatBoxContext, (v) => v.appId); - const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId); + const chatBoxOutLinkAuthData = useContextSelector(ChatBoxContext, (v) => v?.outLinkAuthData); + const chatBoxAppId = useContextSelector(ChatBoxContext, (v) => v?.appId); + const chatBoxChatId = useContextSelector(ChatBoxContext, (v) => v?.chatId); + + const pluginOutLinkAuthData = useContextSelector(PluginRunContext, (v) => v?.outLinkAuthData); + const pluginAppId = useContextSelector(PluginRunContext, (v) => v?.appId); + const pluginChatId = useContextSelector(PluginRunContext, (v) => v?.chatId); + + const chatItemAppId = useContextSelector(ChatItemContext, (v) => v?.chatBoxData?.appId); + const chatItemChatId = useContextSelector(ChatItemContext, (v) => v?.chatBoxData?.chatId); + + const outLinkAuthData = useMemo( + () => ({ + ...(chatBoxOutLinkAuthData || {}), + ...(pluginOutLinkAuthData || {}) + }), + [chatBoxOutLinkAuthData, pluginOutLinkAuthData] + ); + const appId = useMemo( + () => chatBoxAppId || pluginAppId || chatItemAppId || '', + [chatBoxAppId, pluginAppId, chatItemAppId] + ); + const chatId = useMemo( + () => chatBoxChatId || pluginChatId || chatItemChatId || '', + [chatBoxChatId, pluginChatId, chatItemChatId] + ); + + const [cloneFiles, setCloneFiles] = useState(() => { + return fileUrls + .map((item) => { + const url = typeof item === 'string' ? item : item?.url || item?.key; + const key = typeof item === 'string' ? undefined : item?.key; + const name = typeof item === 'string' ? undefined : item?.name; + const type = typeof item === 'string' ? undefined : item?.type; + + if (!url) return null as unknown as UserInputFileItemType; - const [cloneFiles, setCloneFiles] = useState( - fileUrls - .map((url) => { const fileType = parseUrlToFileType(url); - if (!fileType) return null as unknown as UserInputFileItemType; + if (!fileType && !type) return null as unknown as UserInputFileItemType; return { id: getNanoid(6), - name: fileType.name || url, - type: fileType.type, - icon: getFileIcon(fileType.name || url), - url: fileType.url, + name: name || fileType?.name || url, + type: type || fileType?.type || ChatFileTypeEnum.file, + icon: getFileIcon(name || fileType?.name || url), + url: typeof item === 'string' ? fileType?.url : item?.url, status: 1, - key: url.startsWith('chat/') ? url : undefined + key: key || (typeof item === 'string' && url.startsWith('chat/') ? url : undefined) }; }) - .filter(Boolean) as UserInputFileItemType[] - ); - // 采用异步更新顶层的方式 - useDebounceEffect( - () => { - onChange(cloneFiles.map((file) => file.key || file.url || '').filter(Boolean)); - }, - [cloneFiles], - { - wait: 1000 - } - ); + .filter(Boolean) as UserInputFileItemType[]; + }); + + useEffect(() => { + const fileObjects = cloneFiles + .filter((file) => file.url || file.key) + .map((file) => { + const fileObj = { + type: file.type, + name: file.name, + key: file.key, + url: file.url || file.key || '' + }; + return fileObj; + }); + onChange(fileObjects as any); + }, [cloneFiles, onChange]); const fileType = useMemo(() => { return getUploadFileType({ @@ -378,9 +416,10 @@ const FileSelector = ({ borderColor={'myGray.250'} borderRadius={'md'} userSelect={'none'} - {...(isMaxSelected + {...(isMaxSelected || isDisabled ? { - cursor: 'not-allowed' + cursor: 'not-allowed', + opacity: isDisabled ? 0.6 : 1 } : { cursor: 'pointer', @@ -397,10 +436,10 @@ const FileSelector = ({ })} > - {isMaxSelected ? ( + {isMaxSelected || isDisabled ? ( <> - {t('file:reached_max_file_count')} + {isDisabled ? t('common:Running') : t('file:reached_max_file_count')} ) : ( @@ -427,7 +466,7 @@ const FileSelector = ({ zIndex={10} /> setUrlInput(e.target.value)} onBlur={(e) => handleAddUrl(e.target.value)} @@ -437,7 +476,11 @@ const FileSelector = ({ pl={8} py={1.5} placeholder={ - isMaxSelected ? t('file:reached_max_file_count') : t('chat:click_to_add_url') + isDisabled + ? t('common:Running') + : isMaxSelected + ? t('file:reached_max_file_count') + : t('chat:click_to_add_url') } /> @@ -476,6 +519,7 @@ const FileSelector = ({ aria-label={'Delete file'} icon={} onClick={() => handleDeleteFile(file.id)} + isDisabled={isDisabled} /> ) : ( diff --git a/projects/app/src/components/core/app/formRender/index.tsx b/projects/app/src/components/core/app/formRender/index.tsx index c7ec86917..46f537246 100644 --- a/projects/app/src/components/core/app/formRender/index.tsx +++ b/projects/app/src/components/core/app/formRender/index.tsx @@ -224,6 +224,7 @@ const InputRender = (props: InputRenderProps) => { customFileExtensionList={props.customFileExtensionList} canLocalUpload={props.canLocalUpload} canUrlUpload={props.canUrlUpload} + isDisabled={isDisabled} /> ); } diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx index 7b6592896..bea06cddd 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/components/RenderInput.tsx @@ -282,6 +282,7 @@ const RenderInput = () => { fieldName={inputKey} modelList={llmModelList} isRichText={false} + canLocalUpload={input.canLocalUpload ?? true} /> ); }} diff --git a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx index 5a9946403..067290ad2 100644 --- a/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx +++ b/projects/app/src/pageComponents/app/detail/WorkflowComponents/Flow/nodes/NodePluginIO/InputEditModal.tsx @@ -25,7 +25,9 @@ export const defaultInput: FlowNodeInputItemType = { list: [{ label: '', value: '' }], maxFiles: 5, canSelectFile: true, - canSelectImg: true + canSelectImg: true, + canLocalUpload: true, + canUrlUpload: false }; const FieldEditModal = ({ @@ -153,6 +155,7 @@ const FieldEditModal = ({ const onSubmitSuccess = useCallback( (data: FlowNodeInputItemType, action: 'confirm' | 'continue') => { + console.log('data', data); data.label = data?.label?.trim(); if (!data.label) {