@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.
- package/.github/workflows/e2e.yml +6 -12
- package/.github/workflows/test.yml +3 -3
- package/CHANGELOG.md +59 -0
- package/CLAUDE.md +1 -1
- package/changelog/v1.json +18 -0
- package/docs/development/basic/feature-development.mdx +4 -5
- package/docs/development/basic/feature-development.zh-CN.mdx +4 -5
- 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/ar/components.json +1 -0
- package/locales/ar/file.json +4 -0
- package/locales/ar/models.json +29 -0
- package/locales/ar/setting.json +7 -0
- package/locales/bg-BG/components.json +1 -0
- package/locales/bg-BG/file.json +4 -0
- package/locales/bg-BG/models.json +1 -0
- package/locales/bg-BG/setting.json +7 -0
- package/locales/de-DE/components.json +1 -0
- package/locales/de-DE/file.json +4 -0
- package/locales/de-DE/models.json +29 -0
- package/locales/de-DE/setting.json +7 -0
- package/locales/en-US/common.json +0 -1
- package/locales/en-US/components.json +1 -0
- package/locales/en-US/file.json +4 -0
- package/locales/en-US/models.json +1 -0
- package/locales/en-US/setting.json +3 -0
- package/locales/es-ES/components.json +1 -0
- package/locales/es-ES/file.json +4 -0
- package/locales/es-ES/models.json +43 -0
- package/locales/es-ES/setting.json +7 -0
- package/locales/fa-IR/components.json +1 -0
- package/locales/fa-IR/file.json +4 -0
- package/locales/fa-IR/models.json +54 -0
- package/locales/fa-IR/setting.json +7 -0
- package/locales/fr-FR/components.json +1 -0
- package/locales/fr-FR/file.json +4 -0
- package/locales/fr-FR/models.json +31 -0
- package/locales/fr-FR/setting.json +7 -0
- package/locales/it-IT/components.json +1 -0
- package/locales/it-IT/file.json +4 -0
- package/locales/it-IT/models.json +43 -0
- package/locales/it-IT/setting.json +7 -0
- package/locales/ja-JP/components.json +1 -0
- package/locales/ja-JP/file.json +4 -0
- package/locales/ja-JP/models.json +28 -0
- package/locales/ja-JP/setting.json +7 -0
- package/locales/ko-KR/components.json +1 -0
- package/locales/ko-KR/file.json +4 -0
- package/locales/ko-KR/models.json +37 -0
- package/locales/ko-KR/setting.json +7 -0
- package/locales/nl-NL/components.json +1 -0
- package/locales/nl-NL/file.json +4 -0
- package/locales/nl-NL/models.json +13 -0
- package/locales/nl-NL/setting.json +7 -0
- package/locales/pl-PL/components.json +1 -0
- package/locales/pl-PL/file.json +4 -0
- package/locales/pl-PL/models.json +13 -0
- package/locales/pl-PL/setting.json +7 -0
- package/locales/pt-BR/components.json +1 -0
- package/locales/pt-BR/file.json +4 -0
- package/locales/pt-BR/models.json +29 -0
- package/locales/pt-BR/setting.json +7 -0
- package/locales/ru-RU/components.json +1 -0
- package/locales/ru-RU/file.json +4 -0
- package/locales/ru-RU/models.json +1 -0
- package/locales/ru-RU/setting.json +7 -0
- package/locales/tr-TR/components.json +1 -0
- package/locales/tr-TR/file.json +4 -0
- package/locales/tr-TR/models.json +29 -0
- package/locales/tr-TR/setting.json +7 -0
- package/locales/vi-VN/components.json +1 -0
- package/locales/vi-VN/file.json +4 -0
- package/locales/vi-VN/models.json +1 -0
- package/locales/vi-VN/setting.json +7 -0
- package/locales/zh-CN/file.json +4 -0
- package/locales/zh-CN/models.json +46 -0
- package/locales/zh-CN/setting.json +3 -0
- package/locales/zh-TW/components.json +1 -0
- package/locales/zh-TW/file.json +4 -0
- package/locales/zh-TW/models.json +35 -0
- package/locales/zh-TW/setting.json +7 -0
- package/package.json +5 -5
- package/packages/const/src/index.ts +1 -0
- package/packages/const/src/lobehubSkill.ts +55 -0
- 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)/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/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/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/_helpers/resolveContext.ts +8 -8
- package/src/server/routers/lambda/agent.ts +1 -1
- package/src/server/routers/lambda/aiModel.ts +1 -1
- package/src/server/routers/lambda/comfyui.ts +1 -1
- package/src/server/routers/lambda/document.ts +44 -0
- package/src/server/routers/lambda/exporter.ts +1 -1
- package/src/server/routers/lambda/image.ts +13 -13
- package/src/server/routers/lambda/klavis.ts +10 -10
- package/src/server/routers/lambda/market/index.ts +6 -6
- package/src/server/routers/lambda/message.ts +2 -2
- package/src/server/routers/lambda/plugin.ts +1 -1
- package/src/server/routers/lambda/ragEval.ts +2 -2
- package/src/server/routers/lambda/topic.ts +3 -3
- package/src/server/routers/lambda/user.ts +10 -10
- package/src/server/routers/lambda/userMemories.ts +6 -6
- 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/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 +1 -0
- package/src/features/FileViewer/Renderer/JavaScript/index.tsx +0 -66
- package/src/features/FileViewer/Renderer/TXT/index.tsx +0 -50
|
@@ -36,7 +36,7 @@ export const klavisRouter = router({
|
|
|
36
36
|
.mutation(async ({ input, ctx }) => {
|
|
37
37
|
const { serverName, userId, identifier } = input;
|
|
38
38
|
|
|
39
|
-
//
|
|
39
|
+
// Create a single server instance
|
|
40
40
|
const response = await ctx.klavisClient.mcpServer.createServerInstance({
|
|
41
41
|
serverName: serverName as any,
|
|
42
42
|
userId,
|
|
@@ -44,11 +44,11 @@ export const klavisRouter = router({
|
|
|
44
44
|
|
|
45
45
|
const { serverUrl, instanceId, oauthUrl } = response;
|
|
46
46
|
|
|
47
|
-
//
|
|
47
|
+
// Get the tool list for this server
|
|
48
48
|
const toolsResponse = await ctx.klavisClient.mcpServer.getTools(serverName as any);
|
|
49
49
|
const tools = toolsResponse.tools || [];
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// Save to database using the provided identifier (format: lowercase, spaces replaced with hyphens)
|
|
52
52
|
const manifest: LobeChatPluginManifest = {
|
|
53
53
|
api: tools.map((tool: any) => ({
|
|
54
54
|
description: tool.description || '',
|
|
@@ -64,8 +64,8 @@ export const klavisRouter = router({
|
|
|
64
64
|
type: 'default',
|
|
65
65
|
};
|
|
66
66
|
|
|
67
|
-
//
|
|
68
|
-
const isAuthenticated = !oauthUrl; //
|
|
67
|
+
// Save to database with oauthUrl and isAuthenticated status
|
|
68
|
+
const isAuthenticated = !oauthUrl; // If there's no oauthUrl, authentication is not required or already authenticated
|
|
69
69
|
await ctx.pluginModel.create({
|
|
70
70
|
customParams: {
|
|
71
71
|
klavis: {
|
|
@@ -104,10 +104,10 @@ export const klavisRouter = router({
|
|
|
104
104
|
}),
|
|
105
105
|
)
|
|
106
106
|
.mutation(async ({ input, ctx }) => {
|
|
107
|
-
//
|
|
107
|
+
// Call Klavis API to delete server instance
|
|
108
108
|
await ctx.klavisClient.mcpServer.deleteServerInstance(input.instanceId);
|
|
109
109
|
|
|
110
|
-
//
|
|
110
|
+
// Delete from database (using identifier)
|
|
111
111
|
await ctx.pluginModel.delete(input.identifier);
|
|
112
112
|
|
|
113
113
|
return { success: true };
|
|
@@ -200,10 +200,10 @@ export const klavisRouter = router({
|
|
|
200
200
|
const { identifier, serverName, serverUrl, instanceId, tools, isAuthenticated, oauthUrl } =
|
|
201
201
|
input;
|
|
202
202
|
|
|
203
|
-
//
|
|
203
|
+
// Get existing plugin (using identifier)
|
|
204
204
|
const existingPlugin = await ctx.pluginModel.findById(identifier);
|
|
205
205
|
|
|
206
|
-
//
|
|
206
|
+
// Build manifest containing all tools
|
|
207
207
|
const manifest: LobeChatPluginManifest = {
|
|
208
208
|
api: tools.map((tool) => ({
|
|
209
209
|
description: tool.description || '',
|
|
@@ -229,7 +229,7 @@ export const klavisRouter = router({
|
|
|
229
229
|
},
|
|
230
230
|
};
|
|
231
231
|
|
|
232
|
-
//
|
|
232
|
+
// Update or create plugin
|
|
233
233
|
if (existingPlugin) {
|
|
234
234
|
await ctx.pluginModel.update(identifier, { customParams, manifest });
|
|
235
235
|
} else {
|
|
@@ -559,11 +559,11 @@ export const marketRouter = router({
|
|
|
559
559
|
log('get access token, expiresIn value:', expiresIn);
|
|
560
560
|
log('expiresIn type:', typeof expiresIn);
|
|
561
561
|
|
|
562
|
-
const expirationTime = new Date(Date.now() + (expiresIn - 60) * 1000); //
|
|
562
|
+
const expirationTime = new Date(Date.now() + (expiresIn - 60) * 1000); // Expire 60 seconds early
|
|
563
563
|
|
|
564
564
|
log('expirationTime:', expirationTime.toISOString());
|
|
565
565
|
|
|
566
|
-
//
|
|
566
|
+
// Set HTTP-Only Cookie to store the actual access token
|
|
567
567
|
const tokenCookie = serialize('mp_token', accessToken, {
|
|
568
568
|
expires: expirationTime,
|
|
569
569
|
httpOnly: true,
|
|
@@ -572,7 +572,7 @@ export const marketRouter = router({
|
|
|
572
572
|
secure: process.env.NODE_ENV === 'production',
|
|
573
573
|
});
|
|
574
574
|
|
|
575
|
-
//
|
|
575
|
+
// Set client-readable status marker cookie (without actual token)
|
|
576
576
|
const statusCookie = serialize('mp_token_status', 'active', {
|
|
577
577
|
expires: expirationTime,
|
|
578
578
|
httpOnly: false,
|
|
@@ -581,7 +581,7 @@ export const marketRouter = router({
|
|
|
581
581
|
secure: process.env.NODE_ENV === 'production',
|
|
582
582
|
});
|
|
583
583
|
|
|
584
|
-
//
|
|
584
|
+
// Set Set-Cookie header via context's resHeaders
|
|
585
585
|
ctx.resHeaders?.append('Set-Cookie', tokenCookie);
|
|
586
586
|
ctx.resHeaders?.append('Set-Cookie', statusCookie);
|
|
587
587
|
|
|
@@ -650,7 +650,7 @@ export const marketRouter = router({
|
|
|
650
650
|
return { success: true };
|
|
651
651
|
} catch (error) {
|
|
652
652
|
console.error('Error reporting call: %O', error);
|
|
653
|
-
//
|
|
653
|
+
// Don't throw error, as reporting failure should not affect main flow
|
|
654
654
|
return { success: false };
|
|
655
655
|
}
|
|
656
656
|
}),
|
|
@@ -678,7 +678,7 @@ export const marketRouter = router({
|
|
|
678
678
|
return { success: true };
|
|
679
679
|
} catch (error) {
|
|
680
680
|
log('Error reporting MCP installation result: %O', error);
|
|
681
|
-
//
|
|
681
|
+
// Don't throw error, as reporting failure should not affect main flow
|
|
682
682
|
return { success: false };
|
|
683
683
|
}
|
|
684
684
|
}),
|
|
@@ -75,13 +75,13 @@ export const messageRouter = router({
|
|
|
75
75
|
createMessage: messageProcedure
|
|
76
76
|
.input(CreateNewMessageParamsSchema)
|
|
77
77
|
.mutation(async ({ input, ctx }) => {
|
|
78
|
-
//
|
|
78
|
+
// If there's no agentId but has sessionId, resolve agentId from sessionId
|
|
79
79
|
let agentId = input.agentId;
|
|
80
80
|
if (!agentId && input.sessionId) {
|
|
81
81
|
agentId = (await resolveAgentIdFromSession(input.sessionId, ctx.serverDB, ctx.userId))!;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// Create message with the resolved agentId
|
|
85
85
|
return ctx.messageService.createMessage({ ...input, agentId } as any);
|
|
86
86
|
}),
|
|
87
87
|
|
|
@@ -65,7 +65,7 @@ export const pluginRouter = router({
|
|
|
65
65
|
return data.identifier;
|
|
66
66
|
}),
|
|
67
67
|
|
|
68
|
-
// TODO:
|
|
68
|
+
// TODO: In the future, this method also needs to use authedProcedure
|
|
69
69
|
getPlugins: publicProcedure.query(async ({ ctx }): Promise<LobeTool[]> => {
|
|
70
70
|
if (!ctx.userId) return [];
|
|
71
71
|
|
|
@@ -251,7 +251,7 @@ export const ragEvalRouter = router({
|
|
|
251
251
|
const isSuccess = records.every((record) => record.status === EvalEvaluationStatus.Success);
|
|
252
252
|
|
|
253
253
|
if (isSuccess) {
|
|
254
|
-
//
|
|
254
|
+
// Upload results to S3
|
|
255
255
|
|
|
256
256
|
const evalRecords = records.map((record) => ({
|
|
257
257
|
question: record.question,
|
|
@@ -265,7 +265,7 @@ export const ragEvalRouter = router({
|
|
|
265
265
|
|
|
266
266
|
await ctx.fileService.uploadContent(path, JSONL.stringify(evalRecords));
|
|
267
267
|
|
|
268
|
-
//
|
|
268
|
+
// Save data
|
|
269
269
|
await ctx.evaluationModel.update(input.id, {
|
|
270
270
|
status: EvalEvaluationStatus.Success,
|
|
271
271
|
evalRecordsUrl: await ctx.fileService.getFullFileUrl(path),
|
|
@@ -49,7 +49,7 @@ export const topicRouter = router({
|
|
|
49
49
|
),
|
|
50
50
|
)
|
|
51
51
|
.mutation(async ({ input, ctx }): Promise<BatchTaskResult> => {
|
|
52
|
-
//
|
|
52
|
+
// Resolve sessionId for each topic
|
|
53
53
|
const resolvedTopics = await Promise.all(
|
|
54
54
|
input.map(async (item) => {
|
|
55
55
|
const { agentId, ...rest } = item;
|
|
@@ -162,7 +162,7 @@ export const topicRouter = router({
|
|
|
162
162
|
return { items: result.items, total: result.total };
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
165
|
+
// If sessionId is provided but no agentId, need to reverse lookup agentId
|
|
166
166
|
let effectiveAgentId = rest.agentId;
|
|
167
167
|
if (!effectiveAgentId && sessionId) {
|
|
168
168
|
effectiveAgentId = await resolveAgentIdFromSession(sessionId, ctx.serverDB, ctx.userId);
|
|
@@ -430,7 +430,7 @@ export const topicRouter = router({
|
|
|
430
430
|
.mutation(async ({ input, ctx }) => {
|
|
431
431
|
const { agentId, ...restValue } = input.value;
|
|
432
432
|
|
|
433
|
-
//
|
|
433
|
+
// If agentId is provided, resolve to sessionId
|
|
434
434
|
let resolvedSessionId = restValue.sessionId;
|
|
435
435
|
if (agentId && !resolvedSessionId) {
|
|
436
436
|
const resolved = await resolveContext({ agentId }, ctx.serverDB, ctx.userId);
|
|
@@ -150,7 +150,7 @@ export const userRouter = router({
|
|
|
150
150
|
firstName: state.firstName,
|
|
151
151
|
fullName: state.fullName,
|
|
152
152
|
|
|
153
|
-
//
|
|
153
|
+
// Has conversation if there are messages or has created any assistant
|
|
154
154
|
hasConversation: hasAnyMessages || hasExtraSession,
|
|
155
155
|
|
|
156
156
|
interests: state.interests,
|
|
@@ -190,40 +190,40 @@ export const userRouter = router({
|
|
|
190
190
|
}),
|
|
191
191
|
|
|
192
192
|
updateAvatar: userProcedure.input(z.string()).mutation(async ({ ctx, input }) => {
|
|
193
|
-
//
|
|
193
|
+
// If it's Base64 data, need to upload to S3
|
|
194
194
|
if (input.startsWith('data:image')) {
|
|
195
195
|
try {
|
|
196
|
-
//
|
|
196
|
+
// Extract mimeType, e.g., "image/png"
|
|
197
197
|
const prefix = 'data:';
|
|
198
198
|
const semicolonIndex = input.indexOf(';');
|
|
199
199
|
const mimeType =
|
|
200
200
|
semicolonIndex !== -1 ? input.slice(prefix.length, semicolonIndex) : 'image/png';
|
|
201
201
|
const fileType = mimeType.split('/')[1];
|
|
202
202
|
|
|
203
|
-
//
|
|
203
|
+
// Split string to get the Base64 part
|
|
204
204
|
const commaIndex = input.indexOf(',');
|
|
205
205
|
if (commaIndex === -1) {
|
|
206
206
|
throw new Error('Invalid Base64 data');
|
|
207
207
|
}
|
|
208
208
|
const base64Data = input.slice(commaIndex + 1);
|
|
209
209
|
|
|
210
|
-
//
|
|
210
|
+
// Create S3 client
|
|
211
211
|
const s3 = new FileS3();
|
|
212
212
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
213
|
+
// Use UUID to generate unique filename to prevent caching issues
|
|
214
|
+
// Get old avatar URL for later deletion
|
|
215
215
|
const userState = await ctx.userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults);
|
|
216
216
|
const oldAvatarUrl = userState.avatar;
|
|
217
217
|
|
|
218
218
|
const fileName = `${uuidv4()}.${fileType}`;
|
|
219
219
|
const filePath = `user/avatar/${ctx.userId}/${fileName}`;
|
|
220
220
|
|
|
221
|
-
//
|
|
221
|
+
// Convert Base64 data to Buffer and upload to S3
|
|
222
222
|
const buffer = Buffer.from(base64Data, 'base64');
|
|
223
223
|
|
|
224
224
|
await s3.uploadBuffer(filePath, buffer, mimeType);
|
|
225
225
|
|
|
226
|
-
//
|
|
226
|
+
// Delete old avatar
|
|
227
227
|
if (oldAvatarUrl && oldAvatarUrl.startsWith('/webapi/')) {
|
|
228
228
|
const oldFilePath = oldAvatarUrl.replace('/webapi/', '');
|
|
229
229
|
await s3.deleteFile(oldFilePath);
|
|
@@ -239,7 +239,7 @@ export const userRouter = router({
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
//
|
|
242
|
+
// If it's not Base64 data, directly use URL to update user avatar
|
|
243
243
|
return ctx.userModel.updateUser({ avatar: input });
|
|
244
244
|
}),
|
|
245
245
|
|
|
@@ -305,11 +305,11 @@ export const userMemoriesRouter = router({
|
|
|
305
305
|
}
|
|
306
306
|
}),
|
|
307
307
|
|
|
308
|
-
// REVIEW
|
|
309
|
-
// REVIEW
|
|
310
|
-
// REVIEW
|
|
311
|
-
//
|
|
312
|
-
//
|
|
308
|
+
// REVIEW: Extract memories directly from current topic
|
|
309
|
+
// REVIEW: We need a function implementation that can be triggered both by cron and manually by users for "daily/weekly/periodic" memory extraction/generation
|
|
310
|
+
// REVIEW: Scheduled task
|
|
311
|
+
// Don't use tRPC, use server/service directly
|
|
312
|
+
// Reference: https://github.com/lobehub/lobe-chat-cloud/blob/886ff2fcd44b7b00a3aa8906f84914a6dcaa1815/src/app/(backend)/cron/reset-budgets/route.ts#L214
|
|
313
313
|
reEmbedMemories: memoryProcedure
|
|
314
314
|
.input(reEmbedInputSchema.optional())
|
|
315
315
|
.mutation(async ({ ctx, input }) => {
|
|
@@ -740,7 +740,7 @@ export const userMemoriesRouter = router({
|
|
|
740
740
|
}
|
|
741
741
|
}),
|
|
742
742
|
|
|
743
|
-
// REVIEW:
|
|
743
|
+
// REVIEW: Need to implement tool memory api
|
|
744
744
|
toolAddContextMemory: memoryProcedure
|
|
745
745
|
.input(ContextMemoryItemSchema)
|
|
746
746
|
.mutation(async ({ input, ctx }) => {
|
|
@@ -7,6 +7,7 @@ import { z } from 'zod';
|
|
|
7
7
|
import { type ToolCallContent } from '@/libs/mcp';
|
|
8
8
|
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
|
9
9
|
import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
|
|
10
|
+
import { marketSDK, requireMarketAuth } from '@/libs/trpc/lambda/middleware/marketSDK';
|
|
10
11
|
import { generateTrustedClientToken, isTrustedClientEnabled } from '@/libs/trusted-client';
|
|
11
12
|
import { FileS3 } from '@/server/modules/S3';
|
|
12
13
|
import { DiscoverService } from '@/server/services/discover';
|
|
@@ -41,6 +42,23 @@ const marketToolProcedure = authedProcedure
|
|
|
41
42
|
});
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
// ============================== LobeHub Skill Procedures ==============================
|
|
46
|
+
/**
|
|
47
|
+
* LobeHub Skill procedure with SDK and optional auth
|
|
48
|
+
* Used for routes that may work without auth (like listing providers)
|
|
49
|
+
*/
|
|
50
|
+
const lobehubSkillBaseProcedure = authedProcedure
|
|
51
|
+
.use(serverDatabase)
|
|
52
|
+
.use(telemetry)
|
|
53
|
+
.use(marketUserInfo)
|
|
54
|
+
.use(marketSDK);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* LobeHub Skill procedure with required auth
|
|
58
|
+
* Used for routes that require user authentication
|
|
59
|
+
*/
|
|
60
|
+
const lobehubSkillAuthProcedure = lobehubSkillBaseProcedure.use(requireMarketAuth);
|
|
61
|
+
|
|
44
62
|
// ============================== Schema Definitions ==============================
|
|
45
63
|
|
|
46
64
|
// Schema for metadata that frontend needs to pass (for cloud MCP reporting)
|
|
@@ -269,6 +287,249 @@ export const marketRouter = router({
|
|
|
269
287
|
}
|
|
270
288
|
}),
|
|
271
289
|
|
|
290
|
+
// ============================== LobeHub Skill ==============================
|
|
291
|
+
/**
|
|
292
|
+
* Call a LobeHub Skill tool
|
|
293
|
+
*/
|
|
294
|
+
connectCallTool: lobehubSkillAuthProcedure
|
|
295
|
+
.input(
|
|
296
|
+
z.object({
|
|
297
|
+
args: z.record(z.any()).optional(),
|
|
298
|
+
provider: z.string(),
|
|
299
|
+
toolName: z.string(),
|
|
300
|
+
}),
|
|
301
|
+
)
|
|
302
|
+
.mutation(async ({ input, ctx }) => {
|
|
303
|
+
const { provider, toolName, args } = input;
|
|
304
|
+
log('connectCallTool: provider=%s, tool=%s', provider, toolName);
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const response = await ctx.marketSDK.skills.callTool(provider, {
|
|
308
|
+
args: args || {},
|
|
309
|
+
tool: toolName,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
log('connectCallTool response: %O', response);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
data: response.data,
|
|
316
|
+
success: response.success,
|
|
317
|
+
};
|
|
318
|
+
} catch (error) {
|
|
319
|
+
const errorMessage = (error as Error).message;
|
|
320
|
+
log('connectCallTool error: %s', errorMessage);
|
|
321
|
+
|
|
322
|
+
if (errorMessage.includes('NOT_CONNECTED')) {
|
|
323
|
+
throw new TRPCError({
|
|
324
|
+
code: 'UNAUTHORIZED',
|
|
325
|
+
message: 'Provider not connected. Please authorize first.',
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (errorMessage.includes('TOKEN_EXPIRED')) {
|
|
330
|
+
throw new TRPCError({
|
|
331
|
+
code: 'UNAUTHORIZED',
|
|
332
|
+
message: 'Token expired. Please re-authorize.',
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
throw new TRPCError({
|
|
337
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
338
|
+
message: `Failed to call tool: ${errorMessage}`,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}),
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get all connections health status
|
|
345
|
+
*/
|
|
346
|
+
connectGetAllHealth: lobehubSkillAuthProcedure.query(async ({ ctx }) => {
|
|
347
|
+
log('connectGetAllHealth');
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const response = await ctx.marketSDK.connect.getAllHealth();
|
|
351
|
+
return {
|
|
352
|
+
connections: response.connections || [],
|
|
353
|
+
summary: response.summary,
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
log('connectGetAllHealth error: %O', error);
|
|
357
|
+
throw new TRPCError({
|
|
358
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
359
|
+
message: `Failed to get connections health: ${(error as Error).message}`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get authorize URL for a provider
|
|
366
|
+
* This calls the SDK's authorize method which generates a secure authorization URL
|
|
367
|
+
*/
|
|
368
|
+
connectGetAuthorizeUrl: lobehubSkillAuthProcedure
|
|
369
|
+
.input(
|
|
370
|
+
z.object({
|
|
371
|
+
provider: z.string(),
|
|
372
|
+
redirectUri: z.string().optional(),
|
|
373
|
+
scopes: z.array(z.string()).optional(),
|
|
374
|
+
}),
|
|
375
|
+
)
|
|
376
|
+
.query(async ({ input, ctx }) => {
|
|
377
|
+
log('connectGetAuthorizeUrl: provider=%s', input.provider);
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const response = await ctx.marketSDK.connect.authorize(input.provider, {
|
|
381
|
+
redirect_uri: input.redirectUri,
|
|
382
|
+
scopes: input.scopes,
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
authorizeUrl: response.authorize_url,
|
|
387
|
+
code: response.code,
|
|
388
|
+
expiresIn: response.expires_in,
|
|
389
|
+
};
|
|
390
|
+
} catch (error) {
|
|
391
|
+
log('connectGetAuthorizeUrl error: %O', error);
|
|
392
|
+
throw new TRPCError({
|
|
393
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
394
|
+
message: `Failed to get authorize URL: ${(error as Error).message}`,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}),
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get connection status for a provider
|
|
401
|
+
*/
|
|
402
|
+
connectGetStatus: lobehubSkillAuthProcedure
|
|
403
|
+
.input(z.object({ provider: z.string() }))
|
|
404
|
+
.query(async ({ input, ctx }) => {
|
|
405
|
+
log('connectGetStatus: provider=%s', input.provider);
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const response = await ctx.marketSDK.connect.getStatus(input.provider);
|
|
409
|
+
return {
|
|
410
|
+
connected: response.connected,
|
|
411
|
+
connection: response.connection,
|
|
412
|
+
icon: (response as any).icon,
|
|
413
|
+
providerName: (response as any).providerName,
|
|
414
|
+
};
|
|
415
|
+
} catch (error) {
|
|
416
|
+
log('connectGetStatus error: %O', error);
|
|
417
|
+
throw new TRPCError({
|
|
418
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
419
|
+
message: `Failed to get status: ${(error as Error).message}`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}),
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* List all user connections
|
|
426
|
+
*/
|
|
427
|
+
connectListConnections: lobehubSkillAuthProcedure.query(async ({ ctx }) => {
|
|
428
|
+
log('connectListConnections');
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const response = await ctx.marketSDK.connect.listConnections();
|
|
432
|
+
// Debug logging
|
|
433
|
+
log('connectListConnections raw response: %O', response);
|
|
434
|
+
log('connectListConnections connections: %O', response.connections);
|
|
435
|
+
return {
|
|
436
|
+
connections: response.connections || [],
|
|
437
|
+
};
|
|
438
|
+
} catch (error) {
|
|
439
|
+
log('connectListConnections error: %O', error);
|
|
440
|
+
throw new TRPCError({
|
|
441
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
442
|
+
message: `Failed to list connections: ${(error as Error).message}`,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}),
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* List available providers (public, no auth required)
|
|
449
|
+
*/
|
|
450
|
+
connectListProviders: lobehubSkillBaseProcedure.query(async ({ ctx }) => {
|
|
451
|
+
log('connectListProviders');
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const response = await ctx.marketSDK.skills.listProviders();
|
|
455
|
+
return {
|
|
456
|
+
providers: response.providers || [],
|
|
457
|
+
};
|
|
458
|
+
} catch (error) {
|
|
459
|
+
log('connectListProviders error: %O', error);
|
|
460
|
+
throw new TRPCError({
|
|
461
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
462
|
+
message: `Failed to list providers: ${(error as Error).message}`,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
}),
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* List tools for a provider
|
|
469
|
+
*/
|
|
470
|
+
connectListTools: lobehubSkillBaseProcedure
|
|
471
|
+
.input(z.object({ provider: z.string() }))
|
|
472
|
+
.query(async ({ input, ctx }) => {
|
|
473
|
+
log('connectListTools: provider=%s', input.provider);
|
|
474
|
+
|
|
475
|
+
try {
|
|
476
|
+
const response = await ctx.marketSDK.skills.listTools(input.provider);
|
|
477
|
+
return {
|
|
478
|
+
provider: input.provider,
|
|
479
|
+
tools: response.tools || [],
|
|
480
|
+
};
|
|
481
|
+
} catch (error) {
|
|
482
|
+
log('connectListTools error: %O', error);
|
|
483
|
+
throw new TRPCError({
|
|
484
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
485
|
+
message: `Failed to list tools: ${(error as Error).message}`,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}),
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Refresh token for a provider
|
|
492
|
+
*/
|
|
493
|
+
connectRefresh: lobehubSkillAuthProcedure
|
|
494
|
+
.input(z.object({ provider: z.string() }))
|
|
495
|
+
.mutation(async ({ input, ctx }) => {
|
|
496
|
+
log('connectRefresh: provider=%s', input.provider);
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
const response = await ctx.marketSDK.connect.refresh(input.provider);
|
|
500
|
+
return {
|
|
501
|
+
connection: response.connection,
|
|
502
|
+
refreshed: response.refreshed,
|
|
503
|
+
};
|
|
504
|
+
} catch (error) {
|
|
505
|
+
log('connectRefresh error: %O', error);
|
|
506
|
+
throw new TRPCError({
|
|
507
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
508
|
+
message: `Failed to refresh token: ${(error as Error).message}`,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}),
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Revoke connection for a provider
|
|
515
|
+
*/
|
|
516
|
+
connectRevoke: lobehubSkillAuthProcedure
|
|
517
|
+
.input(z.object({ provider: z.string() }))
|
|
518
|
+
.mutation(async ({ input, ctx }) => {
|
|
519
|
+
log('connectRevoke: provider=%s', input.provider);
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
await ctx.marketSDK.connect.revoke(input.provider);
|
|
523
|
+
return { success: true };
|
|
524
|
+
} catch (error) {
|
|
525
|
+
log('connectRevoke error: %O', error);
|
|
526
|
+
throw new TRPCError({
|
|
527
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
528
|
+
message: `Failed to revoke connection: ${(error as Error).message}`,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}),
|
|
532
|
+
|
|
272
533
|
/**
|
|
273
534
|
* Export a file from sandbox and upload to S3, then create a persistent file record
|
|
274
535
|
* This combines the previous getExportFileUploadUrl + callCodeInterpreterTool + createFileRecord flow
|
|
@@ -100,6 +100,28 @@ export class DocumentService {
|
|
|
100
100
|
return document;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Create multiple documents in batch (optimized for folder creation)
|
|
105
|
+
* Returns array of created documents with same order as input
|
|
106
|
+
*/
|
|
107
|
+
async createDocuments(
|
|
108
|
+
documents: Array<{
|
|
109
|
+
content?: string;
|
|
110
|
+
editorData: Record<string, any>;
|
|
111
|
+
fileType?: string;
|
|
112
|
+
knowledgeBaseId?: string;
|
|
113
|
+
metadata?: Record<string, any>;
|
|
114
|
+
parentId?: string;
|
|
115
|
+
slug?: string;
|
|
116
|
+
title: string;
|
|
117
|
+
}>,
|
|
118
|
+
): Promise<DocumentItem[]> {
|
|
119
|
+
// Create all documents in parallel for better performance
|
|
120
|
+
const results = await Promise.all(documents.map((params) => this.createDocument(params)));
|
|
121
|
+
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
/**
|
|
104
126
|
* Query documents with pagination
|
|
105
127
|
*/
|
|
@@ -28,6 +28,10 @@ export class DocumentService {
|
|
|
28
28
|
return lambdaClient.document.createDocument.mutate(params);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
async createDocuments(documents: CreateDocumentParams[]): Promise<DocumentItem[]> {
|
|
32
|
+
return lambdaClient.document.createDocuments.mutate({ documents });
|
|
33
|
+
}
|
|
34
|
+
|
|
31
35
|
async queryDocuments(params?: {
|
|
32
36
|
current?: number;
|
|
33
37
|
fileTypes?: string[];
|
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
|
|