@lobehub/lobehub 2.0.0-next.193 → 2.0.0-next.195

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 (46) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/models.json +39 -0
  4. package/locales/bg-BG/models.json +40 -0
  5. package/locales/de-DE/models.json +31 -0
  6. package/locales/es-ES/models.json +47 -0
  7. package/locales/fa-IR/models.json +1 -0
  8. package/locales/fr-FR/models.json +1 -0
  9. package/locales/it-IT/models.json +1 -0
  10. package/locales/ja-JP/models.json +53 -0
  11. package/locales/ko-KR/models.json +38 -0
  12. package/locales/nl-NL/models.json +1 -0
  13. package/locales/pl-PL/models.json +1 -0
  14. package/locales/pt-BR/models.json +15 -0
  15. package/locales/ru-RU/models.json +35 -0
  16. package/locales/tr-TR/models.json +29 -0
  17. package/locales/vi-VN/models.json +1 -0
  18. package/locales/zh-CN/models.json +59 -0
  19. package/locales/zh-TW/models.json +31 -0
  20. package/package.json +1 -1
  21. package/packages/database/src/models/user.ts +8 -0
  22. package/packages/database/src/repositories/aiInfra/index.test.ts +11 -8
  23. package/packages/database/src/repositories/dataExporter/index.test.ts +11 -9
  24. package/packages/database/src/repositories/tableViewer/index.test.ts +13 -14
  25. package/packages/model-runtime/src/providers/zhipu/index.ts +6 -6
  26. package/src/envs/app.ts +2 -0
  27. package/src/libs/trpc/lambda/middleware/index.ts +1 -0
  28. package/src/libs/trpc/lambda/middleware/telemetry.test.ts +237 -0
  29. package/src/libs/trpc/lambda/middleware/telemetry.ts +74 -0
  30. package/src/server/routers/lambda/market/index.ts +1 -93
  31. package/src/server/routers/tools/_helpers/index.ts +1 -0
  32. package/src/server/routers/tools/_helpers/scheduleToolCallReport.ts +113 -0
  33. package/src/server/routers/tools/index.ts +2 -2
  34. package/src/server/routers/tools/market.ts +375 -0
  35. package/src/server/routers/tools/mcp.ts +77 -20
  36. package/src/services/chat/index.ts +0 -2
  37. package/src/services/codeInterpreter.ts +6 -6
  38. package/src/services/mcp.test.ts +60 -46
  39. package/src/services/mcp.ts +67 -48
  40. package/src/store/chat/slices/plugin/action.test.ts +191 -0
  41. package/src/store/chat/slices/plugin/actions/internals.ts +2 -18
  42. package/src/store/chat/slices/plugin/actions/pluginTypes.ts +31 -44
  43. package/packages/database/src/client/db.test.ts +0 -52
  44. package/packages/database/src/client/db.ts +0 -195
  45. package/packages/database/src/client/type.ts +0 -6
  46. package/src/server/routers/tools/codeInterpreter.ts +0 -255
@@ -0,0 +1,375 @@
1
+ import { type CodeInterpreterToolName, MarketSDK } from '@lobehub/market-sdk';
2
+ import { TRPCError } from '@trpc/server';
3
+ import debug from 'debug';
4
+ import { z } from 'zod';
5
+
6
+ import { DocumentModel } from '@/database/models/document';
7
+ import { FileModel } from '@/database/models/file';
8
+ import { type ToolCallContent } from '@/libs/mcp';
9
+ import { authedProcedure, router } from '@/libs/trpc/lambda';
10
+ import { marketUserInfo, serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
11
+ import { generateTrustedClientToken } from '@/libs/trusted-client';
12
+ import { FileS3 } from '@/server/modules/S3';
13
+ import { DiscoverService } from '@/server/services/discover';
14
+ import { FileService } from '@/server/services/file';
15
+ import {
16
+ contentBlocksToString,
17
+ processContentBlocks,
18
+ } from '@/server/services/mcp/contentProcessor';
19
+
20
+ import { scheduleToolCallReport } from './_helpers';
21
+
22
+ const log = debug('lobe-server:tools:market');
23
+
24
+ // ============================== Common Procedure ==============================
25
+ const marketToolProcedure = authedProcedure
26
+ .use(serverDatabase)
27
+ .use(telemetry)
28
+ .use(marketUserInfo)
29
+ .use(async ({ ctx, next }) => {
30
+ const { UserModel } = await import('@/database/models/user');
31
+ const userModel = new UserModel(ctx.serverDB, ctx.userId);
32
+
33
+ return next({
34
+ ctx: {
35
+ discoverService: new DiscoverService({
36
+ accessToken: ctx.marketAccessToken,
37
+ userInfo: ctx.marketUserInfo,
38
+ }),
39
+ fileService: new FileService(ctx.serverDB, ctx.userId),
40
+ userModel,
41
+ },
42
+ });
43
+ });
44
+
45
+ // ============================== Schema Definitions ==============================
46
+
47
+ // Schema for metadata that frontend needs to pass (for cloud MCP reporting)
48
+ const metaSchema = z
49
+ .object({
50
+ customPluginInfo: z
51
+ .object({
52
+ avatar: z.string().optional(),
53
+ description: z.string().optional(),
54
+ name: z.string().optional(),
55
+ })
56
+ .optional(),
57
+ isCustomPlugin: z.boolean().optional(),
58
+ sessionId: z.string().optional(),
59
+ version: z.string().optional(),
60
+ })
61
+ .optional();
62
+
63
+ // Schema for code interpreter tool call request
64
+ const callCodeInterpreterToolSchema = z.object({
65
+ marketAccessToken: z.string().optional(),
66
+ params: z.record(z.any()),
67
+ toolName: z.string(),
68
+ topicId: z.string(),
69
+ userId: z.string(),
70
+ });
71
+
72
+ // Schema for getting export file upload URL
73
+ const getExportFileUploadUrlSchema = z.object({
74
+ filename: z.string(),
75
+ topicId: z.string(),
76
+ });
77
+
78
+ // Schema for saving exported file content to document
79
+ const saveExportedFileContentSchema = z.object({
80
+ content: z.string(),
81
+ fileId: z.string(),
82
+ fileType: z.string(),
83
+ filename: z.string(),
84
+ url: z.string(),
85
+ });
86
+
87
+ // Schema for cloud MCP endpoint call
88
+ const callCloudMcpEndpointSchema = z.object({
89
+ apiParams: z.record(z.any()),
90
+ identifier: z.string(),
91
+ meta: metaSchema,
92
+ toolName: z.string(),
93
+ });
94
+
95
+ // ============================== Type Exports ==============================
96
+ export type CallCodeInterpreterToolInput = z.infer<typeof callCodeInterpreterToolSchema>;
97
+ export type GetExportFileUploadUrlInput = z.infer<typeof getExportFileUploadUrlSchema>;
98
+ export type SaveExportedFileContentInput = z.infer<typeof saveExportedFileContentSchema>;
99
+
100
+ export interface CallToolResult {
101
+ error?: {
102
+ message: string;
103
+ name?: string;
104
+ };
105
+ result: any;
106
+ sessionExpiredAndRecreated?: boolean;
107
+ success: boolean;
108
+ }
109
+
110
+ export interface GetExportFileUploadUrlResult {
111
+ downloadUrl: string;
112
+ error?: {
113
+ message: string;
114
+ };
115
+ key: string;
116
+ success: boolean;
117
+ uploadUrl: string;
118
+ }
119
+
120
+ export interface SaveExportedFileContentResult {
121
+ documentId?: string;
122
+ error?: {
123
+ message: string;
124
+ };
125
+ success: boolean;
126
+ }
127
+
128
+ // ============================== Router ==============================
129
+ export const marketRouter = router({
130
+ // ============================== Cloud MCP Gateway ==============================
131
+ callCloudMcpEndpoint: marketToolProcedure
132
+ .input(callCloudMcpEndpointSchema)
133
+ .mutation(async ({ input, ctx }) => {
134
+ log('callCloudMcpEndpoint input: %O', input);
135
+
136
+ const startTime = Date.now();
137
+ let success = true;
138
+ let errorCode: string | undefined;
139
+ let errorMessage: string | undefined;
140
+ let result: { content: string; state: any; success: boolean } | undefined;
141
+
142
+ try {
143
+ // Query user_settings to get market.accessToken
144
+ const userState = await ctx.userModel.getUserState(async () => ({}));
145
+ const userAccessToken = userState.settings?.market?.accessToken;
146
+
147
+ log('callCloudMcpEndpoint: userAccessToken exists=%s', !!userAccessToken);
148
+
149
+ if (!userAccessToken) {
150
+ throw new TRPCError({
151
+ code: 'UNAUTHORIZED',
152
+ message: 'User access token not found. Please sign in to Market first.',
153
+ });
154
+ }
155
+
156
+ const cloudResult = await ctx.discoverService.callCloudMcpEndpoint({
157
+ apiParams: input.apiParams,
158
+ identifier: input.identifier,
159
+ toolName: input.toolName,
160
+ userAccessToken,
161
+ });
162
+ const cloudResultContent = (cloudResult?.content ?? []) as ToolCallContent[];
163
+
164
+ // Format the cloud result to MCPToolCallResult format
165
+ // Process content blocks (upload images, etc.)
166
+ const newContent =
167
+ cloudResult?.isError || !ctx.fileService
168
+ ? cloudResultContent
169
+ : await processContentBlocks(cloudResultContent, ctx.fileService);
170
+
171
+ // Convert content blocks to string
172
+ const content = contentBlocksToString(newContent);
173
+ const state = { ...cloudResult, content: newContent };
174
+
175
+ result = { content, state, success: true };
176
+ return result;
177
+ } catch (error) {
178
+ success = false;
179
+ const err = error as Error;
180
+ errorCode = 'CALL_FAILED';
181
+ errorMessage = err.message;
182
+
183
+ log('Error calling cloud MCP endpoint: %O', error);
184
+
185
+ // Re-throw TRPCError as-is
186
+ if (error instanceof TRPCError) {
187
+ throw error;
188
+ }
189
+
190
+ throw new TRPCError({
191
+ code: 'INTERNAL_SERVER_ERROR',
192
+ message: 'Failed to call cloud MCP endpoint',
193
+ });
194
+ } finally {
195
+ scheduleToolCallReport({
196
+ errorCode,
197
+ errorMessage,
198
+ identifier: input.identifier,
199
+ marketAccessToken: ctx.marketAccessToken,
200
+ mcpType: 'cloud',
201
+ meta: input.meta,
202
+ requestPayload: input.apiParams,
203
+ result,
204
+ startTime,
205
+ success,
206
+ telemetryEnabled: ctx.telemetryEnabled,
207
+ toolName: input.toolName,
208
+ });
209
+ }
210
+ }),
211
+
212
+ // ============================== Code Interpreter ==============================
213
+ callCodeInterpreterTool: marketToolProcedure
214
+ .input(callCodeInterpreterToolSchema)
215
+ .mutation(async ({ input, ctx }) => {
216
+ const { toolName, params, userId, topicId, marketAccessToken } = input;
217
+
218
+ log('Calling cloud code interpreter tool: %s with params: %O', toolName, {
219
+ params,
220
+ topicId,
221
+ userId,
222
+ });
223
+ log('Market access token available: %s', marketAccessToken ? 'yes' : 'no');
224
+
225
+ // Generate trusted client token if user info is available
226
+ const trustedClientToken = ctx.marketUserInfo
227
+ ? generateTrustedClientToken(ctx.marketUserInfo)
228
+ : undefined;
229
+
230
+ try {
231
+ // Initialize MarketSDK with market access token and trusted client token
232
+ const market = new MarketSDK({
233
+ accessToken: marketAccessToken,
234
+ baseURL: process.env.NEXT_PUBLIC_MARKET_BASE_URL,
235
+ trustedClientToken,
236
+ });
237
+
238
+ // Call market-sdk's runBuildInTool
239
+ const response = await market.plugins.runBuildInTool(
240
+ toolName as CodeInterpreterToolName,
241
+ params as any,
242
+ { topicId, userId },
243
+ );
244
+
245
+ log('Cloud code interpreter tool %s response: %O', toolName, response);
246
+
247
+ if (!response.success) {
248
+ return {
249
+ error: {
250
+ message: response.error?.message || 'Unknown error',
251
+ name: response.error?.code,
252
+ },
253
+ result: null,
254
+ sessionExpiredAndRecreated: false,
255
+ success: false,
256
+ } as CallToolResult;
257
+ }
258
+
259
+ return {
260
+ result: response.data?.result,
261
+ sessionExpiredAndRecreated: response.data?.sessionExpiredAndRecreated || false,
262
+ success: true,
263
+ } as CallToolResult;
264
+ } catch (error) {
265
+ log('Error calling cloud code interpreter tool %s: %O', toolName, error);
266
+
267
+ return {
268
+ error: {
269
+ message: (error as Error).message,
270
+ name: (error as Error).name,
271
+ },
272
+ result: null,
273
+ sessionExpiredAndRecreated: false,
274
+ success: false,
275
+ } as CallToolResult;
276
+ }
277
+ }),
278
+
279
+ /**
280
+ * Generate a pre-signed upload URL for exporting files from sandbox
281
+ */
282
+ getExportFileUploadUrl: marketToolProcedure
283
+ .input(getExportFileUploadUrlSchema)
284
+ .mutation(async ({ input }) => {
285
+ const { filename, topicId } = input;
286
+
287
+ log('Generating export file upload URL for: %s in topic: %s', filename, topicId);
288
+
289
+ try {
290
+ const s3 = new FileS3();
291
+
292
+ // Generate a unique key for the exported file
293
+ const key = `code-interpreter-exports/${topicId}/${filename}`;
294
+
295
+ // Generate pre-signed upload URL
296
+ const uploadUrl = await s3.createPreSignedUrl(key);
297
+
298
+ // Generate download URL (pre-signed for preview)
299
+ const downloadUrl = await s3.createPreSignedUrlForPreview(key);
300
+
301
+ log('Generated upload URL for key: %s', key);
302
+
303
+ return {
304
+ downloadUrl,
305
+ key,
306
+ success: true,
307
+ uploadUrl,
308
+ } as GetExportFileUploadUrlResult;
309
+ } catch (error) {
310
+ log('Error generating export file upload URL: %O', error);
311
+
312
+ return {
313
+ downloadUrl: '',
314
+ error: {
315
+ message: (error as Error).message,
316
+ },
317
+ key: '',
318
+ success: false,
319
+ uploadUrl: '',
320
+ } as GetExportFileUploadUrlResult;
321
+ }
322
+ }),
323
+
324
+ /**
325
+ * Save exported file content to documents table
326
+ */
327
+ saveExportedFileContent: marketToolProcedure
328
+ .input(saveExportedFileContentSchema)
329
+ .mutation(async ({ ctx, input }) => {
330
+ const { content, fileId, fileType, filename, url } = input;
331
+
332
+ log('Saving exported file content: fileId=%s, filename=%s', fileId, filename);
333
+
334
+ try {
335
+ const documentModel = new DocumentModel(ctx.serverDB, ctx.userId);
336
+ const fileModel = new FileModel(ctx.serverDB, ctx.userId);
337
+
338
+ // Verify the file exists
339
+ const file = await fileModel.findById(fileId);
340
+ if (!file) {
341
+ return {
342
+ error: { message: 'File not found' },
343
+ success: false,
344
+ } as SaveExportedFileContentResult;
345
+ }
346
+
347
+ // Create document record with the file content
348
+ const document = await documentModel.create({
349
+ content,
350
+ fileId,
351
+ fileType,
352
+ filename,
353
+ source: url,
354
+ sourceType: 'file',
355
+ title: filename,
356
+ totalCharCount: content.length,
357
+ totalLineCount: content.split('\n').length,
358
+ });
359
+
360
+ log('Created document for exported file: documentId=%s, fileId=%s', document.id, fileId);
361
+
362
+ return {
363
+ documentId: document.id,
364
+ success: true,
365
+ } as SaveExportedFileContentResult;
366
+ } catch (error) {
367
+ log('Error saving exported file content: %O', error);
368
+
369
+ return {
370
+ error: { message: (error as Error).message },
371
+ success: false,
372
+ } as SaveExportedFileContentResult;
373
+ }
374
+ }),
375
+ });
@@ -8,11 +8,13 @@ import { z } from 'zod';
8
8
 
9
9
  import { type ToolCallContent } from '@/libs/mcp';
10
10
  import { authedProcedure, router } from '@/libs/trpc/lambda';
11
- import { serverDatabase } from '@/libs/trpc/lambda/middleware';
11
+ import { serverDatabase, telemetry } from '@/libs/trpc/lambda/middleware';
12
12
  import { FileService } from '@/server/services/file';
13
13
  import { mcpService } from '@/server/services/mcp';
14
14
  import { processContentBlocks } from '@/server/services/mcp/contentProcessor';
15
15
 
16
+ import { scheduleToolCallReport } from './_helpers';
17
+
16
18
  // Define Zod schemas for MCP Client parameters
17
19
  const httpParamsSchema = z.object({
18
20
  auth: StreamableHTTPAuthSchema,
@@ -41,13 +43,36 @@ const checkStdioEnvironment = (params: z.infer<typeof mcpClientParamsSchema>) =>
41
43
  }
42
44
  };
43
45
 
44
- const mcpProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next }) => {
45
- return next({
46
- ctx: {
47
- fileService: new FileService(ctx.serverDB, ctx.userId),
48
- },
46
+ // Schema for metadata that frontend needs to pass (fields that backend cannot determine)
47
+ const metaSchema = z
48
+ .object({
49
+ // Custom plugin info (only for custom plugins)
50
+ customPluginInfo: z
51
+ .object({
52
+ avatar: z.string().optional(),
53
+ description: z.string().optional(),
54
+ name: z.string().optional(),
55
+ })
56
+ .optional(),
57
+ // Whether this is a custom plugin
58
+ isCustomPlugin: z.boolean().optional(),
59
+ // Session/topic ID
60
+ sessionId: z.string().optional(),
61
+ // Plugin manifest version
62
+ version: z.string().optional(),
63
+ })
64
+ .optional();
65
+
66
+ const mcpProcedure = authedProcedure
67
+ .use(serverDatabase)
68
+ .use(telemetry)
69
+ .use(async ({ ctx, next }) => {
70
+ return next({
71
+ ctx: {
72
+ fileService: new FileService(ctx.serverDB, ctx.userId),
73
+ },
74
+ });
49
75
  });
50
- });
51
76
 
52
77
  export const mcpRouter = router({
53
78
  getStreamableMcpServerManifest: mcpProcedure
@@ -100,8 +125,9 @@ export const mcpRouter = router({
100
125
  callTool: mcpProcedure
101
126
  .input(
102
127
  z.object({
103
- params: mcpClientParamsSchema, // Use the unified schema for client params
104
128
  args: z.any(), // Arguments for the tool call
129
+ meta: metaSchema, // Optional metadata for reporting
130
+ params: mcpClientParamsSchema, // Use the unified schema for client params
105
131
  toolName: z.string(),
106
132
  }),
107
133
  )
@@ -109,17 +135,48 @@ export const mcpRouter = router({
109
135
  // Stdio check can be done here or rely on the service/client layer
110
136
  checkStdioEnvironment(input.params);
111
137
 
112
- // Create a closure that binds fileService and userId to processContentBlocks
113
- const boundProcessContentBlocks = async (blocks: ToolCallContent[]) => {
114
- return processContentBlocks(blocks, ctx.fileService);
115
- };
116
-
117
- // Pass the validated params, toolName, args, and bound processContentBlocks to the service
118
- return await mcpService.callTool({
119
- clientParams: input.params,
120
- toolName: input.toolName,
121
- argsStr: input.args,
122
- processContentBlocks: boundProcessContentBlocks,
123
- });
138
+ const startTime = Date.now();
139
+ let success = true;
140
+ let errorCode: string | undefined;
141
+ let errorMessage: string | undefined;
142
+ let result: Awaited<ReturnType<typeof mcpService.callTool>> | undefined;
143
+
144
+ try {
145
+ // Create a closure that binds fileService and userId to processContentBlocks
146
+ const boundProcessContentBlocks = async (blocks: ToolCallContent[]) => {
147
+ return processContentBlocks(blocks, ctx.fileService);
148
+ };
149
+
150
+ // Pass the validated params, toolName, args, and bound processContentBlocks to the service
151
+ result = await mcpService.callTool({
152
+ argsStr: input.args,
153
+ clientParams: input.params,
154
+ processContentBlocks: boundProcessContentBlocks,
155
+ toolName: input.toolName,
156
+ });
157
+
158
+ return result;
159
+ } catch (error) {
160
+ success = false;
161
+ const err = error as Error;
162
+ errorCode = 'CALL_FAILED';
163
+ errorMessage = err.message;
164
+ throw error;
165
+ } finally {
166
+ scheduleToolCallReport({
167
+ errorCode,
168
+ errorMessage,
169
+ identifier: input.params.name,
170
+ marketAccessToken: ctx.marketAccessToken,
171
+ mcpType: 'http',
172
+ meta: input.meta,
173
+ requestPayload: input.args,
174
+ result,
175
+ startTime,
176
+ success,
177
+ telemetryEnabled: ctx.telemetryEnabled,
178
+ toolName: input.toolName,
179
+ });
180
+ }
124
181
  }),
125
182
  });
@@ -129,8 +129,6 @@ class ChatService {
129
129
 
130
130
  const targetAgentId = getTargetAgentId(agentId);
131
131
 
132
- console.log('[chatService.createAssistantMessage] Resolving with scope:', scope);
133
-
134
132
  // Resolve agent config with builtin agent runtime config merged
135
133
  // plugins is already merged (runtime plugins > agent config plugins)
136
134
  const {
@@ -1,12 +1,12 @@
1
1
  import { toolsClient } from '@/libs/trpc/client';
2
2
  import type {
3
- CallToolInput,
3
+ CallCodeInterpreterToolInput,
4
4
  CallToolResult,
5
5
  GetExportFileUploadUrlInput,
6
6
  GetExportFileUploadUrlResult,
7
7
  SaveExportedFileContentInput,
8
8
  SaveExportedFileContentResult,
9
- } from '@/server/routers/tools/codeInterpreter';
9
+ } from '@/server/routers/tools/market';
10
10
  import { useUserStore } from '@/store/user';
11
11
  import { settingsSelectors } from '@/store/user/slices/settings/selectors/settings';
12
12
 
@@ -32,7 +32,7 @@ class CodeInterpreterService {
32
32
  ): Promise<CallToolResult> {
33
33
  const marketAccessToken = getMarketAccessToken();
34
34
 
35
- const input: CallToolInput = {
35
+ const input: CallCodeInterpreterToolInput = {
36
36
  marketAccessToken,
37
37
  params,
38
38
  toolName,
@@ -40,7 +40,7 @@ class CodeInterpreterService {
40
40
  userId: context.userId,
41
41
  };
42
42
 
43
- return toolsClient.codeInterpreter.callTool.mutate(input);
43
+ return toolsClient.market.callCodeInterpreterTool.mutate(input);
44
44
  }
45
45
 
46
46
  /**
@@ -57,7 +57,7 @@ class CodeInterpreterService {
57
57
  topicId,
58
58
  };
59
59
 
60
- return toolsClient.codeInterpreter.getExportFileUploadUrl.mutate(input);
60
+ return toolsClient.market.getExportFileUploadUrl.mutate(input);
61
61
  }
62
62
 
63
63
  /**
@@ -68,7 +68,7 @@ class CodeInterpreterService {
68
68
  async saveExportedFileContent(
69
69
  params: SaveExportedFileContentInput,
70
70
  ): Promise<SaveExportedFileContentResult> {
71
- return toolsClient.codeInterpreter.saveExportedFileContent.mutate(params);
71
+ return toolsClient.market.saveExportedFileContent.mutate(params);
72
72
  }
73
73
  }
74
74