@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
@@ -6,7 +6,11 @@ import { type StateCreator } from 'zustand/vanilla';
6
6
 
7
7
  import { type ChatStore } from '@/store/chat/store';
8
8
  import { useToolStore } from '@/store/tool';
9
- import { klavisStoreSelectors, pluginSelectors } from '@/store/tool/selectors';
9
+ import {
10
+ klavisStoreSelectors,
11
+ lobehubSkillStoreSelectors,
12
+ pluginSelectors,
13
+ } from '@/store/tool/selectors';
10
14
  import { builtinTools } from '@/tools';
11
15
 
12
16
  /**
@@ -34,7 +38,7 @@ export const pluginInternals: StateCreator<
34
38
  const manifests: Record<string, LobeChatPluginManifest> = {};
35
39
 
36
40
  // Track source for each identifier
37
- const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis'> = {};
41
+ const sourceMap: Record<string, 'builtin' | 'plugin' | 'mcp' | 'klavis' | 'lobehubSkill'> = {};
38
42
 
39
43
  // Get all installed plugins
40
44
  const installedPlugins = pluginSelectors.installedPlugins(toolStoreState);
@@ -63,6 +67,15 @@ export const pluginInternals: StateCreator<
63
67
  }
64
68
  }
65
69
 
70
+ // Get all LobeHub Skill tools
71
+ const lobehubSkillTools = lobehubSkillStoreSelectors.lobehubSkillAsLobeTools(toolStoreState);
72
+ for (const tool of lobehubSkillTools) {
73
+ if (tool.manifest) {
74
+ manifests[tool.identifier] = tool.manifest as LobeChatPluginManifest;
75
+ sourceMap[tool.identifier] = 'lobehubSkill';
76
+ }
77
+ }
78
+
66
79
  // Resolve tool calls and add source field
67
80
  const resolved = toolNameResolver.resolve(toolCalls, manifests);
68
81
  return resolved.map((payload) => ({
@@ -60,6 +60,14 @@ export interface PluginTypesAction {
60
60
  */
61
61
  invokeKlavisTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
62
62
 
63
+ /**
64
+ * Invoke LobeHub Skill type plugin
65
+ */
66
+ invokeLobehubSkillTypePlugin: (
67
+ id: string,
68
+ payload: ChatToolPayload,
69
+ ) => Promise<string | undefined>;
70
+
63
71
  /**
64
72
  * Invoke markdown type plugin
65
73
  */
@@ -93,6 +101,11 @@ export const pluginTypes: StateCreator<
93
101
  return await get().invokeKlavisTypePlugin(id, payload);
94
102
  }
95
103
 
104
+ // Check if this is a LobeHub Skill tool by source field
105
+ if (payload.source === 'lobehubSkill') {
106
+ return await get().invokeLobehubSkillTypePlugin(id, payload);
107
+ }
108
+
96
109
  // Check if this is Cloud Code Interpreter - route to specific handler
97
110
  if (payload.identifier === CloudSandboxIdentifier) {
98
111
  return await get().invokeCloudCodeInterpreterTool(id, payload);
@@ -439,6 +452,97 @@ export const pluginTypes: StateCreator<
439
452
  return data.content;
440
453
  },
441
454
 
455
+ invokeLobehubSkillTypePlugin: async (id, payload) => {
456
+ let data: MCPToolCallResult | undefined;
457
+
458
+ // Get message to extract sessionId/topicId
459
+ const message = dbMessageSelectors.getDbMessageById(id)(get());
460
+
461
+ // Get abort controller from operation
462
+ const operationId = get().messageOperationMap[id];
463
+ const operation = operationId ? get().operations[operationId] : undefined;
464
+ const abortController = operation?.abortController;
465
+
466
+ log(
467
+ '[invokeLobehubSkillTypePlugin] messageId=%s, tool=%s, operationId=%s, aborted=%s',
468
+ id,
469
+ payload.apiName,
470
+ operationId,
471
+ abortController?.signal.aborted,
472
+ );
473
+
474
+ try {
475
+ // payload.identifier is the provider id (e.g., 'linear', 'microsoft')
476
+ const provider = payload.identifier;
477
+
478
+ // Parse arguments
479
+ const args = safeParseJSON(payload.arguments) || {};
480
+
481
+ // Call LobeHub Skill tool via store action
482
+ const result = await useToolStore.getState().callLobehubSkillTool({
483
+ args,
484
+ provider,
485
+ toolName: payload.apiName,
486
+ });
487
+
488
+ if (!result.success) {
489
+ throw new Error(result.error || 'LobeHub Skill tool execution failed');
490
+ }
491
+
492
+ // Convert to MCPToolCallResult format
493
+ const content = typeof result.data === 'string' ? result.data : JSON.stringify(result.data);
494
+ data = {
495
+ content,
496
+ error: undefined,
497
+ state: { content: [{ text: content, type: 'text' }] },
498
+ success: true,
499
+ };
500
+ } catch (error) {
501
+ console.error('[invokeLobehubSkillTypePlugin] Error:', error);
502
+
503
+ // ignore the aborted request error
504
+ const err = error as Error;
505
+ if (err.message.includes('aborted')) {
506
+ log(
507
+ '[invokeLobehubSkillTypePlugin] Request aborted: messageId=%s, tool=%s',
508
+ id,
509
+ payload.apiName,
510
+ );
511
+ } else {
512
+ const result = await messageService.updateMessageError(id, error as any, {
513
+ agentId: message?.agentId,
514
+ topicId: message?.topicId,
515
+ });
516
+ if (result?.success && result.messages) {
517
+ get().replaceMessages(result.messages, {
518
+ context: {
519
+ agentId: message?.agentId,
520
+ topicId: message?.topicId,
521
+ },
522
+ });
523
+ }
524
+ }
525
+ }
526
+
527
+ // If error occurred, exit
528
+ if (!data) return;
529
+
530
+ const context = operationId ? { operationId } : undefined;
531
+
532
+ // Use optimisticUpdateToolMessage to update content and state/error in a single call
533
+ await get().optimisticUpdateToolMessage(
534
+ id,
535
+ {
536
+ content: data.content,
537
+ pluginError: data.success ? undefined : data.error,
538
+ pluginState: data.success ? data.state : undefined,
539
+ },
540
+ context,
541
+ );
542
+
543
+ return data.content;
544
+ },
545
+
442
546
  invokeMarkdownTypePlugin: async (id, payload) => {
443
547
  const { internal_callPluginApi } = get();
444
548
 
@@ -278,11 +278,14 @@ describe('FileManagerActions', () => {
278
278
  // Should only dispatch for the valid file
279
279
  expect(dispatchSpy).toHaveBeenCalledWith({
280
280
  atStart: true,
281
- files: [{ file: validFile, id: validFile.name, status: 'pending' }],
281
+ files: [
282
+ expect.objectContaining({ file: validFile, id: validFile.name, status: 'pending' }),
283
+ ],
282
284
  type: 'addFiles',
283
285
  });
284
286
  expect(uploadSpy).toHaveBeenCalledTimes(1);
285
287
  expect(uploadSpy).toHaveBeenCalledWith({
288
+ abortController: expect.any(AbortController),
286
289
  file: validFile,
287
290
  knowledgeBaseId: undefined,
288
291
  onStatusUpdate: expect.any(Function),
@@ -308,6 +311,7 @@ describe('FileManagerActions', () => {
308
311
  });
309
312
 
310
313
  expect(uploadSpy).toHaveBeenCalledWith({
314
+ abortController: expect.any(AbortController),
311
315
  file,
312
316
  knowledgeBaseId: 'kb-123',
313
317
  onStatusUpdate: expect.any(Function),
@@ -502,7 +506,9 @@ describe('FileManagerActions', () => {
502
506
  // Should upload extracted files
503
507
  expect(dispatchSpy).toHaveBeenCalledWith({
504
508
  atStart: true,
505
- files: extractedFiles.map((file) => ({ file, id: file.name, status: 'pending' })),
509
+ files: extractedFiles.map((file) =>
510
+ expect.objectContaining({ file, id: file.name, status: 'pending' }),
511
+ ),
506
512
  type: 'addFiles',
507
513
  });
508
514
  });
@@ -532,7 +538,7 @@ describe('FileManagerActions', () => {
532
538
  // Should fallback to uploading the ZIP file itself
533
539
  expect(dispatchSpy).toHaveBeenCalledWith({
534
540
  atStart: true,
535
- files: [{ file: zipFile, id: zipFile.name, status: 'pending' }],
541
+ files: [expect.objectContaining({ file: zipFile, id: zipFile.name, status: 'pending' })],
536
542
  type: 'addFiles',
537
543
  });
538
544
  });
@@ -1,10 +1,18 @@
1
- import { buildFolderTree, sanitizeFolderName, topologicalSortFolders } from '@lobechat/utils';
1
+ import {
2
+ buildFolderTree,
3
+ createNanoId,
4
+ sanitizeFolderName,
5
+ topologicalSortFolders,
6
+ } from '@lobechat/utils';
7
+ import { t } from 'i18next';
2
8
  import pMap from 'p-map';
3
9
  import type { SWRResponse } from 'swr';
4
10
  import { type StateCreator } from 'zustand/vanilla';
5
11
 
12
+ import { message } from '@/components/AntdStaticMethods';
6
13
  import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file';
7
14
  import { mutate, useClientDataSWR } from '@/libs/swr';
15
+ import { documentService } from '@/services/document';
8
16
  import { FileService, fileService } from '@/services/file';
9
17
  import { ragService } from '@/services/rag';
10
18
  import {
@@ -27,6 +35,7 @@ export interface FolderCrumb {
27
35
  }
28
36
 
29
37
  export interface FileManageAction {
38
+ cancelUpload: (id: string) => void;
30
39
  dispatchDockFileList: (payload: UploadFileListDispatch) => void;
31
40
  embeddingChunks: (fileIds: string[]) => Promise<void>;
32
41
  loadMoreKnowledgeItems: () => Promise<void>;
@@ -67,6 +76,21 @@ export const createFileManageSlice: StateCreator<
67
76
  [],
68
77
  FileManageAction
69
78
  > = (set, get) => ({
79
+ cancelUpload: (id) => {
80
+ const { dockUploadFileList, dispatchDockFileList } = get();
81
+ const uploadItem = dockUploadFileList.find((item) => item.id === id);
82
+
83
+ if (uploadItem?.abortController) {
84
+ uploadItem.abortController.abort();
85
+ }
86
+
87
+ // Update status to cancelled
88
+ dispatchDockFileList({
89
+ id,
90
+ status: 'cancelled',
91
+ type: 'updateFileStatus',
92
+ });
93
+ },
70
94
  dispatchDockFileList: (payload: UploadFileListDispatch) => {
71
95
  const nextValue = uploadFileListReducer(get().dockUploadFileList, payload);
72
96
  if (nextValue === get().dockUploadFileList) return;
@@ -186,19 +210,31 @@ export const createFileManageSlice: StateCreator<
186
210
  // 1. skip file in blacklist
187
211
  const files = filesToUpload.filter((file) => !FILE_UPLOAD_BLACKLIST.includes(file.name));
188
212
 
189
- // 2. Add all files to dock
213
+ // 2. Create upload items with abort controllers
214
+ const uploadFiles = files.map((file) => {
215
+ const abortController = new AbortController();
216
+ return {
217
+ abortController,
218
+ file,
219
+ id: file.name,
220
+ status: 'pending' as const,
221
+ };
222
+ });
223
+
224
+ // 3. Add all files to dock
190
225
  dispatchDockFileList({
191
226
  atStart: true,
192
- files: files.map((file) => ({ file, id: file.name, status: 'pending' })),
227
+ files: uploadFiles,
193
228
  type: 'addFiles',
194
229
  });
195
230
 
196
- // 3. Upload files with concurrency limit using p-map
231
+ // 4. Upload files with concurrency limit using p-map
197
232
  const uploadResults = await pMap(
198
- files,
199
- async (file) => {
233
+ uploadFiles,
234
+ async (uploadFileItem) => {
200
235
  const result = await get().uploadWithProgress({
201
- file,
236
+ abortController: uploadFileItem.abortController,
237
+ file: uploadFileItem.file,
202
238
  knowledgeBaseId,
203
239
  onStatusUpdate: dispatchDockFileList,
204
240
  parentId,
@@ -207,7 +243,11 @@ export const createFileManageSlice: StateCreator<
207
243
  // Note: Don't refresh after each file to avoid flickering
208
244
  // We'll refresh once at the end
209
245
 
210
- return { file, fileId: result?.id, fileType: file.type };
246
+ return {
247
+ file: uploadFileItem.file,
248
+ fileId: result?.id,
249
+ fileType: uploadFileItem.file.type,
250
+ };
211
251
  },
212
252
  { concurrency: MAX_UPLOAD_FILE_COUNT },
213
253
  );
@@ -215,7 +255,7 @@ export const createFileManageSlice: StateCreator<
215
255
  // Refresh the file list once after all uploads are complete
216
256
  await get().refreshFileList();
217
257
 
218
- // 4. auto-embed files that support chunking
258
+ // 5. auto-embed files that support chunking
219
259
  const fileIdsToEmbed = uploadResults
220
260
  .filter(({ fileType, fileId }) => fileId && !isChunkingUnsupported(fileType))
221
261
  .map(({ fileId }) => fileId!);
@@ -353,82 +393,137 @@ export const createFileManageSlice: StateCreator<
353
393
  // 2. Sort folders by depth to ensure parents are created before children
354
394
  const sortedFolderPaths = topologicalSortFolders(folders);
355
395
 
356
- // Map to store created folder IDs: relative path -> folder ID
357
- const folderIdMap = new Map<string, string>();
358
-
359
- // 3. Create all folders sequentially (maintaining hierarchy)
360
- for (const folderPath of sortedFolderPaths) {
361
- const folder = folders[folderPath];
396
+ // Show toast notification if there are folders to create
397
+ const messageKey = 'uploadFolder.creatingFolders';
398
+ if (sortedFolderPaths.length > 0) {
399
+ message.loading({
400
+ content: t('header.actions.uploadFolder.creatingFolders', { ns: 'file' }),
401
+ duration: 0, // Don't auto-dismiss
402
+ key: messageKey,
403
+ });
404
+ }
362
405
 
363
- // Determine parent ID: either from previously created folder or current folder
364
- const parentId = folder.parent ? folderIdMap.get(folder.parent) : currentFolderId;
406
+ try {
407
+ // Map to store created folder IDs: relative path -> folder ID
408
+ const folderIdMap = new Map<string, string>();
409
+
410
+ // 3. Group folders by depth level for batch creation
411
+ const foldersByLevel = new Map<number, string[]>();
412
+ for (const folderPath of sortedFolderPaths) {
413
+ const depth = (folderPath.match(/\//g) || []).length;
414
+ if (!foldersByLevel.has(depth)) {
415
+ foldersByLevel.set(depth, []);
416
+ }
417
+ foldersByLevel.get(depth)!.push(folderPath);
418
+ }
365
419
 
366
- // Sanitize folder name to remove invalid characters
367
- const sanitizedName = sanitizeFolderName(folder.name);
420
+ // 4. Create folders level by level using batch API
421
+ const generateSlug = createNanoId(8);
422
+ const levels = Array.from(foldersByLevel.keys()).sort((a, b) => a - b);
423
+ for (const level of levels) {
424
+ const foldersAtThisLevel = foldersByLevel.get(level)!;
425
+
426
+ // Prepare batch creation data for this level
427
+ const batchCreateData = foldersAtThisLevel.map((folderPath) => {
428
+ const folder = folders[folderPath];
429
+ const parentId = folder.parent ? folderIdMap.get(folder.parent) : currentFolderId;
430
+ const sanitizedName = sanitizeFolderName(folder.name);
431
+
432
+ // Generate unique slug for the folder
433
+ const slug = generateSlug();
434
+
435
+ return {
436
+ content: '',
437
+ editorData: '{}',
438
+ fileType: 'custom/folder',
439
+ knowledgeBaseId,
440
+ metadata: { createdAt: Date.now() },
441
+ parentId,
442
+ slug,
443
+ title: sanitizedName,
444
+ };
445
+ });
368
446
 
369
- // Create folder
370
- const folderId = await get().createFolder(sanitizedName, parentId, knowledgeBaseId);
447
+ // Create all folders at this level in a single batch request
448
+ const createdFolders = await documentService.createDocuments(batchCreateData);
371
449
 
372
- // Store mapping for child folders
373
- folderIdMap.set(folderPath, folderId);
374
- }
450
+ // Store folder ID mappings for the next level
451
+ for (const [i, element] of foldersAtThisLevel.entries()) {
452
+ folderIdMap.set(element, createdFolders[i].id);
453
+ }
454
+ }
375
455
 
376
- // 4. Prepare all file uploads with their target folder IDs
377
- const allUploads: Array<{ file: File; parentId: string | undefined }> = [];
456
+ // Dismiss the toast after folders are created
457
+ if (sortedFolderPaths.length > 0) {
458
+ message.destroy(messageKey);
459
+ }
378
460
 
379
- for (const [folderPath, folderFiles] of Object.entries(filesByFolder)) {
380
- // Root-level files (no folder path) go to currentFolderId
381
- const targetFolderId = folderPath ? folderIdMap.get(folderPath) : currentFolderId;
461
+ // Refresh file list to show the new folders
462
+ await get().refreshFileList();
382
463
 
383
- allUploads.push(
384
- ...folderFiles.map((file) => ({
385
- file,
386
- parentId: targetFolderId,
387
- })),
388
- );
389
- }
464
+ // 5. Prepare all file uploads with their target folder IDs
465
+ const allUploads: Array<{ file: File; parentId: string | undefined }> = [];
390
466
 
391
- // 5. Filter out blacklisted files
392
- const validUploads = allUploads.filter(
393
- ({ file }) => !FILE_UPLOAD_BLACKLIST.includes(file.name),
394
- );
467
+ for (const [folderPath, folderFiles] of Object.entries(filesByFolder)) {
468
+ // Root-level files (no folder path) go to currentFolderId
469
+ const targetFolderId = folderPath ? folderIdMap.get(folderPath) : currentFolderId;
395
470
 
396
- // 6. Add all files to dock
397
- dispatchDockFileList({
398
- atStart: true,
399
- files: validUploads.map(({ file }) => ({ file, id: file.name, status: 'pending' })),
400
- type: 'addFiles',
401
- });
471
+ allUploads.push(
472
+ ...folderFiles.map((file) => ({
473
+ file,
474
+ parentId: targetFolderId,
475
+ })),
476
+ );
477
+ }
402
478
 
403
- // 7. Upload files with concurrency limit
404
- const uploadResults = await pMap(
405
- validUploads,
406
- async ({ file, parentId }) => {
407
- const result = await get().uploadWithProgress({
408
- file,
409
- knowledgeBaseId,
410
- onStatusUpdate: dispatchDockFileList,
411
- parentId,
412
- });
479
+ // 6. Filter out blacklisted files
480
+ const validUploads = allUploads.filter(
481
+ ({ file }) => !FILE_UPLOAD_BLACKLIST.includes(file.name),
482
+ );
413
483
 
414
- // Note: Don't refresh after each file to avoid flickering
415
- // We'll refresh once at the end
484
+ // 7. Add all files to dock
485
+ dispatchDockFileList({
486
+ atStart: true,
487
+ files: validUploads.map(({ file }) => ({ file, id: file.name, status: 'pending' })),
488
+ type: 'addFiles',
489
+ });
416
490
 
417
- return { file, fileId: result?.id, fileType: file.type };
418
- },
419
- { concurrency: MAX_UPLOAD_FILE_COUNT },
420
- );
491
+ // 8. Upload files with concurrency limit
492
+ const uploadResults = await pMap(
493
+ validUploads,
494
+ async ({ file, parentId }) => {
495
+ const result = await get().uploadWithProgress({
496
+ file,
497
+ knowledgeBaseId,
498
+ onStatusUpdate: dispatchDockFileList,
499
+ parentId,
500
+ });
501
+
502
+ // Note: Don't refresh after each file to avoid flickering
503
+ // We'll refresh once at the end
504
+
505
+ return { file, fileId: result?.id, fileType: file.type };
506
+ },
507
+ { concurrency: MAX_UPLOAD_FILE_COUNT },
508
+ );
421
509
 
422
- // Refresh the file list once after all uploads are complete
423
- await get().refreshFileList();
510
+ // Refresh the file list once after all uploads are complete
511
+ await get().refreshFileList();
424
512
 
425
- // 8. Auto-embed files that support chunking
426
- const fileIdsToEmbed = uploadResults
427
- .filter(({ fileType, fileId }) => fileId && !isChunkingUnsupported(fileType))
428
- .map(({ fileId }) => fileId!);
513
+ // 9. Auto-embed files that support chunking
514
+ const fileIdsToEmbed = uploadResults
515
+ .filter(({ fileType, fileId }) => fileId && !isChunkingUnsupported(fileType))
516
+ .map(({ fileId }) => fileId!);
429
517
 
430
- if (fileIdsToEmbed.length > 0) {
431
- await get().parseFilesToChunks(fileIdsToEmbed, { skipExist: false });
518
+ if (fileIdsToEmbed.length > 0) {
519
+ await get().parseFilesToChunks(fileIdsToEmbed, { skipExist: false });
520
+ }
521
+ } catch (error) {
522
+ // Dismiss toast on error
523
+ if (sortedFolderPaths.length > 0) {
524
+ message.destroy(messageKey);
525
+ }
526
+ throw error;
432
527
  }
433
528
  },
434
529
 
@@ -25,6 +25,7 @@ type OnStatusUpdate = (
25
25
  ) => void;
26
26
 
27
27
  interface UploadWithProgressParams {
28
+ abortController?: AbortController;
28
29
  file: File;
29
30
  knowledgeBaseId?: string;
30
31
  onStatusUpdate?: OnStatusUpdate;
@@ -93,6 +94,7 @@ export const createFileUploadSlice: StateCreator<
93
94
  skipCheckFileType,
94
95
  parentId,
95
96
  source,
97
+ abortController,
96
98
  }) => {
97
99
  const fileArrayBuffer = await file.arrayBuffer();
98
100
 
@@ -117,6 +119,7 @@ export const createFileUploadSlice: StateCreator<
117
119
  // 3. if file don't exist, need upload files
118
120
  else {
119
121
  const { data, success } = await uploadService.uploadFileToS3(file, {
122
+ abortController,
120
123
  onNotSupported: () => {
121
124
  onStatusUpdate?.({ id: file.name, type: 'removeFile' });
122
125
  message.info({
@@ -20,6 +20,7 @@ export interface GlobalGeneralAction {
20
20
  openAgentInNewWindow: (agentId: string) => Promise<void>;
21
21
  openTopicInNewWindow: (agentId: string, topicId: string) => Promise<void>;
22
22
  switchLocale: (locale: LocaleMode, params?: { skipBroadcast?: boolean }) => void;
23
+ updateResourceManagerColumnWidth: (column: 'name' | 'date' | 'size', width: number) => void;
23
24
  updateSystemStatus: (status: Partial<SystemStatus>, action?: any) => void;
24
25
  useCheckLatestVersion: (enabledCheck?: boolean) => SWRResponse<string>;
25
26
  useInitSystemStatus: () => SWRResponse;
@@ -110,6 +111,20 @@ export const generalActionSlice: StateCreator<
110
111
  })();
111
112
  }
112
113
  },
114
+ updateResourceManagerColumnWidth: (column, width) => {
115
+ const currentWidths = get().status.resourceManagerColumnWidths || {
116
+ date: 160,
117
+ name: 574,
118
+ size: 140,
119
+ };
120
+
121
+ get().updateSystemStatus({
122
+ resourceManagerColumnWidths: {
123
+ ...currentWidths,
124
+ [column]: width,
125
+ },
126
+ });
127
+ },
113
128
  updateSystemStatus: (status, action) => {
114
129
  if (!get().isStatusInit) return;
115
130
 
@@ -127,6 +127,14 @@ export interface SystemStatus {
127
127
  */
128
128
  pagePageSize?: number;
129
129
  portalWidth: number;
130
+ /**
131
+ * Resource Manager column widths
132
+ */
133
+ resourceManagerColumnWidths?: {
134
+ date: number;
135
+ name: number;
136
+ size: number;
137
+ };
130
138
  showCommandMenu?: boolean;
131
139
  showFilePanel?: boolean;
132
140
  showHotkeyHelper?: boolean;
@@ -192,6 +200,11 @@ export const INITIAL_STATUS = {
192
200
  noWideScreen: true,
193
201
  pagePageSize: 20,
194
202
  portalWidth: 400,
203
+ resourceManagerColumnWidths: {
204
+ date: 160,
205
+ name: 574,
206
+ size: 140,
207
+ },
195
208
  showCommandMenu: false,
196
209
  showFilePanel: true,
197
210
  showHotkeyHelper: false,
@@ -6,6 +6,7 @@ export const serverConfigSelectors = {
6
6
  enableEmailVerification: (s: ServerConfigStore) =>
7
7
  s.serverConfig.enableEmailVerification || false,
8
8
  enableKlavis: (s: ServerConfigStore) => s.serverConfig.enableKlavis || false,
9
+ enableLobehubSkill: (s: ServerConfigStore) => s.serverConfig.enableLobehubSkill || false,
9
10
  enableMagicLink: (s: ServerConfigStore) => s.serverConfig.enableMagicLink || false,
10
11
  enableMarketTrustedClient: (s: ServerConfigStore) =>
11
12
  s.serverConfig.enableMarketTrustedClient || false,
@@ -1,6 +1,13 @@
1
1
  import { type BuiltinToolState, initialBuiltinToolState } from './slices/builtin/initialState';
2
- import { type CustomPluginState, initialCustomPluginState } from './slices/customPlugin/initialState';
2
+ import {
3
+ type CustomPluginState,
4
+ initialCustomPluginState,
5
+ } from './slices/customPlugin/initialState';
3
6
  import { type KlavisStoreState, initialKlavisStoreState } from './slices/klavisStore/initialState';
7
+ import {
8
+ type LobehubSkillStoreState,
9
+ initialLobehubSkillStoreState,
10
+ } from './slices/lobehubSkillStore/initialState';
4
11
  import { type MCPStoreState, initialMCPStoreState } from './slices/mcpStore/initialState';
5
12
  import { type PluginStoreState, initialPluginStoreState } from './slices/oldStore/initialState';
6
13
  import { type PluginState, initialPluginState } from './slices/plugin/initialState';
@@ -10,7 +17,8 @@ export type ToolStoreState = PluginState &
10
17
  PluginStoreState &
11
18
  BuiltinToolState &
12
19
  MCPStoreState &
13
- KlavisStoreState;
20
+ KlavisStoreState &
21
+ LobehubSkillStoreState;
14
22
 
15
23
  export const initialState: ToolStoreState = {
16
24
  ...initialPluginState,
@@ -19,4 +27,5 @@ export const initialState: ToolStoreState = {
19
27
  ...initialBuiltinToolState,
20
28
  ...initialMCPStoreState,
21
29
  ...initialKlavisStoreState,
30
+ ...initialLobehubSkillStoreState,
22
31
  };
@@ -4,6 +4,7 @@ export {
4
4
  } from '../slices/builtin/selectors';
5
5
  export { customPluginSelectors } from '../slices/customPlugin/selectors';
6
6
  export { klavisStoreSelectors } from '../slices/klavisStore/selectors';
7
+ export { lobehubSkillStoreSelectors } from '../slices/lobehubSkillStore/selectors';
7
8
  export { mcpStoreSelectors } from '../slices/mcpStore/selectors';
8
9
  export { pluginStoreSelectors } from '../slices/oldStore/selectors';
9
10
  export { pluginSelectors } from '../slices/plugin/selectors';
@@ -6,12 +6,14 @@ import { type LobeToolMeta } from '@/types/tool/tool';
6
6
 
7
7
  import { type ToolStoreState } from '../initialState';
8
8
  import { builtinToolSelectors } from '../slices/builtin/selectors';
9
+ import { lobehubSkillStoreSelectors } from '../slices/lobehubSkillStore/selectors';
9
10
  import { pluginSelectors } from '../slices/plugin/selectors';
10
11
 
11
12
  const metaList = (s: ToolStoreState): LobeToolMeta[] => {
12
13
  const pluginList = pluginSelectors.installedPluginMetaList(s) as LobeToolMeta[];
14
+ const lobehubSkillList = lobehubSkillStoreSelectors.metaList(s) as LobeToolMeta[];
13
15
 
14
- return builtinToolSelectors.metaList(s).concat(pluginList);
16
+ return builtinToolSelectors.metaList(s).concat(pluginList).concat(lobehubSkillList);
15
17
  };
16
18
 
17
19
  const getMetaById =