@lobehub/lobehub 2.0.0-next.46 → 2.0.0-next.47
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/file.ts +15 -1
- package/packages/database/src/repositories/aiInfra/index.test.ts +1 -1
- package/packages/database/src/repositories/dataExporter/index.test.ts +1 -1
- package/packages/database/src/repositories/tableViewer/index.test.ts +1 -1
- package/packages/types/src/aiProvider.ts +1 -1
- package/packages/types/src/document/index.ts +38 -38
- package/packages/types/src/exportConfig.ts +15 -15
- package/packages/types/src/generation/index.ts +5 -5
- package/packages/types/src/openai/chat.ts +15 -15
- package/packages/types/src/plugins/mcp.ts +29 -29
- package/packages/types/src/plugins/protocol.ts +43 -43
- package/packages/types/src/search.ts +4 -4
- package/packages/types/src/tool/plugin.ts +3 -3
- package/src/app/(backend)/f/[id]/route.ts +55 -0
- package/src/envs/app.ts +4 -3
- package/src/features/Conversation/components/VirtualizedList/index.tsx +2 -1
- package/src/features/PluginsUI/Render/MCPType/index.tsx +26 -6
- package/src/server/routers/desktop/mcp.ts +23 -8
- package/src/server/routers/tools/mcp.ts +24 -4
- package/src/server/services/file/impls/local.ts +4 -1
- package/src/server/services/file/index.ts +96 -1
- package/src/server/services/mcp/contentProcessor.ts +101 -0
- package/src/server/services/mcp/index.test.ts +52 -10
- package/src/server/services/mcp/index.ts +29 -26
- package/src/services/session/index.ts +0 -14
- package/src/utils/server/routeVariants.test.ts +340 -0
|
@@ -6,8 +6,12 @@ import {
|
|
|
6
6
|
import { TRPCError } from '@trpc/server';
|
|
7
7
|
import { z } from 'zod';
|
|
8
8
|
|
|
9
|
+
import { ToolCallContent } from '@/libs/mcp';
|
|
9
10
|
import { authedProcedure, router } from '@/libs/trpc/lambda';
|
|
11
|
+
import { serverDatabase } from '@/libs/trpc/lambda/middleware';
|
|
12
|
+
import { FileService } from '@/server/services/file';
|
|
10
13
|
import { mcpService } from '@/server/services/mcp';
|
|
14
|
+
import { processContentBlocks } from '@/server/services/mcp/contentProcessor';
|
|
11
15
|
|
|
12
16
|
// Define Zod schemas for MCP Client parameters
|
|
13
17
|
const httpParamsSchema = z.object({
|
|
@@ -37,7 +41,13 @@ const checkStdioEnvironment = (params: z.infer<typeof mcpClientParamsSchema>) =>
|
|
|
37
41
|
}
|
|
38
42
|
};
|
|
39
43
|
|
|
40
|
-
const mcpProcedure = authedProcedure
|
|
44
|
+
const mcpProcedure = authedProcedure.use(serverDatabase).use(async ({ ctx, next }) => {
|
|
45
|
+
return next({
|
|
46
|
+
ctx: {
|
|
47
|
+
fileService: new FileService(ctx.serverDB, ctx.userId),
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
});
|
|
41
51
|
|
|
42
52
|
export const mcpRouter = router({
|
|
43
53
|
getStreamableMcpServerManifest: mcpProcedure
|
|
@@ -95,11 +105,21 @@ export const mcpRouter = router({
|
|
|
95
105
|
toolName: z.string(),
|
|
96
106
|
}),
|
|
97
107
|
)
|
|
98
|
-
.mutation(async ({ input }) => {
|
|
108
|
+
.mutation(async ({ input, ctx }) => {
|
|
99
109
|
// Stdio check can be done here or rely on the service/client layer
|
|
100
110
|
checkStdioEnvironment(input.params);
|
|
101
111
|
|
|
102
|
-
//
|
|
103
|
-
|
|
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
|
+
});
|
|
104
124
|
}),
|
|
105
125
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
1
2
|
import { sha256 } from 'js-sha256';
|
|
2
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
4
|
import path from 'node:path';
|
|
@@ -8,6 +9,8 @@ import { inferContentTypeFromImageUrl } from '@/utils/url';
|
|
|
8
9
|
import { FileServiceImpl } from './type';
|
|
9
10
|
import { extractKeyFromUrlOrReturnOriginal } from './utils';
|
|
10
11
|
|
|
12
|
+
const log = debug('lobe-file:desktop-local');
|
|
13
|
+
|
|
11
14
|
/**
|
|
12
15
|
* 桌面应用本地文件服务实现
|
|
13
16
|
*/
|
|
@@ -202,7 +205,7 @@ export class DesktopLocalFileImpl implements FileServiceImpl {
|
|
|
202
205
|
throw new Error('Failed to upload file via Electron IPC');
|
|
203
206
|
}
|
|
204
207
|
|
|
205
|
-
|
|
208
|
+
log('File uploaded successfully: %O', result.metadata);
|
|
206
209
|
return { key: result.metadata.path };
|
|
207
210
|
} catch (error) {
|
|
208
211
|
console.error('[DesktopLocalFileImpl] Failed to upload media file:', error);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { LobeChatDatabase } from '@lobechat/database';
|
|
2
|
+
import { inferContentTypeFromImageUrl, nanoid, uuid } from '@lobechat/utils';
|
|
2
3
|
import { TRPCError } from '@trpc/server';
|
|
4
|
+
import { sha256 } from 'js-sha256';
|
|
3
5
|
|
|
4
6
|
import { serverDBEnv } from '@/config/db';
|
|
5
7
|
import { FileModel } from '@/database/models/file';
|
|
6
8
|
import { FileItem } from '@/database/schemas';
|
|
7
9
|
import { TempFileManager } from '@/server/utils/tempFileManager';
|
|
8
|
-
import { nanoid } from '@/utils/uuid';
|
|
9
10
|
|
|
10
11
|
import { FileServiceImpl, createFileServiceModule } from './impls';
|
|
11
12
|
|
|
@@ -94,6 +95,100 @@ export class FileService {
|
|
|
94
95
|
return this.impl.uploadMedia(key, buffer);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Create file record (common method)
|
|
100
|
+
* Automatically handles globalFiles deduplication logic
|
|
101
|
+
*
|
|
102
|
+
* @param params - File parameters
|
|
103
|
+
* @param params.id - Optional custom file ID (defaults to auto-generated)
|
|
104
|
+
* @returns File record and proxy URL
|
|
105
|
+
*/
|
|
106
|
+
public async createFileRecord(params: {
|
|
107
|
+
fileHash: string;
|
|
108
|
+
fileType: string;
|
|
109
|
+
id?: string;
|
|
110
|
+
name: string;
|
|
111
|
+
size: number;
|
|
112
|
+
url: string;
|
|
113
|
+
}): Promise<{ fileId: string; url: string }> {
|
|
114
|
+
// Check if hash already exists in globalFiles
|
|
115
|
+
const { isExist } = await this.fileModel.checkHash(params.fileHash);
|
|
116
|
+
|
|
117
|
+
// Create database record
|
|
118
|
+
// If hash doesn't exist, also create globalFiles record
|
|
119
|
+
const { id } = await this.fileModel.create(
|
|
120
|
+
{
|
|
121
|
+
fileHash: params.fileHash,
|
|
122
|
+
fileType: params.fileType,
|
|
123
|
+
id: params.id, // Use custom ID if provided
|
|
124
|
+
name: params.name,
|
|
125
|
+
size: params.size,
|
|
126
|
+
url: params.url,
|
|
127
|
+
},
|
|
128
|
+
!isExist, // insertToGlobalFiles
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
// Return unified proxy URL: /f/:id
|
|
132
|
+
return {
|
|
133
|
+
fileId: id,
|
|
134
|
+
url: `/f/${id}`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Upload base64 data and create database record
|
|
140
|
+
* @param base64Data - Base64 data (supports data URI format or pure base64)
|
|
141
|
+
* @param pathname - File storage path (must include file extension)
|
|
142
|
+
* @returns Contains key (storage path), fileId (database record ID) and url (proxy access path)
|
|
143
|
+
*/
|
|
144
|
+
public async uploadBase64(
|
|
145
|
+
base64Data: string,
|
|
146
|
+
pathname: string,
|
|
147
|
+
): Promise<{ fileId: string; key: string; url: string }> {
|
|
148
|
+
let base64String: string;
|
|
149
|
+
|
|
150
|
+
// If data URI format (data:image/png;base64,xxx)
|
|
151
|
+
if (base64Data.startsWith('data:')) {
|
|
152
|
+
const commaIndex = base64Data.indexOf(',');
|
|
153
|
+
if (commaIndex === -1) {
|
|
154
|
+
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid base64 data format' });
|
|
155
|
+
}
|
|
156
|
+
base64String = base64Data.slice(commaIndex + 1);
|
|
157
|
+
} else {
|
|
158
|
+
// Pure base64 string
|
|
159
|
+
base64String = base64Data;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Convert to Buffer
|
|
163
|
+
const buffer = Buffer.from(base64String, 'base64');
|
|
164
|
+
|
|
165
|
+
// Upload to storage (S3 or local)
|
|
166
|
+
const { key } = await this.uploadMedia(pathname, buffer);
|
|
167
|
+
|
|
168
|
+
// Extract filename from pathname
|
|
169
|
+
const name = pathname.split('/').pop() || 'unknown';
|
|
170
|
+
|
|
171
|
+
// Calculate file metadata
|
|
172
|
+
const size = buffer.length;
|
|
173
|
+
const fileType = inferContentTypeFromImageUrl(pathname) || 'application/octet-stream';
|
|
174
|
+
const hash = sha256(buffer);
|
|
175
|
+
|
|
176
|
+
// Generate UUID for cleaner URLs
|
|
177
|
+
const fileId = uuid();
|
|
178
|
+
|
|
179
|
+
// Use common method to create file record
|
|
180
|
+
const { fileId: createdId, url } = await this.createFileRecord({
|
|
181
|
+
fileHash: hash,
|
|
182
|
+
fileType,
|
|
183
|
+
id: fileId, // Use UUID instead of auto-generated ID
|
|
184
|
+
name,
|
|
185
|
+
size,
|
|
186
|
+
url: key, // Store original key (S3 key or desktop://)
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
return { fileId: createdId, key, url };
|
|
190
|
+
}
|
|
191
|
+
|
|
97
192
|
async downloadFileToLocal(
|
|
98
193
|
fileId: string,
|
|
99
194
|
): Promise<{ cleanup: () => void; file: FileItem; filePath: string }> {
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import debug from 'debug';
|
|
2
|
+
import pMap from 'p-map';
|
|
3
|
+
import urlJoin from 'url-join';
|
|
4
|
+
|
|
5
|
+
import { appEnv } from '@/envs/app';
|
|
6
|
+
import { fileEnv } from '@/envs/file';
|
|
7
|
+
import { AudioContent, ImageContent, ToolCallContent } from '@/libs/mcp';
|
|
8
|
+
import { FileService } from '@/server/services/file';
|
|
9
|
+
import { nanoid } from '@/utils/uuid';
|
|
10
|
+
|
|
11
|
+
const log = debug('lobe-mcp:content-processor');
|
|
12
|
+
|
|
13
|
+
export type ProcessContentBlocksFn = (blocks: ToolCallContent[]) => Promise<ToolCallContent[]>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 处理 MCP 返回的 content blocks
|
|
17
|
+
* - 上传图片/音频到存储并替换 data 为代理 URL
|
|
18
|
+
* - 保持其他类型的 block 不变
|
|
19
|
+
*/
|
|
20
|
+
export const processContentBlocks = async (
|
|
21
|
+
blocks: ToolCallContent[],
|
|
22
|
+
fileService: FileService,
|
|
23
|
+
): Promise<ToolCallContent[]> => {
|
|
24
|
+
// Use date-based sharding for privacy compliance (GDPR, CCPA)
|
|
25
|
+
const today = new Date().toISOString().split('T')[0]; // e.g., "2025-11-08"
|
|
26
|
+
|
|
27
|
+
return pMap(blocks, async (block) => {
|
|
28
|
+
if (block.type === 'image') {
|
|
29
|
+
const imageBlock = block as ImageContent;
|
|
30
|
+
|
|
31
|
+
// Extract file extension from mimeType (e.g., "image/png" -> "png")
|
|
32
|
+
const fileExtension = imageBlock.mimeType.split('/')[1] || 'png';
|
|
33
|
+
|
|
34
|
+
// Generate unique pathname with date-based sharding
|
|
35
|
+
const pathname = `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/mcp/images/${today}/${nanoid()}.${fileExtension}`;
|
|
36
|
+
|
|
37
|
+
// Upload base64 image and get proxy URL
|
|
38
|
+
const { url } = await fileService.uploadBase64(imageBlock.data, pathname);
|
|
39
|
+
|
|
40
|
+
log(`Image uploaded, proxy URL: ${url}`);
|
|
41
|
+
|
|
42
|
+
return { ...block, data: url };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (block.type === 'audio') {
|
|
46
|
+
const audioBlock = block as AudioContent;
|
|
47
|
+
|
|
48
|
+
// Extract file extension from mimeType (e.g., "audio/mp3" -> "mp3")
|
|
49
|
+
const fileExtension = audioBlock.mimeType.split('/')[1] || 'mp3';
|
|
50
|
+
|
|
51
|
+
// Generate unique pathname with date-based sharding
|
|
52
|
+
const pathname = `${fileEnv.NEXT_PUBLIC_S3_FILE_PATH}/mcp/audio/${today}/${nanoid()}.${fileExtension}`;
|
|
53
|
+
|
|
54
|
+
// Upload base64 audio and get proxy URL
|
|
55
|
+
const { url } = await fileService.uploadBase64(audioBlock.data, pathname);
|
|
56
|
+
|
|
57
|
+
log(`Audio uploaded, proxy URL: ${url}`);
|
|
58
|
+
|
|
59
|
+
return { ...block, data: url };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return block;
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 将 content blocks 转换为字符串
|
|
68
|
+
* - text: 提取 text 字段
|
|
69
|
+
* - image/audio: 提取 data 字段(通常是上传后的代理 URL)
|
|
70
|
+
* - 其他: 返回空字符串
|
|
71
|
+
*/
|
|
72
|
+
export const contentBlocksToString = (blocks: ToolCallContent[] | null | undefined): string => {
|
|
73
|
+
if (!blocks) return '';
|
|
74
|
+
|
|
75
|
+
return blocks
|
|
76
|
+
.map((item) => {
|
|
77
|
+
switch (item.type) {
|
|
78
|
+
case 'text': {
|
|
79
|
+
return item.text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'image': {
|
|
83
|
+
return `})`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'audio': {
|
|
87
|
+
return `<resource type="${item.type}" url="${urlJoin(appEnv.APP_URL, item.data)}" />`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'resource': {
|
|
91
|
+
return `<resource type="${item.type}">${JSON.stringify(item.resource)}</resource>}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
default: {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join('\n\n');
|
|
101
|
+
};
|
|
@@ -39,7 +39,11 @@ describe('MCPService', () => {
|
|
|
39
39
|
isError: false,
|
|
40
40
|
});
|
|
41
41
|
|
|
42
|
-
const result = await mcpService.callTool(
|
|
42
|
+
const result = await mcpService.callTool({
|
|
43
|
+
clientParams: mockParams,
|
|
44
|
+
toolName: 'testTool',
|
|
45
|
+
argsStr: '{}',
|
|
46
|
+
});
|
|
43
47
|
|
|
44
48
|
expect(result.content).toBe('');
|
|
45
49
|
expect(result.success).toBe(true);
|
|
@@ -52,7 +56,11 @@ describe('MCPService', () => {
|
|
|
52
56
|
isError: false,
|
|
53
57
|
});
|
|
54
58
|
|
|
55
|
-
const result = await mcpService.callTool(
|
|
59
|
+
const result = await mcpService.callTool({
|
|
60
|
+
clientParams: mockParams,
|
|
61
|
+
toolName: 'testTool',
|
|
62
|
+
argsStr: '{}',
|
|
63
|
+
});
|
|
56
64
|
|
|
57
65
|
expect(result.content).toBe('');
|
|
58
66
|
expect(result.success).toBe(true);
|
|
@@ -65,7 +73,11 @@ describe('MCPService', () => {
|
|
|
65
73
|
isError: false,
|
|
66
74
|
});
|
|
67
75
|
|
|
68
|
-
const result = await mcpService.callTool(
|
|
76
|
+
const result = await mcpService.callTool({
|
|
77
|
+
clientParams: mockParams,
|
|
78
|
+
toolName: 'testTool',
|
|
79
|
+
argsStr: '{}',
|
|
80
|
+
});
|
|
69
81
|
|
|
70
82
|
expect(result.content).toBe(JSON.stringify(jsonData));
|
|
71
83
|
expect(result.success).toBe(true);
|
|
@@ -78,7 +90,11 @@ describe('MCPService', () => {
|
|
|
78
90
|
isError: false,
|
|
79
91
|
});
|
|
80
92
|
|
|
81
|
-
const result = await mcpService.callTool(
|
|
93
|
+
const result = await mcpService.callTool({
|
|
94
|
+
clientParams: mockParams,
|
|
95
|
+
toolName: 'testTool',
|
|
96
|
+
argsStr: '{}',
|
|
97
|
+
});
|
|
82
98
|
|
|
83
99
|
expect(result.content).toBe(textData);
|
|
84
100
|
expect(result.success).toBe(true);
|
|
@@ -92,7 +108,11 @@ describe('MCPService', () => {
|
|
|
92
108
|
isError: false,
|
|
93
109
|
});
|
|
94
110
|
|
|
95
|
-
const result = await mcpService.callTool(
|
|
111
|
+
const result = await mcpService.callTool({
|
|
112
|
+
clientParams: mockParams,
|
|
113
|
+
toolName: 'testTool',
|
|
114
|
+
argsStr: '{}',
|
|
115
|
+
});
|
|
96
116
|
|
|
97
117
|
expect(result.content).toBe('');
|
|
98
118
|
expect(result.success).toBe(true);
|
|
@@ -111,7 +131,11 @@ describe('MCPService', () => {
|
|
|
111
131
|
isError: false,
|
|
112
132
|
});
|
|
113
133
|
|
|
114
|
-
const result = await mcpService.callTool(
|
|
134
|
+
const result = await mcpService.callTool({
|
|
135
|
+
clientParams: mockParams,
|
|
136
|
+
toolName: 'testTool',
|
|
137
|
+
argsStr: '{}',
|
|
138
|
+
});
|
|
115
139
|
|
|
116
140
|
expect(result.content).toBe('First message\n\nSecond message\n\n{"json": "data"}');
|
|
117
141
|
expect(result.success).toBe(true);
|
|
@@ -129,7 +153,11 @@ describe('MCPService', () => {
|
|
|
129
153
|
isError: false,
|
|
130
154
|
});
|
|
131
155
|
|
|
132
|
-
const result = await mcpService.callTool(
|
|
156
|
+
const result = await mcpService.callTool({
|
|
157
|
+
clientParams: mockParams,
|
|
158
|
+
toolName: 'testTool',
|
|
159
|
+
argsStr: '{}',
|
|
160
|
+
});
|
|
133
161
|
|
|
134
162
|
expect(result.content).toBe('First message\n\nSecond message');
|
|
135
163
|
expect(result.success).toBe(true);
|
|
@@ -144,7 +172,11 @@ describe('MCPService', () => {
|
|
|
144
172
|
|
|
145
173
|
mockClient.callTool.mockResolvedValue(errorResult);
|
|
146
174
|
|
|
147
|
-
const result = await mcpService.callTool(
|
|
175
|
+
const result = await mcpService.callTool({
|
|
176
|
+
clientParams: mockParams,
|
|
177
|
+
toolName: 'testTool',
|
|
178
|
+
argsStr: '{}',
|
|
179
|
+
});
|
|
148
180
|
|
|
149
181
|
expect(result.content).toBe('Error occurred');
|
|
150
182
|
expect(result.success).toBe(true);
|
|
@@ -155,7 +187,13 @@ describe('MCPService', () => {
|
|
|
155
187
|
const error = new Error('MCP client error');
|
|
156
188
|
mockClient.callTool.mockRejectedValue(error);
|
|
157
189
|
|
|
158
|
-
await expect(
|
|
190
|
+
await expect(
|
|
191
|
+
mcpService.callTool({
|
|
192
|
+
clientParams: mockParams,
|
|
193
|
+
toolName: 'testTool',
|
|
194
|
+
argsStr: '{}',
|
|
195
|
+
}),
|
|
196
|
+
).rejects.toThrow(TRPCError);
|
|
159
197
|
});
|
|
160
198
|
|
|
161
199
|
it('should parse args string correctly', async () => {
|
|
@@ -167,7 +205,11 @@ describe('MCPService', () => {
|
|
|
167
205
|
isError: false,
|
|
168
206
|
});
|
|
169
207
|
|
|
170
|
-
await mcpService.callTool(
|
|
208
|
+
await mcpService.callTool({
|
|
209
|
+
clientParams: mockParams,
|
|
210
|
+
toolName: 'testTool',
|
|
211
|
+
argsStr: argsString,
|
|
212
|
+
});
|
|
171
213
|
|
|
172
214
|
expect(mockClient.callTool).toHaveBeenCalledWith('testTool', argsObject);
|
|
173
215
|
});
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
StdioMCPParams,
|
|
16
16
|
} from '@/libs/mcp';
|
|
17
17
|
|
|
18
|
+
import { ProcessContentBlocksFn, contentBlocksToString } from './contentProcessor';
|
|
18
19
|
import { mcpSystemDepsCheckService } from './deps';
|
|
19
20
|
|
|
20
21
|
const log = debug('lobe-mcp:service');
|
|
@@ -154,12 +155,19 @@ export class MCPService {
|
|
|
154
155
|
}
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
// callTool now accepts
|
|
158
|
-
async callTool(
|
|
159
|
-
|
|
158
|
+
// callTool now accepts an object with clientParams, toolName, argsStr, and processContentBlocks
|
|
159
|
+
async callTool(options: {
|
|
160
|
+
argsStr: any;
|
|
161
|
+
clientParams: MCPClientParams;
|
|
162
|
+
processContentBlocks?: ProcessContentBlocksFn;
|
|
163
|
+
toolName: string;
|
|
164
|
+
}): Promise<any> {
|
|
165
|
+
const { clientParams, toolName, argsStr, processContentBlocks } = options;
|
|
166
|
+
|
|
167
|
+
const client = await this.getClient(clientParams); // Get client using params
|
|
160
168
|
|
|
161
169
|
const args = safeParseJSON(argsStr);
|
|
162
|
-
const loggableParams = this.sanitizeForLogging(
|
|
170
|
+
const loggableParams = this.sanitizeForLogging(clientParams);
|
|
163
171
|
|
|
164
172
|
log(
|
|
165
173
|
`Calling tool "${toolName}" using client for params: %O with args: %O`,
|
|
@@ -170,32 +178,27 @@ export class MCPService {
|
|
|
170
178
|
try {
|
|
171
179
|
// Delegate the call to the MCPClient instance
|
|
172
180
|
const result = await client.callTool(toolName, args); // Pass args directly
|
|
181
|
+
|
|
182
|
+
// Process content blocks (upload images, etc.)
|
|
183
|
+
const newContent =
|
|
184
|
+
result.isError || !processContentBlocks
|
|
185
|
+
? result.content
|
|
186
|
+
: await processContentBlocks(result.content);
|
|
187
|
+
|
|
188
|
+
// Convert content blocks to string
|
|
189
|
+
const content = contentBlocksToString(newContent);
|
|
190
|
+
|
|
191
|
+
const state = { ...result, content: newContent };
|
|
192
|
+
|
|
173
193
|
log(
|
|
174
194
|
`Tool "${toolName}" called successfully for params: %O, result: %O`,
|
|
175
195
|
loggableParams,
|
|
176
|
-
|
|
196
|
+
state,
|
|
177
197
|
);
|
|
178
198
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.map((item) => {
|
|
183
|
-
switch (item.type) {
|
|
184
|
-
case 'text': {
|
|
185
|
-
return item.text;
|
|
186
|
-
}
|
|
187
|
-
default: {
|
|
188
|
-
return '';
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
})
|
|
192
|
-
.filter(Boolean)
|
|
193
|
-
.join('\n\n')
|
|
194
|
-
: '';
|
|
195
|
-
|
|
196
|
-
if (result.isError) return { content, state: result, success: true };
|
|
197
|
-
|
|
198
|
-
return { content, state: result, success: true };
|
|
199
|
+
if (result.isError) return { content, state, success: true };
|
|
200
|
+
|
|
201
|
+
return { content, state, success: true };
|
|
199
202
|
} catch (error) {
|
|
200
203
|
if (error instanceof McpError) {
|
|
201
204
|
const mcpError = error as McpError;
|
|
@@ -213,7 +216,7 @@ export class MCPService {
|
|
|
213
216
|
|
|
214
217
|
console.error(
|
|
215
218
|
`Error calling tool "${toolName}" for params %O:`,
|
|
216
|
-
this.sanitizeForLogging(
|
|
219
|
+
this.sanitizeForLogging(clientParams),
|
|
217
220
|
error,
|
|
218
221
|
);
|
|
219
222
|
// Propagate a TRPCError
|
|
@@ -4,14 +4,12 @@ import type { PartialDeep } from 'type-fest';
|
|
|
4
4
|
import { lambdaClient } from '@/libs/trpc/client';
|
|
5
5
|
import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent';
|
|
6
6
|
import { MetaData } from '@/types/meta';
|
|
7
|
-
import { BatchTaskResult } from '@/types/service';
|
|
8
7
|
import {
|
|
9
8
|
ChatSessionList,
|
|
10
9
|
LobeAgentSession,
|
|
11
10
|
LobeSessionType,
|
|
12
11
|
LobeSessions,
|
|
13
12
|
SessionGroupItem,
|
|
14
|
-
SessionGroups,
|
|
15
13
|
SessionRankItem,
|
|
16
14
|
UpdateSessionParams,
|
|
17
15
|
} from '@/types/session';
|
|
@@ -114,18 +112,6 @@ export class SessionService {
|
|
|
114
112
|
return lambdaClient.sessionGroup.createSessionGroup.mutate({ name, sort });
|
|
115
113
|
};
|
|
116
114
|
|
|
117
|
-
getSessionGroups = (): Promise<SessionGroupItem[]> => {
|
|
118
|
-
return lambdaClient.sessionGroup.getSessionGroup.query();
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* 需要废弃
|
|
123
|
-
* @deprecated
|
|
124
|
-
*/
|
|
125
|
-
batchCreateSessionGroups = (groups: SessionGroups): Promise<BatchTaskResult> => {
|
|
126
|
-
return Promise.resolve({ added: 0, ids: [], skips: [], success: true });
|
|
127
|
-
};
|
|
128
|
-
|
|
129
115
|
removeSessionGroup = (id: string, removeChildren?: boolean) => {
|
|
130
116
|
return lambdaClient.sessionGroup.removeSessionGroup.mutate({ id, removeChildren });
|
|
131
117
|
};
|