@lobehub/lobehub 2.0.0-next.232 → 2.0.0-next.234

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 (112) hide show
  1. package/.github/workflows/bundle-analyzer.yml +1 -1
  2. package/.github/workflows/e2e.yml +62 -53
  3. package/.github/workflows/manual-build-desktop.yml +5 -5
  4. package/.github/workflows/pr-build-desktop.yml +4 -4
  5. package/.github/workflows/pr-build-docker.yml +2 -2
  6. package/.github/workflows/release-desktop-beta.yml +4 -4
  7. package/.github/workflows/release-docker.yml +2 -2
  8. package/.github/workflows/test.yml +44 -7
  9. package/CHANGELOG.md +59 -0
  10. package/CLAUDE.md +1 -1
  11. package/changelog/v1.json +14 -0
  12. package/docs/development/basic/feature-development.mdx +4 -5
  13. package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
  14. package/docs/self-hosting/environment-variables/auth.mdx +7 -0
  15. package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +7 -0
  16. package/e2e/README.md +6 -6
  17. package/e2e/src/features/community/detail-pages.feature +9 -9
  18. package/e2e/src/features/community/interactions.feature +13 -13
  19. package/e2e/src/features/community/smoke.feature +6 -6
  20. package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
  21. package/e2e/src/steps/agent/conversation.steps.ts +58 -0
  22. package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
  23. package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
  24. package/e2e/src/steps/community/interactions.steps.ts +145 -32
  25. package/e2e/src/steps/hooks.ts +12 -2
  26. package/locales/en-US/setting.json +3 -0
  27. package/locales/zh-CN/file.json +4 -0
  28. package/locales/zh-CN/setting.json +3 -0
  29. package/package.json +5 -5
  30. package/packages/business/config/src/llm.ts +6 -1
  31. package/packages/const/src/index.ts +1 -0
  32. package/packages/const/src/lobehubSkill.ts +55 -0
  33. package/packages/const/src/settings/image.ts +1 -1
  34. package/packages/model-bank/src/aiModels/azure.ts +2 -2
  35. package/packages/model-bank/src/aiModels/google.ts +1 -0
  36. package/packages/model-bank/src/aiModels/lobehub.ts +33 -13
  37. package/packages/model-bank/src/aiModels/openai.ts +21 -4
  38. package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +4 -1
  39. package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +1 -1
  40. package/packages/ssrf-safe-fetch/index.test.ts +5 -34
  41. package/packages/ssrf-safe-fetch/index.ts +12 -2
  42. package/packages/types/package.json +1 -1
  43. package/packages/types/src/files/upload.ts +11 -1
  44. package/packages/types/src/message/common/tools.ts +1 -1
  45. package/packages/types/src/serverConfig.ts +1 -0
  46. package/public/not-compatible.html +1296 -0
  47. package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/MultiImagesUpload/index.tsx +3 -3
  48. package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +3 -10
  49. package/src/app/[variants]/(main)/image/index.tsx +1 -1
  50. package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
  51. package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
  52. package/src/app/[variants]/layout.tsx +50 -1
  53. package/src/envs/auth.ts +15 -0
  54. package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
  55. package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
  56. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
  57. package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
  58. package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
  59. package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
  60. package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
  61. package/src/features/FileViewer/index.tsx +135 -24
  62. package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
  63. package/src/features/PageEditor/store/initialState.ts +2 -1
  64. package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
  65. package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
  66. package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
  67. package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
  68. package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
  69. package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
  70. package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
  71. package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
  72. package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
  73. package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
  74. package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
  75. package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
  76. package/src/features/ResourceManager/index.tsx +1 -0
  77. package/src/helpers/toolEngineering/index.test.ts +3 -0
  78. package/src/helpers/toolEngineering/index.ts +12 -1
  79. package/src/hooks/useFetchAiImageConfig.ts +54 -10
  80. package/src/libs/trpc/utils/internalJwt.ts +2 -2
  81. package/src/locales/default/file.ts +4 -0
  82. package/src/locales/default/setting.ts +3 -0
  83. package/src/server/globalConfig/index.ts +1 -0
  84. package/src/server/modules/ModelRuntime/index.test.ts +214 -1
  85. package/src/server/modules/ModelRuntime/index.ts +43 -7
  86. package/src/server/routers/lambda/document.ts +44 -0
  87. package/src/server/routers/tools/market.ts +261 -0
  88. package/src/server/services/document/index.ts +22 -0
  89. package/src/services/document/index.ts +4 -0
  90. package/src/services/upload.ts +22 -2
  91. package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
  92. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
  93. package/src/store/file/slices/fileManager/action.test.ts +9 -3
  94. package/src/store/file/slices/fileManager/action.ts +165 -70
  95. package/src/store/file/slices/upload/action.ts +3 -0
  96. package/src/store/global/actions/general.ts +15 -0
  97. package/src/store/global/initialState.ts +13 -0
  98. package/src/store/image/slices/generationConfig/initialState.ts +5 -5
  99. package/src/store/image/slices/generationConfig/selectors.test.ts +11 -4
  100. package/src/store/serverConfig/selectors.ts +1 -0
  101. package/src/store/tool/initialState.ts +11 -2
  102. package/src/store/tool/selectors/index.ts +1 -0
  103. package/src/store/tool/selectors/tool.ts +3 -1
  104. package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
  105. package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
  106. package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
  107. package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
  108. package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
  109. package/src/store/tool/store.ts +8 -2
  110. package/vitest.config.mts +11 -6
  111. package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
  112. package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
@@ -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
 
@@ -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
  });