@lobehub/chat 1.12.9 → 1.12.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +3 -2
- package/.github/workflows/test.yml +2 -1
- package/CHANGELOG.md +50 -0
- package/package.json +57 -57
- package/src/app/(main)/files/[id]/Header.tsx +1 -1
- package/src/config/app.ts +12 -5
- package/src/config/db.ts +4 -0
- package/src/database/server/models/__tests__/chunk.test.ts +60 -1
- package/src/database/server/models/__tests__/file.test.ts +211 -66
- package/src/database/server/models/__tests__/knowledgeBase.test.ts +128 -2
- package/src/database/server/models/__tests__/user.test.ts +36 -1
- package/src/database/server/models/chunk.ts +14 -1
- package/src/database/server/models/file.ts +4 -2
- package/src/features/FileManager/FileList/FileListItem/DropdownMenu.tsx +1 -1
- package/src/features/FileViewer/NotSupport/index.tsx +1 -1
- package/src/server/routers/lambda/file.ts +9 -1
- package/src/services/__tests__/chat.test.ts +0 -57
- package/src/services/file/client.test.ts +8 -5
- package/src/services/file/client.ts +6 -6
- package/src/services/file/server.ts +3 -20
- package/src/services/file/type.ts +2 -2
- package/src/services/upload.ts +14 -0
- package/src/store/chat/slices/builtinTool/action.test.ts +6 -3
- package/src/store/chat/slices/builtinTool/action.ts +37 -14
- package/src/store/chat/slices/builtinTool/initialState.ts +4 -0
- package/src/store/file/slices/chat/action.test.ts +0 -34
- package/src/store/file/slices/chat/action.ts +1 -26
- package/src/store/file/slices/chat/initialState.ts +0 -7
- package/src/store/file/slices/tts/action.ts +2 -2
- package/src/tools/dalle/Render/Item/ImageFileItem.tsx +3 -3
- package/src/tools/dalle/Render/Item/index.tsx +1 -1
- package/src/tools/renders.ts +0 -6
- package/src/types/files/index.ts +0 -12
- package/src/utils/{downloadFile.ts → client/downloadFile.ts} +2 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
|
-
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { eq, inArray } from 'drizzle-orm';
|
|
3
3
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
4
|
|
|
5
|
+
import { getServerDBConfig, serverDBEnv } from '@/config/db';
|
|
5
6
|
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
|
6
7
|
import { FilesTabs, SortType } from '@/types/files';
|
|
7
8
|
|
|
@@ -22,6 +23,20 @@ vi.mock('@/database/server/core/db', async () => ({
|
|
|
22
23
|
},
|
|
23
24
|
}));
|
|
24
25
|
|
|
26
|
+
let DISABLE_REMOVE_GLOBAL_FILE = false;
|
|
27
|
+
|
|
28
|
+
vi.mock('@/config/db', async () => ({
|
|
29
|
+
get serverDBEnv() {
|
|
30
|
+
return {
|
|
31
|
+
get DISABLE_REMOVE_GLOBAL_FILE() {
|
|
32
|
+
return DISABLE_REMOVE_GLOBAL_FILE;
|
|
33
|
+
},
|
|
34
|
+
DATABASE_TEST_URL: process.env.DATABASE_TEST_URL,
|
|
35
|
+
DATABASE_DRIVER: 'node',
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
}));
|
|
39
|
+
|
|
25
40
|
const userId = 'file-model-test-user-id';
|
|
26
41
|
const fileModel = new FileModel(userId);
|
|
27
42
|
|
|
@@ -73,18 +88,21 @@ describe('FileModel', () => {
|
|
|
73
88
|
});
|
|
74
89
|
});
|
|
75
90
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
describe('createGlobalFile', () => {
|
|
92
|
+
it('should create a global file', async () => {
|
|
93
|
+
const globalFile = {
|
|
94
|
+
hashId: 'test-hash',
|
|
95
|
+
fileType: 'text/plain',
|
|
96
|
+
size: 100,
|
|
97
|
+
url: 'https://example.com/global-file.txt',
|
|
98
|
+
metadata: { key: 'value' },
|
|
99
|
+
};
|
|
84
100
|
|
|
85
|
-
|
|
86
|
-
|
|
101
|
+
const result = await fileModel.createGlobalFile(globalFile);
|
|
102
|
+
expect(result[0]).toMatchObject(globalFile);
|
|
103
|
+
});
|
|
87
104
|
});
|
|
105
|
+
|
|
88
106
|
describe('checkHash', () => {
|
|
89
107
|
it('should return isExist: false for non-existent hash', async () => {
|
|
90
108
|
const result = await fileModel.checkHash('non-existent-hash');
|
|
@@ -113,58 +131,183 @@ describe('FileModel', () => {
|
|
|
113
131
|
});
|
|
114
132
|
});
|
|
115
133
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
describe('delete', () => {
|
|
135
|
+
it('should delete a file by id', async () => {
|
|
136
|
+
await fileModel.createGlobalFile({
|
|
137
|
+
hashId: '1',
|
|
138
|
+
url: 'https://example.com/file1.txt',
|
|
139
|
+
size: 100,
|
|
140
|
+
fileType: 'text/plain',
|
|
141
|
+
});
|
|
123
142
|
|
|
124
|
-
|
|
143
|
+
const { id } = await fileModel.create({
|
|
144
|
+
name: 'test-file.txt',
|
|
145
|
+
url: 'https://example.com/test-file.txt',
|
|
146
|
+
size: 100,
|
|
147
|
+
fileType: 'text/plain',
|
|
148
|
+
fileHash: '1',
|
|
149
|
+
});
|
|
125
150
|
|
|
126
|
-
|
|
127
|
-
expect(file).toBeUndefined();
|
|
128
|
-
});
|
|
151
|
+
await fileModel.delete(id);
|
|
129
152
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const file2 = await fileModel.create({
|
|
138
|
-
name: 'file2.txt',
|
|
139
|
-
url: 'https://example.com/file2.txt',
|
|
140
|
-
size: 200,
|
|
141
|
-
fileType: 'text/plain',
|
|
153
|
+
const file = await serverDB.query.files.findFirst({ where: eq(files.id, id) });
|
|
154
|
+
const globalFile = await serverDB.query.globalFiles.findFirst({
|
|
155
|
+
where: eq(globalFiles.hashId, '1'),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(file).toBeUndefined();
|
|
159
|
+
expect(globalFile).toBeUndefined();
|
|
142
160
|
});
|
|
161
|
+
it('should delete a file by id but global file not removed ', async () => {
|
|
162
|
+
DISABLE_REMOVE_GLOBAL_FILE = true;
|
|
163
|
+
await fileModel.createGlobalFile({
|
|
164
|
+
hashId: '1',
|
|
165
|
+
url: 'https://example.com/file1.txt',
|
|
166
|
+
size: 100,
|
|
167
|
+
fileType: 'text/plain',
|
|
168
|
+
});
|
|
143
169
|
|
|
144
|
-
|
|
170
|
+
const { id } = await fileModel.create({
|
|
171
|
+
name: 'test-file.txt',
|
|
172
|
+
url: 'https://example.com/test-file.txt',
|
|
173
|
+
size: 100,
|
|
174
|
+
fileType: 'text/plain',
|
|
175
|
+
fileHash: '1',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await fileModel.delete(id);
|
|
179
|
+
|
|
180
|
+
const file = await serverDB.query.files.findFirst({ where: eq(files.id, id) });
|
|
181
|
+
const globalFile = await serverDB.query.globalFiles.findFirst({
|
|
182
|
+
where: eq(globalFiles.hashId, '1'),
|
|
183
|
+
});
|
|
145
184
|
|
|
146
|
-
|
|
147
|
-
|
|
185
|
+
expect(file).toBeUndefined();
|
|
186
|
+
expect(globalFile).toBeDefined();
|
|
187
|
+
DISABLE_REMOVE_GLOBAL_FILE = false;
|
|
188
|
+
});
|
|
148
189
|
});
|
|
149
190
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
191
|
+
describe('deleteMany', () => {
|
|
192
|
+
it('should delete multiple files', async () => {
|
|
193
|
+
await fileModel.createGlobalFile({
|
|
194
|
+
hashId: '1',
|
|
195
|
+
url: 'https://example.com/file1.txt',
|
|
196
|
+
size: 100,
|
|
197
|
+
fileType: 'text/plain',
|
|
198
|
+
});
|
|
199
|
+
await fileModel.createGlobalFile({
|
|
200
|
+
hashId: '2',
|
|
201
|
+
url: 'https://example.com/file2.txt',
|
|
202
|
+
size: 200,
|
|
203
|
+
fileType: 'text/plain',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const file1 = await fileModel.create({
|
|
207
|
+
name: 'file1.txt',
|
|
208
|
+
url: 'https://example.com/file1.txt',
|
|
209
|
+
size: 100,
|
|
210
|
+
fileHash: '1',
|
|
211
|
+
fileType: 'text/plain',
|
|
212
|
+
});
|
|
213
|
+
const file2 = await fileModel.create({
|
|
214
|
+
name: 'file2.txt',
|
|
215
|
+
url: 'https://example.com/file2.txt',
|
|
216
|
+
size: 200,
|
|
217
|
+
fileType: 'text/plain',
|
|
218
|
+
fileHash: '2',
|
|
219
|
+
});
|
|
220
|
+
const globalFilesResult = await serverDB.query.globalFiles.findMany({
|
|
221
|
+
where: inArray(globalFiles.hashId, ['1', '2']),
|
|
222
|
+
});
|
|
223
|
+
expect(globalFilesResult).toHaveLength(2);
|
|
224
|
+
|
|
225
|
+
await fileModel.deleteMany([file1.id, file2.id]);
|
|
226
|
+
|
|
227
|
+
const remainingFiles = await serverDB.query.files.findMany({
|
|
228
|
+
where: eq(files.userId, userId),
|
|
229
|
+
});
|
|
230
|
+
const globalFilesResult2 = await serverDB.query.globalFiles.findMany({
|
|
231
|
+
where: inArray(
|
|
232
|
+
globalFiles.hashId,
|
|
233
|
+
remainingFiles.map((i) => i.fileHash as string),
|
|
234
|
+
),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(remainingFiles).toHaveLength(0);
|
|
238
|
+
expect(globalFilesResult2).toHaveLength(0);
|
|
156
239
|
});
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
it('should delete multiple files but not remove global files if DISABLE_REMOVE_GLOBAL_FILE=true', async () => {
|
|
241
|
+
DISABLE_REMOVE_GLOBAL_FILE = true;
|
|
242
|
+
await fileModel.createGlobalFile({
|
|
243
|
+
hashId: '1',
|
|
244
|
+
url: 'https://example.com/file1.txt',
|
|
245
|
+
size: 100,
|
|
246
|
+
fileType: 'text/plain',
|
|
247
|
+
});
|
|
248
|
+
await fileModel.createGlobalFile({
|
|
249
|
+
hashId: '2',
|
|
250
|
+
url: 'https://example.com/file2.txt',
|
|
251
|
+
size: 200,
|
|
252
|
+
fileType: 'text/plain',
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const file1 = await fileModel.create({
|
|
256
|
+
name: 'file1.txt',
|
|
257
|
+
url: 'https://example.com/file1.txt',
|
|
258
|
+
size: 100,
|
|
259
|
+
fileType: 'text/plain',
|
|
260
|
+
fileHash: '1',
|
|
261
|
+
});
|
|
262
|
+
const file2 = await fileModel.create({
|
|
263
|
+
name: 'file2.txt',
|
|
264
|
+
url: 'https://example.com/file2.txt',
|
|
265
|
+
size: 200,
|
|
266
|
+
fileType: 'text/plain',
|
|
267
|
+
fileHash: '2',
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const globalFilesResult = await serverDB.query.globalFiles.findMany({
|
|
271
|
+
where: inArray(globalFiles.hashId, ['1', '2']),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(globalFilesResult).toHaveLength(2);
|
|
275
|
+
|
|
276
|
+
await fileModel.deleteMany([file1.id, file2.id]);
|
|
277
|
+
|
|
278
|
+
const remainingFiles = await serverDB.query.files.findMany({
|
|
279
|
+
where: eq(files.userId, userId),
|
|
280
|
+
});
|
|
281
|
+
const globalFilesResult2 = await serverDB.query.globalFiles.findMany({
|
|
282
|
+
where: inArray(globalFiles.hashId, ['1', '2']),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(remainingFiles).toHaveLength(0);
|
|
286
|
+
expect(globalFilesResult2).toHaveLength(2);
|
|
287
|
+
DISABLE_REMOVE_GLOBAL_FILE = false;
|
|
162
288
|
});
|
|
289
|
+
});
|
|
163
290
|
|
|
164
|
-
|
|
291
|
+
describe('clear', () => {
|
|
292
|
+
it('should clear all files for the user', async () => {
|
|
293
|
+
await fileModel.create({
|
|
294
|
+
name: 'test-file-1.txt',
|
|
295
|
+
url: 'https://example.com/test-file-1.txt',
|
|
296
|
+
size: 100,
|
|
297
|
+
fileType: 'text/plain',
|
|
298
|
+
});
|
|
299
|
+
await fileModel.create({
|
|
300
|
+
name: 'test-file-2.txt',
|
|
301
|
+
url: 'https://example.com/test-file-2.txt',
|
|
302
|
+
size: 200,
|
|
303
|
+
fileType: 'text/plain',
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
await fileModel.clear();
|
|
165
307
|
|
|
166
|
-
|
|
167
|
-
|
|
308
|
+
const userFiles = await serverDB.query.files.findMany({ where: eq(files.userId, userId) });
|
|
309
|
+
expect(userFiles).toHaveLength(0);
|
|
310
|
+
});
|
|
168
311
|
});
|
|
169
312
|
|
|
170
313
|
describe('Query', () => {
|
|
@@ -334,22 +477,24 @@ describe('FileModel', () => {
|
|
|
334
477
|
});
|
|
335
478
|
});
|
|
336
479
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
480
|
+
describe('findById', () => {
|
|
481
|
+
it('should find a file by id', async () => {
|
|
482
|
+
const { id } = await fileModel.create({
|
|
483
|
+
name: 'test-file.txt',
|
|
484
|
+
url: 'https://example.com/test-file.txt',
|
|
485
|
+
size: 100,
|
|
486
|
+
fileType: 'text/plain',
|
|
487
|
+
});
|
|
344
488
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
489
|
+
const file = await fileModel.findById(id);
|
|
490
|
+
expect(file).toMatchObject({
|
|
491
|
+
id,
|
|
492
|
+
name: 'test-file.txt',
|
|
493
|
+
url: 'https://example.com/test-file.txt',
|
|
494
|
+
size: 100,
|
|
495
|
+
fileType: 'text/plain',
|
|
496
|
+
userId,
|
|
497
|
+
});
|
|
353
498
|
});
|
|
354
499
|
});
|
|
355
500
|
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
// @vitest-environment node
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
|
-
import { desc } from 'drizzle-orm/expressions';
|
|
3
|
+
import { and, desc } from 'drizzle-orm/expressions';
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
|
|
6
6
|
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
NewKnowledgeBase,
|
|
10
|
+
files,
|
|
11
|
+
globalFiles,
|
|
12
|
+
knowledgeBaseFiles,
|
|
13
|
+
knowledgeBases,
|
|
14
|
+
users,
|
|
15
|
+
} from '../../schemas/lobechat';
|
|
9
16
|
import { KnowledgeBaseModel } from '../knowledgeBase';
|
|
10
17
|
|
|
11
18
|
let serverDB = await getTestDBInstance();
|
|
@@ -21,6 +28,7 @@ const knowledgeBaseModel = new KnowledgeBaseModel(userId);
|
|
|
21
28
|
|
|
22
29
|
beforeEach(async () => {
|
|
23
30
|
await serverDB.delete(users);
|
|
31
|
+
await serverDB.delete(globalFiles);
|
|
24
32
|
await serverDB.insert(users).values([{ id: userId }, { id: 'user2' }]);
|
|
25
33
|
});
|
|
26
34
|
|
|
@@ -129,4 +137,122 @@ describe('KnowledgeBaseModel', () => {
|
|
|
129
137
|
});
|
|
130
138
|
});
|
|
131
139
|
});
|
|
140
|
+
|
|
141
|
+
const fileList = [
|
|
142
|
+
{
|
|
143
|
+
id: 'file1',
|
|
144
|
+
name: 'document.pdf',
|
|
145
|
+
url: 'https://example.com/document.pdf',
|
|
146
|
+
fileHash: 'hash1',
|
|
147
|
+
size: 1000,
|
|
148
|
+
fileType: 'application/pdf',
|
|
149
|
+
userId,
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'file2',
|
|
153
|
+
name: 'image.jpg',
|
|
154
|
+
url: 'https://example.com/image.jpg',
|
|
155
|
+
fileHash: 'hash2',
|
|
156
|
+
size: 500,
|
|
157
|
+
fileType: 'image/jpeg',
|
|
158
|
+
userId,
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
describe('addFilesToKnowledgeBase', () => {
|
|
163
|
+
it('should add files to a knowledge base', async () => {
|
|
164
|
+
await serverDB.insert(globalFiles).values([
|
|
165
|
+
{
|
|
166
|
+
hashId: 'hash1',
|
|
167
|
+
url: 'https://example.com/document.pdf',
|
|
168
|
+
size: 1000,
|
|
169
|
+
fileType: 'application/pdf',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
hashId: 'hash2',
|
|
173
|
+
url: 'https://example.com/image.jpg',
|
|
174
|
+
size: 500,
|
|
175
|
+
fileType: 'image/jpeg',
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
await serverDB.insert(files).values(fileList);
|
|
180
|
+
|
|
181
|
+
const { id: knowledgeBaseId } = await knowledgeBaseModel.create({ name: 'Test Group' });
|
|
182
|
+
const fileIds = ['file1', 'file2'];
|
|
183
|
+
|
|
184
|
+
const result = await knowledgeBaseModel.addFilesToKnowledgeBase(knowledgeBaseId, fileIds);
|
|
185
|
+
|
|
186
|
+
expect(result).toHaveLength(2);
|
|
187
|
+
expect(result).toEqual(
|
|
188
|
+
expect.arrayContaining(
|
|
189
|
+
fileIds.map((fileId) => expect.objectContaining({ fileId, knowledgeBaseId })),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const addedFiles = await serverDB.query.knowledgeBaseFiles.findMany({
|
|
194
|
+
where: eq(knowledgeBaseFiles.knowledgeBaseId, knowledgeBaseId),
|
|
195
|
+
});
|
|
196
|
+
expect(addedFiles).toHaveLength(2);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('removeFilesFromKnowledgeBase', () => {
|
|
201
|
+
it('should remove files from a knowledge base', async () => {
|
|
202
|
+
await serverDB.insert(globalFiles).values([
|
|
203
|
+
{
|
|
204
|
+
hashId: 'hash1',
|
|
205
|
+
url: 'https://example.com/document.pdf',
|
|
206
|
+
size: 1000,
|
|
207
|
+
fileType: 'application/pdf',
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
hashId: 'hash2',
|
|
211
|
+
url: 'https://example.com/image.jpg',
|
|
212
|
+
size: 500,
|
|
213
|
+
fileType: 'image/jpeg',
|
|
214
|
+
},
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
await serverDB.insert(files).values(fileList);
|
|
218
|
+
|
|
219
|
+
const { id: knowledgeBaseId } = await knowledgeBaseModel.create({ name: 'Test Group' });
|
|
220
|
+
const fileIds = ['file1', 'file2'];
|
|
221
|
+
await knowledgeBaseModel.addFilesToKnowledgeBase(knowledgeBaseId, fileIds);
|
|
222
|
+
|
|
223
|
+
const filesToRemove = ['file1'];
|
|
224
|
+
await knowledgeBaseModel.removeFilesFromKnowledgeBase(knowledgeBaseId, filesToRemove);
|
|
225
|
+
|
|
226
|
+
const remainingFiles = await serverDB.query.knowledgeBaseFiles.findMany({
|
|
227
|
+
where: and(eq(knowledgeBaseFiles.knowledgeBaseId, knowledgeBaseId)),
|
|
228
|
+
});
|
|
229
|
+
expect(remainingFiles).toHaveLength(1);
|
|
230
|
+
expect(remainingFiles[0].fileId).toBe('file2');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('static findById', () => {
|
|
235
|
+
it('should find a knowledge base by id without user restriction', async () => {
|
|
236
|
+
const { id } = await knowledgeBaseModel.create({ name: 'Test Group' });
|
|
237
|
+
|
|
238
|
+
const group = await KnowledgeBaseModel.findById(id);
|
|
239
|
+
expect(group).toMatchObject({
|
|
240
|
+
id,
|
|
241
|
+
name: 'Test Group',
|
|
242
|
+
userId,
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should find a knowledge base created by another user', async () => {
|
|
247
|
+
const anotherKnowledgeBaseModel = new KnowledgeBaseModel('user2');
|
|
248
|
+
const { id } = await anotherKnowledgeBaseModel.create({ name: 'Another User Group' });
|
|
249
|
+
|
|
250
|
+
const group = await KnowledgeBaseModel.findById(id);
|
|
251
|
+
expect(group).toMatchObject({
|
|
252
|
+
id,
|
|
253
|
+
name: 'Another User Group',
|
|
254
|
+
userId: 'user2',
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
132
258
|
});
|
|
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
4
4
|
import { INBOX_SESSION_ID } from '@/const/session';
|
|
5
5
|
import { getTestDBInstance } from '@/database/server/core/dbForTest';
|
|
6
6
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
7
|
-
import { UserPreference } from '@/types/user';
|
|
7
|
+
import { UserGuide, UserPreference } from '@/types/user';
|
|
8
8
|
import { UserSettings } from '@/types/user/settings';
|
|
9
9
|
|
|
10
10
|
import { userSettings, users } from '../../schemas/lobechat';
|
|
@@ -167,6 +167,24 @@ describe('UserModel', () => {
|
|
|
167
167
|
const { plaintext } = await gateKeeper.decrypt(updatedSettings!.keyVaults!);
|
|
168
168
|
expect(JSON.parse(plaintext)).toEqual(settings.keyVaults);
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
it('should update user settings with encrypted keyVaults', async () => {
|
|
172
|
+
const settings = {
|
|
173
|
+
general: { language: 'en-US' },
|
|
174
|
+
} as UserSettings;
|
|
175
|
+
await serverDB.insert(users).values({ id: userId });
|
|
176
|
+
await serverDB.insert(userSettings).values({ ...settings, keyVaults: '', id: userId });
|
|
177
|
+
|
|
178
|
+
const newSettings = {
|
|
179
|
+
general: { fontSize: 16, language: 'zh-CN', themeMode: 'dark' },
|
|
180
|
+
} as UserSettings;
|
|
181
|
+
await userModel.updateSetting(userId, newSettings);
|
|
182
|
+
|
|
183
|
+
const updatedSettings = await serverDB.query.userSettings.findFirst({
|
|
184
|
+
where: eq(users.id, userId),
|
|
185
|
+
});
|
|
186
|
+
expect(updatedSettings?.general).toEqual(newSettings.general);
|
|
187
|
+
});
|
|
170
188
|
});
|
|
171
189
|
|
|
172
190
|
describe('updatePreference', () => {
|
|
@@ -183,4 +201,21 @@ describe('UserModel', () => {
|
|
|
183
201
|
expect(updatedUser?.preference).toEqual({ ...preference, ...newPreference });
|
|
184
202
|
});
|
|
185
203
|
});
|
|
204
|
+
|
|
205
|
+
describe('updateGuide', () => {
|
|
206
|
+
it('should update user guide', async () => {
|
|
207
|
+
const preference = { guide: { topic: false } } as UserGuide;
|
|
208
|
+
await serverDB.insert(users).values({ id: userId, preference });
|
|
209
|
+
|
|
210
|
+
const newGuide: Partial<UserGuide> = {
|
|
211
|
+
topic: true,
|
|
212
|
+
moveSettingsToAvatar: true,
|
|
213
|
+
uploadFileInKnowledgeBase: true,
|
|
214
|
+
};
|
|
215
|
+
await userModel.updateGuide(userId, newGuide);
|
|
216
|
+
|
|
217
|
+
const updatedUser = await serverDB.query.users.findFirst({ where: eq(users.id, userId) });
|
|
218
|
+
expect(updatedUser?.preference).toEqual({ ...preference, guide: newGuide });
|
|
219
|
+
});
|
|
220
|
+
});
|
|
186
221
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { asc, cosineDistance, count, eq, inArray, sql } from 'drizzle-orm';
|
|
2
|
-
import { and, desc } from 'drizzle-orm/expressions';
|
|
2
|
+
import { and, desc, isNull } from 'drizzle-orm/expressions';
|
|
3
3
|
|
|
4
4
|
import { serverDB } from '@/database/server';
|
|
5
5
|
import { ChunkMetadata, FileChunk, SemanticSearchChunk } from '@/types/chunk';
|
|
@@ -43,6 +43,19 @@ export class ChunkModel {
|
|
|
43
43
|
return serverDB.delete(chunks).where(and(eq(chunks.id, id), eq(chunks.userId, this.userId)));
|
|
44
44
|
};
|
|
45
45
|
|
|
46
|
+
deleteOrphanChunks = async () => {
|
|
47
|
+
const orphanedChunks = await serverDB
|
|
48
|
+
.select({ chunkId: chunks.id })
|
|
49
|
+
.from(chunks)
|
|
50
|
+
.leftJoin(fileChunks, eq(chunks.id, fileChunks.chunkId))
|
|
51
|
+
.where(isNull(fileChunks.fileId));
|
|
52
|
+
|
|
53
|
+
const ids = orphanedChunks.map((chunk) => chunk.chunkId);
|
|
54
|
+
if (ids.length === 0) return;
|
|
55
|
+
|
|
56
|
+
await serverDB.delete(chunks).where(inArray(chunks.id, ids));
|
|
57
|
+
};
|
|
58
|
+
|
|
46
59
|
findById = async (id: string) => {
|
|
47
60
|
return serverDB.query.chunks.findFirst({
|
|
48
61
|
where: and(eq(chunks.id, id)),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { asc, count, eq, ilike, inArray, notExists } from 'drizzle-orm';
|
|
2
2
|
import { and, desc } from 'drizzle-orm/expressions';
|
|
3
3
|
|
|
4
|
+
import { serverDBEnv } from '@/config/db';
|
|
4
5
|
import { serverDB } from '@/database/server/core/db';
|
|
5
6
|
import { FilesTabs, QueryFileListParams, SortType } from '@/types/files';
|
|
6
7
|
|
|
@@ -77,7 +78,8 @@ export class FileModel {
|
|
|
77
78
|
const fileCount = result[0].count;
|
|
78
79
|
|
|
79
80
|
// delete the file from global file if it is not used by other files
|
|
80
|
-
if
|
|
81
|
+
// if `DISABLE_REMOVE_GLOBAL_FILE` is true, we will not remove the global file
|
|
82
|
+
if (fileCount === 0 && !serverDBEnv.DISABLE_REMOVE_GLOBAL_FILE) {
|
|
81
83
|
await trx.delete(globalFiles).where(eq(globalFiles.hashId, fileHash));
|
|
82
84
|
|
|
83
85
|
return file;
|
|
@@ -118,7 +120,7 @@ export class FileModel {
|
|
|
118
120
|
|
|
119
121
|
const needToDeleteList = fileHashCounts.filter((item) => item.count === 0);
|
|
120
122
|
|
|
121
|
-
if (needToDeleteList.length === 0) return;
|
|
123
|
+
if (needToDeleteList.length === 0 || serverDBEnv.DISABLE_REMOVE_GLOBAL_FILE) return;
|
|
122
124
|
|
|
123
125
|
// delete the file from global file if it is not used by other files
|
|
124
126
|
await trx.delete(globalFiles).where(
|
|
@@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
15
15
|
import { useAddFilesToKnowledgeBaseModal } from '@/features/KnowledgeBaseModal';
|
|
16
16
|
import { useFileStore } from '@/store/file';
|
|
17
17
|
import { useKnowledgeBaseStore } from '@/store/knowledgeBase';
|
|
18
|
-
import { downloadFile } from '@/utils/downloadFile';
|
|
18
|
+
import { downloadFile } from '@/utils/client/downloadFile';
|
|
19
19
|
|
|
20
20
|
interface DropdownMenuProps {
|
|
21
21
|
filename: string;
|
|
@@ -5,7 +5,7 @@ import React, { ComponentType, useState } from 'react';
|
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Center, Flexbox } from 'react-layout-kit';
|
|
7
7
|
|
|
8
|
-
import { downloadFile } from '@/utils/downloadFile';
|
|
8
|
+
import { downloadFile } from '@/utils/client/downloadFile';
|
|
9
9
|
|
|
10
10
|
const useStyles = createStyles(({ css, token }) => ({
|
|
11
11
|
page: css`
|
|
@@ -66,7 +66,10 @@ export const fileRouter = router({
|
|
|
66
66
|
}),
|
|
67
67
|
)
|
|
68
68
|
.query(async ({ ctx, input }) => {
|
|
69
|
-
|
|
69
|
+
const item = await ctx.fileModel.findById(input.id);
|
|
70
|
+
if (!item) throw new TRPCError({ code: 'BAD_REQUEST', message: 'File not found' });
|
|
71
|
+
|
|
72
|
+
return { ...item, url: getFullFileUrl(item?.url) };
|
|
70
73
|
}),
|
|
71
74
|
|
|
72
75
|
getFileItemById: fileProcedure
|
|
@@ -147,6 +150,8 @@ export const fileRouter = router({
|
|
|
147
150
|
removeFile: fileProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
|
148
151
|
const file = await ctx.fileModel.delete(input.id);
|
|
149
152
|
|
|
153
|
+
// delete the orphan chunks
|
|
154
|
+
await ctx.chunkModel.deleteOrphanChunks();
|
|
150
155
|
if (!file) return;
|
|
151
156
|
|
|
152
157
|
// delele the file from remove from S3 if it is not used by other files
|
|
@@ -159,6 +164,9 @@ export const fileRouter = router({
|
|
|
159
164
|
.mutation(async ({ input, ctx }) => {
|
|
160
165
|
const needToRemoveFileList = await ctx.fileModel.deleteMany(input.ids);
|
|
161
166
|
|
|
167
|
+
// delete the orphan chunks
|
|
168
|
+
await ctx.chunkModel.deleteOrphanChunks();
|
|
169
|
+
|
|
162
170
|
if (!needToRemoveFileList || needToRemoveFileList.length === 0) return;
|
|
163
171
|
|
|
164
172
|
// remove from S3
|