@lobehub/lobehub 2.0.0-next.233 → 2.0.0-next.235

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 (169) hide show
  1. package/.github/workflows/e2e.yml +6 -12
  2. package/.github/workflows/test.yml +3 -3
  3. package/CHANGELOG.md +59 -0
  4. package/CLAUDE.md +1 -1
  5. package/changelog/v1.json +18 -0
  6. package/docs/development/basic/feature-development.mdx +4 -5
  7. package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
  8. package/e2e/README.md +6 -6
  9. package/e2e/src/features/community/detail-pages.feature +9 -9
  10. package/e2e/src/features/community/interactions.feature +13 -13
  11. package/e2e/src/features/community/smoke.feature +6 -6
  12. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
  13. package/e2e/src/steps/agent/conversation.steps.ts +58 -0
  14. package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
  15. package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
  16. package/e2e/src/steps/community/interactions.steps.ts +145 -32
  17. package/e2e/src/steps/hooks.ts +12 -2
  18. package/locales/ar/components.json +1 -0
  19. package/locales/ar/file.json +4 -0
  20. package/locales/ar/models.json +29 -0
  21. package/locales/ar/setting.json +7 -0
  22. package/locales/bg-BG/components.json +1 -0
  23. package/locales/bg-BG/file.json +4 -0
  24. package/locales/bg-BG/models.json +1 -0
  25. package/locales/bg-BG/setting.json +7 -0
  26. package/locales/de-DE/components.json +1 -0
  27. package/locales/de-DE/file.json +4 -0
  28. package/locales/de-DE/models.json +29 -0
  29. package/locales/de-DE/setting.json +7 -0
  30. package/locales/en-US/common.json +0 -1
  31. package/locales/en-US/components.json +1 -0
  32. package/locales/en-US/file.json +4 -0
  33. package/locales/en-US/models.json +1 -0
  34. package/locales/en-US/setting.json +3 -0
  35. package/locales/es-ES/components.json +1 -0
  36. package/locales/es-ES/file.json +4 -0
  37. package/locales/es-ES/models.json +43 -0
  38. package/locales/es-ES/setting.json +7 -0
  39. package/locales/fa-IR/components.json +1 -0
  40. package/locales/fa-IR/file.json +4 -0
  41. package/locales/fa-IR/models.json +54 -0
  42. package/locales/fa-IR/setting.json +7 -0
  43. package/locales/fr-FR/components.json +1 -0
  44. package/locales/fr-FR/file.json +4 -0
  45. package/locales/fr-FR/models.json +31 -0
  46. package/locales/fr-FR/setting.json +7 -0
  47. package/locales/it-IT/components.json +1 -0
  48. package/locales/it-IT/file.json +4 -0
  49. package/locales/it-IT/models.json +43 -0
  50. package/locales/it-IT/setting.json +7 -0
  51. package/locales/ja-JP/components.json +1 -0
  52. package/locales/ja-JP/file.json +4 -0
  53. package/locales/ja-JP/models.json +28 -0
  54. package/locales/ja-JP/setting.json +7 -0
  55. package/locales/ko-KR/components.json +1 -0
  56. package/locales/ko-KR/file.json +4 -0
  57. package/locales/ko-KR/models.json +37 -0
  58. package/locales/ko-KR/setting.json +7 -0
  59. package/locales/nl-NL/components.json +1 -0
  60. package/locales/nl-NL/file.json +4 -0
  61. package/locales/nl-NL/models.json +13 -0
  62. package/locales/nl-NL/setting.json +7 -0
  63. package/locales/pl-PL/components.json +1 -0
  64. package/locales/pl-PL/file.json +4 -0
  65. package/locales/pl-PL/models.json +13 -0
  66. package/locales/pl-PL/setting.json +7 -0
  67. package/locales/pt-BR/components.json +1 -0
  68. package/locales/pt-BR/file.json +4 -0
  69. package/locales/pt-BR/models.json +29 -0
  70. package/locales/pt-BR/setting.json +7 -0
  71. package/locales/ru-RU/components.json +1 -0
  72. package/locales/ru-RU/file.json +4 -0
  73. package/locales/ru-RU/models.json +1 -0
  74. package/locales/ru-RU/setting.json +7 -0
  75. package/locales/tr-TR/components.json +1 -0
  76. package/locales/tr-TR/file.json +4 -0
  77. package/locales/tr-TR/models.json +29 -0
  78. package/locales/tr-TR/setting.json +7 -0
  79. package/locales/vi-VN/components.json +1 -0
  80. package/locales/vi-VN/file.json +4 -0
  81. package/locales/vi-VN/models.json +1 -0
  82. package/locales/vi-VN/setting.json +7 -0
  83. package/locales/zh-CN/file.json +4 -0
  84. package/locales/zh-CN/models.json +46 -0
  85. package/locales/zh-CN/setting.json +3 -0
  86. package/locales/zh-TW/components.json +1 -0
  87. package/locales/zh-TW/file.json +4 -0
  88. package/locales/zh-TW/models.json +35 -0
  89. package/locales/zh-TW/setting.json +7 -0
  90. package/package.json +5 -5
  91. package/packages/const/src/index.ts +1 -0
  92. package/packages/const/src/lobehubSkill.ts +55 -0
  93. package/packages/types/package.json +1 -1
  94. package/packages/types/src/files/upload.ts +11 -1
  95. package/packages/types/src/message/common/tools.ts +1 -1
  96. package/packages/types/src/serverConfig.ts +1 -0
  97. package/public/not-compatible.html +1296 -0
  98. package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
  99. package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
  100. package/src/app/[variants]/layout.tsx +50 -1
  101. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
  102. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
  103. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
  104. package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
  105. package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
  106. package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
  107. package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
  108. package/src/features/FileViewer/index.tsx +135 -24
  109. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
  110. package/src/features/PageEditor/store/initialState.ts +2 -1
  111. package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
  112. package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
  113. package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
  114. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
  115. package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
  116. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
  117. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
  118. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
  119. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
  120. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
  121. package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
  122. package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
  123. package/src/features/ResourceManager/index.tsx +1 -0
  124. package/src/helpers/toolEngineering/index.test.ts +3 -0
  125. package/src/helpers/toolEngineering/index.ts +12 -1
  126. package/src/locales/default/file.ts +4 -0
  127. package/src/locales/default/setting.ts +3 -0
  128. package/src/server/globalConfig/index.ts +1 -0
  129. package/src/server/modules/ModelRuntime/index.test.ts +214 -1
  130. package/src/server/modules/ModelRuntime/index.ts +43 -7
  131. package/src/server/routers/lambda/_helpers/resolveContext.ts +8 -8
  132. package/src/server/routers/lambda/agent.ts +1 -1
  133. package/src/server/routers/lambda/aiModel.ts +1 -1
  134. package/src/server/routers/lambda/comfyui.ts +1 -1
  135. package/src/server/routers/lambda/document.ts +44 -0
  136. package/src/server/routers/lambda/exporter.ts +1 -1
  137. package/src/server/routers/lambda/image.ts +13 -13
  138. package/src/server/routers/lambda/klavis.ts +10 -10
  139. package/src/server/routers/lambda/market/index.ts +6 -6
  140. package/src/server/routers/lambda/message.ts +2 -2
  141. package/src/server/routers/lambda/plugin.ts +1 -1
  142. package/src/server/routers/lambda/ragEval.ts +2 -2
  143. package/src/server/routers/lambda/topic.ts +3 -3
  144. package/src/server/routers/lambda/user.ts +10 -10
  145. package/src/server/routers/lambda/userMemories.ts +6 -6
  146. package/src/server/routers/tools/market.ts +261 -0
  147. package/src/server/services/document/index.ts +22 -0
  148. package/src/services/document/index.ts +4 -0
  149. package/src/services/upload.ts +22 -2
  150. package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
  151. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
  152. package/src/store/file/slices/fileManager/action.test.ts +9 -3
  153. package/src/store/file/slices/fileManager/action.ts +165 -70
  154. package/src/store/file/slices/upload/action.ts +3 -0
  155. package/src/store/global/actions/general.ts +15 -0
  156. package/src/store/global/initialState.ts +13 -0
  157. package/src/store/serverConfig/selectors.ts +1 -0
  158. package/src/store/tool/initialState.ts +11 -2
  159. package/src/store/tool/selectors/index.ts +1 -0
  160. package/src/store/tool/selectors/tool.ts +3 -1
  161. package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
  162. package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
  163. package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
  164. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
  165. package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
  166. package/src/store/tool/store.ts +8 -2
  167. package/vitest.config.mts +1 -0
  168. package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
  169. package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
@@ -36,7 +36,7 @@ export const klavisRouter = router({
36
36
  .mutation(async ({ input, ctx }) => {
37
37
  const { serverName, userId, identifier } = input;
38
38
 
39
- // 创建单个服务器实例
39
+ // Create a single server instance
40
40
  const response = await ctx.klavisClient.mcpServer.createServerInstance({
41
41
  serverName: serverName as any,
42
42
  userId,
@@ -44,11 +44,11 @@ export const klavisRouter = router({
44
44
 
45
45
  const { serverUrl, instanceId, oauthUrl } = response;
46
46
 
47
- // 获取该服务器的工具列表
47
+ // Get the tool list for this server
48
48
  const toolsResponse = await ctx.klavisClient.mcpServer.getTools(serverName as any);
49
49
  const tools = toolsResponse.tools || [];
50
50
 
51
- // 保存到数据库,使用传入的 identifier(格式:小写,空格替换为连字符)
51
+ // Save to database using the provided identifier (format: lowercase, spaces replaced with hyphens)
52
52
  const manifest: LobeChatPluginManifest = {
53
53
  api: tools.map((tool: any) => ({
54
54
  description: tool.description || '',
@@ -64,8 +64,8 @@ export const klavisRouter = router({
64
64
  type: 'default',
65
65
  };
66
66
 
67
- // 保存到数据库,包含 oauthUrl isAuthenticated 状态
68
- const isAuthenticated = !oauthUrl; // 如果没有 oauthUrl,说明不需要认证或已认证
67
+ // Save to database with oauthUrl and isAuthenticated status
68
+ const isAuthenticated = !oauthUrl; // If there's no oauthUrl, authentication is not required or already authenticated
69
69
  await ctx.pluginModel.create({
70
70
  customParams: {
71
71
  klavis: {
@@ -104,10 +104,10 @@ export const klavisRouter = router({
104
104
  }),
105
105
  )
106
106
  .mutation(async ({ input, ctx }) => {
107
- // 调用 Klavis API 删除服务器实例
107
+ // Call Klavis API to delete server instance
108
108
  await ctx.klavisClient.mcpServer.deleteServerInstance(input.instanceId);
109
109
 
110
- // 从数据库删除(使用 identifier
110
+ // Delete from database (using identifier)
111
111
  await ctx.pluginModel.delete(input.identifier);
112
112
 
113
113
  return { success: true };
@@ -200,10 +200,10 @@ export const klavisRouter = router({
200
200
  const { identifier, serverName, serverUrl, instanceId, tools, isAuthenticated, oauthUrl } =
201
201
  input;
202
202
 
203
- // 获取现有插件(使用 identifier
203
+ // Get existing plugin (using identifier)
204
204
  const existingPlugin = await ctx.pluginModel.findById(identifier);
205
205
 
206
- // 构建包含所有工具的 manifest
206
+ // Build manifest containing all tools
207
207
  const manifest: LobeChatPluginManifest = {
208
208
  api: tools.map((tool) => ({
209
209
  description: tool.description || '',
@@ -229,7 +229,7 @@ export const klavisRouter = router({
229
229
  },
230
230
  };
231
231
 
232
- // 更新或创建插件
232
+ // Update or create plugin
233
233
  if (existingPlugin) {
234
234
  await ctx.pluginModel.update(identifier, { customParams, manifest });
235
235
  } else {
@@ -559,11 +559,11 @@ export const marketRouter = router({
559
559
  log('get access token, expiresIn value:', expiresIn);
560
560
  log('expiresIn type:', typeof expiresIn);
561
561
 
562
- const expirationTime = new Date(Date.now() + (expiresIn - 60) * 1000); // 提前 60 秒过期
562
+ const expirationTime = new Date(Date.now() + (expiresIn - 60) * 1000); // Expire 60 seconds early
563
563
 
564
564
  log('expirationTime:', expirationTime.toISOString());
565
565
 
566
- // 设置 HTTP-Only Cookie 存储实际的 access token
566
+ // Set HTTP-Only Cookie to store the actual access token
567
567
  const tokenCookie = serialize('mp_token', accessToken, {
568
568
  expires: expirationTime,
569
569
  httpOnly: true,
@@ -572,7 +572,7 @@ export const marketRouter = router({
572
572
  secure: process.env.NODE_ENV === 'production',
573
573
  });
574
574
 
575
- // 设置客户端可读的状态标记 cookie(不包含实际 token
575
+ // Set client-readable status marker cookie (without actual token)
576
576
  const statusCookie = serialize('mp_token_status', 'active', {
577
577
  expires: expirationTime,
578
578
  httpOnly: false,
@@ -581,7 +581,7 @@ export const marketRouter = router({
581
581
  secure: process.env.NODE_ENV === 'production',
582
582
  });
583
583
 
584
- // 通过 context resHeaders 设置 Set-Cookie 头
584
+ // Set Set-Cookie header via context's resHeaders
585
585
  ctx.resHeaders?.append('Set-Cookie', tokenCookie);
586
586
  ctx.resHeaders?.append('Set-Cookie', statusCookie);
587
587
 
@@ -650,7 +650,7 @@ export const marketRouter = router({
650
650
  return { success: true };
651
651
  } catch (error) {
652
652
  console.error('Error reporting call: %O', error);
653
- // 不抛出错误,因为上报失败不应影响主流程
653
+ // Don't throw error, as reporting failure should not affect main flow
654
654
  return { success: false };
655
655
  }
656
656
  }),
@@ -678,7 +678,7 @@ export const marketRouter = router({
678
678
  return { success: true };
679
679
  } catch (error) {
680
680
  log('Error reporting MCP installation result: %O', error);
681
- // 不抛出错误,因为上报失败不应影响主流程
681
+ // Don't throw error, as reporting failure should not affect main flow
682
682
  return { success: false };
683
683
  }
684
684
  }),
@@ -75,13 +75,13 @@ export const messageRouter = router({
75
75
  createMessage: messageProcedure
76
76
  .input(CreateNewMessageParamsSchema)
77
77
  .mutation(async ({ input, ctx }) => {
78
- // 如果没有 agentId 但有 sessionId,从 sessionId 解析出 agentId
78
+ // If there's no agentId but has sessionId, resolve agentId from sessionId
79
79
  let agentId = input.agentId;
80
80
  if (!agentId && input.sessionId) {
81
81
  agentId = (await resolveAgentIdFromSession(input.sessionId, ctx.serverDB, ctx.userId))!;
82
82
  }
83
83
 
84
- // 使用解析后的 agentId 创建消息
84
+ // Create message with the resolved agentId
85
85
  return ctx.messageService.createMessage({ ...input, agentId } as any);
86
86
  }),
87
87
 
@@ -65,7 +65,7 @@ export const pluginRouter = router({
65
65
  return data.identifier;
66
66
  }),
67
67
 
68
- // TODO: 未来这部分方法也需要使用 authedProcedure
68
+ // TODO: In the future, this method also needs to use authedProcedure
69
69
  getPlugins: publicProcedure.query(async ({ ctx }): Promise<LobeTool[]> => {
70
70
  if (!ctx.userId) return [];
71
71
 
@@ -251,7 +251,7 @@ export const ragEvalRouter = router({
251
251
  const isSuccess = records.every((record) => record.status === EvalEvaluationStatus.Success);
252
252
 
253
253
  if (isSuccess) {
254
- // 将结果上传到 S3
254
+ // Upload results to S3
255
255
 
256
256
  const evalRecords = records.map((record) => ({
257
257
  question: record.question,
@@ -265,7 +265,7 @@ export const ragEvalRouter = router({
265
265
 
266
266
  await ctx.fileService.uploadContent(path, JSONL.stringify(evalRecords));
267
267
 
268
- // 保存数据
268
+ // Save data
269
269
  await ctx.evaluationModel.update(input.id, {
270
270
  status: EvalEvaluationStatus.Success,
271
271
  evalRecordsUrl: await ctx.fileService.getFullFileUrl(path),
@@ -49,7 +49,7 @@ export const topicRouter = router({
49
49
  ),
50
50
  )
51
51
  .mutation(async ({ input, ctx }): Promise<BatchTaskResult> => {
52
- // 解析每个 topic sessionId
52
+ // Resolve sessionId for each topic
53
53
  const resolvedTopics = await Promise.all(
54
54
  input.map(async (item) => {
55
55
  const { agentId, ...rest } = item;
@@ -162,7 +162,7 @@ export const topicRouter = router({
162
162
  return { items: result.items, total: result.total };
163
163
  }
164
164
 
165
- // 如果提供了 sessionId 但没有 agentId,需要反向查找 agentId
165
+ // If sessionId is provided but no agentId, need to reverse lookup agentId
166
166
  let effectiveAgentId = rest.agentId;
167
167
  if (!effectiveAgentId && sessionId) {
168
168
  effectiveAgentId = await resolveAgentIdFromSession(sessionId, ctx.serverDB, ctx.userId);
@@ -430,7 +430,7 @@ export const topicRouter = router({
430
430
  .mutation(async ({ input, ctx }) => {
431
431
  const { agentId, ...restValue } = input.value;
432
432
 
433
- // 如果提供了 agentId,解析为 sessionId
433
+ // If agentId is provided, resolve to sessionId
434
434
  let resolvedSessionId = restValue.sessionId;
435
435
  if (agentId && !resolvedSessionId) {
436
436
  const resolved = await resolveContext({ agentId }, ctx.serverDB, ctx.userId);
@@ -150,7 +150,7 @@ export const userRouter = router({
150
150
  firstName: state.firstName,
151
151
  fullName: state.fullName,
152
152
 
153
- // 有消息,或者创建过助理,则认为有 conversation
153
+ // Has conversation if there are messages or has created any assistant
154
154
  hasConversation: hasAnyMessages || hasExtraSession,
155
155
 
156
156
  interests: state.interests,
@@ -190,40 +190,40 @@ export const userRouter = router({
190
190
  }),
191
191
 
192
192
  updateAvatar: userProcedure.input(z.string()).mutation(async ({ ctx, input }) => {
193
- // 如果是 Base64 数据,需要上传到 S3
193
+ // If it's Base64 data, need to upload to S3
194
194
  if (input.startsWith('data:image')) {
195
195
  try {
196
- // 提取 mimeType,例如 "image/png"
196
+ // Extract mimeType, e.g., "image/png"
197
197
  const prefix = 'data:';
198
198
  const semicolonIndex = input.indexOf(';');
199
199
  const mimeType =
200
200
  semicolonIndex !== -1 ? input.slice(prefix.length, semicolonIndex) : 'image/png';
201
201
  const fileType = mimeType.split('/')[1];
202
202
 
203
- // 分割字符串,获取 Base64 部分
203
+ // Split string to get the Base64 part
204
204
  const commaIndex = input.indexOf(',');
205
205
  if (commaIndex === -1) {
206
206
  throw new Error('Invalid Base64 data');
207
207
  }
208
208
  const base64Data = input.slice(commaIndex + 1);
209
209
 
210
- // 创建 S3 客户端
210
+ // Create S3 client
211
211
  const s3 = new FileS3();
212
212
 
213
- // 使用 UUID 生成唯一文件名,防止缓存问题
214
- // 获取旧头像 URL, 后面删除该头像
213
+ // Use UUID to generate unique filename to prevent caching issues
214
+ // Get old avatar URL for later deletion
215
215
  const userState = await ctx.userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);
216
216
  const oldAvatarUrl = userState.avatar;
217
217
 
218
218
  const fileName = `${uuidv4()}.${fileType}`;
219
219
  const filePath = `user/avatar/${ctx.userId}/${fileName}`;
220
220
 
221
- // Base64 数据转换为 Buffer 再上传到 S3
221
+ // Convert Base64 data to Buffer and upload to S3
222
222
  const buffer = Buffer.from(base64Data, 'base64');
223
223
 
224
224
  await s3.uploadBuffer(filePath, buffer, mimeType);
225
225
 
226
- // 删除旧头像
226
+ // Delete old avatar
227
227
  if (oldAvatarUrl && oldAvatarUrl.startsWith('/webapi/')) {
228
228
  const oldFilePath = oldAvatarUrl.replace('/webapi/', '');
229
229
  await s3.deleteFile(oldFilePath);
@@ -239,7 +239,7 @@ export const userRouter = router({
239
239
  }
240
240
  }
241
241
 
242
- // 如果不是 Base64 数据,直接使用 URL 更新用户头像
242
+ // If it's not Base64 data, directly use URL to update user avatar
243
243
  return ctx.userModel.updateUser({ avatar: input });
244
244
  }),
245
245
 
@@ -305,11 +305,11 @@ export const userMemoriesRouter = router({
305
305
  }
306
306
  }),
307
307
 
308
- // REVIEW:根据当前 topic 直接提取记忆
309
- // REVIEW 我们需要一个既可以 cron 也可以主动用户触发进行「每日/每周/每隔一段时间的」记忆提取/生成的函数实现
310
- // REVIEW 定时任务
311
- // 不用 tRPC,直接 server/service
312
- // 可以参考 https://github.com/lobehub/lobe-chat-cloud/blob/886ff2fcd44b7b00a3aa8906f84914a6dcaa1815/src/app/(backend)/cron/reset-budgets/route.ts#L214
308
+ // REVIEW: Extract memories directly from current topic
309
+ // REVIEW: We need a function implementation that can be triggered both by cron and manually by users for "daily/weekly/periodic" memory extraction/generation
310
+ // REVIEW: Scheduled task
311
+ // Don't use tRPC, use server/service directly
312
+ // Reference: https://github.com/lobehub/lobe-chat-cloud/blob/886ff2fcd44b7b00a3aa8906f84914a6dcaa1815/src/app/(backend)/cron/reset-budgets/route.ts#L214
313
313
  reEmbedMemories: memoryProcedure
314
314
  .input(reEmbedInputSchema.optional())
315
315
  .mutation(async ({ ctx, input }) => {
@@ -740,7 +740,7 @@ export const userMemoriesRouter = router({
740
740
  }
741
741
  }),
742
742
 
743
- // REVIEW: 需要实现 tool memory api
743
+ // REVIEW: Need to implement tool memory api
744
744
  toolAddContextMemory: memoryProcedure
745
745
  .input(ContextMemoryItemSchema)
746
746
  .mutation(async ({ input, ctx }) => {
@@ -7,6 +7,7 @@ import { z } from 'zod';
7
7
  import { type ToolCallContent } from '@/libs/mcp';
8
8
  import { authedProcedure, router } from '@/libs/trpc/lambda';
9
9
  import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
10
+ import { marketSDK, requireMarketAuth } from '@/libs/trpc/lambda/middleware/marketSDK';
10
11
  import { generateTrustedClientToken, isTrustedClientEnabled } from '@/libs/trusted-client';
11
12
  import { FileS3 } from '@/server/modules/S3';
12
13
  import { DiscoverService } from '@/server/services/discover';
@@ -41,6 +42,23 @@ const marketToolProcedure = authedProcedure
41
42
  });
42
43
  });
43
44
 
45
+ // ============================== LobeHub Skill Procedures ==============================
46
+ /**
47
+ * LobeHub Skill procedure with SDK and optional auth
48
+ * Used for routes that may work without auth (like listing providers)
49
+ */
50
+ const lobehubSkillBaseProcedure = authedProcedure
51
+ .use(serverDatabase)
52
+ .use(telemetry)
53
+ .use(marketUserInfo)
54
+ .use(marketSDK);
55
+
56
+ /**
57
+ * LobeHub Skill procedure with required auth
58
+ * Used for routes that require user authentication
59
+ */
60
+ const lobehubSkillAuthProcedure = lobehubSkillBaseProcedure.use(requireMarketAuth);
61
+
44
62
  // ============================== Schema Definitions ==============================
45
63
 
46
64
  // Schema for metadata that frontend needs to pass (for cloud MCP reporting)
@@ -269,6 +287,249 @@ export const marketRouter = router({
269
287
  }
270
288
  }),
271
289
 
290
+ // ============================== LobeHub Skill ==============================
291
+ /**
292
+ * Call a LobeHub Skill tool
293
+ */
294
+ connectCallTool: lobehubSkillAuthProcedure
295
+ .input(
296
+ z.object({
297
+ args: z.record(z.any()).optional(),
298
+ provider: z.string(),
299
+ toolName: z.string(),
300
+ }),
301
+ )
302
+ .mutation(async ({ input, ctx }) => {
303
+ const { provider, toolName, args } = input;
304
+ log('connectCallTool: provider=%s, tool=%s', provider, toolName);
305
+
306
+ try {
307
+ const response = await ctx.marketSDK.skills.callTool(provider, {
308
+ args: args || {},
309
+ tool: toolName,
310
+ });
311
+
312
+ log('connectCallTool response: %O', response);
313
+
314
+ return {
315
+ data: response.data,
316
+ success: response.success,
317
+ };
318
+ } catch (error) {
319
+ const errorMessage = (error as Error).message;
320
+ log('connectCallTool error: %s', errorMessage);
321
+
322
+ if (errorMessage.includes('NOT_CONNECTED')) {
323
+ throw new TRPCError({
324
+ code: 'UNAUTHORIZED',
325
+ message: 'Provider not connected. Please authorize first.',
326
+ });
327
+ }
328
+
329
+ if (errorMessage.includes('TOKEN_EXPIRED')) {
330
+ throw new TRPCError({
331
+ code: 'UNAUTHORIZED',
332
+ message: 'Token expired. Please re-authorize.',
333
+ });
334
+ }
335
+
336
+ throw new TRPCError({
337
+ code: 'INTERNAL_SERVER_ERROR',
338
+ message: `Failed to call tool: ${errorMessage}`,
339
+ });
340
+ }
341
+ }),
342
+
343
+ /**
344
+ * Get all connections health status
345
+ */
346
+ connectGetAllHealth: lobehubSkillAuthProcedure.query(async ({ ctx }) => {
347
+ log('connectGetAllHealth');
348
+
349
+ try {
350
+ const response = await ctx.marketSDK.connect.getAllHealth();
351
+ return {
352
+ connections: response.connections || [],
353
+ summary: response.summary,
354
+ };
355
+ } catch (error) {
356
+ log('connectGetAllHealth error: %O', error);
357
+ throw new TRPCError({
358
+ code: 'INTERNAL_SERVER_ERROR',
359
+ message: `Failed to get connections health: ${(error as Error).message}`,
360
+ });
361
+ }
362
+ }),
363
+
364
+ /**
365
+ * Get authorize URL for a provider
366
+ * This calls the SDK's authorize method which generates a secure authorization URL
367
+ */
368
+ connectGetAuthorizeUrl: lobehubSkillAuthProcedure
369
+ .input(
370
+ z.object({
371
+ provider: z.string(),
372
+ redirectUri: z.string().optional(),
373
+ scopes: z.array(z.string()).optional(),
374
+ }),
375
+ )
376
+ .query(async ({ input, ctx }) => {
377
+ log('connectGetAuthorizeUrl: provider=%s', input.provider);
378
+
379
+ try {
380
+ const response = await ctx.marketSDK.connect.authorize(input.provider, {
381
+ redirect_uri: input.redirectUri,
382
+ scopes: input.scopes,
383
+ });
384
+
385
+ return {
386
+ authorizeUrl: response.authorize_url,
387
+ code: response.code,
388
+ expiresIn: response.expires_in,
389
+ };
390
+ } catch (error) {
391
+ log('connectGetAuthorizeUrl error: %O', error);
392
+ throw new TRPCError({
393
+ code: 'INTERNAL_SERVER_ERROR',
394
+ message: `Failed to get authorize URL: ${(error as Error).message}`,
395
+ });
396
+ }
397
+ }),
398
+
399
+ /**
400
+ * Get connection status for a provider
401
+ */
402
+ connectGetStatus: lobehubSkillAuthProcedure
403
+ .input(z.object({ provider: z.string() }))
404
+ .query(async ({ input, ctx }) => {
405
+ log('connectGetStatus: provider=%s', input.provider);
406
+
407
+ try {
408
+ const response = await ctx.marketSDK.connect.getStatus(input.provider);
409
+ return {
410
+ connected: response.connected,
411
+ connection: response.connection,
412
+ icon: (response as any).icon,
413
+ providerName: (response as any).providerName,
414
+ };
415
+ } catch (error) {
416
+ log('connectGetStatus error: %O', error);
417
+ throw new TRPCError({
418
+ code: 'INTERNAL_SERVER_ERROR',
419
+ message: `Failed to get status: ${(error as Error).message}`,
420
+ });
421
+ }
422
+ }),
423
+
424
+ /**
425
+ * List all user connections
426
+ */
427
+ connectListConnections: lobehubSkillAuthProcedure.query(async ({ ctx }) => {
428
+ log('connectListConnections');
429
+
430
+ try {
431
+ const response = await ctx.marketSDK.connect.listConnections();
432
+ // Debug logging
433
+ log('connectListConnections raw response: %O', response);
434
+ log('connectListConnections connections: %O', response.connections);
435
+ return {
436
+ connections: response.connections || [],
437
+ };
438
+ } catch (error) {
439
+ log('connectListConnections error: %O', error);
440
+ throw new TRPCError({
441
+ code: 'INTERNAL_SERVER_ERROR',
442
+ message: `Failed to list connections: ${(error as Error).message}`,
443
+ });
444
+ }
445
+ }),
446
+
447
+ /**
448
+ * List available providers (public, no auth required)
449
+ */
450
+ connectListProviders: lobehubSkillBaseProcedure.query(async ({ ctx }) => {
451
+ log('connectListProviders');
452
+
453
+ try {
454
+ const response = await ctx.marketSDK.skills.listProviders();
455
+ return {
456
+ providers: response.providers || [],
457
+ };
458
+ } catch (error) {
459
+ log('connectListProviders error: %O', error);
460
+ throw new TRPCError({
461
+ code: 'INTERNAL_SERVER_ERROR',
462
+ message: `Failed to list providers: ${(error as Error).message}`,
463
+ });
464
+ }
465
+ }),
466
+
467
+ /**
468
+ * List tools for a provider
469
+ */
470
+ connectListTools: lobehubSkillBaseProcedure
471
+ .input(z.object({ provider: z.string() }))
472
+ .query(async ({ input, ctx }) => {
473
+ log('connectListTools: provider=%s', input.provider);
474
+
475
+ try {
476
+ const response = await ctx.marketSDK.skills.listTools(input.provider);
477
+ return {
478
+ provider: input.provider,
479
+ tools: response.tools || [],
480
+ };
481
+ } catch (error) {
482
+ log('connectListTools error: %O', error);
483
+ throw new TRPCError({
484
+ code: 'INTERNAL_SERVER_ERROR',
485
+ message: `Failed to list tools: ${(error as Error).message}`,
486
+ });
487
+ }
488
+ }),
489
+
490
+ /**
491
+ * Refresh token for a provider
492
+ */
493
+ connectRefresh: lobehubSkillAuthProcedure
494
+ .input(z.object({ provider: z.string() }))
495
+ .mutation(async ({ input, ctx }) => {
496
+ log('connectRefresh: provider=%s', input.provider);
497
+
498
+ try {
499
+ const response = await ctx.marketSDK.connect.refresh(input.provider);
500
+ return {
501
+ connection: response.connection,
502
+ refreshed: response.refreshed,
503
+ };
504
+ } catch (error) {
505
+ log('connectRefresh error: %O', error);
506
+ throw new TRPCError({
507
+ code: 'INTERNAL_SERVER_ERROR',
508
+ message: `Failed to refresh token: ${(error as Error).message}`,
509
+ });
510
+ }
511
+ }),
512
+
513
+ /**
514
+ * Revoke connection for a provider
515
+ */
516
+ connectRevoke: lobehubSkillAuthProcedure
517
+ .input(z.object({ provider: z.string() }))
518
+ .mutation(async ({ input, ctx }) => {
519
+ log('connectRevoke: provider=%s', input.provider);
520
+
521
+ try {
522
+ await ctx.marketSDK.connect.revoke(input.provider);
523
+ return { success: true };
524
+ } catch (error) {
525
+ log('connectRevoke error: %O', error);
526
+ throw new TRPCError({
527
+ code: 'INTERNAL_SERVER_ERROR',
528
+ message: `Failed to revoke connection: ${(error as Error).message}`,
529
+ });
530
+ }
531
+ }),
532
+
272
533
  /**
273
534
  * Export a file from sandbox and upload to S3, then create a persistent file record
274
535
  * This combines the previous getExportFileUploadUrl + callCodeInterpreterTool + createFileRecord flow
@@ -100,6 +100,28 @@ export class DocumentService {
100
100
  return document;
101
101
  }
102
102
 
103
+ /**
104
+ * Create multiple documents in batch (optimized for folder creation)
105
+ * Returns array of created documents with same order as input
106
+ */
107
+ async createDocuments(
108
+ documents: Array<{
109
+ content?: string;
110
+ editorData: Record<string, any>;
111
+ fileType?: string;
112
+ knowledgeBaseId?: string;
113
+ metadata?: Record<string, any>;
114
+ parentId?: string;
115
+ slug?: string;
116
+ title: string;
117
+ }>,
118
+ ): Promise<DocumentItem[]> {
119
+ // Create all documents in parallel for better performance
120
+ const results = await Promise.all(documents.map((params) => this.createDocument(params)));
121
+
122
+ return results;
123
+ }
124
+
103
125
  /**
104
126
  * Query documents with pagination
105
127
  */
@@ -28,6 +28,10 @@ export class DocumentService {
28
28
  return lambdaClient.document.createDocument.mutate(params);
29
29
  }
30
30
 
31
+ async createDocuments(documents: CreateDocumentParams[]): Promise<DocumentItem[]> {
32
+ return lambdaClient.document.createDocuments.mutate({ documents });
33
+ }
34
+
31
35
  async queryDocuments(params?: {
32
36
  current?: number;
33
37
  fileTypes?: string[];
@@ -44,6 +44,7 @@ const generateFilePathMetadata = (
44
44
  };
45
45
 
46
46
  interface UploadFileToS3Options {
47
+ abortController?: AbortController;
47
48
  directory?: string;
48
49
  filename?: string;
49
50
  onNotSupported?: () => void;
@@ -58,13 +59,18 @@ class UploadService {
58
59
  */
59
60
  uploadFileToS3 = async (
60
61
  file: File,
61
- { onProgress, directory, pathname }: UploadFileToS3Options,
62
+ { onProgress, directory, pathname, abortController }: UploadFileToS3Options,
62
63
  ): Promise<{ data: FileMetadata; success: boolean }> => {
63
64
  // Server-side upload logic
64
65
 
65
66
  // if is server mode, upload to server s3,
66
67
 
67
- const data = await this.uploadToServerS3(file, { directory, onProgress, pathname });
68
+ const data = await this.uploadToServerS3(file, {
69
+ abortController,
70
+ directory,
71
+ onProgress,
72
+ pathname,
73
+ });
68
74
  return { data, success: true };
69
75
  };
70
76
 
@@ -129,7 +135,9 @@ class UploadService {
129
135
  onProgress,
130
136
  directory,
131
137
  pathname,
138
+ abortController,
132
139
  }: {
140
+ abortController?: AbortController;
133
141
  directory?: string;
134
142
  onProgress?: (status: FileUploadStatus, state: FileUploadState) => void;
135
143
  pathname?: string;
@@ -139,6 +147,14 @@ class UploadService {
139
147
 
140
148
  const { preSignUrl, ...result } = await this.getSignedUploadUrl(file, { directory, pathname });
141
149
  let startTime = Date.now();
150
+
151
+ // Setup abort listener
152
+ if (abortController) {
153
+ abortController.signal.addEventListener('abort', () => {
154
+ xhr.abort();
155
+ });
156
+ }
157
+
142
158
  xhr.upload.addEventListener('progress', (event) => {
143
159
  if (event.lengthComputable) {
144
160
  const progress = Number(((event.loaded / event.total) * 100).toFixed(1));
@@ -177,6 +193,10 @@ class UploadService {
177
193
  if (xhr.status === 0) reject(UPLOAD_NETWORK_ERROR);
178
194
  else reject(xhr.statusText);
179
195
  });
196
+ xhr.addEventListener('abort', () => {
197
+ onProgress?.('cancelled', { progress: 0, restTime: 0, speed: 0 });
198
+ reject(new Error('Upload cancelled by user'));
199
+ });
180
200
  xhr.send(data);
181
201
  });
182
202