fix: plugin file selector (#5871)

* fix: plugin file selector

* fix: render

* fix: upload

* fix: file selector auth

---------

Co-authored-by: archer <545436317@qq.com>
This commit is contained in:
伍闲犬
2025-11-07 16:48:10 +08:00
committed by GitHub
parent 8ca5ebecd4
commit 74e16204e3
5 changed files with 87 additions and 38 deletions
@@ -3,7 +3,7 @@ import { ObjectIdSchema } from '@fastgpt/global/common/type/mongo';
export const ChatFileUploadSchema = z.object({ export const ChatFileUploadSchema = z.object({
appId: ObjectIdSchema, appId: ObjectIdSchema,
chatId: z.string().length(24), chatId: z.string().nonempty(),
uId: z.string().nonempty(), uId: z.string().nonempty(),
filename: z.string().nonempty() filename: z.string().nonempty()
}); });
@@ -11,7 +11,7 @@ export type CheckChatFileKeys = z.infer<typeof ChatFileUploadSchema>;
export const DelChatFileByPrefixSchema = z.object({ export const DelChatFileByPrefixSchema = z.object({
appId: ObjectIdSchema, appId: ObjectIdSchema,
chatId: z.string().length(24).optional(), chatId: z.string().nonempty().optional(),
uId: z.string().nonempty().optional() uId: z.string().nonempty().optional()
}); });
export type DelChatFileByPrefixParams = z.infer<typeof DelChatFileByPrefixSchema>; export type DelChatFileByPrefixParams = z.infer<typeof DelChatFileByPrefixSchema>;
@@ -1,5 +1,5 @@
import type { DragEvent } from 'react'; 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 type { UserInputFileItemType } from '../../chat/ChatContainer/ChatBox/type';
import { import {
Box, Box,
@@ -31,8 +31,9 @@ import { ChatBoxContext } from '../../chat/ChatContainer/ChatBox/Provider';
import { POST } from '@/web/common/api/request'; import { POST } from '@/web/common/api/request';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest'; import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useDebounceEffect } from 'ahooks';
import { formatFileSize, parseUrlToFileType } from '@fastgpt/global/common/file/tools'; 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 = ({ const FileSelector = ({
fileUrls, fileUrls,
@@ -45,49 +46,86 @@ const FileSelector = ({
canSelectCustomFileExtension, canSelectCustomFileExtension,
customFileExtensionList, customFileExtensionList,
canLocalUpload, canLocalUpload,
canUrlUpload canUrlUpload,
isDisabled = false
}: AppFileSelectConfigType & { }: AppFileSelectConfigType & {
fileUrls: string[]; fileUrls: string[] | any[]; // Can be string[] or file object[]
onChange: (e: string[]) => void; onChange: (e: any[]) => void;
canLocalUpload?: boolean; canLocalUpload?: boolean;
canUrlUpload?: boolean; canUrlUpload?: boolean;
isDisabled?: boolean;
}) => { }) => {
const { feConfigs } = useSystemStore(); const { feConfigs } = useSystemStore();
const { toast } = useToast(); const { toast } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData); const chatBoxOutLinkAuthData = useContextSelector(ChatBoxContext, (v) => v?.outLinkAuthData);
const appId = useContextSelector(ChatBoxContext, (v) => v.appId); const chatBoxAppId = useContextSelector(ChatBoxContext, (v) => v?.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId); 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<UserInputFileItemType[]>(() => {
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<UserInputFileItemType[]>(
fileUrls
.map((url) => {
const fileType = parseUrlToFileType(url); const fileType = parseUrlToFileType(url);
if (!fileType) return null as unknown as UserInputFileItemType; if (!fileType && !type) return null as unknown as UserInputFileItemType;
return { return {
id: getNanoid(6), id: getNanoid(6),
name: fileType.name || url, name: name || fileType?.name || url,
type: fileType.type, type: type || fileType?.type || ChatFileTypeEnum.file,
icon: getFileIcon(fileType.name || url), icon: getFileIcon(name || fileType?.name || url),
url: fileType.url, url: typeof item === 'string' ? fileType?.url : item?.url,
status: 1, status: 1,
key: url.startsWith('chat/') ? url : undefined key: key || (typeof item === 'string' && url.startsWith('chat/') ? url : undefined)
}; };
}) })
.filter(Boolean) as UserInputFileItemType[] .filter(Boolean) as UserInputFileItemType[];
); });
// 采用异步更新顶层的方式
useDebounceEffect( useEffect(() => {
() => { const fileObjects = cloneFiles
onChange(cloneFiles.map((file) => file.key || file.url || '').filter(Boolean)); .filter((file) => file.url || file.key)
}, .map((file) => {
[cloneFiles], const fileObj = {
{ type: file.type,
wait: 1000 name: file.name,
} key: file.key,
); url: file.url || file.key || ''
};
return fileObj;
});
onChange(fileObjects as any);
}, [cloneFiles, onChange]);
const fileType = useMemo(() => { const fileType = useMemo(() => {
return getUploadFileType({ return getUploadFileType({
@@ -378,9 +416,10 @@ const FileSelector = ({
borderColor={'myGray.250'} borderColor={'myGray.250'}
borderRadius={'md'} borderRadius={'md'}
userSelect={'none'} userSelect={'none'}
{...(isMaxSelected {...(isMaxSelected || isDisabled
? { ? {
cursor: 'not-allowed' cursor: 'not-allowed',
opacity: isDisabled ? 0.6 : 1
} }
: { : {
cursor: 'pointer', cursor: 'pointer',
@@ -397,10 +436,10 @@ const FileSelector = ({
})} })}
> >
<MyIcon name={'common/uploadFileFill'} w={'32px'} /> <MyIcon name={'common/uploadFileFill'} w={'32px'} />
{isMaxSelected ? ( {isMaxSelected || isDisabled ? (
<> <>
<Box fontWeight={'500'} fontSize={'sm'}> <Box fontWeight={'500'} fontSize={'sm'}>
{t('file:reached_max_file_count')} {isDisabled ? t('common:Running') : t('file:reached_max_file_count')}
</Box> </Box>
</> </>
) : ( ) : (
@@ -427,7 +466,7 @@ const FileSelector = ({
zIndex={10} zIndex={10}
/> />
<Input <Input
isDisabled={isMaxSelected} isDisabled={isMaxSelected || isDisabled}
value={urlInput} value={urlInput}
onChange={(e) => setUrlInput(e.target.value)} onChange={(e) => setUrlInput(e.target.value)}
onBlur={(e) => handleAddUrl(e.target.value)} onBlur={(e) => handleAddUrl(e.target.value)}
@@ -437,7 +476,11 @@ const FileSelector = ({
pl={8} pl={8}
py={1.5} py={1.5}
placeholder={ 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')
} }
/> />
</InputGroup> </InputGroup>
@@ -476,6 +519,7 @@ const FileSelector = ({
aria-label={'Delete file'} aria-label={'Delete file'}
icon={<MyIcon name={'close'} w={'1rem'} />} icon={<MyIcon name={'close'} w={'1rem'} />}
onClick={() => handleDeleteFile(file.id)} onClick={() => handleDeleteFile(file.id)}
isDisabled={isDisabled}
/> />
) : ( ) : (
<HStack w={'24px'} h={'24px'} justifyContent={'center'}> <HStack w={'24px'} h={'24px'} justifyContent={'center'}>
@@ -224,6 +224,7 @@ const InputRender = (props: InputRenderProps) => {
customFileExtensionList={props.customFileExtensionList} customFileExtensionList={props.customFileExtensionList}
canLocalUpload={props.canLocalUpload} canLocalUpload={props.canLocalUpload}
canUrlUpload={props.canUrlUpload} canUrlUpload={props.canUrlUpload}
isDisabled={isDisabled}
/> />
); );
} }
@@ -282,6 +282,7 @@ const RenderInput = () => {
fieldName={inputKey} fieldName={inputKey}
modelList={llmModelList} modelList={llmModelList}
isRichText={false} isRichText={false}
canLocalUpload={input.canLocalUpload ?? true}
/> />
); );
}} }}
@@ -25,7 +25,9 @@ export const defaultInput: FlowNodeInputItemType = {
list: [{ label: '', value: '' }], list: [{ label: '', value: '' }],
maxFiles: 5, maxFiles: 5,
canSelectFile: true, canSelectFile: true,
canSelectImg: true canSelectImg: true,
canLocalUpload: true,
canUrlUpload: false
}; };
const FieldEditModal = ({ const FieldEditModal = ({
@@ -153,6 +155,7 @@ const FieldEditModal = ({
const onSubmitSuccess = useCallback( const onSubmitSuccess = useCallback(
(data: FlowNodeInputItemType, action: 'confirm' | 'continue') => { (data: FlowNodeInputItemType, action: 'confirm' | 'continue') => {
console.log('data', data);
data.label = data?.label?.trim(); data.label = data?.label?.trim();
if (!data.label) { if (!data.label) {