@lobehub/lobehub 2.0.0-next.97 → 2.0.0-next.99

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 (150) hide show
  1. package/.console-log-whitelist.json +14 -0
  2. package/.github/workflows/check-console-log.yml +117 -0
  3. package/.github/workflows/desktop-pr-build.yml +4 -4
  4. package/.github/workflows/release-desktop-beta.yml +4 -4
  5. package/.github/workflows/release.yml +1 -1
  6. package/.github/workflows/test.yml +5 -5
  7. package/CHANGELOG.md +58 -0
  8. package/apps/desktop/src/main/services/__tests__/fileSrv.test.ts +603 -0
  9. package/changelog/v1.json +21 -0
  10. package/codecov.yml +1 -0
  11. package/docs/development/database-schema.dbml +1 -0
  12. package/e2e/package.json +1 -1
  13. package/locales/ar/file.json +9 -11
  14. package/locales/ar/plugin.json +34 -22
  15. package/locales/ar/tool.json +8 -0
  16. package/locales/bg-BG/file.json +8 -10
  17. package/locales/bg-BG/plugin.json +34 -22
  18. package/locales/bg-BG/tool.json +8 -0
  19. package/locales/de-DE/file.json +9 -11
  20. package/locales/de-DE/plugin.json +34 -22
  21. package/locales/de-DE/tool.json +8 -0
  22. package/locales/en-US/file.json +12 -14
  23. package/locales/en-US/plugin.json +34 -22
  24. package/locales/en-US/tool.json +8 -0
  25. package/locales/es-ES/file.json +7 -9
  26. package/locales/es-ES/plugin.json +34 -22
  27. package/locales/es-ES/tool.json +8 -0
  28. package/locales/fa-IR/file.json +9 -11
  29. package/locales/fa-IR/plugin.json +34 -22
  30. package/locales/fa-IR/tool.json +8 -0
  31. package/locales/fr-FR/file.json +6 -8
  32. package/locales/fr-FR/plugin.json +34 -22
  33. package/locales/fr-FR/tool.json +8 -0
  34. package/locales/it-IT/file.json +8 -10
  35. package/locales/it-IT/plugin.json +34 -22
  36. package/locales/it-IT/tool.json +8 -0
  37. package/locales/ja-JP/file.json +10 -12
  38. package/locales/ja-JP/plugin.json +34 -22
  39. package/locales/ja-JP/tool.json +8 -0
  40. package/locales/ko-KR/file.json +8 -10
  41. package/locales/ko-KR/plugin.json +34 -22
  42. package/locales/ko-KR/tool.json +8 -0
  43. package/locales/nl-NL/file.json +8 -10
  44. package/locales/nl-NL/plugin.json +34 -22
  45. package/locales/nl-NL/tool.json +8 -0
  46. package/locales/pl-PL/file.json +7 -9
  47. package/locales/pl-PL/plugin.json +34 -22
  48. package/locales/pl-PL/tool.json +8 -0
  49. package/locales/pt-BR/file.json +7 -9
  50. package/locales/pt-BR/plugin.json +34 -22
  51. package/locales/pt-BR/tool.json +8 -0
  52. package/locales/ru-RU/file.json +9 -11
  53. package/locales/ru-RU/plugin.json +34 -22
  54. package/locales/ru-RU/tool.json +8 -0
  55. package/locales/tr-TR/file.json +8 -10
  56. package/locales/tr-TR/plugin.json +34 -22
  57. package/locales/tr-TR/tool.json +8 -0
  58. package/locales/vi-VN/file.json +9 -11
  59. package/locales/vi-VN/plugin.json +34 -22
  60. package/locales/vi-VN/tool.json +8 -0
  61. package/locales/zh-CN/file.json +10 -12
  62. package/locales/zh-CN/plugin.json +34 -22
  63. package/locales/zh-CN/tool.json +8 -0
  64. package/locales/zh-TW/file.json +10 -12
  65. package/locales/zh-TW/plugin.json +34 -22
  66. package/locales/zh-TW/tool.json +8 -0
  67. package/package.json +3 -2
  68. package/packages/database/migrations/0047_add_slug_document.sql +6 -0
  69. package/packages/database/migrations/meta/0047_snapshot.json +7891 -0
  70. package/packages/database/migrations/meta/_journal.json +7 -0
  71. package/packages/database/src/core/migrations.json +16 -7
  72. package/packages/database/src/models/__tests__/document.test.ts +149 -0
  73. package/packages/database/src/models/chunk.ts +3 -1
  74. package/packages/database/src/models/document.ts +10 -4
  75. package/packages/database/src/schemas/file.ts +7 -1
  76. package/packages/model-bank/src/aiModels/qwen.ts +5 -3
  77. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +21 -21
  78. package/packages/obervability-otel/package.json +2 -2
  79. package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatFileContents.test.ts.snap +75 -0
  80. package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatNoSearchResults.test.ts.snap +45 -0
  81. package/packages/prompts/src/prompts/knowledgeBaseQA/__snapshots__/formatSearchResults.test.ts.snap +82 -0
  82. package/packages/prompts/src/prompts/knowledgeBaseQA/formatFileContents.test.ts +118 -0
  83. package/packages/prompts/src/prompts/knowledgeBaseQA/formatFileContents.ts +31 -0
  84. package/packages/prompts/src/prompts/knowledgeBaseQA/formatNoSearchResults.test.ts +25 -0
  85. package/packages/prompts/src/prompts/knowledgeBaseQA/formatNoSearchResults.ts +13 -0
  86. package/packages/prompts/src/prompts/knowledgeBaseQA/formatSearchResults.test.ts +191 -0
  87. package/packages/prompts/src/prompts/knowledgeBaseQA/formatSearchResults.ts +50 -0
  88. package/packages/prompts/src/prompts/knowledgeBaseQA/index.ts +6 -0
  89. package/packages/types/src/rag.ts +13 -4
  90. package/scripts/checkConsoleLog.mts +148 -0
  91. package/scripts/prebuild.mts +5 -5
  92. package/src/app/[variants]/(main)/changelog/index.tsx +1 -1
  93. package/src/app/[variants]/(main)/settings/_layout/Desktop/index.tsx +20 -16
  94. package/src/app/[variants]/(main)/settings/provider/(list)/ProviderGrid/Card.tsx +6 -3
  95. package/src/app/[variants]/(main)/settings/provider/(list)/index.tsx +3 -2
  96. package/src/app/[variants]/(main)/settings/provider/detail/index.tsx +14 -4
  97. package/src/app/[variants]/desktopRouter.config.tsx +23 -0
  98. package/src/app/[variants]/mobileRouter.config.tsx +23 -0
  99. package/src/features/ChatInput/ActionBar/Token/TokenTag.tsx +2 -2
  100. package/src/features/ChatInput/ActionBar/Token/TokenTagForGroupChat.tsx +2 -2
  101. package/src/features/ChatList/Messages/Group/Tool/Inspector/ToolTitle.tsx +5 -23
  102. package/src/features/ChatList/Messages/Tool/Inspector/ToolTitle.tsx +5 -25
  103. package/src/features/KnowledgeManager/DocumentExplorer/NoteEditorModal.tsx +0 -20
  104. package/src/features/KnowledgeManager/DocumentExplorer/index.tsx +3 -3
  105. package/src/features/KnowledgeManager/FileExplorer/MasonryFileItem/index.tsx +0 -20
  106. package/src/features/KnowledgeManager/Header/AddButton.tsx +0 -1
  107. package/src/features/KnowledgeManager/Header/NewNoteButton.tsx +1 -1
  108. package/src/features/KnowledgeManager/Header/TogglePanelButton.tsx +2 -2
  109. package/src/features/KnowledgeManager/Home/UploadEntries.tsx +2 -2
  110. package/src/features/KnowledgeManager/Home/index.tsx +4 -4
  111. package/src/features/PluginsUI/Render/BuiltinType/index.tsx +1 -1
  112. package/src/features/User/UserPanel/useMenu.tsx +7 -3
  113. package/src/helpers/toolEngineering/index.test.ts +3 -3
  114. package/src/helpers/toolEngineering/index.ts +17 -4
  115. package/src/libs/trpc/client/lambda.ts +0 -6
  116. package/src/locales/default/file.ts +11 -13
  117. package/src/locales/default/plugin.ts +34 -22
  118. package/src/locales/default/tool.ts +13 -5
  119. package/src/server/routers/lambda/chunk.ts +168 -41
  120. package/src/services/chat/chat.test.ts +3 -3
  121. package/src/services/chat/index.ts +2 -2
  122. package/src/services/rag.ts +6 -2
  123. package/src/store/chat/agents/__tests__/createAgentExecutors/helpers/testExecutor.ts +1 -4
  124. package/src/store/chat/slices/aiChat/actions/__tests__/conversationLifecycle.test.ts +11 -0
  125. package/src/store/chat/slices/aiChat/actions/__tests__/rag.test.ts +0 -87
  126. package/src/store/chat/slices/aiChat/actions/__tests__/streamingExecutor.test.ts +2 -69
  127. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +0 -2
  128. package/src/store/chat/slices/aiChat/actions/rag.ts +0 -47
  129. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +8 -69
  130. package/src/store/chat/slices/builtinTool/actions/index.ts +4 -1
  131. package/src/store/chat/slices/builtinTool/actions/knowledgeBase.ts +174 -0
  132. package/src/store/chat/slices/operation/types.ts +1 -0
  133. package/src/store/chat/slices/thread/action.test.ts +0 -1
  134. package/src/store/chat/slices/thread/action.ts +0 -1
  135. package/src/tools/executionRuntimes.ts +3 -0
  136. package/src/tools/identifiers.ts +13 -0
  137. package/src/tools/index.ts +7 -0
  138. package/src/tools/knowledge-base/ExecutionRuntime/index.ts +96 -0
  139. package/src/tools/knowledge-base/Render/ReadKnowledge/FileCard.tsx +135 -0
  140. package/src/tools/knowledge-base/Render/ReadKnowledge/index.tsx +27 -0
  141. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/Item/index.tsx +54 -0
  142. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/Item/style.ts +51 -0
  143. package/src/tools/knowledge-base/Render/SearchKnowledgeBase/index.tsx +23 -0
  144. package/src/tools/knowledge-base/Render/index.ts +7 -0
  145. package/src/tools/knowledge-base/index.ts +64 -0
  146. package/src/tools/knowledge-base/systemRole.ts +102 -0
  147. package/src/tools/knowledge-base/type.ts +25 -0
  148. package/src/tools/local-system/Intervention/WriteFile/index.tsx +1 -1
  149. package/src/tools/renders.ts +4 -0
  150. package/src/store/chat/agents/createToolEngine.ts +0 -22
@@ -13,6 +13,9 @@ import { WebBrowsingManifest } from '@/tools/web-browsing';
13
13
  import { getSearchConfig } from '../getSearchConfig';
14
14
  import { isCanUseFC } from '../isCanUseFC';
15
15
  import { shouldEnableTool } from '../toolFilters';
16
+ import { KnowledgeBaseManifest } from '@/tools/knowledge-base';
17
+ import { getAgentStoreState } from '@/store/agent';
18
+ import { agentSelectors } from '@/store/agent/slices/chat';
16
19
 
17
20
  /**
18
21
  * Tools engine configuration options
@@ -53,23 +56,33 @@ export const createToolsEngine = (config: ToolsEngineConfig = {}): ToolsEngine =
53
56
  });
54
57
  };
55
58
 
56
- export const createChatToolsEngine = (workingModel: WorkingModel) =>
59
+ export const createAgentToolsEngine = (workingModel: WorkingModel) =>
57
60
  createToolsEngine({
58
- // Add WebBrowsingManifest as default tool
59
- defaultToolIds: [WebBrowsingManifest.identifier],
61
+ // Add default tools based on configuration
62
+ defaultToolIds: [
63
+ WebBrowsingManifest.identifier,
64
+ // Only add KnowledgeBase tool if knowledge is enabled
65
+ KnowledgeBaseManifest.identifier,
66
+ ],
60
67
  // Create search-aware enableChecker for this request
61
68
  enableChecker: ({ pluginId }) => {
62
69
  // Check platform-specific constraints (e.g., LocalSystem desktop-only)
63
70
  if (!shouldEnableTool(pluginId)) {
64
71
  return false;
65
72
  }
66
-
67
73
  // For WebBrowsingManifest, apply search logic
68
74
  if (pluginId === WebBrowsingManifest.identifier) {
69
75
  const searchConfig = getSearchConfig(workingModel.model, workingModel.provider);
70
76
  return searchConfig.useApplicationBuiltinSearchTool;
71
77
  }
72
78
 
79
+ // For KnowledgeBaseManifest, only enable if knowledge is enabled
80
+ if (pluginId === KnowledgeBaseManifest.identifier) {
81
+ const agentState = getAgentStoreState();
82
+
83
+ return agentSelectors.hasEnabledKnowledge(agentState);
84
+ }
85
+
73
86
  // For all other plugins, enable by default
74
87
  return true;
75
88
  },
@@ -34,9 +34,6 @@ const errorHandlingLink: TRPCLink<LambdaRouter> = () => {
34
34
  // Don't show notifications for abort errors
35
35
  if (showError && !isAbortError) {
36
36
  const { loginRequired } = await import('@/components/Error/loginRequiredNotification');
37
- const { fetchErrorNotification } = await import(
38
- '@/components/Error/fetchErrorNotification'
39
- );
40
37
 
41
38
  switch (status) {
42
39
  case 401: {
@@ -53,9 +50,6 @@ const errorHandlingLink: TRPCLink<LambdaRouter> = () => {
53
50
 
54
51
  default: {
55
52
  console.error(err);
56
- if (fetchErrorNotification && status) {
57
- fetchErrorNotification.error({ errorMessage: err.message, status });
58
- }
59
53
  }
60
54
  }
61
55
  }
@@ -55,11 +55,11 @@ export default {
55
55
  },
56
56
  documentList: {
57
57
  copyContent: '复制全文',
58
- documentCount: '共 {{count}} 个文档',
59
58
  duplicate: '创建副本',
60
- empty: '暂无文档,点击上方按钮创建你的第一篇文档',
61
- noResults: '未找到匹配的文档',
62
- selectNote: '选择一个文档开始编辑',
59
+ empty: '暂无文稿,点击上方按钮创建你的第一篇文稿',
60
+ noResults: '未找到匹配的文稿',
61
+ pageCount: '共 {{count}} 个文稿',
62
+ selectNote: '选择一个文稿开始编辑',
63
63
  untitled: '无标题',
64
64
  },
65
65
  empty: '暂无已上传文件/文件夹',
@@ -70,7 +70,6 @@ export default {
70
70
  uploadFile: '上传文件',
71
71
  uploadFolder: '上传文件夹',
72
72
  },
73
- newDocumentButton: '新建文档',
74
73
  newNoteDialog: {
75
74
  cancel: '取消',
76
75
  editTitle: '编辑文档',
@@ -83,14 +82,15 @@ export default {
83
82
  title: '新建文档',
84
83
  updateSuccess: '文档更新成功',
85
84
  },
85
+ newPageButton: '新建文稿',
86
86
  uploadButton: '上传',
87
87
  },
88
88
  home: {
89
89
  getStarted: '开始使用',
90
90
  greeting: '开始',
91
91
  quickActions: '快捷操作',
92
- recentDocuments: '最近文档',
93
92
  recentFiles: '最近文件',
93
+ recentPages: '最近文稿',
94
94
  subtitle: '欢迎使用知识库,从这里开始管理你的文档和文档',
95
95
  uploadEntries: {
96
96
  files: {
@@ -102,8 +102,8 @@ export default {
102
102
  knowledgeBase: {
103
103
  title: '新建知识库',
104
104
  },
105
- newDocument: {
106
- title: '新建文档',
105
+ newPage: {
106
+ title: '新建文稿',
107
107
  },
108
108
  },
109
109
  },
@@ -117,8 +117,8 @@ export default {
117
117
  title: '知识库',
118
118
  },
119
119
  menu: {
120
- allDocuments: '全部文档',
121
120
  allFiles: '全部文件',
121
+ allPages: '全部文稿',
122
122
  },
123
123
  networkError: '获取知识库失败,请检测网络连接后重试',
124
124
  notSupportGuide: {
@@ -143,8 +143,8 @@ export default {
143
143
  downloadFile: '下载文件',
144
144
  unsupportedFileAndContact: '此文件格式暂不支持在线预览,如有预览诉求,欢迎<1>反馈给我们</1>',
145
145
  },
146
- searchDocumentPlaceholder: '搜索文档',
147
146
  searchFilePlaceholder: '搜索文件',
147
+ searchPagePlaceholder: '搜索文稿',
148
148
  tab: {
149
149
  all: '全部',
150
150
  audios: '语音',
@@ -157,9 +157,7 @@ export default {
157
157
  websites: '网页',
158
158
  },
159
159
  title: '知识库',
160
- toggleLeftPanel: {
161
- title: '显示/隐藏左侧面板',
162
- },
160
+ toggleLeftPanel: '显示/隐藏左侧面板',
163
161
  uploadDock: {
164
162
  body: {
165
163
  collapse: '收起',
@@ -1,4 +1,38 @@
1
1
  export default {
2
+ builtins: {
3
+ 'lobe-knowledge-base': {
4
+ apiName: {
5
+ readKnowledge: '读取知识库内容',
6
+ searchKnowledgeBase: '搜索知识库',
7
+ },
8
+ title: '知识库',
9
+ },
10
+ 'lobe-local-system': {
11
+ apiName: {
12
+ editLocalFile: '编辑文件',
13
+ getCommandOutput: '获取代码输出',
14
+ globLocalFiles: '匹配搜索文件',
15
+ grepContent: '搜索内容',
16
+ killCommand: '终止代码执行',
17
+ listLocalFiles: '查看文件列表',
18
+ moveLocalFiles: '移动文件',
19
+ readLocalFile: '读取文件内容',
20
+ renameLocalFile: '重命名',
21
+ runCommand: '执行代码',
22
+ searchLocalFiles: '搜索文件',
23
+ writeLocalFile: '写入文件',
24
+ },
25
+ title: '本地系统',
26
+ },
27
+ 'lobe-web-browsing': {
28
+ apiName: {
29
+ crawlMultiPages: '读取多个页面内容',
30
+ crawlSinglePage: '读取页面内容',
31
+ search: '搜索页面',
32
+ },
33
+ title: '联网搜索',
34
+ },
35
+ },
2
36
  confirm: '确定',
3
37
  debug: {
4
38
  arguments: '调用参数',
@@ -253,23 +287,6 @@ export default {
253
287
  content: '调用插件中...',
254
288
  plugin: '插件运行中...',
255
289
  },
256
- localSystem: {
257
- apiName: {
258
- editLocalFile: '编辑文件',
259
- getCommandOutput: '获取代码输出',
260
- globLocalFiles: '匹配搜索文件',
261
- grepContent: '搜索内容',
262
- killCommand: '终止代码执行',
263
- listLocalFiles: '查看文件列表',
264
- moveLocalFiles: '移动文件',
265
- readLocalFile: '读取文件内容',
266
- renameLocalFile: '重命名',
267
- runCommand: '执行代码',
268
- searchLocalFiles: '搜索文件',
269
- writeLocalFile: '写入文件',
270
- },
271
- title: '本地系统',
272
- },
273
290
  mcpInstall: {
274
291
  CHECKING_INSTALLATION: '检查安装环境...',
275
292
  COMPLETED: '安装完成',
@@ -378,11 +395,6 @@ export default {
378
395
  warning: '⚠️ 请确认您信任此插件的来源,恶意插件可能会危害您的系统安全。',
379
396
  },
380
397
  search: {
381
- apiName: {
382
- crawlMultiPages: '读取多个页面内容',
383
- crawlSinglePage: '读取页面内容',
384
- search: '搜索页面',
385
- },
386
398
  config: {
387
399
  addKey: '添加秘钥',
388
400
  close: '删除',
@@ -1,12 +1,12 @@
1
1
  export default {
2
- codeInterpreter: {
2
+ 'codeInterpreter': {
3
3
  error: '执行错误',
4
4
  executing: '执行中...',
5
5
  files: '文件:',
6
6
  output: '输出:',
7
7
  returnValue: '返回值:',
8
8
  },
9
- dalle: {
9
+ 'dalle': {
10
10
  autoGenerate: '自动生成',
11
11
  downloading: 'DallE3 生成的图片链接有效期仅1小时,正在缓存图片到本地...',
12
12
  generate: '生成',
@@ -14,7 +14,15 @@ export default {
14
14
  images: '图片:',
15
15
  prompt: '提示词',
16
16
  },
17
- localFiles: {
17
+ 'lobe-knowledge-base': {
18
+ readKnowledge: {
19
+ meta: {
20
+ chars: '字符数',
21
+ lines: '行数',
22
+ },
23
+ },
24
+ },
25
+ 'localFiles': {
18
26
  editFile: {
19
27
  newString: '替换为',
20
28
  oldString: '查找内容',
@@ -47,7 +55,7 @@ export default {
47
55
  truncated: '已截断',
48
56
  },
49
57
  },
50
- search: {
58
+ 'search': {
51
59
  createNewSearch: '创建新的搜索记录',
52
60
  emptyResult: '没有搜索到结果,请修改关键词后重试',
53
61
  genAiMessage: '创建助手消息',
@@ -94,7 +102,7 @@ export default {
94
102
  summaryTooltip: '总结当前内容',
95
103
  viewMoreResults: '查看更多 {{results}} 个结果',
96
104
  },
97
- updateArgs: {
105
+ 'updateArgs': {
98
106
  duplicateKeyError: '字段键必须唯一',
99
107
  form: {
100
108
  add: '添加一项',
@@ -1,32 +1,50 @@
1
1
  import { DEFAULT_FILE_EMBEDDING_MODEL_ITEM } from '@lobechat/const';
2
- import { SemanticSearchSchema } from '@lobechat/types';
2
+ import {
3
+ ChatSemanticSearchChunk,
4
+ FileSearchResult,
5
+ ProviderConfig,
6
+ SemanticSearchSchema,
7
+ } from '@lobechat/types';
3
8
  import { TRPCError } from '@trpc/server';
4
9
  import { inArray } from 'drizzle-orm';
10
+ import pMap from 'p-map';
5
11
  import { z } from 'zod';
6
12
 
7
13
  import { AsyncTaskModel } from '@/database/models/asyncTask';
8
14
  import { ChunkModel } from '@/database/models/chunk';
15
+ import { DocumentModel } from '@/database/models/document';
9
16
  import { EmbeddingModel } from '@/database/models/embedding';
10
17
  import { FileModel } from '@/database/models/file';
11
18
  import { MessageModel } from '@/database/models/message';
19
+ import { AiInfraRepos } from '@/database/repositories/aiInfra';
12
20
  import { knowledgeBaseFiles } from '@/database/schemas';
13
21
  import { authedProcedure, router } from '@/libs/trpc/lambda';
14
22
  import { keyVaults, serverDatabase } from '@/libs/trpc/lambda/middleware';
15
- import { getServerDefaultFilesConfig } from '@/server/globalConfig';
23
+ import { getServerDefaultFilesConfig, getServerGlobalConfig } from '@/server/globalConfig';
24
+ import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
16
25
  import { initModelRuntimeWithUserPayload } from '@/server/modules/ModelRuntime';
17
26
  import { ChunkService } from '@/server/services/chunk';
27
+ import { DocumentService } from '@/server/services/document';
18
28
 
19
29
  const chunkProcedure = authedProcedure
20
30
  .use(serverDatabase)
21
31
  .use(keyVaults)
22
32
  .use(async (opts) => {
23
33
  const { ctx } = opts;
34
+ const { aiProvider } = await getServerGlobalConfig();
24
35
 
25
36
  return opts.next({
26
37
  ctx: {
38
+ aiInfraRepos: new AiInfraRepos(
39
+ ctx.serverDB,
40
+ ctx.userId,
41
+ aiProvider as Record<string, ProviderConfig>,
42
+ ),
27
43
  asyncTaskModel: new AsyncTaskModel(ctx.serverDB, ctx.userId),
28
44
  chunkModel: new ChunkModel(ctx.serverDB, ctx.userId),
29
45
  chunkService: new ChunkService(ctx.serverDB, ctx.userId),
46
+ documentModel: new DocumentModel(ctx.serverDB, ctx.userId),
47
+ documentService: new DocumentService(ctx.serverDB, ctx.userId),
30
48
  embeddingModel: new EmbeddingModel(ctx.serverDB, ctx.userId),
31
49
  fileModel: new FileModel(ctx.serverDB, ctx.userId),
32
50
  messageModel: new MessageModel(ctx.serverDB, ctx.userId),
@@ -34,6 +52,50 @@ const chunkProcedure = authedProcedure
34
52
  });
35
53
  });
36
54
 
55
+ /**
56
+ * Group chunks by file and calculate relevance scores
57
+ */
58
+ const groupAndRankFiles = (chunks: ChatSemanticSearchChunk[], topK: number): FileSearchResult[] => {
59
+ const fileMap = new Map<string, FileSearchResult>();
60
+
61
+ // Group chunks by file
62
+ for (const chunk of chunks) {
63
+ const fileId = chunk.fileId || 'unknown';
64
+ const fileName = chunk.fileName || `File ${fileId}`;
65
+
66
+ if (!fileMap.has(fileId)) {
67
+ fileMap.set(fileId, {
68
+ fileId,
69
+ fileName,
70
+ relevanceScore: 0,
71
+ topChunks: [],
72
+ });
73
+ }
74
+
75
+ const fileResult = fileMap.get(fileId)!;
76
+ fileResult.topChunks.push({
77
+ id: chunk.id,
78
+ similarity: chunk.similarity,
79
+ text: chunk.text || '',
80
+ });
81
+ }
82
+
83
+ // Calculate relevance score for each file (average of top 3 chunks)
84
+ for (const fileResult of fileMap.values()) {
85
+ fileResult.topChunks.sort((a, b) => b.similarity - a.similarity);
86
+ const top3 = fileResult.topChunks.slice(0, 3);
87
+ fileResult.relevanceScore =
88
+ top3.reduce((sum, chunk) => sum + chunk.similarity, 0) / top3.length;
89
+ // Keep only top chunks per file
90
+ fileResult.topChunks = fileResult.topChunks.slice(0, 3);
91
+ }
92
+
93
+ // Sort files by relevance score and return top K
94
+ return Array.from(fileMap.values())
95
+ .sort((a, b) => b.relevanceScore - a.relevanceScore)
96
+ .slice(0, topK);
97
+ };
98
+
37
99
  export const chunkRouter = router({
38
100
  createEmbeddingChunksTask: chunkProcedure
39
101
  .input(
@@ -78,6 +140,66 @@ export const chunkRouter = router({
78
140
  };
79
141
  }),
80
142
 
143
+ getFileContents: chunkProcedure
144
+ .input(
145
+ z.object({
146
+ fileIds: z.array(z.string()),
147
+ }),
148
+ )
149
+ .mutation(async ({ ctx, input }) => {
150
+ return await pMap(
151
+ input.fileIds,
152
+ async (fileId) => {
153
+ // 1. Find file information
154
+ const file = await ctx.fileModel.findById(fileId);
155
+ if (!file) {
156
+ return {
157
+ content: '',
158
+ error: 'File not found',
159
+ fileId,
160
+ filename: `Unknown file ${fileId}`,
161
+ };
162
+ }
163
+
164
+ // 2. Find existing parsed document
165
+ let document = await ctx.documentModel.findByFileId(fileId);
166
+
167
+ // 3. If not exists, parse the file
168
+ if (!document) {
169
+ try {
170
+ document = await ctx.documentService.parseFile(fileId);
171
+ } catch (error) {
172
+ return {
173
+ content: '',
174
+ error: `Failed to parse file: ${(error as Error).message}`,
175
+ fileId,
176
+ filename: file.name,
177
+ };
178
+ }
179
+ }
180
+
181
+ // 4. Calculate file statistics
182
+ const content = document.content || '';
183
+ const lines = content.split('\n');
184
+ const totalLineCount = lines.length;
185
+ const totalCharCount = content.length;
186
+ const preview = lines.slice(0, 5).join('\n');
187
+
188
+ // 5. Return content with details
189
+ return {
190
+ content,
191
+ fileId,
192
+ filename: file.name,
193
+ metadata: document.metadata,
194
+ preview,
195
+ totalCharCount,
196
+ totalLineCount,
197
+ };
198
+ },
199
+ { concurrency: 3 },
200
+ );
201
+ }),
202
+
81
203
  retryParseFileTask: chunkProcedure
82
204
  .input(
83
205
  z.object({
@@ -117,7 +239,6 @@ export const chunkRouter = router({
117
239
  input: input.query,
118
240
  model,
119
241
  });
120
- console.timeEnd('embedding');
121
242
 
122
243
  return ctx.chunkModel.semanticSearch({
123
244
  embedding: embeddings![0],
@@ -130,47 +251,30 @@ export const chunkRouter = router({
130
251
  .input(SemanticSearchSchema)
131
252
  .mutation(async ({ ctx, input }) => {
132
253
  try {
133
- const item = await ctx.messageModel.findMessageQueriesById(input.messageId);
134
254
  const { model, provider } =
135
255
  getServerDefaultFilesConfig().embeddingModel || DEFAULT_FILE_EMBEDDING_MODEL_ITEM;
136
256
  let embedding: number[];
137
- let ragQueryId: string;
138
-
139
- // if there is no message rag or it's embeddings, then we need to create one
140
- if (!item || !item.embeddings) {
141
- // TODO: need to support customize
142
- const agentRuntime = await initModelRuntimeWithUserPayload(provider, ctx.jwtPayload);
143
-
144
- // slice content to make sure in the context window limit
145
- const query =
146
- input.rewriteQuery.length > 8000
147
- ? input.rewriteQuery.slice(0, 8000)
148
- : input.rewriteQuery;
149
-
150
- const embeddings = await agentRuntime.embeddings({
151
- dimensions: 1024,
152
- input: query,
153
- model,
154
- });
155
257
 
156
- embedding = embeddings![0];
157
- const embeddingsId = await ctx.embeddingModel.create({
158
- embeddings: embedding,
159
- model,
160
- });
258
+ const providerDetail = await ctx.aiInfraRepos.getAiProviderDetail(
259
+ provider,
260
+ KeyVaultsGateKeeper.getUserKeyVaults,
261
+ );
161
262
 
162
- const result = await ctx.messageModel.createMessageQuery({
163
- embeddingsId,
164
- messageId: input.messageId,
165
- rewriteQuery: input.rewriteQuery,
166
- userQuery: input.userQuery,
167
- });
263
+ const modelRuntime = initModelRuntimeWithUserPayload(
264
+ provider,
265
+ providerDetail.keyVaults || {},
266
+ );
168
267
 
169
- ragQueryId = result.id;
170
- } else {
171
- embedding = item.embeddings;
172
- ragQueryId = item.id;
173
- }
268
+ // slice content to make sure in the context window limit
269
+ const query = input.query.length > 8000 ? input.query.slice(0, 8000) : input.query;
270
+
271
+ const embeddings = await modelRuntime.embeddings({
272
+ dimensions: 1024,
273
+ input: query,
274
+ model,
275
+ });
276
+
277
+ embedding = embeddings![0];
174
278
 
175
279
  let finalFileIds = input.fileIds ?? [];
176
280
 
@@ -185,18 +289,41 @@ export const chunkRouter = router({
185
289
  const chunks = await ctx.chunkModel.semanticSearchForChat({
186
290
  embedding,
187
291
  fileIds: finalFileIds,
188
- query: input.rewriteQuery,
292
+ query: input.query,
293
+ topK: input.topK,
189
294
  });
190
295
 
296
+ // Group chunks by file and calculate relevance scores
297
+ const fileResults = groupAndRankFiles(chunks, input.topK || 15);
298
+
191
299
  // TODO: need to rerank the chunks
192
300
 
193
- return { chunks, queryId: ragQueryId };
301
+ return { chunks, fileResults };
194
302
  } catch (e) {
195
303
  console.error(e);
196
304
 
305
+ const error = e as any;
306
+ const errorType = error.errorType;
307
+
308
+ // Map business error types to appropriate HTTP status codes
309
+ if (errorType === 'InvalidProviderAPIKey') {
310
+ throw new TRPCError({
311
+ code: 'METHOD_NOT_SUPPORTED',
312
+ message: error.message || 'Invalid API key for embedding provider',
313
+ });
314
+ }
315
+
316
+ if (errorType === 'ProviderBizError') {
317
+ throw new TRPCError({
318
+ code: 'BAD_REQUEST',
319
+ message: error.message || 'Provider service error',
320
+ });
321
+ }
322
+
323
+ // For unknown errors, still return 500 but with proper message
197
324
  throw new TRPCError({
198
325
  code: 'INTERNAL_SERVER_ERROR',
199
- message: (e as any).errorType || JSON.stringify(e),
326
+ message: error.message || errorType || 'Failed to perform semantic search',
200
327
  });
201
328
  }
202
329
  }),
@@ -913,7 +913,7 @@ describe('ChatService', () => {
913
913
  enabledToolIds: [WebBrowsingManifest.identifier],
914
914
  }),
915
915
  };
916
- vi.spyOn(toolEngineeringModule, 'createChatToolsEngine').mockReturnValue(
916
+ vi.spyOn(toolEngineeringModule, 'createAgentToolsEngine').mockReturnValue(
917
917
  mockToolsEngine as any,
918
918
  );
919
919
 
@@ -964,7 +964,7 @@ describe('ChatService', () => {
964
964
  enabledToolIds: [WebBrowsingManifest.identifier],
965
965
  }),
966
966
  };
967
- vi.spyOn(toolEngineeringModule, 'createChatToolsEngine').mockReturnValue(
967
+ vi.spyOn(toolEngineeringModule, 'createAgentToolsEngine').mockReturnValue(
968
968
  mockToolsEngine as any,
969
969
  );
970
970
 
@@ -1009,7 +1009,7 @@ describe('ChatService', () => {
1009
1009
  enabledToolIds: [WebBrowsingManifest.identifier],
1010
1010
  }),
1011
1011
  };
1012
- vi.spyOn(toolEngineeringModule, 'createChatToolsEngine').mockReturnValue(
1012
+ vi.spyOn(toolEngineeringModule, 'createAgentToolsEngine').mockReturnValue(
1013
1013
  mockToolsEngine as any,
1014
1014
  );
1015
1015
 
@@ -14,7 +14,7 @@ import { enableAuth } from '@/const/auth';
14
14
  import { DEFAULT_AGENT_CONFIG } from '@/const/settings';
15
15
  import { isDesktop } from '@/const/version';
16
16
  import { getSearchConfig } from '@/helpers/getSearchConfig';
17
- import { createChatToolsEngine, createToolsEngine } from '@/helpers/toolEngineering';
17
+ import { createAgentToolsEngine, createToolsEngine } from '@/helpers/toolEngineering';
18
18
  import { getAgentStoreState } from '@/store/agent';
19
19
  import { agentChatConfigSelectors, agentSelectors } from '@/store/agent/selectors';
20
20
  import { aiModelSelectors, aiProviderSelectors, getAiInfraStoreState } from '@/store/aiInfra';
@@ -90,7 +90,7 @@ class ChatService {
90
90
 
91
91
  const pluginIds = [...(enabledPlugins || [])];
92
92
 
93
- const toolsEngine = createChatToolsEngine({
93
+ const toolsEngine = createAgentToolsEngine({
94
94
  model: payload.model,
95
95
  provider: payload.provider!,
96
96
  });
@@ -22,8 +22,12 @@ class RAGService {
22
22
  return lambdaClient.chunk.semanticSearch.mutate({ fileIds, query });
23
23
  };
24
24
 
25
- semanticSearchForChat = async (params: SemanticSearchSchemaType) => {
26
- return lambdaClient.chunk.semanticSearchForChat.mutate(params);
25
+ semanticSearchForChat = async (params: SemanticSearchSchemaType, signal?: AbortSignal) => {
26
+ return lambdaClient.chunk.semanticSearchForChat.mutate(params, { signal });
27
+ };
28
+
29
+ getFileContents = async (fileIds: string[], signal?: AbortSignal) => {
30
+ return lambdaClient.chunk.getFileContents.mutate({ fileIds }, { signal });
27
31
  };
28
32
 
29
33
  deleteMessageRagQuery = async (id: string) => {
@@ -1,7 +1,4 @@
1
- import type {
2
- AgentInstruction,
3
- AgentState,
4
- } from '@lobechat/agent-runtime';
1
+ import type { AgentInstruction, AgentState } from '@lobechat/agent-runtime';
5
2
 
6
3
  import { createAgentExecutors } from '@/store/chat/agents/createAgentExecutors';
7
4
  import type { OperationType } from '@/store/chat/slices/operation/types';
@@ -15,6 +15,17 @@ import {
15
15
  // Keep zustand mock as it's needed globally
16
16
  vi.mock('zustand/traditional');
17
17
 
18
+ // Mock lambdaClient to prevent network requests
19
+ vi.mock('@/libs/trpc/client', () => ({
20
+ lambdaClient: {
21
+ session: {
22
+ updateSession: {
23
+ mutate: vi.fn().mockResolvedValue(undefined),
24
+ },
25
+ },
26
+ },
27
+ }));
28
+
18
29
  beforeEach(() => {
19
30
  resetTestEnvironment();
20
31
  setupMockSelectors();