@lobehub/lobehub 2.0.0-next.54 → 2.0.0-next.55

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/changelog/v1.json +9 -0
  3. package/locales/ar/common.json +1 -0
  4. package/locales/ar/file.json +85 -2
  5. package/locales/bg-BG/common.json +1 -0
  6. package/locales/bg-BG/file.json +85 -2
  7. package/locales/de-DE/common.json +1 -0
  8. package/locales/de-DE/file.json +85 -2
  9. package/locales/en-US/common.json +1 -0
  10. package/locales/en-US/file.json +85 -2
  11. package/locales/es-ES/common.json +1 -0
  12. package/locales/es-ES/file.json +85 -2
  13. package/locales/fa-IR/common.json +1 -0
  14. package/locales/fa-IR/file.json +85 -2
  15. package/locales/fr-FR/common.json +1 -0
  16. package/locales/fr-FR/file.json +85 -2
  17. package/locales/it-IT/common.json +1 -0
  18. package/locales/it-IT/file.json +85 -2
  19. package/locales/ja-JP/common.json +1 -0
  20. package/locales/ja-JP/file.json +85 -2
  21. package/locales/ko-KR/common.json +1 -0
  22. package/locales/ko-KR/file.json +85 -2
  23. package/locales/nl-NL/common.json +1 -0
  24. package/locales/nl-NL/file.json +85 -2
  25. package/locales/pl-PL/common.json +1 -0
  26. package/locales/pl-PL/file.json +85 -2
  27. package/locales/pt-BR/common.json +1 -0
  28. package/locales/pt-BR/file.json +85 -2
  29. package/locales/ru-RU/common.json +1 -0
  30. package/locales/ru-RU/file.json +85 -2
  31. package/locales/tr-TR/common.json +1 -0
  32. package/locales/tr-TR/file.json +85 -2
  33. package/locales/vi-VN/common.json +1 -0
  34. package/locales/vi-VN/file.json +85 -2
  35. package/locales/zh-CN/common.json +1 -0
  36. package/locales/zh-CN/file.json +85 -2
  37. package/locales/zh-TW/common.json +1 -0
  38. package/locales/zh-TW/file.json +85 -2
  39. package/package.json +1 -1
  40. package/packages/database/src/models/__tests__/file.test.ts +94 -29
  41. package/packages/database/src/models/file.ts +15 -4
  42. package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
  43. package/packages/database/src/repositories/knowledge/index.ts +420 -0
  44. package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
  45. package/packages/model-bank/src/aiModels/google.ts +9 -5
  46. package/packages/model-bank/src/aiModels/openai.ts +2 -35
  47. package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
  48. package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
  49. package/packages/model-bank/src/types/aiModel.ts +15 -2
  50. package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
  51. package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
  52. package/packages/types/src/document/index.ts +14 -2
  53. package/packages/types/src/files/index.ts +2 -0
  54. package/packages/types/src/files/list.ts +10 -0
  55. package/packages/types/src/llm.ts +1 -1
  56. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
  57. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
  58. package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
  59. package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
  60. package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
  61. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
  62. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
  63. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
  64. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
  65. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
  66. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
  67. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
  68. package/src/components/FileIcon/index.tsx +3 -1
  69. package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
  70. package/src/features/FileSidePanel/index.tsx +1 -1
  71. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
  72. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
  73. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
  74. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
  75. package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
  76. package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
  77. package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
  78. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
  79. package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
  80. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
  81. package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
  82. package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
  83. package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
  84. package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
  85. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
  86. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
  87. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
  88. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
  89. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
  90. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
  91. package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
  92. package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +68 -16
  93. package/src/features/KnowledgeManager/Header/AddButton.tsx +107 -0
  94. package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
  95. package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
  96. package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
  97. package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
  98. package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
  99. package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
  100. package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +83 -0
  101. package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
  102. package/src/features/KnowledgeManager/Home/index.tsx +221 -0
  103. package/src/features/KnowledgeManager/index.tsx +75 -0
  104. package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
  105. package/src/features/Portal/FilePreview/Header.tsx +1 -1
  106. package/src/locales/default/common.ts +1 -0
  107. package/src/locales/default/file.ts +85 -2
  108. package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
  109. package/src/server/routers/lambda/document.ts +57 -0
  110. package/src/server/routers/lambda/file.ts +72 -0
  111. package/src/server/routers/lambda/knowledge.ts +94 -0
  112. package/src/server/services/document/index.ts +103 -0
  113. package/src/services/document/index.ts +44 -0
  114. package/src/services/file/index.ts +5 -3
  115. package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
  116. package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
  117. package/src/store/file/initialState.ts +6 -1
  118. package/src/store/file/slices/chat/action.ts +3 -3
  119. package/src/store/file/slices/document/action.ts +359 -0
  120. package/src/store/file/slices/document/index.ts +3 -0
  121. package/src/store/file/slices/document/initialState.ts +22 -0
  122. package/src/store/file/slices/document/selectors.ts +25 -0
  123. package/src/store/file/slices/fileManager/action.test.ts +16 -9
  124. package/src/store/file/slices/fileManager/action.ts +11 -11
  125. package/src/store/file/store.ts +3 -0
  126. package/src/store/global/initialState.ts +3 -1
  127. package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
  128. package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
  129. package/src/features/FileManager/index.tsx +0 -36
  130. /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
  131. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
  132. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
  133. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
  134. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
  135. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
  136. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
  137. /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
  138. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
  139. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
  140. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
  141. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
  142. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
  143. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
  144. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
  145. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
  146. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
  147. /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
  148. /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
  149. /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
  150. /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
  151. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
  152. /package/src/features/{FileManager → KnowledgeManager}/UploadDock/index.tsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.54",
3
+ "version": "2.0.0-next.55",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -4,7 +4,16 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  import { FilesTabs, SortType } from '@/types/files';
6
6
 
7
- import { chunks, embeddings, fileChunks, files, globalFiles, knowledgeBaseFiles, knowledgeBases, users } from '../../schemas';
7
+ import {
8
+ chunks,
9
+ embeddings,
10
+ fileChunks,
11
+ files,
12
+ globalFiles,
13
+ knowledgeBaseFiles,
14
+ knowledgeBases,
15
+ users,
16
+ } from '../../schemas';
8
17
  import { LobeChatDatabase } from '../../type';
9
18
  import { FileModel } from '../file';
10
19
  import { getTestDB } from './_util';
@@ -340,13 +349,15 @@ describe('FileModel', () => {
340
349
  ];
341
350
 
342
351
  it('should query files for the user', async () => {
343
- await fileModel.create({
352
+ const file1 = await fileModel.create({
344
353
  name: 'test-file-1.txt',
345
354
  url: 'https://example.com/test-file-1.txt',
346
355
  size: 100,
347
356
  fileType: 'text/plain',
348
357
  });
349
- await fileModel.create({
358
+ // Add a small delay to ensure different timestamps
359
+ await new Promise((resolve) => setTimeout(resolve, 10));
360
+ const file2 = await fileModel.create({
350
361
  name: 'test-file-2.txt',
351
362
  url: 'https://example.com/test-file-2.txt',
352
363
  size: 200,
@@ -362,8 +373,9 @@ describe('FileModel', () => {
362
373
 
363
374
  const userFiles = await fileModel.query();
364
375
  expect(userFiles).toHaveLength(2);
365
- expect(userFiles[0].name).toBe('test-file-2.txt');
366
- expect(userFiles[1].name).toBe('test-file-1.txt');
376
+ // file2 should be first since it was created more recently
377
+ expect(userFiles[0].id).toBe(file2.id);
378
+ expect(userFiles[1].id).toBe(file1.id);
367
379
  });
368
380
 
369
381
  it('should filter files by name', async () => {
@@ -381,6 +393,14 @@ describe('FileModel', () => {
381
393
  expect(imageFiles[0].name).toBe('image.jpg');
382
394
  });
383
395
 
396
+ it('should filter audio files by category', async () => {
397
+ await serverDB.insert(files).values(sharedFileList);
398
+
399
+ const audioFiles = await fileModel.query({ category: FilesTabs.Audios });
400
+ expect(audioFiles).toHaveLength(1);
401
+ expect(audioFiles[0].name).toBe('audio.mp3');
402
+ });
403
+
384
404
  it('should sort files by name in ascending order', async () => {
385
405
  await serverDB.insert(files).values(sharedFileList);
386
406
 
@@ -1022,8 +1042,58 @@ describe('FileModel', () => {
1022
1042
  });
1023
1043
 
1024
1044
  describe('private getFileTypePrefix method', () => {
1045
+ beforeEach(async () => {
1046
+ // Create test files for all categories
1047
+ await serverDB.insert(files).values([
1048
+ {
1049
+ id: 'video-file',
1050
+ name: 'video.mp4',
1051
+ url: 'https://example.com/video.mp4',
1052
+ size: 1000,
1053
+ fileType: 'video/mp4',
1054
+ userId,
1055
+ },
1056
+ {
1057
+ id: 'page-file',
1058
+ name: 'page.html',
1059
+ url: 'https://example.com/page.html',
1060
+ size: 500,
1061
+ fileType: 'text/html',
1062
+ userId,
1063
+ },
1064
+ {
1065
+ id: 'unknown-file',
1066
+ name: 'unknown.xyz',
1067
+ url: 'https://example.com/unknown.xyz',
1068
+ size: 200,
1069
+ fileType: 'application/xyz',
1070
+ userId,
1071
+ },
1072
+ ]);
1073
+ });
1074
+
1075
+ it('should filter video files correctly', async () => {
1076
+ const result = await fileModel.query({ category: FilesTabs.Videos });
1077
+ expect(result).toHaveLength(1);
1078
+ expect(result[0].id).toBe('video-file');
1079
+ });
1080
+
1081
+ it('should filter website/page files correctly', async () => {
1082
+ const result = await fileModel.query({ category: FilesTabs.Websites });
1083
+ expect(result).toHaveLength(1);
1084
+ expect(result[0].id).toBe('page-file');
1085
+ });
1086
+
1087
+ it('should handle Pages category (should use text/html like Websites)', async () => {
1088
+ // FilesTabs.Pages is not explicitly handled in switch, falls to default
1089
+ // which returns empty string, so it won't filter by file type
1090
+ const result = await fileModel.query({ category: FilesTabs.Pages });
1091
+ // Should return all files since default case returns empty string
1092
+ expect(result.length).toBeGreaterThan(0);
1093
+ });
1094
+
1025
1095
  it('should handle unknown file category', async () => {
1026
- // This tests the default case in switch statement (line 312-313)
1096
+ // This tests the default case in switch statement
1027
1097
  const unknownCategory = 'unknown' as FilesTabs;
1028
1098
 
1029
1099
  // We need to access the private method indirectly by testing the query method
@@ -1059,7 +1129,7 @@ describe('FileModel', () => {
1059
1129
  // Note: This is a simplified test since we can't easily create 3000+ chunks
1060
1130
  // But it will still exercise the batch deletion code path
1061
1131
 
1062
- // Insert chunks (this might need to be done through proper API)
1132
+ // Insert chunks (this might need to be done through proper API)
1063
1133
  // For testing purposes, we'll delete the file which should trigger the batch deletion
1064
1134
  await fileModel.delete(fileId, true);
1065
1135
 
@@ -1112,9 +1182,9 @@ describe('FileModel', () => {
1112
1182
 
1113
1183
  // 插入embeddings (1024维向量)
1114
1184
  const testEmbedding = new Array(1024).fill(0.1);
1115
- await serverDB.insert(embeddings).values([
1116
- { chunkId: chunkId1, embeddings: testEmbedding, model: 'test-model', userId },
1117
- ]);
1185
+ await serverDB
1186
+ .insert(embeddings)
1187
+ .values([{ chunkId: chunkId1, embeddings: testEmbedding, model: 'test-model', userId }]);
1118
1188
 
1119
1189
  // 跳过 documentChunks 测试,因为需要先创建 documents 记录
1120
1190
 
@@ -1163,20 +1233,18 @@ describe('FileModel', () => {
1163
1233
  const chunkId = '550e8400-e29b-41d4-a716-446655440003';
1164
1234
 
1165
1235
  // 插入chunk
1166
- await serverDB.insert(chunks).values([
1167
- { id: chunkId, text: 'complete test chunk', userId, type: 'text' },
1168
- ]);
1236
+ await serverDB
1237
+ .insert(chunks)
1238
+ .values([{ id: chunkId, text: 'complete test chunk', userId, type: 'text' }]);
1169
1239
 
1170
1240
  // 插入fileChunks关联
1171
- await serverDB.insert(fileChunks).values([
1172
- { fileId, chunkId, userId },
1173
- ]);
1241
+ await serverDB.insert(fileChunks).values([{ fileId, chunkId, userId }]);
1174
1242
 
1175
1243
  // 插入embeddings
1176
1244
  const testEmbedding = new Array(1024).fill(0.1);
1177
- await serverDB.insert(embeddings).values([
1178
- { chunkId, embeddings: testEmbedding, model: 'test-model', userId },
1179
- ]);
1245
+ await serverDB
1246
+ .insert(embeddings)
1247
+ .values([{ chunkId, embeddings: testEmbedding, model: 'test-model', userId }]);
1180
1248
 
1181
1249
  // 删除文件
1182
1250
  await fileModel.delete(fileId, true);
@@ -1206,7 +1274,6 @@ describe('FileModel', () => {
1206
1274
  expect(remainingFileChunks).toHaveLength(0);
1207
1275
  });
1208
1276
 
1209
-
1210
1277
  it('should delete files that are in knowledge bases (removed protection)', async () => {
1211
1278
  // 测试修复后的逻辑:知识库中的文件也应该被删除
1212
1279
  const testFile = {
@@ -1223,19 +1290,17 @@ describe('FileModel', () => {
1223
1290
  const chunkId = '550e8400-e29b-41d4-a716-446655440007';
1224
1291
 
1225
1292
  // 插入chunk和关联数据
1226
- await serverDB.insert(chunks).values([
1227
- { id: chunkId, text: 'knowledge base chunk', userId, type: 'text' },
1228
- ]);
1293
+ await serverDB
1294
+ .insert(chunks)
1295
+ .values([{ id: chunkId, text: 'knowledge base chunk', userId, type: 'text' }]);
1229
1296
 
1230
- await serverDB.insert(fileChunks).values([
1231
- { fileId, chunkId, userId },
1232
- ]);
1297
+ await serverDB.insert(fileChunks).values([{ fileId, chunkId, userId }]);
1233
1298
 
1234
1299
  // 插入embeddings (1024维向量)
1235
1300
  const testEmbedding = new Array(1024).fill(0.1);
1236
- await serverDB.insert(embeddings).values([
1237
- { chunkId, embeddings: testEmbedding, model: 'test-model', userId },
1238
- ]);
1301
+ await serverDB
1302
+ .insert(embeddings)
1303
+ .values([{ chunkId, embeddings: testEmbedding, model: 'test-model', userId }]);
1239
1304
 
1240
1305
  // 验证文件确实在知识库中
1241
1306
  const kbFile = await serverDB.query.knowledgeBaseFiles.findFirst({
@@ -211,9 +211,17 @@ export class FileModel {
211
211
  q ? ilike(files.name, `%${q}%`) : undefined,
212
212
  eq(files.userId, this.userId),
213
213
  );
214
- if (category && category !== FilesTabs.All) {
214
+ if (category && category !== FilesTabs.All && category !== FilesTabs.Home) {
215
215
  const fileTypePrefix = this.getFileTypePrefix(category as FilesTabs);
216
- whereClause = and(whereClause, ilike(files.fileType, `${fileTypePrefix}%`));
216
+ if (Array.isArray(fileTypePrefix)) {
217
+ // For multiple file types (e.g., Documents includes 'application' and 'custom')
218
+ whereClause = and(
219
+ whereClause,
220
+ or(...fileTypePrefix.map((prefix) => ilike(files.fileType, `${prefix}%`))),
221
+ );
222
+ } else {
223
+ whereClause = and(whereClause, ilike(files.fileType, `${fileTypePrefix}%`));
224
+ }
217
225
  }
218
226
 
219
227
  // 2. order part
@@ -308,13 +316,13 @@ export class FileModel {
308
316
  /**
309
317
  * get the corresponding file type prefix according to FilesTabs
310
318
  */
311
- private getFileTypePrefix = (category: FilesTabs): string => {
319
+ private getFileTypePrefix = (category: FilesTabs): string | string[] => {
312
320
  switch (category) {
313
321
  case FilesTabs.Audios: {
314
322
  return 'audio';
315
323
  }
316
324
  case FilesTabs.Documents: {
317
- return 'application';
325
+ return ['application', 'custom'];
318
326
  }
319
327
  case FilesTabs.Images: {
320
328
  return 'image';
@@ -322,6 +330,9 @@ export class FileModel {
322
330
  case FilesTabs.Videos: {
323
331
  return 'video';
324
332
  }
333
+ case FilesTabs.Websites: {
334
+ return 'text/html';
335
+ }
325
336
  default: {
326
337
  return '';
327
338
  }
@@ -0,0 +1,300 @@
1
+ // @vitest-environment node
2
+ import { FilesTabs } from '@lobechat/types';
3
+ import { beforeEach, describe, expect, it } from 'vitest';
4
+
5
+ import { getTestDB } from '../../models/__tests__/_util';
6
+ import { NewDocument, documents } from '../../schemas/document';
7
+ import { NewFile, files } from '../../schemas/file';
8
+ import { users } from '../../schemas/user';
9
+ import { LobeChatDatabase } from '../../type';
10
+ import { KnowledgeRepo } from './index';
11
+
12
+ const userId = 'knowledge-test-user';
13
+ const otherUserId = 'other-knowledge-user';
14
+
15
+ let knowledgeRepo: KnowledgeRepo;
16
+
17
+ const serverDB: LobeChatDatabase = await getTestDB();
18
+
19
+ beforeEach(async () => {
20
+ // Clean up
21
+ await serverDB.delete(users);
22
+
23
+ // Create test users
24
+ await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
25
+
26
+ // Initialize repo
27
+ knowledgeRepo = new KnowledgeRepo(serverDB, userId);
28
+ });
29
+
30
+ describe('KnowledgeRepo', () => {
31
+ describe('query - Documents category filtering', () => {
32
+ beforeEach(async () => {
33
+ // Create test files
34
+ const testFiles: NewFile[] = [
35
+ {
36
+ fileType: 'application/pdf',
37
+ name: 'regular-pdf-file.pdf',
38
+ size: 1024,
39
+ url: 'file-pdf-url',
40
+ userId,
41
+ },
42
+ {
43
+ fileType: 'custom/other',
44
+ name: 'custom-file.txt',
45
+ size: 512,
46
+ url: 'custom-file-url',
47
+ userId,
48
+ },
49
+ ];
50
+
51
+ await serverDB.insert(files).values(testFiles);
52
+
53
+ // Create test documents
54
+ const testDocuments: NewDocument[] = [
55
+ // This should be EXCLUDED (sourceType='file')
56
+ {
57
+ content: 'PDF from file upload',
58
+ fileType: 'application/pdf',
59
+ filename: 'uploaded-pdf.pdf',
60
+ source: 'upload-source',
61
+ sourceType: 'file',
62
+ totalCharCount: 100,
63
+ totalLineCount: 10,
64
+ userId,
65
+ },
66
+ // This should be EXCLUDED (fileType='custom/document')
67
+ {
68
+ content: 'Editor document',
69
+ fileType: 'custom/document',
70
+ filename: 'editor-doc.md',
71
+ source: 'editor-source',
72
+ sourceType: 'file',
73
+ totalCharCount: 200,
74
+ totalLineCount: 20,
75
+ userId,
76
+ },
77
+ // This should be INCLUDED (application/pdf with sourceType='api')
78
+ {
79
+ content: 'PDF from API',
80
+ fileType: 'application/pdf',
81
+ filename: 'api-pdf.pdf',
82
+ source: 'api-source',
83
+ sourceType: 'api',
84
+ totalCharCount: 300,
85
+ totalLineCount: 30,
86
+ userId,
87
+ },
88
+ // This should be INCLUDED (custom/other with sourceType='web')
89
+ {
90
+ content: 'Custom web document',
91
+ fileType: 'custom/other',
92
+ filename: 'web-doc.txt',
93
+ source: 'web-source',
94
+ sourceType: 'web',
95
+ totalCharCount: 400,
96
+ totalLineCount: 40,
97
+ userId,
98
+ },
99
+ ];
100
+
101
+ await serverDB.insert(documents).values(testDocuments);
102
+ });
103
+
104
+ it('should exclude documents with fileType="custom/document" from Documents category', async () => {
105
+ const results = await knowledgeRepo.query({ category: FilesTabs.Documents });
106
+
107
+ // Should not include editor document (custom/document)
108
+ const editorDoc = results.find((item) => item.name === 'editor-doc.md');
109
+ expect(editorDoc).toBeUndefined();
110
+ });
111
+
112
+ it('should exclude documents with sourceType="file" from Documents category', async () => {
113
+ const results = await knowledgeRepo.query({ category: FilesTabs.Documents });
114
+
115
+ // Should not include uploaded PDF document (sourceType='file')
116
+ const uploadedPdf = results.find((item) => item.name === 'uploaded-pdf.pdf');
117
+ expect(uploadedPdf).toBeUndefined();
118
+ });
119
+
120
+ it('should include documents with sourceType="api" in Documents category', async () => {
121
+ const results = await knowledgeRepo.query({ category: FilesTabs.Documents });
122
+
123
+ // Should include API PDF (application/pdf with sourceType='api')
124
+ const apiPdf = results.find((item) => item.name === 'api-pdf.pdf');
125
+ expect(apiPdf).toBeDefined();
126
+ expect(apiPdf?.sourceType).toBe('document');
127
+ expect(apiPdf?.fileType).toBe('application/pdf');
128
+ });
129
+
130
+ it('should include documents with sourceType="web" in Documents category', async () => {
131
+ const results = await knowledgeRepo.query({ category: FilesTabs.Documents });
132
+
133
+ // Should include web document (custom/other with sourceType='web')
134
+ const webDoc = results.find((item) => item.name === 'web-doc.txt');
135
+ expect(webDoc).toBeDefined();
136
+ expect(webDoc?.sourceType).toBe('document');
137
+ expect(webDoc?.fileType).toBe('custom/other');
138
+ });
139
+
140
+ it('should include files from files table in Documents category', async () => {
141
+ const results = await knowledgeRepo.query({ category: FilesTabs.Documents });
142
+
143
+ // Should include regular files
144
+ const regularFile = results.find((item) => item.name === 'regular-pdf-file.pdf');
145
+ expect(regularFile).toBeDefined();
146
+ expect(regularFile?.sourceType).toBe('file');
147
+ });
148
+
149
+ it('should show all documents in All category (no filtering)', async () => {
150
+ const results = await knowledgeRepo.query({ category: FilesTabs.All });
151
+
152
+ // All category should include everything
153
+ expect(results.length).toBeGreaterThanOrEqual(6); // 2 files + 4 documents
154
+
155
+ const editorDoc = results.find((item) => item.name === 'editor-doc.md');
156
+ const uploadedPdf = results.find((item) => item.name === 'uploaded-pdf.pdf');
157
+
158
+ expect(editorDoc).toBeDefined();
159
+ expect(uploadedPdf).toBeDefined();
160
+ });
161
+
162
+ it('should apply both filters together in Documents category', async () => {
163
+ const results = await knowledgeRepo.query({ category: FilesTabs.Documents });
164
+
165
+ // Count documents with sourceType='document'
166
+ const documentTypeItems = results.filter((item) => item.sourceType === 'document');
167
+
168
+ // Should have exactly 2 documents (api-pdf and web-doc)
169
+ // Excluded: uploaded-pdf (sourceType='file') and editor-doc (fileType='custom/document')
170
+ expect(documentTypeItems).toHaveLength(2);
171
+
172
+ const names = documentTypeItems.map((item) => item.name).sort();
173
+ expect(names).toEqual(['api-pdf.pdf', 'web-doc.txt']);
174
+ });
175
+ });
176
+
177
+ describe('query - user isolation', () => {
178
+ beforeEach(async () => {
179
+ // Create files for current user
180
+ await serverDB.insert(files).values({
181
+ fileType: 'application/pdf',
182
+ name: 'user-file.pdf',
183
+ size: 1024,
184
+ url: 'user-file-url',
185
+ userId,
186
+ });
187
+
188
+ // Create files for other user
189
+ await serverDB.insert(files).values({
190
+ fileType: 'application/pdf',
191
+ name: 'other-user-file.pdf',
192
+ size: 1024,
193
+ url: 'other-file-url',
194
+ userId: otherUserId,
195
+ });
196
+
197
+ // Create documents for current user
198
+ await serverDB.insert(documents).values({
199
+ content: 'User document',
200
+ fileType: 'application/pdf',
201
+ filename: 'user-doc.pdf',
202
+ source: 'user-source',
203
+ sourceType: 'api',
204
+ totalCharCount: 100,
205
+ totalLineCount: 10,
206
+ userId,
207
+ });
208
+
209
+ // Create documents for other user
210
+ await serverDB.insert(documents).values({
211
+ content: 'Other user document',
212
+ fileType: 'application/pdf',
213
+ filename: 'other-doc.pdf',
214
+ source: 'other-source',
215
+ sourceType: 'api',
216
+ totalCharCount: 100,
217
+ totalLineCount: 10,
218
+ userId: otherUserId,
219
+ });
220
+ });
221
+
222
+ it('should only return current user items', async () => {
223
+ const results = await knowledgeRepo.query({ category: FilesTabs.All });
224
+
225
+ // Should only have items from current user
226
+ expect(results).toHaveLength(2);
227
+
228
+ const names = results.map((item) => item.name).sort();
229
+ expect(names).toEqual(['user-doc.pdf', 'user-file.pdf']);
230
+
231
+ // Should not include other user's items
232
+ const otherUserFile = results.find((item) => item.name === 'other-user-file.pdf');
233
+ const otherUserDoc = results.find((item) => item.name === 'other-doc.pdf');
234
+
235
+ expect(otherUserFile).toBeUndefined();
236
+ expect(otherUserDoc).toBeUndefined();
237
+ });
238
+ });
239
+
240
+ describe('query - search filtering', () => {
241
+ beforeEach(async () => {
242
+ await serverDB.insert(files).values([
243
+ {
244
+ fileType: 'application/pdf',
245
+ name: 'report-2024.pdf',
246
+ size: 1024,
247
+ url: 'report-url',
248
+ userId,
249
+ },
250
+ {
251
+ fileType: 'application/pdf',
252
+ name: 'invoice.pdf',
253
+ size: 512,
254
+ url: 'invoice-url',
255
+ userId,
256
+ },
257
+ ]);
258
+
259
+ await serverDB.insert(documents).values([
260
+ {
261
+ content: 'Annual report content',
262
+ fileType: 'application/pdf',
263
+ filename: 'annual-report.pdf',
264
+ source: 'api-source',
265
+ sourceType: 'api',
266
+ title: 'Annual Report',
267
+ totalCharCount: 1000,
268
+ totalLineCount: 100,
269
+ userId,
270
+ },
271
+ {
272
+ content: 'Meeting notes',
273
+ fileType: 'custom/other',
274
+ filename: 'notes.txt',
275
+ source: 'web-source',
276
+ sourceType: 'web',
277
+ title: 'Meeting Notes',
278
+ totalCharCount: 500,
279
+ totalLineCount: 50,
280
+ userId,
281
+ },
282
+ ]);
283
+ });
284
+
285
+ it('should filter by search query in file names', async () => {
286
+ const results = await knowledgeRepo.query({ category: FilesTabs.All, q: 'report' });
287
+
288
+ expect(results).toHaveLength(2);
289
+ const names = results.map((item) => item.name).sort();
290
+ expect(names).toEqual(['Annual Report', 'report-2024.pdf']);
291
+ });
292
+
293
+ it('should filter by search query in document titles', async () => {
294
+ const results = await knowledgeRepo.query({ category: FilesTabs.All, q: 'meeting' });
295
+
296
+ expect(results).toHaveLength(1);
297
+ expect(results[0].name).toBe('Meeting Notes');
298
+ });
299
+ });
300
+ });