@lobehub/lobehub 2.0.0-next.194 → 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.
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +9 -0
- package/package.json +1 -1
- package/packages/database/src/models/user.ts +8 -0
- package/packages/database/src/repositories/aiInfra/index.test.ts +11 -8
- package/packages/database/src/repositories/dataExporter/index.test.ts +11 -9
- package/packages/database/src/repositories/tableViewer/index.test.ts +13 -14
- package/packages/model-runtime/src/providers/zhipu/index.ts +6 -6
- package/src/envs/app.ts +2 -0
- package/src/libs/trpc/lambda/middleware/index.ts +1 -0
- package/src/libs/trpc/lambda/middleware/telemetry.test.ts +237 -0
- package/src/libs/trpc/lambda/middleware/telemetry.ts +74 -0
- package/src/server/routers/lambda/market/index.ts +1 -93
- package/src/server/routers/tools/_helpers/index.ts +1 -0
- package/src/server/routers/tools/_helpers/scheduleToolCallReport.ts +113 -0
- package/src/server/routers/tools/index.ts +2 -2
- package/src/server/routers/tools/market.ts +375 -0
- package/src/server/routers/tools/mcp.ts +77 -20
- package/src/services/chat/index.ts +0 -2
- package/src/services/codeInterpreter.ts +6 -6
- package/src/services/mcp.test.ts +60 -46
- package/src/services/mcp.ts +67 -48
- package/src/store/chat/slices/plugin/action.test.ts +191 -0
- package/src/store/chat/slices/plugin/actions/internals.ts +2 -18
- package/src/store/chat/slices/plugin/actions/pluginTypes.ts +31 -44
- package/packages/database/src/client/db.test.ts +0 -52
- package/packages/database/src/client/db.ts +0 -195
- package/packages/database/src/client/type.ts +0 -6
- package/src/server/routers/tools/codeInterpreter.ts +0 -255
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './scheduleToolCallReport';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { CURRENT_VERSION } from '@lobechat/const';
|
|
2
|
+
import { type CallReportRequest } from '@lobehub/market-types';
|
|
3
|
+
import { after } from 'next/server';
|
|
4
|
+
|
|
5
|
+
import { DiscoverService } from '@/server/services/discover';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculate byte size of object
|
|
9
|
+
*/
|
|
10
|
+
const calculateObjectSizeBytes = (obj: unknown): number => {
|
|
11
|
+
try {
|
|
12
|
+
const jsonString = JSON.stringify(obj);
|
|
13
|
+
return new TextEncoder().encode(jsonString).length;
|
|
14
|
+
} catch {
|
|
15
|
+
return 0;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface ToolCallReportMeta {
|
|
20
|
+
customPluginInfo?: {
|
|
21
|
+
avatar?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
name?: string;
|
|
24
|
+
};
|
|
25
|
+
isCustomPlugin?: boolean;
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ScheduleToolCallReportParams {
|
|
31
|
+
/** Error code if call failed */
|
|
32
|
+
errorCode?: string;
|
|
33
|
+
/** Error message if call failed */
|
|
34
|
+
errorMessage?: string;
|
|
35
|
+
/** Plugin/tool identifier */
|
|
36
|
+
identifier: string;
|
|
37
|
+
/** Market access token for reporting */
|
|
38
|
+
marketAccessToken?: string;
|
|
39
|
+
/** MCP connection type */
|
|
40
|
+
mcpType: string;
|
|
41
|
+
/** Metadata for reporting */
|
|
42
|
+
meta?: ToolCallReportMeta;
|
|
43
|
+
/** Request payload for size calculation */
|
|
44
|
+
requestPayload: unknown;
|
|
45
|
+
/** Result for size calculation */
|
|
46
|
+
result?: unknown;
|
|
47
|
+
/** Start time of the call */
|
|
48
|
+
startTime: number;
|
|
49
|
+
/** Whether the call was successful */
|
|
50
|
+
success: boolean;
|
|
51
|
+
/** Whether telemetry is enabled */
|
|
52
|
+
telemetryEnabled: boolean;
|
|
53
|
+
/** Tool/method name */
|
|
54
|
+
toolName: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Schedule a tool call report to be sent after the response.
|
|
59
|
+
* Uses Next.js after() to avoid blocking the response.
|
|
60
|
+
*/
|
|
61
|
+
export function scheduleToolCallReport(params: ScheduleToolCallReportParams): void {
|
|
62
|
+
const {
|
|
63
|
+
telemetryEnabled,
|
|
64
|
+
marketAccessToken,
|
|
65
|
+
startTime,
|
|
66
|
+
success,
|
|
67
|
+
errorCode,
|
|
68
|
+
errorMessage,
|
|
69
|
+
result,
|
|
70
|
+
meta,
|
|
71
|
+
identifier,
|
|
72
|
+
toolName,
|
|
73
|
+
mcpType,
|
|
74
|
+
requestPayload,
|
|
75
|
+
} = params;
|
|
76
|
+
|
|
77
|
+
// Only report when telemetry is enabled and marketAccessToken exists
|
|
78
|
+
if (!telemetryEnabled || !marketAccessToken) return;
|
|
79
|
+
|
|
80
|
+
// Use Next.js after() to report after response is sent
|
|
81
|
+
after(async () => {
|
|
82
|
+
try {
|
|
83
|
+
const callDurationMs = Date.now() - startTime;
|
|
84
|
+
const requestSizeBytes = calculateObjectSizeBytes(requestPayload);
|
|
85
|
+
const responseSizeBytes = success && result ? calculateObjectSizeBytes(result) : 0;
|
|
86
|
+
|
|
87
|
+
const reportData: CallReportRequest = {
|
|
88
|
+
callDurationMs,
|
|
89
|
+
customPluginInfo: meta?.customPluginInfo,
|
|
90
|
+
errorCode,
|
|
91
|
+
errorMessage,
|
|
92
|
+
identifier,
|
|
93
|
+
isCustomPlugin: meta?.isCustomPlugin,
|
|
94
|
+
metadata: {
|
|
95
|
+
appVersion: CURRENT_VERSION,
|
|
96
|
+
mcpType,
|
|
97
|
+
},
|
|
98
|
+
methodName: toolName,
|
|
99
|
+
methodType: 'tool',
|
|
100
|
+
requestSizeBytes,
|
|
101
|
+
responseSizeBytes,
|
|
102
|
+
sessionId: meta?.sessionId,
|
|
103
|
+
success,
|
|
104
|
+
version: meta?.version || 'unknown',
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const discoverService = new DiscoverService({ accessToken: marketAccessToken });
|
|
108
|
+
await discoverService.reportCall(reportData);
|
|
109
|
+
} catch (reportError) {
|
|
110
|
+
console.error('Failed to report tool call: %O', reportError);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { publicProcedure, router } from '@/libs/trpc/lambda';
|
|
2
2
|
|
|
3
|
-
import { codeInterpreterRouter } from './codeInterpreter';
|
|
4
3
|
import { klavisRouter } from './klavis';
|
|
4
|
+
import { marketRouter } from './market';
|
|
5
5
|
import { mcpRouter } from './mcp';
|
|
6
6
|
import { searchRouter } from './search';
|
|
7
7
|
|
|
8
8
|
export const toolsRouter = router({
|
|
9
|
-
codeInterpreter: codeInterpreterRouter,
|
|
10
9
|
healthcheck: publicProcedure.query(() => "i'm live!"),
|
|
11
10
|
klavis: klavisRouter,
|
|
11
|
+
market: marketRouter,
|
|
12
12
|
mcp: mcpRouter,
|
|
13
13
|
search: searchRouter,
|
|
14
14
|
});
|
|
@@ -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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
3
|
+
CallCodeInterpreterToolInput,
|
|
4
4
|
CallToolResult,
|
|
5
5
|
GetExportFileUploadUrlInput,
|
|
6
6
|
GetExportFileUploadUrlResult,
|
|
7
7
|
SaveExportedFileContentInput,
|
|
8
8
|
SaveExportedFileContentResult,
|
|
9
|
-
} from '@/server/routers/tools/
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
71
|
+
return toolsClient.market.saveExportedFileContent.mutate(params);
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|