@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.
- package/CHANGELOG.md +27 -0
- package/changelog/v1.json +9 -0
- package/locales/ar/common.json +1 -0
- package/locales/ar/file.json +85 -2
- package/locales/bg-BG/common.json +1 -0
- package/locales/bg-BG/file.json +85 -2
- package/locales/de-DE/common.json +1 -0
- package/locales/de-DE/file.json +85 -2
- package/locales/en-US/common.json +1 -0
- package/locales/en-US/file.json +85 -2
- package/locales/es-ES/common.json +1 -0
- package/locales/es-ES/file.json +85 -2
- package/locales/fa-IR/common.json +1 -0
- package/locales/fa-IR/file.json +85 -2
- package/locales/fr-FR/common.json +1 -0
- package/locales/fr-FR/file.json +85 -2
- package/locales/it-IT/common.json +1 -0
- package/locales/it-IT/file.json +85 -2
- package/locales/ja-JP/common.json +1 -0
- package/locales/ja-JP/file.json +85 -2
- package/locales/ko-KR/common.json +1 -0
- package/locales/ko-KR/file.json +85 -2
- package/locales/nl-NL/common.json +1 -0
- package/locales/nl-NL/file.json +85 -2
- package/locales/pl-PL/common.json +1 -0
- package/locales/pl-PL/file.json +85 -2
- package/locales/pt-BR/common.json +1 -0
- package/locales/pt-BR/file.json +85 -2
- package/locales/ru-RU/common.json +1 -0
- package/locales/ru-RU/file.json +85 -2
- package/locales/tr-TR/common.json +1 -0
- package/locales/tr-TR/file.json +85 -2
- package/locales/vi-VN/common.json +1 -0
- package/locales/vi-VN/file.json +85 -2
- package/locales/zh-CN/common.json +1 -0
- package/locales/zh-CN/file.json +85 -2
- package/locales/zh-TW/common.json +1 -0
- package/locales/zh-TW/file.json +85 -2
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/file.test.ts +94 -29
- package/packages/database/src/models/file.ts +15 -4
- package/packages/database/src/repositories/knowledge/index.test.ts +300 -0
- package/packages/database/src/repositories/knowledge/index.ts +420 -0
- package/packages/model-bank/src/aiModels/aihubmix.ts +1 -0
- package/packages/model-bank/src/aiModels/google.ts +9 -5
- package/packages/model-bank/src/aiModels/openai.ts +2 -35
- package/packages/model-bank/src/aiModels/openrouter.ts +1 -0
- package/packages/model-bank/src/aiModels/vertexai.ts +2 -0
- package/packages/model-bank/src/types/aiModel.ts +15 -2
- package/packages/model-runtime/src/core/usageConverters/index.ts +1 -0
- package/packages/model-runtime/src/core/usageConverters/utils/resolveImageSinglePrice.ts +34 -0
- package/packages/types/src/document/index.ts +14 -2
- package/packages/types/src/files/index.ts +2 -0
- package/packages/types/src/files/list.ts +10 -0
- package/packages/types/src/llm.ts +1 -1
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/ModelSelect/ImageModelItem.tsx +93 -0
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/{ModelSelect.tsx → ModelSelect/index.tsx} +17 -2
- package/src/app/[variants]/(main)/knowledge/KnowledgeRouter.tsx +2 -1
- package/src/app/[variants]/(main)/knowledge/components/KnowledgeBaseItem/index.tsx +0 -2
- package/src/app/[variants]/(main)/knowledge/hooks/useFileCategory.ts +6 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/index.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/{MenuItems.tsx → CategoryMenu.tsx} +3 -3
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeBaseDetail/menu/Menu.tsx +2 -2
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/index.tsx +40 -18
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/layout/Container.tsx +1 -1
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/CategoryMenu.tsx +148 -0
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/KnowledgeBase.tsx +20 -7
- package/src/components/FileIcon/index.tsx +3 -1
- package/src/features/ChatInput/ActionBar/Knowledge/index.tsx +2 -2
- package/src/features/FileSidePanel/index.tsx +1 -1
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItem.tsx +80 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/Item/MasonryItemWrapper.tsx +27 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/List.tsx +104 -23
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/MasonrySkeleton.tsx +62 -0
- package/src/features/KnowledgeBaseModal/AssignKnowledgeBase/index.tsx +3 -2
- package/src/features/KnowledgeBaseModal/CreateNew/CreateForm.tsx +1 -1
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentActions.tsx +111 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditor.tsx +723 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentEditorPlaceholder.tsx +169 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListItem.tsx +148 -0
- package/src/features/KnowledgeManager/DocumentExplorer/DocumentListSkeleton.tsx +39 -0
- package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +348 -0
- package/src/features/KnowledgeManager/DocumentExplorer/RenamePopover.tsx +163 -0
- package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +318 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/index.tsx +48 -9
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/DefaultFileItem.tsx +149 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/ImageFileItem.tsx +245 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/MarkdownFileItem.tsx +232 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/NoteFileItem.tsx +230 -0
- package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +398 -0
- package/src/features/KnowledgeManager/FileExplorer/ToolBar/ViewSwitcher.tsx +45 -0
- package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/index.tsx +68 -16
- package/src/features/KnowledgeManager/Header/AddButton.tsx +107 -0
- package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +33 -0
- package/src/features/{FileManager → KnowledgeManager}/Header/index.tsx +3 -9
- package/src/features/KnowledgeManager/Home/RecentDocumentCard.tsx +116 -0
- package/src/features/KnowledgeManager/Home/RecentDocuments.tsx +77 -0
- package/src/features/KnowledgeManager/Home/RecentFileCard.tsx +121 -0
- package/src/features/KnowledgeManager/Home/RecentFiles.tsx +73 -0
- package/src/features/KnowledgeManager/Home/RecentFilesSkeleton.tsx +83 -0
- package/src/features/KnowledgeManager/Home/UploadEntries.tsx +208 -0
- package/src/features/KnowledgeManager/Home/index.tsx +221 -0
- package/src/features/KnowledgeManager/index.tsx +75 -0
- package/src/features/Portal/FilePreview/Body/index.tsx +1 -1
- package/src/features/Portal/FilePreview/Header.tsx +1 -1
- package/src/locales/default/common.ts +1 -0
- package/src/locales/default/file.ts +85 -2
- package/src/server/routers/lambda/__tests__/file.test.ts +85 -6
- package/src/server/routers/lambda/document.ts +57 -0
- package/src/server/routers/lambda/file.ts +72 -0
- package/src/server/routers/lambda/knowledge.ts +94 -0
- package/src/server/services/document/index.ts +103 -0
- package/src/services/document/index.ts +44 -0
- package/src/services/file/index.ts +5 -3
- package/src/store/aiInfra/slices/aiProvider/__tests__/action.test.ts +125 -229
- package/src/store/aiInfra/slices/aiProvider/action.ts +113 -33
- package/src/store/file/initialState.ts +6 -1
- package/src/store/file/slices/chat/action.ts +3 -3
- package/src/store/file/slices/document/action.ts +359 -0
- package/src/store/file/slices/document/index.ts +3 -0
- package/src/store/file/slices/document/initialState.ts +22 -0
- package/src/store/file/slices/document/selectors.ts +25 -0
- package/src/store/file/slices/fileManager/action.test.ts +16 -9
- package/src/store/file/slices/fileManager/action.ts +11 -11
- package/src/store/file/store.ts +3 -0
- package/src/store/global/initialState.ts +3 -1
- package/src/app/[variants]/(main)/knowledge/routes/KnowledgeHome/menu/FileMenu.tsx +0 -75
- package/src/features/FileManager/FileList/MasonryFileItem/index.tsx +0 -582
- package/src/features/FileManager/index.tsx +0 -36
- /package/src/features/{FileManager/FileList/ToolBar → KnowledgeBaseModal/AssignKnowledgeBase}/ViewSwitcher.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/ChunkItem.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/ChunkList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Content.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/Loading/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/Item.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/SimilaritySearchList/index.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/ChunkDrawer/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/EmptyStatus.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/ChunkTag.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileListItem/DropdownMenu.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/FileSkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonryFileItem/MasonryItemWrapper.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/MasonrySkeleton.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/Config.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/MultiSelectActions.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/ToolBar/index.tsx +0 -0
- /package/src/features/{FileManager/FileList → KnowledgeManager/FileExplorer}/useCheckTaskStatus.ts +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/FilesSearchBar.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/TogglePanelButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/Header/UploadFileButton.tsx +0 -0
- /package/src/features/{FileManager → KnowledgeManager}/UploadDock/Item.tsx +0 -0
- /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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
366
|
-
expect(userFiles[
|
|
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
|
|
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
|
|
1116
|
-
|
|
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
|
|
1167
|
-
|
|
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
|
|
1178
|
-
|
|
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
|
|
1227
|
-
|
|
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
|
|
1237
|
-
|
|
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
|
-
|
|
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
|
+
});
|