Skip to content

Commit 97ceee3

Browse files
committed
perf: file variable ttl & quick create dataset with temp s3 bucket
1 parent 25bed8d commit 97ceee3

File tree

5 files changed

+182
-51
lines changed

5 files changed

+182
-51
lines changed

packages/service/core/chat/saveChat.ts

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { addLog } from '../../common/system/log';
88
import { mongoSessionRun } from '../../common/mongo/sessionRun';
99
import { type StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
1010
import { getAppChatConfig, getGuideModule } from '@fastgpt/global/core/workflow/utils';
11-
import { type AppChatConfigType } from '@fastgpt/global/core/app/type';
11+
import { type AppChatConfigType, type VariableItemType } from '@fastgpt/global/core/app/type';
1212
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
1313
import { pushChatLog } from './pushChatLog';
1414
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
@@ -19,6 +19,7 @@ import { MongoChatItemResponse } from './chatItemResponseSchema';
1919
import { chatValue2RuntimePrompt } from '@fastgpt/global/core/chat/adapt';
2020
import type { ClientSession } from '../../common/mongo';
2121
import { removeS3TTL } from '../../common/s3/utils';
22+
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
2223

2324
type Props = {
2425
chatId: string;
@@ -51,27 +52,68 @@ const beforProcess = (props: Props) => {
5152
};
5253
const afterProcess = async ({
5354
contents,
55+
variables,
56+
variableList,
5457
session
5558
}: {
5659
contents: (UserChatItemType | AIChatItemType)[];
60+
variables?: Record<string, any>;
61+
variableList?: VariableItemType[];
5762
session: ClientSession;
5863
}) => {
59-
const fileKeys = contents
64+
const contentFileKeys = contents
6065
.map((item) => {
6166
if (item.value && Array.isArray(item.value)) {
62-
return item.value.map((valueItem) => {
67+
return item.value.flatMap((valueItem) => {
68+
const keys: string[] = [];
69+
70+
// 1. chat file
6371
if (valueItem.type === ChatItemValueTypeEnum.file && valueItem.file?.key) {
64-
return valueItem.file.key;
72+
keys.push(valueItem.file.key);
73+
}
74+
75+
// 2. plugin input
76+
if (valueItem.type === 'text' && valueItem.text?.content) {
77+
try {
78+
const parsed = JSON.parse(valueItem.text.content);
79+
if (Array.isArray(parsed)) {
80+
parsed.forEach((field) => {
81+
if (field.value && Array.isArray(field.value)) {
82+
field.value.forEach((file: { key: string }) => {
83+
if (file.key && typeof file.key === 'string') {
84+
keys.push(file.key);
85+
}
86+
});
87+
}
88+
});
89+
}
90+
} catch (err) {}
6591
}
92+
93+
return keys;
6694
});
6795
}
6896
return [];
6997
})
7098
.flat()
7199
.filter(Boolean) as string[];
72100

73-
if (fileKeys.length > 0) {
74-
await removeS3TTL({ key: fileKeys, bucketName: 'private', session });
101+
const variableFileKeys: string[] = [];
102+
if (variables && variableList) {
103+
variableList.forEach((varItem) => {
104+
if (varItem.type === VariableInputEnum.file) {
105+
const varValue = variables[varItem.key];
106+
if (Array.isArray(varValue)) {
107+
variableFileKeys.push(...varValue.map((item) => item.key));
108+
}
109+
}
110+
});
111+
}
112+
113+
const allFileKeys = [...new Set([...contentFileKeys, ...variableFileKeys])];
114+
115+
if (allFileKeys.length > 0) {
116+
await removeS3TTL({ key: allFileKeys, bucketName: 'private', session });
75117
}
76118
};
77119

@@ -254,7 +296,12 @@ export async function saveChat(props: Props) {
254296
}
255297
);
256298

257-
await afterProcess({ contents: processedContent, session });
299+
await afterProcess({
300+
contents: processedContent,
301+
variables,
302+
variableList,
303+
session
304+
});
258305

259306
pushChatLog({
260307
chatId,
@@ -336,8 +383,18 @@ export async function saveChat(props: Props) {
336383
export const updateInteractiveChat = async (props: Props) => {
337384
beforProcess(props);
338385

339-
const { teamId, chatId, appId, userContent, aiContent, variables, durationSeconds, errorMsg } =
340-
props;
386+
const {
387+
teamId,
388+
chatId,
389+
appId,
390+
nodes,
391+
appChatConfig,
392+
userContent,
393+
aiContent,
394+
variables,
395+
durationSeconds,
396+
errorMsg
397+
} = props;
341398
if (!chatId) return;
342399

343400
const chatItem = await MongoChatItem.findOne({ appId, chatId, obj: ChatRoleEnum.AI }).sort({
@@ -371,6 +428,12 @@ export const updateInteractiveChat = async (props: Props) => {
371428
errorMsg
372429
});
373430

431+
const { variables: variableList } = getAppChatConfig({
432+
chatConfig: appChatConfig,
433+
systemConfigNode: getGuideModule(nodes),
434+
isPublicFetch: false
435+
});
436+
374437
let finalInteractive = extractDeepestInteractive(interactiveValue.interactive);
375438

376439
if (finalInteractive.type === 'userSelect') {
@@ -460,7 +523,12 @@ export const updateInteractiveChat = async (props: Props) => {
460523
);
461524
}
462525

463-
await afterProcess({ contents: [userContent, aiContent], session });
526+
await afterProcess({
527+
contents: [userContent, aiContent],
528+
variables,
529+
variableList,
530+
session
531+
});
464532
});
465533

466534
// Push chat data logs

projects/app/src/pageComponents/app/detail/components/QuickCreateDatasetModal.tsx

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
1919
import { useUploadAvatar } from '@fastgpt/web/common/file/hooks/useUploadAvatar';
2020
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
2121
import { postCreateDatasetWithFiles, getDatasetById } from '@/web/core/dataset/api';
22-
import { getUploadAvatarPresignedUrl } from '@/web/common/file/api';
23-
import { uploadFile2DB } from '@/web/common/file/controller';
22+
import { getUploadAvatarPresignedUrl, getUploadTempFilePresignedUrl } from '@/web/common/file/api';
23+
import { POST } from '@/web/common/api/request';
2424
import { useSystemStore } from '@/web/common/system/useSystemStore';
2525
import { getWebDefaultEmbeddingModel, getWebDefaultLLMModel } from '@/web/common/system/utils';
26-
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
2726
import { getErrText } from '@fastgpt/global/common/error/utils';
2827
import { formatFileSize } from '@fastgpt/global/common/file/tools';
2928
import { getFileIcon } from '@fastgpt/global/common/file/icon';
29+
import { parseS3UploadError } from '@fastgpt/global/common/error/s3';
3030
import type { SelectedDatasetType } from '@fastgpt/global/core/workflow/type/io';
3131
import type { ImportSourceItemType } from '@/web/core/dataset/type';
3232
import FileSelector, {
@@ -82,11 +82,21 @@ const QuickCreateDatasetModal = ({
8282
await Promise.all(
8383
files.map(async ({ fileId, file }) => {
8484
try {
85-
const { fileId: uploadFileId } = await uploadFile2DB({
86-
file,
87-
bucketName: BucketNameEnum.dataset,
88-
data: { datasetId: '' },
89-
percentListen: (percent) => {
85+
const { url, fields, maxSize } = await getUploadTempFilePresignedUrl({
86+
filename: file.name
87+
});
88+
89+
const formData = new FormData();
90+
Object.entries(fields).forEach(([k, v]) => formData.set(k, v));
91+
formData.set('file', file);
92+
93+
await POST(url, formData, {
94+
headers: {
95+
'Content-Type': 'multipart/form-data; charset=utf-8'
96+
},
97+
onUploadProgress: (e) => {
98+
if (!e.total) return;
99+
const percent = Math.round((e.loaded / e.total) * 100);
90100
setSelectFiles((state) =>
91101
state.map((item) =>
92102
item.id === fileId
@@ -100,20 +110,22 @@ const QuickCreateDatasetModal = ({
100110
)
101111
);
102112
}
103-
});
104-
105-
setSelectFiles((state) =>
106-
state.map((item) =>
107-
item.id === fileId
108-
? {
109-
...item,
110-
dbFileId: uploadFileId,
111-
isUploading: false,
112-
uploadedFileRate: 100
113-
}
114-
: item
115-
)
116-
);
113+
})
114+
.then(() => {
115+
setSelectFiles((state) =>
116+
state.map((item) =>
117+
item.id === fileId
118+
? {
119+
...item,
120+
dbFileId: fields.key,
121+
isUploading: false,
122+
uploadedFileRate: 100
123+
}
124+
: item
125+
)
126+
);
127+
})
128+
.catch((error) => Promise.reject(parseS3UploadError({ t, error, maxSize })));
117129
} catch (error) {
118130
setSelectFiles((state) =>
119131
state.map((item) =>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { ApiRequestProps } from '@fastgpt/service/type/next';
2+
import { NextAPI } from '@/service/middleware/entry';
3+
import { type CreatePostPresignedUrlResult } from '@fastgpt/service/common/s3/type';
4+
import { authUserPer } from '@fastgpt/service/support/permission/user/auth';
5+
import { TeamDatasetCreatePermissionVal } from '@fastgpt/global/support/permission/user/constant';
6+
import { getFileS3Key } from '@fastgpt/service/common/s3/utils';
7+
import { S3PrivateBucket } from '@fastgpt/service/common/s3/buckets/private';
8+
import { authFrequencyLimit } from '@/service/common/frequencyLimit/api';
9+
import { addSeconds } from 'date-fns';
10+
11+
export type PresignTempFilePostUrlParams = {
12+
filename: string;
13+
};
14+
15+
const authUploadLimit = (tmbId: string) => {
16+
if (!global.feConfigs.uploadFileMaxAmount) return;
17+
return authFrequencyLimit({
18+
eventId: `${tmbId}-uploadfile`,
19+
maxAmount: global.feConfigs.uploadFileMaxAmount * 2,
20+
expiredTime: addSeconds(new Date(), 30) // 30s
21+
});
22+
};
23+
24+
async function handler(
25+
req: ApiRequestProps<PresignTempFilePostUrlParams>
26+
): Promise<CreatePostPresignedUrlResult> {
27+
const { filename } = req.body;
28+
29+
const { teamId, tmbId } = await authUserPer({
30+
req,
31+
authToken: true,
32+
authApiKey: true,
33+
per: TeamDatasetCreatePermissionVal
34+
});
35+
36+
await authUploadLimit(tmbId);
37+
38+
const bucket = new S3PrivateBucket();
39+
const { fileKey } = getFileS3Key.temp({ teamId, filename });
40+
41+
return await bucket.createPostPresignedUrl({ rawKey: fileKey, filename }, { expiredHours: 1 });
42+
}
43+
44+
export default NextAPI(handler);

projects/app/src/pages/api/core/dataset/createWithFiles.ts

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@ import { getI18nDatasetType } from '@fastgpt/service/support/user/audit/util';
3333
import { MongoResourcePermission } from '@fastgpt/service/support/permission/schema';
3434
import { getS3AvatarSource } from '@fastgpt/service/common/s3/sources/avatar';
3535
import { createCollectionAndInsertData } from '@fastgpt/service/core/dataset/collection/controller';
36-
import { getFileById, delFileByFileIdList } from '@fastgpt/service/common/file/gridfs/controller';
37-
import { BucketNameEnum } from '@fastgpt/global/common/file/constants';
38-
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
3936
import type { EmbeddingModelItemType } from '@fastgpt/global/core/ai/model.d';
37+
import { S3PrivateBucket } from '@fastgpt/service/common/s3/buckets/private';
38+
import { getFileS3Key } from '@fastgpt/service/common/s3/utils';
4039

4140
export type DatasetCreateWithFilesQuery = {};
4241
export type DatasetCreateWithFilesBody = {
@@ -124,16 +123,26 @@ async function handler(
124123
// 3. Refresh avatar
125124
await getS3AvatarSource().refreshAvatar(avatar, undefined, session);
126125

127-
// 4. Create collections for each file
126+
// 4. Move temp files to dataset directory and create collections
127+
const bucket = new S3PrivateBucket();
128+
128129
for (const file of files) {
129-
const gridFile = await getFileById({
130-
bucketName: BucketNameEnum.dataset,
131-
fileId: file.fileId
130+
if (!file.fileId.startsWith('temp/')) {
131+
return Promise.reject('Only temp files are supported');
132+
}
133+
134+
const { fileKey: newKey } = getFileS3Key.dataset({
135+
datasetId: String(dataset._id),
136+
filename: file.name
132137
});
133138

134-
if (!gridFile) {
135-
return Promise.reject(CommonErrEnum.fileNotFound);
136-
}
139+
await bucket.copy({
140+
from: file.fileId,
141+
to: newKey,
142+
options: {
143+
temporary: false
144+
}
145+
});
137146

138147
await createCollectionAndInsertData({
139148
dataset,
@@ -142,10 +151,10 @@ async function handler(
142151
teamId,
143152
tmbId,
144153
type: DatasetCollectionTypeEnum.file,
145-
name: file.name || gridFile.filename,
146-
fileId: file.fileId,
154+
name: file.name,
155+
fileId: newKey,
147156
metadata: {
148-
relatedImgId: file.fileId
157+
relatedImgId: newKey
149158
},
150159
trainingType: DatasetCollectionDataProcessModeEnum.chunk,
151160
chunkTriggerType: ChunkTriggerConfigTypeEnum.minSize,
@@ -190,12 +199,6 @@ async function handler(
190199

191200
return result;
192201
} catch (error) {
193-
const fileIds = files.map((file) => file.fileId);
194-
await delFileByFileIdList({
195-
bucketName: BucketNameEnum.dataset,
196-
fileIdList: fileIds
197-
});
198-
199202
return Promise.reject(error);
200203
}
201204
}

projects/app/src/web/common/file/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,7 @@ export const getUploadDatasetFilePresignedUrl = (params: {
6161
}) => {
6262
return POST<CreatePostPresignedUrlResult>('/core/dataset/presignDatasetFilePostUrl', params);
6363
};
64+
65+
export const getUploadTempFilePresignedUrl = (params: { filename: string }) => {
66+
return POST<CreatePostPresignedUrlResult>('/common/file/presignTempFilePostUrl', params);
67+
};

0 commit comments

Comments
 (0)