@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.
- package/.github/workflows/bundle-analyzer.yml +1 -1
- package/.github/workflows/e2e.yml +62 -53
- package/.github/workflows/manual-build-desktop.yml +5 -5
- package/.github/workflows/pr-build-desktop.yml +4 -4
- package/.github/workflows/pr-build-docker.yml +2 -2
- package/.github/workflows/release-desktop-beta.yml +4 -4
- package/.github/workflows/release-docker.yml +2 -2
- package/.github/workflows/test.yml +44 -7
- package/CHANGELOG.md +59 -0
- package/CLAUDE.md +1 -1
- package/changelog/v1.json +14 -0
- package/docs/development/basic/feature-development.mdx +4 -5
- package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
- package/docs/self-hosting/environment-variables/auth.mdx +7 -0
- package/docs/self-hosting/environment-variables/auth.zh-CN.mdx +7 -0
- package/e2e/README.md +6 -6
- package/e2e/src/features/community/detail-pages.feature +9 -9
- package/e2e/src/features/community/interactions.feature +13 -13
- package/e2e/src/features/community/smoke.feature +6 -6
- package/e2e/src/steps/agent/conversation-mgmt.steps.ts +196 -25
- package/e2e/src/steps/agent/conversation.steps.ts +58 -0
- package/e2e/src/steps/agent/message-ops.steps.ts +20 -15
- package/e2e/src/steps/community/detail-pages.steps.ts +60 -19
- package/e2e/src/steps/community/interactions.steps.ts +145 -32
- package/e2e/src/steps/hooks.ts +12 -2
- package/locales/en-US/setting.json +3 -0
- package/locales/zh-CN/file.json +4 -0
- package/locales/zh-CN/setting.json +3 -0
- package/package.json +5 -5
- package/packages/business/config/src/llm.ts +6 -1
- package/packages/const/src/index.ts +1 -0
- package/packages/const/src/lobehubSkill.ts +55 -0
- package/packages/const/src/settings/image.ts +1 -1
- package/packages/model-bank/src/aiModels/azure.ts +2 -2
- package/packages/model-bank/src/aiModels/google.ts +1 -0
- package/packages/model-bank/src/aiModels/lobehub.ts +33 -13
- package/packages/model-bank/src/aiModels/openai.ts +21 -4
- package/packages/model-runtime/src/core/openaiCompatibleFactory/createImage.ts +4 -1
- package/packages/model-runtime/src/providers/openai/__snapshots__/index.test.ts.snap +1 -1
- package/packages/ssrf-safe-fetch/index.test.ts +5 -34
- package/packages/ssrf-safe-fetch/index.ts +12 -2
- package/packages/types/package.json +1 -1
- package/packages/types/src/files/upload.ts +11 -1
- package/packages/types/src/message/common/tools.ts +1 -1
- package/packages/types/src/serverConfig.ts +1 -0
- package/public/not-compatible.html +1296 -0
- package/src/app/[variants]/(main)/image/_layout/ConfigPanel/components/MultiImagesUpload/index.tsx +3 -3
- package/src/app/[variants]/(main)/image/features/GenerationFeed/index.tsx +3 -10
- package/src/app/[variants]/(main)/image/index.tsx +1 -1
- package/src/app/[variants]/(main)/resource/features/FileDetail.tsx +20 -12
- package/src/app/[variants]/(main)/resource/features/modal/FullscreenModal.tsx +2 -4
- package/src/app/[variants]/layout.tsx +50 -1
- package/src/envs/auth.ts +15 -0
- package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +304 -0
- package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +74 -10
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +9 -0
- package/src/features/FileViewer/Renderer/Code/index.tsx +224 -0
- package/src/features/FileViewer/Renderer/Image/index.tsx +8 -1
- package/src/features/FileViewer/Renderer/PDF/index.tsx +3 -1
- package/src/features/FileViewer/Renderer/PDF/style.ts +2 -1
- package/src/features/FileViewer/index.tsx +135 -24
- package/src/features/PageEditor/EditorCanvas/useSlashItems.tsx +7 -4
- package/src/features/PageEditor/store/initialState.ts +2 -1
- package/src/features/ResourceManager/components/Editor/FileContent.tsx +1 -4
- package/src/features/ResourceManager/components/Editor/FileCopilot.tsx +64 -0
- package/src/features/ResourceManager/components/Editor/index.tsx +98 -31
- package/src/features/ResourceManager/components/Explorer/ItemDropdown/useFileItemDropdown.tsx +3 -2
- package/src/features/ResourceManager/components/Explorer/ListView/ColumnResizeHandle.tsx +119 -0
- package/src/features/ResourceManager/components/Explorer/ListView/ListItem/index.tsx +67 -22
- package/src/features/ResourceManager/components/Explorer/ListView/Skeleton.tsx +46 -11
- package/src/features/ResourceManager/components/Explorer/ListView/index.tsx +140 -81
- package/src/features/ResourceManager/components/Explorer/ToolBar/SortDropdown.tsx +20 -12
- package/src/features/ResourceManager/components/Explorer/ToolBar/ViewSwitcher.tsx +18 -10
- package/src/features/ResourceManager/components/UploadDock/Item.tsx +38 -6
- package/src/features/ResourceManager/components/UploadDock/index.tsx +62 -41
- package/src/features/ResourceManager/index.tsx +1 -0
- package/src/helpers/toolEngineering/index.test.ts +3 -0
- package/src/helpers/toolEngineering/index.ts +12 -1
- package/src/hooks/useFetchAiImageConfig.ts +54 -10
- package/src/libs/trpc/utils/internalJwt.ts +2 -2
- package/src/locales/default/file.ts +4 -0
- package/src/locales/default/setting.ts +3 -0
- package/src/server/globalConfig/index.ts +1 -0
- package/src/server/modules/ModelRuntime/index.test.ts +214 -1
- package/src/server/modules/ModelRuntime/index.ts +43 -7
- package/src/server/routers/lambda/document.ts +44 -0
- package/src/server/routers/tools/market.ts +261 -0
- package/src/server/services/document/index.ts +22 -0
- package/src/services/document/index.ts +4 -0
- package/src/services/upload.ts +22 -2
- package/src/store/chat/slices/plugin/actions/internals.ts +15 -2
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +104 -0
- package/src/store/file/slices/fileManager/action.test.ts +9 -3
- package/src/store/file/slices/fileManager/action.ts +165 -70
- package/src/store/file/slices/upload/action.ts +3 -0
- package/src/store/global/actions/general.ts +15 -0
- package/src/store/global/initialState.ts +13 -0
- package/src/store/image/slices/generationConfig/initialState.ts +5 -5
- package/src/store/image/slices/generationConfig/selectors.test.ts +11 -4
- package/src/store/serverConfig/selectors.ts +1 -0
- package/src/store/tool/initialState.ts +11 -2
- package/src/store/tool/selectors/index.ts +1 -0
- package/src/store/tool/selectors/tool.ts +3 -1
- package/src/store/tool/slices/lobehubSkillStore/action.ts +361 -0
- package/src/store/tool/slices/lobehubSkillStore/index.ts +4 -0
- package/src/store/tool/slices/lobehubSkillStore/initialState.ts +24 -0
- package/src/store/tool/slices/lobehubSkillStore/selectors.ts +145 -0
- package/src/store/tool/slices/lobehubSkillStore/types.ts +100 -0
- package/src/store/tool/store.ts +8 -2
- package/vitest.config.mts +11 -6
- package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
- 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[];
|
package/src/services/upload.ts
CHANGED
|
@@ -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, {
|
|
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 {
|
|
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: [
|
|
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) =>
|
|
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
|
});
|