diff --git a/document/content/self-host/upgrading/4-14/41418.mdx b/document/content/self-host/upgrading/4-14/41418.mdx new file mode 100644 index 0000000000..9bef9ec6d3 --- /dev/null +++ b/document/content/self-host/upgrading/4-14/41418.mdx @@ -0,0 +1,15 @@ +--- +title: 'V4.14.18(处理中)' +description: 'FastGPT V4.14.18 更新说明' +--- + +## 升级指南 + +### 1. 更新镜像 tag + +- 更新 fastgpt-app(fastgpt 主服务) 镜像 tag: v4.14.18 +- 更新 fastgpt-pro(fastgpt 商业版) 镜像 tag: v4.14.18 + +## 🐛 修复 + +1. 修复了部分`工作流工具`、`用户表单节点`无法正确根据文件类型过滤并上传文件的问题 diff --git a/document/content/self-host/upgrading/4-14/meta.en.json b/document/content/self-host/upgrading/4-14/meta.en.json index a234171923..1f02e0eac1 100644 --- a/document/content/self-host/upgrading/4-14/meta.en.json +++ b/document/content/self-host/upgrading/4-14/meta.en.json @@ -2,6 +2,7 @@ "title": "4.14.x", "description": "", "pages": [ + "41418", "41417", "41416", "41415", diff --git a/document/content/self-host/upgrading/4-14/meta.json b/document/content/self-host/upgrading/4-14/meta.json index a234171923..1f02e0eac1 100644 --- a/document/content/self-host/upgrading/4-14/meta.json +++ b/document/content/self-host/upgrading/4-14/meta.json @@ -2,6 +2,7 @@ "title": "4.14.x", "description": "", "pages": [ + "41418", "41417", "41416", "41415", diff --git a/document/content/toc.mdx b/document/content/toc.mdx index 186f1cdc3a..d5b6c7fa5f 100644 --- a/document/content/toc.mdx +++ b/document/content/toc.mdx @@ -120,6 +120,7 @@ description: FastGPT 文档目录 - [/self-host/upgrading/4-14/41415](/self-host/upgrading/4-14/41415) - [/self-host/upgrading/4-14/41416](/self-host/upgrading/4-14/41416) - [/self-host/upgrading/4-14/41417](/self-host/upgrading/4-14/41417) +- [/self-host/upgrading/4-14/41418](/self-host/upgrading/4-14/41418) - [/self-host/upgrading/4-14/4142](/self-host/upgrading/4-14/4142) - [/self-host/upgrading/4-14/4143](/self-host/upgrading/4-14/4143) - [/self-host/upgrading/4-14/4144](/self-host/upgrading/4-14/4144) diff --git a/document/data/doc-last-modified.json b/document/data/doc-last-modified.json index 95ecdd4e65..b1ee4951ca 100644 --- a/document/data/doc-last-modified.json +++ b/document/data/doc-last-modified.json @@ -95,6 +95,8 @@ "content/introduction/guide/knowledge_base/collection_tags.mdx": "2026-04-26T21:08:47+08:00", "content/introduction/guide/knowledge_base/dataset_engine.en.mdx": "2026-04-26T21:08:47+08:00", "content/introduction/guide/knowledge_base/dataset_engine.mdx": "2026-04-26T21:08:47+08:00", + "content/introduction/guide/knowledge_base/dingtalk_dataset.en.mdx": "2026-04-29T20:39:24+08:00", + "content/introduction/guide/knowledge_base/dingtalk_dataset.mdx": "2026-04-29T20:39:24+08:00", "content/introduction/guide/knowledge_base/lark_dataset.en.mdx": "2026-04-26T21:08:47+08:00", "content/introduction/guide/knowledge_base/lark_dataset.mdx": "2026-04-26T21:08:47+08:00", "content/introduction/guide/knowledge_base/rag.en.mdx": "2026-04-26T21:08:47+08:00", @@ -232,6 +234,7 @@ "content/self-host/upgrading/4-14/41416.en.mdx": "2026-04-26T21:28:27+08:00", "content/self-host/upgrading/4-14/41416.mdx": "2026-04-26T22:41:57+08:00", "content/self-host/upgrading/4-14/41417.mdx": "2026-04-28T18:03:38+08:00", + "content/self-host/upgrading/4-14/41418.mdx": "2026-05-06T10:14:45+08:00", "content/self-host/upgrading/4-14/4142.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4142.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4143.en.mdx": "2026-04-26T21:08:47+08:00", @@ -252,7 +255,7 @@ "content/self-host/upgrading/4-14/41481.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00", - "content/self-host/upgrading/4-15/4150.mdx": "2026-04-29T14:53:45+08:00", + "content/self-host/upgrading/4-15/4150.mdx": "2026-04-29T20:39:24+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00", @@ -393,8 +396,8 @@ "content/self-host/upgrading/outdated/499.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/upgrade-intruction.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/upgrade-intruction.mdx": "2026-04-26T21:08:47+08:00", - "content/toc.en.mdx": "2026-04-26T21:28:27+08:00", - "content/toc.mdx": "2026-04-28T18:03:38+08:00", + "content/toc.en.mdx": "2026-04-29T20:39:24+08:00", + "content/toc.mdx": "2026-05-06T10:14:45+08:00", "content/use-cases/app-cases/dalle3.en.mdx": "2026-04-26T21:08:47+08:00", "content/use-cases/app-cases/dalle3.mdx": "2026-04-26T21:08:47+08:00", "content/use-cases/app-cases/english_essay_correction_bot.en.mdx": "2026-04-26T21:08:47+08:00", diff --git a/document/package.json b/document/package.json index aa84dab574..cb07d06d6a 100644 --- a/document/package.json +++ b/document/package.json @@ -7,7 +7,7 @@ "dev": "next dev --turbo", "start": "next start", "postinstall": "fumadocs-mdx", - "format-doc": "zhlint --dir ./ *.mdx --fix", + "format-doc": "pnpx zhlint --dir ./ *.mdx --fix", "initDocTime": "node ./script/initDocTime.js", "initDocToc": "node ./script/generateToc.js", "checkDocRefs": "node ./script/checkDocRefs.js", diff --git a/packages/global/openapi/core/chat/file/api.ts b/packages/global/openapi/core/chat/file/api.ts index 2ad171f324..817b5d20e6 100644 --- a/packages/global/openapi/core/chat/file/api.ts +++ b/packages/global/openapi/core/chat/file/api.ts @@ -28,8 +28,7 @@ export const PresignChatFilePostUrlSchema = z filename: z.string().min(1).describe('文件名'), appId: ObjectIdSchema.describe('应用ID'), chatId: z.string().min(1).describe('对话ID'), - fileSelectConfig: - AppFileSelectConfigTypeSchema.optional().describe('调试态前端当前文件选择配置'), + fileSelectConfig: AppFileSelectConfigTypeSchema.describe('本次上传控件的文件选择配置'), outLinkAuthData: OutLinkChatAuthSchema.optional().describe('外链鉴权数据') }) .meta({ diff --git a/packages/service/core/dataset/utils.ts b/packages/service/core/dataset/utils.ts index 0cc055b434..bc45013f5f 100644 --- a/packages/service/core/dataset/utils.ts +++ b/packages/service/core/dataset/utils.ts @@ -1,8 +1,6 @@ import { authDatasetByTmbId } from '../../support/permission/dataset/auth'; import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant'; import { S3Sources } from '../../common/s3/contracts/type'; -import { getS3DatasetSource, S3DatasetSource } from '../../common/s3/sources/dataset'; -import { getS3ChatSource } from '../../common/s3/sources/chat'; import { jwtSignS3DownloadToken, isS3ObjectKey } from '../../common/s3/utils'; import { getLogger, LogCategories } from '../../common/logger'; import { S3Buckets } from '../../common/s3/config/constants'; @@ -67,7 +65,10 @@ export function replaceS3KeyToPreviewUrl(documentQuoteText: string, expiredTime: for (const match of matches.slice().reverse()) { const [full, bang, alt, objectKey] = match; - if (isS3ObjectKey(objectKey, 'dataset') || isS3ObjectKey(objectKey, 'chat')) { + const allowedKeys: (keyof typeof S3Sources)[] = ['dataset', 'chat', 'temp']; + const allowedKeysGuard = allowedKeys.some((key) => isS3ObjectKey(objectKey, key)); + + if (allowedKeysGuard) { const url = jwtSignS3DownloadToken({ objectKey, bucketName: S3Buckets.private, diff --git a/packages/service/test/core/dataset/utils.test.ts b/packages/service/test/core/dataset/utils.test.ts index 70d150b9dc..f5ccd3ae04 100644 --- a/packages/service/test/core/dataset/utils.test.ts +++ b/packages/service/test/core/dataset/utils.test.ts @@ -134,10 +134,11 @@ describe('replaceS3KeyToPreviewUrl', () => { expect(result).toBe(text); }); - it('temp 前缀不应被替换', () => { + it('temp 前缀应被替换', () => { const text = '![临时文件](temp/team1/temp-file.png)'; const result = replaceS3KeyToPreviewUrl(text, expiredTime); - expect(result).toBe(text); + expect(result).toContain('https://example.com/api/system/file/download/mock-jwt-token-'); + expect(result).toContain('temp/team1/temp-file.png'); }); }); diff --git a/pro b/pro index 1a56dfc0d3..c8a75dd14a 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 1a56dfc0d3c2fc1dc3e4a2f23ad1847926c804a8 +Subproject commit c8a75dd14a1591f7270652824f51594687d07328 diff --git a/projects/app/src/components/core/app/FileSelector/index.tsx b/projects/app/src/components/core/app/FileSelector/index.tsx index d5a9f3b71b..128ada6913 100644 --- a/projects/app/src/components/core/app/FileSelector/index.tsx +++ b/projects/app/src/components/core/app/FileSelector/index.tsx @@ -62,10 +62,6 @@ const FileSelector = ({ const appId = useContextSelector(WorkflowRuntimeContext, (v) => v.appId); const chatId = useContextSelector(WorkflowRuntimeContext, (v) => v.chatId); const outLinkAuthData = useContextSelector(WorkflowRuntimeContext, (v) => v.outLinkAuthData); - const runtimeFileSelectConfig = useContextSelector( - WorkflowRuntimeContext, - (v) => v.runtimeFileSelectConfig - ); const setFileUploadingCount = useContextSelector( WorkflowRuntimeContext, (v) => v.setFileUploadingCount @@ -95,6 +91,26 @@ const FileSelector = ({ canSelectCustomFileExtension, customFileExtensionList ]); + const fileSelectConfig = useMemo( + () => ({ + maxFiles, + canSelectFile, + canSelectImg, + canSelectVideo, + canSelectAudio, + canSelectCustomFileExtension, + customFileExtensionList + }), + [ + maxFiles, + canSelectFile, + canSelectImg, + canSelectVideo, + canSelectAudio, + canSelectCustomFileExtension, + customFileExtensionList + ] + ); // 文件数量限制:组件参数 || 团队套餐 || 系统配置 || 默认值 const maxSelectFiles = maxFiles || @@ -131,7 +147,7 @@ const FileSelector = ({ filename: file.rawFile.name, appId, chatId, - fileSelectConfig: runtimeFileSelectConfig, + fileSelectConfig, outLinkAuthData }); @@ -181,14 +197,7 @@ const FileSelector = ({ }) ); }, - [ - handleChangeFiles, - setFileUploadingCount, - appId, - chatId, - runtimeFileSelectConfig, - outLinkAuthData - ] + [handleChangeFiles, setFileUploadingCount, appId, chatId, fileSelectConfig, outLinkAuthData] ); // Selector props diff --git a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx index e0dce145fe..fadf2dc31b 100644 --- a/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/ChatBox/Provider.tsx @@ -23,7 +23,6 @@ import { ChatItemContext } from '@/web/core/chat/context/chatItemContext'; import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext'; import { useCreation } from 'ahooks'; import type { ChatTypeEnum } from './constants'; -import { ChatTypeEnum as ChatTypeEnumValue } from './constants'; import type { ChatQuickAppType } from '@fastgpt/global/core/chat/setting/type'; import { WorkflowRuntimeContextProvider } from '@/components/core/chat/ChatContainer/context/workflowRuntimeContext'; @@ -254,22 +253,11 @@ const Provider = ({ getHistoryResponseData, chatType }; - const runtimeFileSelectConfig = useMemo(() => { - if (chatType === ChatTypeEnumValue.test) { - return fileSelectConfig; - } - if (chatType === ChatTypeEnumValue.home && !props.currentQuickAppId) { - return fileSelectConfig; - } - return undefined; - }, [chatType, fileSelectConfig, props.currentQuickAppId]); - return ( {children} 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 20d76677e4..8d98f0f617 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 @@ -17,8 +17,6 @@ import { type OutLinkChatAuthProps } from '@fastgpt/global/support/permission/ch import { getPresignedChatFileGetUrl, getUploadChatFilePresignedUrl } from '@/web/common/file/api'; import { getUploadFileType } from '@fastgpt/global/core/app/constants'; import { putFileToS3 } from '@fastgpt/web/common/file/utils'; -import { WorkflowRuntimeContext } from '../../context/workflowRuntimeContext'; -import { useContextSelector } from 'use-context-selector'; type UseFileUploadOptions = { fileSelectConfig: AppFileSelectConfigType; @@ -35,10 +33,6 @@ export const useFileUpload = (props: UseFileUploadOptions) => { const { t } = useTranslation(); const { feConfigs } = useSystemStore(); const { teamPlanStatus } = useUserStore(); - const runtimeFileSelectConfig = useContextSelector( - WorkflowRuntimeContext, - (v) => v.runtimeFileSelectConfig - ); const { update: updateFiles, @@ -196,7 +190,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => { filename: copyFile.rawFile.name, appId, chatId, - fileSelectConfig: runtimeFileSelectConfig, + fileSelectConfig, outLinkAuthData }); @@ -242,10 +236,10 @@ export const useFileUpload = (props: UseFileUploadOptions) => { appId, chatId, fileList, + fileSelectConfig, outLinkAuthData, removeFiles, replaceFiles, - runtimeFileSelectConfig, t, toast, updateFiles diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx index 7b0376b3f5..195c41b77f 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/context.tsx @@ -294,7 +294,6 @@ const PluginRunContextProvider = ({ appId={props.appId} chatId={props.chatId} outLinkAuthData={props.outLinkAuthData || {}} - runtimeFileSelectConfig={props.runtimeFileSelectConfig} > {children} diff --git a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/type.ts b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/type.ts index cfa8f8d2ba..15a7c365ae 100644 --- a/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/type.ts +++ b/projects/app/src/components/core/chat/ChatContainer/PluginRunBox/type.ts @@ -5,7 +5,6 @@ import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/ch import React from 'react'; import type { onStartChatType } from '../type'; import { ChatBoxInputFormType } from '../ChatBox/type'; -import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config.schema'; export type PluginRunBoxProps = { appId: string; @@ -15,5 +14,4 @@ export type PluginRunBoxProps = { onStartChat?: onStartChatType; onNewChat?: () => void; showTab?: PluginRunBoxTabEnum; // 如何设置了该字段,全局都 tab 不生效 - runtimeFileSelectConfig?: AppFileSelectConfigType; }; diff --git a/projects/app/src/components/core/chat/ChatContainer/context/workflowRuntimeContext.tsx b/projects/app/src/components/core/chat/ChatContainer/context/workflowRuntimeContext.tsx index 3ec135b0f4..4a07516608 100644 --- a/projects/app/src/components/core/chat/ChatContainer/context/workflowRuntimeContext.tsx +++ b/projects/app/src/components/core/chat/ChatContainer/context/workflowRuntimeContext.tsx @@ -1,5 +1,4 @@ import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat'; -import type { AppFileSelectConfigType } from '@fastgpt/global/core/app/type/config.schema'; import { useMemoEnhance } from '@fastgpt/web/hooks/useMemoEnhance'; import { useState } from 'react'; import { createContext } from 'use-context-selector'; @@ -8,7 +7,6 @@ type WorkflowRuntimeContextType = { outLinkAuthData: OutLinkChatAuthProps; appId: string; chatId: string; - runtimeFileSelectConfig?: AppFileSelectConfigType; fileUploading: boolean; setFileUploadingCount: React.Dispatch>; @@ -18,7 +16,6 @@ export const WorkflowRuntimeContext = createContext( outLinkAuthData: {}, appId: '', chatId: '', - runtimeFileSelectConfig: undefined, fileUploading: false, setFileUploadingCount: () => {} }); @@ -27,13 +24,11 @@ export const WorkflowRuntimeContextProvider = ({ appId, chatId, outLinkAuthData, - runtimeFileSelectConfig, children }: { appId: string; chatId: string; outLinkAuthData: OutLinkChatAuthProps; - runtimeFileSelectConfig?: AppFileSelectConfigType; children: React.ReactNode; }) => { const [fileUploadingCount, setFileUploadingCount] = useState(0); @@ -44,11 +39,10 @@ export const WorkflowRuntimeContextProvider = ({ outLinkAuthData, appId, chatId, - runtimeFileSelectConfig, fileUploading, setFileUploadingCount }), - [outLinkAuthData, appId, chatId, runtimeFileSelectConfig, fileUploading, setFileUploadingCount] + [outLinkAuthData, appId, chatId, fileUploading, setFileUploadingCount] ); return ( diff --git a/projects/app/src/pageComponents/app/detail/useChatTest.tsx b/projects/app/src/pageComponents/app/detail/useChatTest.tsx index 6df7d5d181..bff60a257b 100644 --- a/projects/app/src/pageComponents/app/detail/useChatTest.tsx +++ b/projects/app/src/pageComponents/app/detail/useChatTest.tsx @@ -158,7 +158,6 @@ export const useChatTest = ({ chatId={chatId} onNewChat={restartChat} onStartChat={startChat} - runtimeFileSelectConfig={chatConfig.fileSelectConfig} /> ) : ( 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 d4248e8f1a..56c2b5c6d6 100644 --- a/projects/app/src/pageComponents/dashboard/skill/detail/preview/usePreviewFileUpload.tsx +++ b/projects/app/src/pageComponents/dashboard/skill/detail/preview/usePreviewFileUpload.tsx @@ -133,6 +133,11 @@ export const usePreviewFileUpload = ({ fileCtrl, appId, chatId }: UsePreviewFile filename: copyFile.rawFile.name, appId, chatId, + fileSelectConfig: { + maxFiles: maxSelectFiles, + canSelectFile: true, + canSelectImg: true + }, outLinkAuthData: undefined }); @@ -171,7 +176,7 @@ export const usePreviewFileUpload = ({ fileCtrl, appId, chatId }: UsePreviewFile ); removeFiles(errorIndexes); - }, [appId, chatId, fileList, removeFiles, replaceFiles, t, toast, updateFiles]); + }, [appId, chatId, fileList, maxSelectFiles, removeFiles, replaceFiles, t, toast, updateFiles]); const sortFileList = useMemo( () => diff --git a/projects/app/src/pages/api/core/chat/file/presignChatFilePostUrl.ts b/projects/app/src/pages/api/core/chat/file/presignChatFilePostUrl.ts index 081f99330d..8ed2ef1381 100644 --- a/projects/app/src/pages/api/core/chat/file/presignChatFilePostUrl.ts +++ b/projects/app/src/pages/api/core/chat/file/presignChatFilePostUrl.ts @@ -3,16 +3,12 @@ import { NextAPI } from '@/service/middleware/entry'; import type { CreatePostPresignedUrlResponseType } from '@fastgpt/global/common/file/s3/type'; import { getS3ChatSource } from '@fastgpt/service/common/s3/sources/chat'; import { getAllowedExtensionsFromFileSelectConfig } from '@fastgpt/service/common/s3/utils/uploadConstraints'; -import { MongoApp } from '@fastgpt/service/core/app/schema'; import { authChatCrud } from '@/service/support/permission/auth/chat'; import { authFrequencyLimit } from '@fastgpt/service/common/system/frequencyLimit/utils'; import { addSeconds } from 'date-fns'; import { PresignChatFilePostUrlSchema } from '@fastgpt/global/openapi/core/chat/file/api'; import { getTeamPlanStatus } from '@fastgpt/service/support/wallet/sub/utils'; -import { authApp } from '@fastgpt/service/support/permission/app/auth'; -import { WritePermissionVal } from '@fastgpt/global/support/permission/constant'; import { S3ErrEnum } from '@fastgpt/global/common/error/code/s3'; -import { MongoChatSetting } from '@fastgpt/service/core/chat/setting/schema'; import { env } from '@fastgpt/service/env'; async function handler(req: ApiRequestProps): Promise { @@ -27,26 +23,8 @@ async function handler(req: ApiRequestProps): Promise { - const isHomeApp = await MongoChatSetting.exists({ teamId, appId }); - - if (!isHomeApp) { - await authApp({ - req, - authToken: true, - appId, - per: WritePermissionVal - }); - } - return fileSelectConfig; - })() - : app?.chatConfig?.fileSelectConfig; - const allowedExtensions = getAllowedExtensionsFromFileSelectConfig(effectiveFileSelectConfig); + const planStatus = await getTeamPlanStatus({ teamId }); + const allowedExtensions = getAllowedExtensionsFromFileSelectConfig(fileSelectConfig); if (!env.SKIP_FILE_TYPE_CHECK && allowedExtensions.length === 0) { return Promise.reject(S3ErrEnum.fileUploadDisabled); diff --git a/projects/app/test/pages/api/core/chat/file/presignChatFilePostUrl.test.ts b/projects/app/test/pages/api/core/chat/file/presignChatFilePostUrl.test.ts new file mode 100644 index 0000000000..e22340a4a1 --- /dev/null +++ b/projects/app/test/pages/api/core/chat/file/presignChatFilePostUrl.test.ts @@ -0,0 +1,142 @@ +import { S3ErrEnum } from '@fastgpt/global/common/error/code/s3'; +import type { ApiRequestProps } from '@fastgpt/service/type/next'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + authChatCrud: vi.fn(), + getTeamPlanStatus: vi.fn(), + authFrequencyLimit: vi.fn(), + createUploadChatFileURL: vi.fn(), + findAppById: vi.fn(), + chatSettingExists: vi.fn(), + authApp: vi.fn() +})); + +vi.mock('@/service/middleware/entry', () => ({ + NextAPI: (handler: unknown) => handler +})); + +vi.mock('@/service/support/permission/auth/chat', () => ({ + authChatCrud: mocks.authChatCrud +})); + +vi.mock('@fastgpt/service/support/wallet/sub/utils', () => ({ + getTeamPlanStatus: mocks.getTeamPlanStatus +})); + +vi.mock('@fastgpt/service/common/system/frequencyLimit/utils', () => ({ + authFrequencyLimit: mocks.authFrequencyLimit +})); + +vi.mock('@fastgpt/service/common/s3/sources/chat', () => ({ + getS3ChatSource: () => ({ + createUploadChatFileURL: mocks.createUploadChatFileURL + }) +})); + +vi.mock('@fastgpt/service/env', () => ({ + env: { + SKIP_FILE_TYPE_CHECK: false + } +})); + +vi.mock('@fastgpt/service/core/app/schema', () => ({ + MongoApp: { + findById: mocks.findAppById + } +})); + +vi.mock('@fastgpt/service/core/chat/setting/schema', () => ({ + MongoChatSetting: { + exists: mocks.chatSettingExists + } +})); + +vi.mock('@fastgpt/service/support/permission/app/auth', () => ({ + authApp: mocks.authApp +})); + +import handler from '@/pages/api/core/chat/file/presignChatFilePostUrl'; + +const appId = '507f1f77bcf86cd799439011'; +const chatId = 'chat-id'; +const filename = 'demo.png'; +const presignHandler = handler as unknown as (req: ApiRequestProps) => Promise; + +const callHandler = (body: Record) => + presignHandler({ + body + } as ApiRequestProps); + +describe('presignChatFilePostUrl', () => { + beforeEach(() => { + vi.clearAllMocks(); + + mocks.authChatCrud.mockResolvedValue({ + teamId: 'team-id', + uid: 'user-id' + }); + mocks.getTeamPlanStatus.mockResolvedValue({ + standard: { + maxUploadFileCount: 20, + maxUploadFileSize: 15 + } + }); + mocks.createUploadChatFileURL.mockResolvedValue({ + url: 'https://example.com/upload-token', + key: 'chat/app/user/chat/demo.png', + headers: { + 'content-type': 'image/png' + }, + maxSize: 15 * 1024 * 1024 + }); + }); + + it('uses request fileSelectConfig as upload constraints without reading app chatConfig', async () => { + await expect( + callHandler({ + filename, + appId, + chatId, + fileSelectConfig: { + canSelectImg: true + } + }) + ).resolves.toMatchObject({ + url: 'https://example.com/upload-token' + }); + + expect(mocks.findAppById).not.toHaveBeenCalled(); + expect(mocks.chatSettingExists).not.toHaveBeenCalled(); + expect(mocks.authApp).not.toHaveBeenCalled(); + expect(mocks.createUploadChatFileURL).toHaveBeenCalledWith( + expect.objectContaining({ + appId, + chatId, + filename, + uId: 'user-id', + allowedExtensions: expect.arrayContaining([ + '.jpg', + '.jpeg', + '.png', + '.gif', + '.bmp', + '.webp' + ]) + }) + ); + }); + + it('rejects upload when fileSelectConfig does not enable any file type', async () => { + await expect( + callHandler({ + filename, + appId, + chatId, + fileSelectConfig: {} + }) + ).rejects.toBe(S3ErrEnum.fileUploadDisabled); + + expect(mocks.createUploadChatFileURL).not.toHaveBeenCalled(); + }); +});