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

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 +52 -0
  2. package/changelog/v1.json +18 -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 +55 -16
  93. package/src/features/KnowledgeManager/Header/AddButton.tsx +118 -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 +81 -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 +87 -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
@@ -1,5 +1,8 @@
1
1
  export default {
2
- desc: '管理你的知识',
2
+ addFolder: '创建文件夹',
3
+ addKnowledge: '添加知识',
4
+ addPage: '创建文稿',
5
+ desc: '管理你的工作、学习与生活知识。',
3
6
  detail: {
4
7
  basic: {
5
8
  createdAt: '创建时间',
@@ -21,15 +24,89 @@ export default {
21
24
  embeddingStatus: '向量化',
22
25
  },
23
26
  },
27
+ documentEditor: {
28
+ addIcon: '添加图标',
29
+ autoSaveMessage: '文档会自动保存,无需手动保存',
30
+ chooseIcon: '选择图标',
31
+ deleteConfirm: {
32
+ content: '即将删除该文档,删除后将不可恢复,请谨慎操作。',
33
+ title: '删除文档',
34
+ },
35
+ deleteError: '删除文档失败',
36
+ deleteSuccess: '文档删除成功',
37
+ editedAt: '最后编辑于 {{time}}',
38
+ editedBy: '最后编辑者 {{name}}',
39
+ editorPlaceholder: '输入文档内容,按 / 打开命令菜单',
40
+ empty: {
41
+ createNewDocument: '创建新文档',
42
+ title: '选择一个文档以开始',
43
+ uploadMarkdown: '上传 Markdown 文件',
44
+ },
45
+ linkCopied: '链接已复制',
46
+ menu: {
47
+ copyLink: '复制链接',
48
+ exportDocument: '导出文档',
49
+ importDocument: '导入文档',
50
+ pin: '置顶文档',
51
+ },
52
+ saving: '保存中...',
53
+ titlePlaceholder: '无标题',
54
+ wordCount: '{{wordCount}} 字',
55
+ },
56
+ documentList: {
57
+ copyContent: '复制全文',
58
+ documentCount: '共 {{count}} 个文档',
59
+ duplicate: '创建副本',
60
+ empty: '暂无文档,点击上方按钮创建你的第一篇文档',
61
+ noResults: '未找到匹配的文档',
62
+ selectNote: '选择一个文档开始编辑',
63
+ untitled: '无标题',
64
+ },
24
65
  empty: '暂无已上传文件/文件夹',
25
66
  header: {
26
67
  actions: {
27
68
  newFolder: '新建文件夹',
69
+ newPage: '新建文稿',
28
70
  uploadFile: '上传文件',
29
71
  uploadFolder: '上传文件夹',
30
72
  },
73
+ newDocumentButton: '新建文档',
74
+ newNoteDialog: {
75
+ cancel: '取消',
76
+ editTitle: '编辑文档',
77
+ emptyContent: '文档内容不能为空',
78
+ loadError: '加载文档失败,请重试',
79
+ loading: '加载中...',
80
+ save: '保存',
81
+ saveError: '保存文档失败,请重试',
82
+ saveSuccess: '文档保存成功',
83
+ title: '新建文档',
84
+ updateSuccess: '文档更新成功',
85
+ },
31
86
  uploadButton: '上传',
32
87
  },
88
+ home: {
89
+ getStarted: '开始使用',
90
+ greeting: '开始',
91
+ quickActions: '快捷操作',
92
+ recentDocuments: '最近文档',
93
+ recentFiles: '最近文件',
94
+ subtitle: '欢迎使用知识库,从这里开始管理你的文档和文档',
95
+ uploadEntries: {
96
+ files: {
97
+ title: '上传文件',
98
+ },
99
+ folder: {
100
+ title: '上传文件夹',
101
+ },
102
+ knowledgeBase: {
103
+ title: '新建知识库',
104
+ },
105
+ newDocument: {
106
+ title: '新建文档',
107
+ },
108
+ },
109
+ },
33
110
  knowledgeBase: {
34
111
  list: {
35
112
  confirmRemoveKnowledgeBase:
@@ -39,6 +116,10 @@ export default {
39
116
  new: '新建知识库',
40
117
  title: '知识库',
41
118
  },
119
+ menu: {
120
+ allDocuments: '全部文档',
121
+ allFiles: '全部文件',
122
+ },
42
123
  networkError: '获取知识库失败,请检测网络连接后重试',
43
124
  notSupportGuide: {
44
125
  desc: '当前部署实例为客户端数据库模式,无法使用文件管理功能。请切换到<1>服务端数据库部署模式</1>,或直接使用 <3>LobeChat Cloud</3>',
@@ -62,12 +143,16 @@ export default {
62
143
  downloadFile: '下载文件',
63
144
  unsupportedFileAndContact: '此文件格式暂不支持在线预览,如有预览诉求,欢迎<1>反馈给我们</1>',
64
145
  },
146
+ searchDocumentPlaceholder: '搜索文档',
65
147
  searchFilePlaceholder: '搜索文件',
66
148
  tab: {
67
- all: '全部文件',
149
+ all: '全部',
68
150
  audios: '语音',
69
151
  documents: '文档',
152
+ home: '首页',
70
153
  images: '图片',
154
+ moreTypes: '更多类型',
155
+ pages: '文稿',
71
156
  videos: '视频',
72
157
  websites: '网页',
73
158
  },
@@ -39,13 +39,21 @@ function createCallerWithCtx(partialCtx: any = {}) {
39
39
  delete: vi.fn(),
40
40
  };
41
41
 
42
+ const knowledgeRepo = {
43
+ query: vi.fn().mockResolvedValue([]),
44
+ };
45
+
46
+ const documentModel = {};
47
+
42
48
  const ctx = {
43
49
  serverDB: {} as any,
44
50
  userId: 'test-user',
45
51
  asyncTaskModel,
46
52
  chunkModel,
53
+ documentModel,
47
54
  fileModel,
48
55
  fileService,
56
+ knowledgeRepo,
49
57
  ...partialCtx,
50
58
  };
51
59
 
@@ -58,18 +66,24 @@ vi.mock('@/config/db', () => ({
58
66
  },
59
67
  }));
60
68
 
69
+ const mockAsyncTaskFindByIds = vi.fn();
70
+ const mockAsyncTaskFindById = vi.fn();
71
+ const mockAsyncTaskDelete = vi.fn();
72
+ const mockChunkCountByFileIds = vi.fn();
73
+ const mockChunkCountByFileId = vi.fn();
74
+
61
75
  vi.mock('@/database/models/asyncTask', () => ({
62
76
  AsyncTaskModel: vi.fn(() => ({
63
- findById: vi.fn(),
64
- findByIds: vi.fn(),
65
- delete: vi.fn(),
77
+ delete: mockAsyncTaskDelete,
78
+ findById: mockAsyncTaskFindById,
79
+ findByIds: mockAsyncTaskFindByIds,
66
80
  })),
67
81
  }));
68
82
 
69
83
  vi.mock('@/database/models/chunk', () => ({
70
84
  ChunkModel: vi.fn(() => ({
71
- countByFileId: vi.fn(),
72
- countByFileIds: vi.fn(),
85
+ countByFileId: mockChunkCountByFileId,
86
+ countByFileIds: mockChunkCountByFileIds,
73
87
  })),
74
88
  }));
75
89
 
@@ -85,14 +99,28 @@ vi.mock('@/database/models/file', () => ({
85
99
  })),
86
100
  }));
87
101
 
102
+ const mockFileServiceGetFullFileUrl = vi.fn();
103
+
88
104
  vi.mock('@/server/services/file', () => ({
89
105
  FileService: vi.fn(() => ({
90
- getFullFileUrl: vi.fn(),
91
106
  deleteFile: vi.fn(),
92
107
  deleteFiles: vi.fn(),
108
+ getFullFileUrl: mockFileServiceGetFullFileUrl,
93
109
  })),
94
110
  }));
95
111
 
112
+ const mockKnowledgeRepoQuery = vi.fn().mockResolvedValue([]);
113
+
114
+ vi.mock('@/database/repositories/knowledge', () => ({
115
+ KnowledgeRepo: vi.fn(() => ({
116
+ query: mockKnowledgeRepoQuery,
117
+ })),
118
+ }));
119
+
120
+ vi.mock('@/database/models/document', () => ({
121
+ DocumentModel: vi.fn(() => ({})),
122
+ }));
123
+
96
124
  describe('fileRouter', () => {
97
125
  let ctx: any;
98
126
  let caller: any;
@@ -169,6 +197,57 @@ describe('fileRouter', () => {
169
197
  });
170
198
  });
171
199
 
200
+ describe('getKnowledgeItems', () => {
201
+ it('should return knowledge items with files and documents', async () => {
202
+ const knowledgeItems = [
203
+ {
204
+ ...mockFile,
205
+ chunkTaskId: 'chunk-1',
206
+ embeddingTaskId: 'emb-1',
207
+ id: 'file-1',
208
+ sourceType: 'file' as const,
209
+ },
210
+ {
211
+ editorData: { content: 'test' },
212
+ id: 'doc-1',
213
+ name: 'Document 1',
214
+ sourceType: 'document' as const,
215
+ },
216
+ ];
217
+
218
+ mockKnowledgeRepoQuery.mockResolvedValue(knowledgeItems);
219
+ mockChunkCountByFileIds.mockResolvedValue([{ count: 10, id: 'file-1' }]);
220
+ mockAsyncTaskFindByIds
221
+ .mockResolvedValueOnce([{ error: null, id: 'chunk-1', status: AsyncTaskStatus.Success }])
222
+ .mockResolvedValueOnce([{ error: null, id: 'emb-1', status: AsyncTaskStatus.Success }]);
223
+ mockFileServiceGetFullFileUrl.mockResolvedValue('https://example.com/test-url');
224
+
225
+ const result = await caller.getKnowledgeItems({});
226
+
227
+ expect(result).toHaveLength(2);
228
+ expect(result[0]).toMatchObject({
229
+ chunkCount: 10,
230
+ chunkingStatus: AsyncTaskStatus.Success,
231
+ embeddingStatus: AsyncTaskStatus.Success,
232
+ finishEmbedding: true,
233
+ id: 'file-1',
234
+ sourceType: 'file',
235
+ url: 'https://example.com/test-url',
236
+ });
237
+ expect(result[1]).toMatchObject({
238
+ chunkCount: null,
239
+ chunkingError: null,
240
+ chunkingStatus: null,
241
+ editorData: { content: 'test' },
242
+ embeddingError: null,
243
+ embeddingStatus: null,
244
+ finishEmbedding: false,
245
+ id: 'doc-1',
246
+ name: 'Document 1',
247
+ });
248
+ });
249
+ });
250
+
172
251
  describe('removeFile', () => {
173
252
  it('should do nothing when file not found', async () => {
174
253
  ctx.fileModel.delete.mockResolvedValue(null);
@@ -21,6 +21,38 @@ const documentProcedure = authedProcedure.use(serverDatabase).use(async (opts) =
21
21
  });
22
22
 
23
23
  export const documentRouter = router({
24
+ createDocument: documentProcedure
25
+ .input(
26
+ z.object({
27
+ content: z.string().optional(),
28
+ editorData: z.string(),
29
+ fileType: z.string().optional(),
30
+ knowledgeBaseId: z.string().optional(),
31
+ metadata: z.record(z.any()).optional(),
32
+ title: z.string(),
33
+ }),
34
+ )
35
+ .mutation(async ({ ctx, input }) => {
36
+ // Parse editorData from JSON string to object
37
+ const editorData = JSON.parse(input.editorData);
38
+ return ctx.documentService.createDocument({
39
+ ...input,
40
+ editorData,
41
+ });
42
+ }),
43
+
44
+ deleteDocument: documentProcedure
45
+ .input(z.object({ id: z.string() }))
46
+ .mutation(async ({ ctx, input }) => {
47
+ return ctx.documentService.deleteDocument(input.id);
48
+ }),
49
+
50
+ getDocumentById: documentProcedure
51
+ .input(z.object({ id: z.string() }))
52
+ .query(async ({ ctx, input }) => {
53
+ return ctx.documentService.getDocumentById(input.id);
54
+ }),
55
+
24
56
  parseFileContent: documentProcedure
25
57
  .input(
26
58
  z.object({
@@ -33,4 +65,29 @@ export const documentRouter = router({
33
65
 
34
66
  return lobeDocument;
35
67
  }),
68
+
69
+ queryDocuments: documentProcedure.query(async ({ ctx }) => {
70
+ return ctx.documentService.queryDocuments();
71
+ }),
72
+
73
+ updateDocument: documentProcedure
74
+ .input(
75
+ z.object({
76
+ content: z.string().optional(),
77
+ editorData: z.string().optional(),
78
+ id: z.string(),
79
+ metadata: z.record(z.any()).optional(),
80
+ rawData: z.string().optional(),
81
+ title: z.string().optional(),
82
+ }),
83
+ )
84
+ .mutation(async ({ ctx, input }) => {
85
+ const { id, editorData: editorDataString, ...params } = input;
86
+ // Parse editorData from JSON string to object if present
87
+ const editorData = editorDataString ? JSON.parse(editorDataString) : undefined;
88
+ return ctx.documentService.updateDocument(id, {
89
+ ...params,
90
+ editorData,
91
+ });
92
+ }),
36
93
  });
@@ -4,7 +4,9 @@ import { z } from 'zod';
4
4
  import { serverDBEnv } from '@/config/db';
5
5
  import { AsyncTaskModel } from '@/database/models/asyncTask';
6
6
  import { ChunkModel } from '@/database/models/chunk';
7
+ import { DocumentModel } from '@/database/models/document';
7
8
  import { FileModel } from '@/database/models/file';
9
+ import { KnowledgeRepo } from '@/database/repositories/knowledge';
8
10
  import { authedProcedure, router } from '@/libs/trpc/lambda';
9
11
  import { serverDatabase } from '@/libs/trpc/lambda/middleware';
10
12
  import { FileService } from '@/server/services/file';
@@ -18,8 +20,10 @@ const fileProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
18
20
  ctx: {
19
21
  asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId),
20
22
  chunkModel: new ChunkModel(ctx.serverDB, ctx.userId),
23
+ documentModel: new DocumentModel(ctx.serverDB, ctx.userId),
21
24
  fileModel: new FileModel(ctx.serverDB, ctx.userId),
22
25
  fileService: new FileService(ctx.serverDB, ctx.userId),
26
+ knowledgeRepo: new KnowledgeRepo(ctx.serverDB, ctx.userId),
23
27
  },
24
28
  });
25
29
  });
@@ -95,6 +99,8 @@ export const fileRouter = router({
95
99
  embeddingError: embeddingTask?.error,
96
100
  embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
97
101
  finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
102
+ metadata: item.metadata as Record<string, any> | null | undefined,
103
+ sourceType: 'file' as const,
98
104
  url: await ctx.fileService.getFullFileUrl(item.url!),
99
105
  };
100
106
  }),
@@ -132,6 +138,7 @@ export const fileRouter = router({
132
138
  embeddingError: embeddingTask?.error ?? null,
133
139
  embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
134
140
  finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
141
+ sourceType: 'file' as const,
135
142
  url: await ctx.fileService.getFullFileUrl(item.url!),
136
143
  } as FileListItem;
137
144
  resultFiles.push(fileItem);
@@ -140,6 +147,71 @@ export const fileRouter = router({
140
147
  return resultFiles;
141
148
  }),
142
149
 
150
+ getKnowledgeItems: fileProcedure.input(QueryFileListSchema).query(async ({ ctx, input }) => {
151
+ const knowledgeItems = await ctx.knowledgeRepo.query(input);
152
+
153
+ // Process files (add chunk info and async task status)
154
+ const fileItems = knowledgeItems.filter((item) => item.sourceType === 'file');
155
+ const fileIds = fileItems.map((item) => item.id);
156
+ const chunks = await ctx.chunkModel.countByFileIds(fileIds);
157
+
158
+ const chunkTaskIds = fileItems.map((item) => item.chunkTaskId).filter(Boolean) as string[];
159
+ const chunkTasks = await ctx.asyncTaskModel.findByIds(chunkTaskIds, AsyncTaskType.Chunking);
160
+
161
+ const embeddingTaskIds = fileItems
162
+ .map((item) => item.embeddingTaskId)
163
+ .filter(Boolean) as string[];
164
+ const embeddingTasks = await ctx.asyncTaskModel.findByIds(
165
+ embeddingTaskIds,
166
+ AsyncTaskType.Embedding,
167
+ );
168
+
169
+ // Combine all items with their metadata
170
+ const resultItems = [] as any[];
171
+ for (const item of knowledgeItems) {
172
+ if (item.sourceType === 'file') {
173
+ const chunkTask = item.chunkTaskId
174
+ ? chunkTasks.find((task) => task.id === item.chunkTaskId)
175
+ : null;
176
+ const embeddingTask = item.embeddingTaskId
177
+ ? embeddingTasks.find((task) => task.id === item.embeddingTaskId)
178
+ : null;
179
+
180
+ resultItems.push({
181
+ ...item,
182
+ chunkCount: chunks.find((chunk) => chunk.id === item.id)?.count ?? null,
183
+ chunkingError: chunkTask?.error ?? null,
184
+ chunkingStatus: chunkTask?.status as AsyncTaskStatus,
185
+ editorData: null,
186
+ embeddingError: embeddingTask?.error ?? null,
187
+ embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
188
+ finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
189
+ url: await ctx.fileService.getFullFileUrl(item.url!),
190
+ } as FileListItem);
191
+ } else {
192
+ // Document item - no chunk processing needed, includes editorData
193
+ const documentItem = {
194
+ ...item,
195
+ chunkCount: null,
196
+ chunkingError: null,
197
+ chunkingStatus: null,
198
+ embeddingError: null,
199
+ embeddingStatus: null,
200
+ finishEmbedding: false,
201
+ } as FileListItem;
202
+ console.log('[API getKnowledgeItems] Processing document:', {
203
+ editorDataPreview: item.editorData ? JSON.stringify(item.editorData).slice(0, 100) : null,
204
+ hasEditorData: !!item.editorData,
205
+ id: item.id,
206
+ name: item.name,
207
+ });
208
+ resultItems.push(documentItem);
209
+ }
210
+ }
211
+
212
+ return resultItems;
213
+ }),
214
+
143
215
  removeAllFiles: fileProcedure.mutation(async ({ ctx }) => {
144
216
  return ctx.fileModel.clear();
145
217
  }),
@@ -0,0 +1,94 @@
1
+ import { AsyncTaskModel } from '@/database/models/asyncTask';
2
+ import { ChunkModel } from '@/database/models/chunk';
3
+ import { DocumentModel } from '@/database/models/document';
4
+ import { FileModel } from '@/database/models/file';
5
+ import { KnowledgeRepo } from '@/database/repositories/knowledge';
6
+ import { authedProcedure, router } from '@/libs/trpc/lambda';
7
+ import { serverDatabase } from '@/libs/trpc/lambda/middleware';
8
+ import { FileService } from '@/server/services/file';
9
+ import { AsyncTaskStatus, AsyncTaskType } from '@/types/asyncTask';
10
+ import { FileListItem, QueryFileListSchema } from '@/types/files';
11
+
12
+ const knowledgeProcedure = authedProcedure.use(serverDatabase).use(async (opts) => {
13
+ const { ctx } = opts;
14
+
15
+ return opts.next({
16
+ ctx: {
17
+ asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId),
18
+ chunkModel: new ChunkModel(ctx.serverDB, ctx.userId),
19
+ documentModel: new DocumentModel(ctx.serverDB, ctx.userId),
20
+ fileModel: new FileModel(ctx.serverDB, ctx.userId),
21
+ fileService: new FileService(ctx.serverDB, ctx.userId),
22
+ knowledgeRepo: new KnowledgeRepo(ctx.serverDB, ctx.userId),
23
+ },
24
+ });
25
+ });
26
+
27
+ export const knowledgeRouter = router({
28
+ getKnowledgeItems: knowledgeProcedure.input(QueryFileListSchema).query(async ({ ctx, input }) => {
29
+ const knowledgeItems = await ctx.knowledgeRepo.query(input);
30
+
31
+ // Process files (add chunk info and async task status)
32
+ const fileItems = knowledgeItems.filter((item) => item.sourceType === 'file');
33
+ const fileIds = fileItems.map((item) => item.id);
34
+ const chunks = await ctx.chunkModel.countByFileIds(fileIds);
35
+
36
+ const chunkTaskIds = fileItems.map((item) => item.chunkTaskId).filter(Boolean) as string[];
37
+ const chunkTasks = await ctx.asyncTaskModel.findByIds(chunkTaskIds, AsyncTaskType.Chunking);
38
+
39
+ const embeddingTaskIds = fileItems
40
+ .map((item) => item.embeddingTaskId)
41
+ .filter(Boolean) as string[];
42
+ const embeddingTasks = await ctx.asyncTaskModel.findByIds(
43
+ embeddingTaskIds,
44
+ AsyncTaskType.Embedding,
45
+ );
46
+
47
+ // Combine all items with their metadata
48
+ const resultItems = [] as any[];
49
+ for (const item of knowledgeItems) {
50
+ if (item.sourceType === 'file') {
51
+ const chunkTask = item.chunkTaskId
52
+ ? chunkTasks.find((task) => task.id === item.chunkTaskId)
53
+ : null;
54
+ const embeddingTask = item.embeddingTaskId
55
+ ? embeddingTasks.find((task) => task.id === item.embeddingTaskId)
56
+ : null;
57
+
58
+ resultItems.push({
59
+ ...item,
60
+ chunkCount: chunks.find((chunk) => chunk.id === item.id)?.count ?? null,
61
+ chunkingError: chunkTask?.error ?? null,
62
+ chunkingStatus: chunkTask?.status as AsyncTaskStatus,
63
+ editorData: null,
64
+ embeddingError: embeddingTask?.error ?? null,
65
+ embeddingStatus: embeddingTask?.status as AsyncTaskStatus,
66
+ finishEmbedding: embeddingTask?.status === AsyncTaskStatus.Success,
67
+ url: await ctx.fileService.getFullFileUrl(item.url!),
68
+ } as FileListItem);
69
+ } else {
70
+ // Document item - no chunk processing needed, includes editorData
71
+ const documentItem = {
72
+ ...item,
73
+ chunkCount: null,
74
+ chunkingError: null,
75
+ chunkingStatus: null,
76
+ embeddingError: null,
77
+ embeddingStatus: null,
78
+ finishEmbedding: false,
79
+ } as FileListItem;
80
+ console.log('[API getKnowledgeItems] Processing document:', {
81
+ editorDataPreview: item.editorData ? JSON.stringify(item.editorData).slice(0, 100) : null,
82
+ hasEditorData: !!item.editorData,
83
+ id: item.id,
84
+ name: item.name,
85
+ });
86
+ resultItems.push(documentItem);
87
+ }
88
+ }
89
+
90
+ return resultItems;
91
+ }),
92
+ });
93
+
94
+ export type KnowledgeRouter = typeof knowledgeRouter;
@@ -1,4 +1,5 @@
1
1
  import { LobeChatDatabase } from '@lobechat/database';
2
+ import { DocumentItem } from '@lobechat/database/schemas';
2
3
  import { loadFile } from '@lobechat/file-loaders';
3
4
  import debug from 'debug';
4
5
 
@@ -15,14 +16,116 @@ export class DocumentService {
15
16
  private fileModel: FileModel;
16
17
  private documentModel: DocumentModel;
17
18
  private fileService: FileService;
19
+ private db: LobeChatDatabase;
18
20
 
19
21
  constructor(db: LobeChatDatabase, userId: string) {
20
22
  this.userId = userId;
23
+ this.db = db;
21
24
  this.fileModel = new FileModel(db, userId);
22
25
  this.fileService = new FileService(db, userId);
23
26
  this.documentModel = new DocumentModel(db, userId);
24
27
  }
25
28
 
29
+ /**
30
+ * Create a document
31
+ */
32
+ async createDocument(params: {
33
+ content?: string;
34
+ editorData: Record<string, any>;
35
+ fileType?: string;
36
+ knowledgeBaseId?: string;
37
+ metadata?: Record<string, any>;
38
+ rawData?: string;
39
+ title: string;
40
+ }): Promise<DocumentItem> {
41
+ const {
42
+ content,
43
+ editorData,
44
+ title,
45
+ fileType = 'custom/document',
46
+ metadata,
47
+ knowledgeBaseId,
48
+ } = params;
49
+
50
+ // Calculate character and line counts
51
+ const totalCharCount = content?.length || 0;
52
+ const totalLineCount = content?.split('\n').length || 0;
53
+
54
+ const document = await this.documentModel.create({
55
+ content,
56
+ editorData,
57
+ fileId: knowledgeBaseId ? null : undefined,
58
+ fileType,
59
+ filename: title,
60
+ metadata,
61
+ pages: undefined,
62
+ source: 'document',
63
+ sourceType: 'api',
64
+ title,
65
+ totalCharCount,
66
+ totalLineCount,
67
+ });
68
+
69
+ return document;
70
+ }
71
+
72
+ /**
73
+ * Query all documents
74
+ */
75
+ async queryDocuments() {
76
+ return this.documentModel.query();
77
+ }
78
+
79
+ /**
80
+ * Get document by ID
81
+ */
82
+ async getDocumentById(id: string) {
83
+ return this.documentModel.findById(id);
84
+ }
85
+
86
+ /**
87
+ * Delete document
88
+ */
89
+ async deleteDocument(id: string) {
90
+ return this.documentModel.delete(id);
91
+ }
92
+
93
+ /**
94
+ * Update document
95
+ */
96
+ async updateDocument(
97
+ id: string,
98
+ params: {
99
+ content?: string;
100
+ editorData?: Record<string, any>;
101
+ metadata?: Record<string, any>;
102
+ title?: string;
103
+ },
104
+ ) {
105
+ const updates: any = {};
106
+
107
+ if (params.content !== undefined) {
108
+ updates.content = params.content;
109
+ updates.totalCharCount = params.content.length;
110
+ updates.totalLineCount = params.content.split('\n').length;
111
+ }
112
+
113
+ if (params.editorData !== undefined) {
114
+ updates.editorData = params.editorData;
115
+ }
116
+
117
+ if (params.title !== undefined) {
118
+ updates.title = params.title;
119
+ updates.filename = params.title;
120
+ }
121
+
122
+ if (params.metadata !== undefined) {
123
+ updates.metadata = params.metadata;
124
+ }
125
+
126
+ return this.documentModel.update(id, updates);
127
+ }
128
+
26
129
  /**
27
130
  * 解析文件内容
28
131
  *