@lobehub/chat 1.81.9 → 1.82.1
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/.cursor/rules/desktop-local-tools-implement.mdc +80 -0
- package/.env.desktop +2 -1
- package/.github/scripts/pr-comment.js +4 -9
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/locales/ar/electron.json +38 -2
- package/locales/ar/plugin.json +33 -2
- package/locales/bg-BG/electron.json +38 -2
- package/locales/bg-BG/plugin.json +33 -2
- package/locales/de-DE/electron.json +38 -2
- package/locales/de-DE/plugin.json +28 -2
- package/locales/en-US/electron.json +38 -2
- package/locales/en-US/plugin.json +28 -2
- package/locales/es-ES/electron.json +38 -2
- package/locales/es-ES/plugin.json +33 -2
- package/locales/fa-IR/electron.json +38 -2
- package/locales/fa-IR/plugin.json +33 -2
- package/locales/fr-FR/electron.json +38 -2
- package/locales/fr-FR/plugin.json +33 -2
- package/locales/it-IT/electron.json +38 -2
- package/locales/it-IT/plugin.json +33 -2
- package/locales/ja-JP/electron.json +38 -2
- package/locales/ja-JP/plugin.json +33 -2
- package/locales/ko-KR/electron.json +38 -2
- package/locales/ko-KR/plugin.json +28 -2
- package/locales/nl-NL/electron.json +38 -2
- package/locales/nl-NL/plugin.json +33 -2
- package/locales/pl-PL/electron.json +38 -2
- package/locales/pl-PL/plugin.json +28 -2
- package/locales/pt-BR/electron.json +38 -2
- package/locales/pt-BR/plugin.json +33 -2
- package/locales/ru-RU/electron.json +38 -2
- package/locales/ru-RU/plugin.json +33 -2
- package/locales/tr-TR/electron.json +38 -2
- package/locales/tr-TR/plugin.json +33 -2
- package/locales/vi-VN/electron.json +38 -2
- package/locales/vi-VN/plugin.json +28 -2
- package/locales/zh-CN/electron.json +38 -2
- package/locales/zh-CN/plugin.json +38 -2
- package/locales/zh-TW/electron.json +38 -2
- package/locales/zh-TW/plugin.json +33 -2
- package/package.json +1 -1
- package/packages/electron-client-ipc/src/events/update.ts +3 -3
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Mode.tsx +222 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Option.tsx +104 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Sync.tsx +42 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/Waiting.tsx +203 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/Connection/index.tsx +57 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateModal.tsx +242 -0
- package/src/app/[variants]/(main)/_layout/Desktop/ElectronTitlebar/UpdateNotification.tsx +193 -0
- package/src/app/[variants]/(main)/_layout/Desktop/{Titlebar.tsx → ElectronTitlebar/index.tsx} +15 -1
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/BottomActions.tsx +3 -2
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +1 -1
- package/src/app/[variants]/layout.tsx +2 -1
- package/src/components/ManifestPreviewer/index.tsx +4 -1
- package/src/features/ChatInput/ActionBar/Tools/Dropdown.tsx +2 -1
- package/src/features/Conversation/Extras/Usage/index.tsx +7 -1
- package/src/features/Conversation/Messages/Assistant/Tool/Render/CustomRender.tsx +1 -1
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/LocalFile.tsx +65 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/Render/index.tsx +29 -0
- package/src/features/Conversation/components/MarkdownElements/LocalFile/index.ts +16 -0
- package/src/features/Conversation/components/MarkdownElements/index.ts +7 -1
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/__snapshots__/createRemarkSelfClosingTagPlugin.test.ts.snap +260 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.test.ts +204 -0
- package/src/features/Conversation/components/MarkdownElements/remarkPlugins/createRemarkSelfClosingTagPlugin.ts +133 -0
- package/src/features/Conversation/components/MarkdownElements/type.ts +5 -1
- package/src/features/PluginAvatar/index.tsx +2 -1
- package/src/features/PluginDevModal/MCPManifestForm/ArgsInput.tsx +20 -0
- package/src/features/PluginDevModal/MCPManifestForm/MCPTypeSelect.tsx +176 -0
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +226 -0
- package/src/features/PluginDevModal/PluginPreview.tsx +4 -3
- package/src/features/PluginDevModal/index.tsx +43 -34
- package/src/features/PluginStore/AddPluginButton.tsx +3 -1
- package/src/features/PluginStore/PluginItem/Action.tsx +5 -2
- package/src/features/PluginStore/PluginItem/PluginAvatar.tsx +25 -0
- package/src/features/PluginStore/PluginItem/index.tsx +4 -3
- package/src/features/PluginTag/index.tsx +8 -2
- package/src/{server/modules/MCPClient → libs/mcp}/__tests__/__snapshots__/index.test.ts.snap +0 -56
- package/src/{server/modules/MCPClient → libs/mcp}/__tests__/index.test.ts +2 -2
- package/src/{server/modules/MCPClient/index.ts → libs/mcp/client.ts} +29 -33
- package/src/libs/mcp/index.ts +2 -0
- package/src/libs/mcp/types.ts +27 -0
- package/src/locales/default/electron.ts +38 -2
- package/src/locales/default/plugin.ts +41 -3
- package/src/server/modules/ElectronIPCClient/index.ts +36 -0
- package/src/server/routers/lambda/session.ts +2 -6
- package/src/server/routers/tools/index.ts +2 -0
- package/src/server/routers/tools/mcp.ts +85 -0
- package/src/server/services/file/impls/index.ts +9 -1
- package/src/server/services/file/impls/local.test.ts +299 -0
- package/src/server/services/file/impls/local.ts +183 -0
- package/src/server/services/mcp/index.ts +176 -0
- package/src/services/aiModel/index.ts +5 -1
- package/src/services/aiProvider/index.ts +5 -1
- package/src/services/electron/autoUpdate.ts +4 -0
- package/src/services/file/index.ts +5 -1
- package/src/services/mcp.ts +36 -0
- package/src/services/message/index.ts +5 -1
- package/src/services/plugin/index.ts +5 -1
- package/src/services/session/index.ts +5 -1
- package/src/services/tableViewer/desktop.ts +15 -0
- package/src/services/tableViewer/index.ts +4 -1
- package/src/services/thread/index.ts +5 -1
- package/src/services/topic/index.ts +5 -1
- package/src/services/user/index.ts +5 -1
- package/src/store/chat/slices/plugin/action.ts +46 -2
- package/src/store/electron/actions/app.ts +59 -0
- package/src/store/electron/actions/sync.ts +5 -1
- package/src/store/electron/initialState.ts +3 -1
- package/src/store/electron/store.ts +6 -1
- package/src/store/tool/slices/customPlugin/action.ts +16 -4
- package/src/types/tool/plugin.ts +9 -0
- package/src/utils/client/GlobalAgentContextManager.ts +85 -0
- package/src/utils/promptTemplate.test.ts +78 -0
- package/src/utils/promptTemplate.ts +17 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
2
|
+
import path from 'node:path';
|
3
|
+
|
4
|
+
import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
|
5
|
+
|
6
|
+
import { FileServiceImpl } from './type';
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 桌面应用本地文件服务实现
|
10
|
+
*/
|
11
|
+
export class DesktopLocalFileImpl implements FileServiceImpl {
|
12
|
+
/**
|
13
|
+
* 获取本地文件的URL
|
14
|
+
* Electron返回文件的绝对路径,然后在服务端将文件转为base64
|
15
|
+
*/
|
16
|
+
private async getLocalFileUrl(key: string): Promise<string> {
|
17
|
+
try {
|
18
|
+
// 从Electron获取文件的绝对路径
|
19
|
+
const filePath = await electronIpcClient.getFilePathById(key);
|
20
|
+
|
21
|
+
// 检查文件是否存在
|
22
|
+
if (!existsSync(filePath)) {
|
23
|
+
console.error(`File not found: ${filePath}`);
|
24
|
+
return key;
|
25
|
+
}
|
26
|
+
|
27
|
+
// 读取文件内容
|
28
|
+
const fileContent = readFileSync(filePath);
|
29
|
+
|
30
|
+
// 确定文件的MIME类型
|
31
|
+
const mimeType = this.getMimeTypeFromPath(filePath);
|
32
|
+
|
33
|
+
// 转换为base64并返回data URL
|
34
|
+
const base64 = fileContent.toString('base64');
|
35
|
+
return `data:${mimeType};base64,${base64}`;
|
36
|
+
} catch (e) {
|
37
|
+
console.error('[DesktopLocalFileImpl] Failed to process file from Electron IPC:', e);
|
38
|
+
return '';
|
39
|
+
}
|
40
|
+
}
|
41
|
+
|
42
|
+
/**
|
43
|
+
* 根据文件路径获取MIME类型
|
44
|
+
*/
|
45
|
+
private getMimeTypeFromPath(filePath: string): string {
|
46
|
+
const extension = path.extname(filePath).toLowerCase();
|
47
|
+
|
48
|
+
// 常见文件类型的MIME映射
|
49
|
+
const mimeTypes: Record<string, string> = {
|
50
|
+
'.css': 'text/css',
|
51
|
+
'.gif': 'image/gif',
|
52
|
+
'.html': 'text/html',
|
53
|
+
'.jpeg': 'image/jpeg',
|
54
|
+
'.jpg': 'image/jpeg',
|
55
|
+
'.js': 'application/javascript',
|
56
|
+
'.json': 'application/json',
|
57
|
+
'.pdf': 'application/pdf',
|
58
|
+
'.png': 'image/png',
|
59
|
+
'.svg': 'image/svg+xml',
|
60
|
+
'.txt': 'text/plain',
|
61
|
+
'.webp': 'image/webp',
|
62
|
+
};
|
63
|
+
|
64
|
+
return mimeTypes[extension] || 'application/octet-stream';
|
65
|
+
}
|
66
|
+
|
67
|
+
/**
|
68
|
+
* 创建预签名上传URL(本地版实际上是直接返回文件路径,可能需要进一步扩展)
|
69
|
+
*/
|
70
|
+
async createPreSignedUrl(key: string): Promise<string> {
|
71
|
+
// 在桌面应用本地文件实现中,不需要预签名URL
|
72
|
+
// 直接返回文件路径
|
73
|
+
return key;
|
74
|
+
}
|
75
|
+
|
76
|
+
/**
|
77
|
+
* 创建预签名预览URL(本地版是通过Electron获取本地文件URL)
|
78
|
+
*/
|
79
|
+
async createPreSignedUrlForPreview(key: string): Promise<string> {
|
80
|
+
return this.getLocalFileUrl(key);
|
81
|
+
}
|
82
|
+
|
83
|
+
async deleteFile(key: string): Promise<any> {
|
84
|
+
return await this.deleteFiles([key]);
|
85
|
+
}
|
86
|
+
|
87
|
+
/**
|
88
|
+
* 批量删除文件
|
89
|
+
*/
|
90
|
+
async deleteFiles(keys: string[]): Promise<any> {
|
91
|
+
try {
|
92
|
+
if (!keys || keys.length === 0) return { success: true };
|
93
|
+
|
94
|
+
// 确保所有路径都是合法的desktop://路径
|
95
|
+
const invalidKeys = keys.filter((key) => !key.startsWith('desktop://'));
|
96
|
+
if (invalidKeys.length > 0) {
|
97
|
+
console.error('Invalid desktop file paths:', invalidKeys);
|
98
|
+
return {
|
99
|
+
errors: invalidKeys.map((key) => ({ message: 'Invalid desktop file path', path: key })),
|
100
|
+
success: false,
|
101
|
+
};
|
102
|
+
}
|
103
|
+
|
104
|
+
// 使用electronIpcClient的专用方法
|
105
|
+
return await electronIpcClient.deleteFiles(keys);
|
106
|
+
} catch (error) {
|
107
|
+
console.error('Failed to delete files:', error);
|
108
|
+
return {
|
109
|
+
errors: [
|
110
|
+
{
|
111
|
+
message: `Batch delete failed: ${(error as Error).message}`,
|
112
|
+
path: 'batch',
|
113
|
+
},
|
114
|
+
],
|
115
|
+
success: false,
|
116
|
+
};
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
/**
|
121
|
+
* 获取文件字节数组
|
122
|
+
*/
|
123
|
+
async getFileByteArray(key: string): Promise<Uint8Array> {
|
124
|
+
try {
|
125
|
+
// 从Electron获取文件的绝对路径
|
126
|
+
const filePath = await electronIpcClient.getFilePathById(key);
|
127
|
+
|
128
|
+
// 检查文件是否存在
|
129
|
+
if (!existsSync(filePath)) {
|
130
|
+
console.error(`File not found: ${filePath}`);
|
131
|
+
return new Uint8Array();
|
132
|
+
}
|
133
|
+
|
134
|
+
// 读取文件内容并转换为Uint8Array
|
135
|
+
const buffer = readFileSync(filePath);
|
136
|
+
return new Uint8Array(buffer);
|
137
|
+
} catch (e) {
|
138
|
+
console.error('Failed to get file byte array:', e);
|
139
|
+
return new Uint8Array();
|
140
|
+
}
|
141
|
+
}
|
142
|
+
|
143
|
+
/**
|
144
|
+
* 获取文件内容
|
145
|
+
*/
|
146
|
+
async getFileContent(key: string): Promise<string> {
|
147
|
+
try {
|
148
|
+
// 从Electron获取文件的绝对路径
|
149
|
+
const filePath = await electronIpcClient.getFilePathById(key);
|
150
|
+
|
151
|
+
// 检查文件是否存在
|
152
|
+
if (!existsSync(filePath)) {
|
153
|
+
console.error(`File not found: ${filePath}`);
|
154
|
+
return '';
|
155
|
+
}
|
156
|
+
|
157
|
+
// 读取文件内容并转换为字符串
|
158
|
+
return readFileSync(filePath, 'utf8');
|
159
|
+
} catch (e) {
|
160
|
+
console.error('Failed to get file content:', e);
|
161
|
+
return '';
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
/**
|
166
|
+
* 获取完整文件URL
|
167
|
+
*/
|
168
|
+
async getFullFileUrl(url?: string | null): Promise<string> {
|
169
|
+
if (!url) return '';
|
170
|
+
return this.getLocalFileUrl(url);
|
171
|
+
}
|
172
|
+
|
173
|
+
/**
|
174
|
+
* 上传内容
|
175
|
+
* 注意:这个功能可能需要扩展Electron IPC接口
|
176
|
+
*/
|
177
|
+
async uploadContent(filePath: string, content: string): Promise<any> {
|
178
|
+
// 这里需要扩展electronIpcClient以支持上传文件内容
|
179
|
+
// 例如: return electronIpcClient.uploadContent(filePath, content);
|
180
|
+
console.warn('uploadContent not implemented for Desktop local file service', filePath, content);
|
181
|
+
return;
|
182
|
+
}
|
183
|
+
}
|
@@ -0,0 +1,176 @@
|
|
1
|
+
import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk';
|
2
|
+
import { TRPCError } from '@trpc/server';
|
3
|
+
import debug from 'debug';
|
4
|
+
|
5
|
+
import { MCPClient, MCPClientParams } from '@/libs/mcp';
|
6
|
+
import { safeParseJSON } from '@/utils/safeParseJSON';
|
7
|
+
|
8
|
+
const log = debug('lobe-mcp:service');
|
9
|
+
|
10
|
+
// Removed MCPConnection interface as it's no longer needed
|
11
|
+
|
12
|
+
class MCPService {
|
13
|
+
// Store instances of the custom MCPClient, keyed by serialized MCPClientParams
|
14
|
+
private clients: Map<string, MCPClient> = new Map();
|
15
|
+
|
16
|
+
constructor() {
|
17
|
+
log('MCPService initialized');
|
18
|
+
}
|
19
|
+
|
20
|
+
// --- MCP Interaction ---
|
21
|
+
|
22
|
+
// listTools now accepts MCPClientParams
|
23
|
+
async listTools(params: MCPClientParams): Promise<LobeChatPluginApi[]> {
|
24
|
+
const client = await this.getClient(params); // Get client using params
|
25
|
+
log(`Listing tools using client for params: %O`, params);
|
26
|
+
|
27
|
+
try {
|
28
|
+
const result = await client.listTools();
|
29
|
+
log(`Tools listed successfully for params: %O, result count: %d`, params, result.length);
|
30
|
+
return result.map<LobeChatPluginApi>((item) => ({
|
31
|
+
// Assuming identifier is the unique name/id
|
32
|
+
description: item.description,
|
33
|
+
name: item.name,
|
34
|
+
parameters: item.inputSchema as PluginSchema,
|
35
|
+
}));
|
36
|
+
} catch (error) {
|
37
|
+
console.error(`Error listing tools for params %O:`, params, error);
|
38
|
+
// Propagate a TRPCError for better handling upstream
|
39
|
+
throw new TRPCError({
|
40
|
+
cause: error,
|
41
|
+
code: 'INTERNAL_SERVER_ERROR',
|
42
|
+
message: `Error listing tools from MCP server: ${(error as Error).message}`,
|
43
|
+
});
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
// callTool now accepts MCPClientParams, toolName, and args
|
48
|
+
async callTool(params: MCPClientParams, toolName: string, argsStr: any): Promise<any> {
|
49
|
+
const client = await this.getClient(params); // Get client using params
|
50
|
+
|
51
|
+
const args = safeParseJSON(argsStr);
|
52
|
+
|
53
|
+
log(`Calling tool "${toolName}" using client for params: %O with args: %O`, params, args);
|
54
|
+
|
55
|
+
try {
|
56
|
+
// Delegate the call to the MCPClient instance
|
57
|
+
const result = await client.callTool(toolName, args); // Pass args directly
|
58
|
+
log(`Tool "${toolName}" called successfully for params: %O, result: %O`, params, result);
|
59
|
+
const { content, isError } = result;
|
60
|
+
if (!isError) return content;
|
61
|
+
|
62
|
+
return result;
|
63
|
+
} catch (error) {
|
64
|
+
console.error(`Error calling tool "${toolName}" for params %O:`, params, error);
|
65
|
+
// Propagate a TRPCError
|
66
|
+
throw new TRPCError({
|
67
|
+
cause: error,
|
68
|
+
code: 'INTERNAL_SERVER_ERROR',
|
69
|
+
message: `Error calling tool "${toolName}" on MCP server: ${(error as Error).message}`,
|
70
|
+
});
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
// TODO: Consider adding methods for managing the client lifecycle if needed,
|
75
|
+
// e.g., explicitly closing clients on shutdown or after inactivity,
|
76
|
+
// although for serverless, on-demand creation/retrieval might be sufficient.
|
77
|
+
|
78
|
+
// TODO: Implement methods like listResources, getResource, listPrompts, getPrompt if needed,
|
79
|
+
// following the pattern of accepting MCPClientParams.
|
80
|
+
|
81
|
+
// --- Client Management (Replaces Connection Management) ---
|
82
|
+
|
83
|
+
// Private method to get or initialize a client based on parameters
|
84
|
+
private async getClient(params: MCPClientParams): Promise<MCPClient> {
|
85
|
+
const key = this.serializeParams(params); // Use custom serialization
|
86
|
+
log(`Attempting to get client for key: ${key} (params: %O)`, params);
|
87
|
+
|
88
|
+
if (this.clients.has(key)) {
|
89
|
+
log(`Returning cached client for key: ${key}`);
|
90
|
+
return this.clients.get(key)!;
|
91
|
+
}
|
92
|
+
|
93
|
+
log(`No cached client found for key: ${key}. Initializing new client.`);
|
94
|
+
try {
|
95
|
+
// Ensure stdio is only attempted in desktop/server environments within the client itself
|
96
|
+
// or add a check here if MCPClient doesn't handle it.
|
97
|
+
// Example check (adjust based on where environment check is best handled):
|
98
|
+
// if (params.type === 'stdio' && typeof window !== 'undefined') {
|
99
|
+
// throw new Error('Stdio MCP type is not supported in browser environment.');
|
100
|
+
// }
|
101
|
+
|
102
|
+
const client = new MCPClient(params);
|
103
|
+
await client.initialize(); // Initialization logic should be within MCPClient
|
104
|
+
this.clients.set(key, client);
|
105
|
+
log(`New client initialized and cached for key: ${key}`);
|
106
|
+
return client;
|
107
|
+
} catch (error) {
|
108
|
+
console.error(`Failed to initialize MCP client for key ${key}:`, error);
|
109
|
+
// Do not cache failed initializations
|
110
|
+
throw new TRPCError({
|
111
|
+
cause: error,
|
112
|
+
code: 'INTERNAL_SERVER_ERROR',
|
113
|
+
message: `Failed to initialize MCP client: ${(error as Error).message}`,
|
114
|
+
});
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
// Custom serialization function to ensure consistent keys
|
119
|
+
private serializeParams(params: MCPClientParams): string {
|
120
|
+
const sortedKeys = Object.keys(params).sort();
|
121
|
+
const sortedParams: Record<string, any> = {};
|
122
|
+
|
123
|
+
for (const key of sortedKeys) {
|
124
|
+
const value = (params as any)[key];
|
125
|
+
// Sort the 'args' array if it exists
|
126
|
+
if (key === 'args' && Array.isArray(value)) {
|
127
|
+
sortedParams[key] = JSON.stringify(key);
|
128
|
+
} else {
|
129
|
+
sortedParams[key] = value;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
return JSON.stringify(sortedParams);
|
134
|
+
}
|
135
|
+
|
136
|
+
async getStreamableMcpServerManifest(
|
137
|
+
identifier: string,
|
138
|
+
url: string,
|
139
|
+
): Promise<LobeChatPluginManifest> {
|
140
|
+
const tools = await this.listTools({ name: identifier, type: 'http', url }); // Get client using params
|
141
|
+
|
142
|
+
return {
|
143
|
+
api: tools,
|
144
|
+
identifier,
|
145
|
+
meta: {
|
146
|
+
avatar: 'MCP_AVATAR',
|
147
|
+
description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
|
148
|
+
title: identifier,
|
149
|
+
},
|
150
|
+
// TODO: temporary
|
151
|
+
type: 'mcp' as any,
|
152
|
+
};
|
153
|
+
}
|
154
|
+
async getStdioMcpServerManifest(
|
155
|
+
identifier: string,
|
156
|
+
command: string,
|
157
|
+
args: string[],
|
158
|
+
): Promise<LobeChatPluginManifest> {
|
159
|
+
const tools = await this.listTools({ args, command, name: identifier, type: 'stdio' }); // Get client using params
|
160
|
+
|
161
|
+
return {
|
162
|
+
api: tools,
|
163
|
+
identifier,
|
164
|
+
meta: {
|
165
|
+
avatar: 'MCP_AVATAR',
|
166
|
+
description: `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`,
|
167
|
+
title: identifier,
|
168
|
+
},
|
169
|
+
// TODO: temporary
|
170
|
+
type: 'mcp' as any,
|
171
|
+
};
|
172
|
+
}
|
173
|
+
}
|
174
|
+
|
175
|
+
// Export a singleton instance
|
176
|
+
export const mcpService = new MCPService();
|
@@ -1,5 +1,9 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService } from './client';
|
2
4
|
import { ServerService } from './server';
|
3
5
|
|
4
6
|
export const aiModelService =
|
5
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
7
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
8
|
+
? new ServerService()
|
9
|
+
: new ClientService();
|
@@ -1,5 +1,9 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService } from './client';
|
2
4
|
import { ServerService } from './server';
|
3
5
|
|
4
6
|
export const aiProviderService =
|
5
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
7
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
8
|
+
? new ServerService()
|
9
|
+
: new ClientService();
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService as DeprecatedService } from './_deprecated';
|
2
4
|
import { ClientService } from './client';
|
3
5
|
import { ServerService } from './server';
|
@@ -6,4 +8,6 @@ const clientService =
|
|
6
8
|
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
9
|
|
8
10
|
export const fileService =
|
9
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
11
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
12
|
+
? new ServerService()
|
13
|
+
: clientService;
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { toolsClient } from '@/libs/trpc/client';
|
2
|
+
import { ChatToolPayload } from '@/types/message';
|
3
|
+
|
4
|
+
class MCPService {
|
5
|
+
async invokeMcpToolCall(payload: ChatToolPayload, { signal }: { signal?: AbortSignal }) {
|
6
|
+
const { pluginSelectors } = await import('@/store/tool/selectors');
|
7
|
+
const { getToolStoreState } = await import('@/store/tool/store');
|
8
|
+
|
9
|
+
const s = getToolStoreState();
|
10
|
+
const { identifier, arguments: args, apiName } = payload;
|
11
|
+
|
12
|
+
const plugin = pluginSelectors.getCustomPluginById(identifier)(s);
|
13
|
+
|
14
|
+
if (!plugin) return;
|
15
|
+
|
16
|
+
return toolsClient.mcp.callTool.mutate(
|
17
|
+
{ args, params: { ...plugin.customParams?.mcp, name: identifier } as any, toolName: apiName },
|
18
|
+
{ signal },
|
19
|
+
);
|
20
|
+
}
|
21
|
+
|
22
|
+
async getStreamableMcpServerManifest(identifier: string, url: string) {
|
23
|
+
return toolsClient.mcp.getStreamableMcpServerManifest.query({ identifier, url });
|
24
|
+
}
|
25
|
+
|
26
|
+
async getStdioMcpServerManifest(identifier: string, command: string, args?: string[]) {
|
27
|
+
return toolsClient.mcp.getStdioMcpServerManifest.query({
|
28
|
+
args: args,
|
29
|
+
command,
|
30
|
+
name: identifier,
|
31
|
+
type: 'stdio',
|
32
|
+
});
|
33
|
+
}
|
34
|
+
}
|
35
|
+
|
36
|
+
export const mcpService = new MCPService();
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService as DeprecatedService } from './_deprecated';
|
2
4
|
import { ClientService } from './client';
|
3
5
|
import { ServerService } from './server';
|
@@ -6,4 +8,6 @@ const clientService =
|
|
6
8
|
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
9
|
|
8
10
|
export const messageService =
|
9
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
11
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
12
|
+
? new ServerService()
|
13
|
+
: clientService;
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService as DeprecatedService } from './_deprecated';
|
2
4
|
import { ClientService } from './client';
|
3
5
|
import { ServerService } from './server';
|
@@ -6,4 +8,6 @@ const clientService =
|
|
6
8
|
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
9
|
|
8
10
|
export const pluginService =
|
9
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
11
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
12
|
+
? new ServerService()
|
13
|
+
: clientService;
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService as DeprecatedService } from './_deprecated';
|
2
4
|
import { ClientService } from './client';
|
3
5
|
import { ServerService } from './server';
|
@@ -6,4 +8,6 @@ const clientService =
|
|
6
8
|
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
9
|
|
8
10
|
export const sessionService =
|
9
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
11
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
12
|
+
? new ServerService()
|
13
|
+
: clientService;
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import { desktopClient } from '@/libs/trpc/client/desktop';
|
2
|
+
|
3
|
+
export class DesktopService {
|
4
|
+
getAllTables = async () => {
|
5
|
+
return desktopClient.pgTable.getAllTables.query();
|
6
|
+
};
|
7
|
+
|
8
|
+
getTableDetails = async (tableName: string) => {
|
9
|
+
return desktopClient.pgTable.getTableDetails.query({ tableName });
|
10
|
+
};
|
11
|
+
|
12
|
+
getTableData = async (tableName: string) => {
|
13
|
+
return desktopClient.pgTable.getTableData.query({ page: 1, pageSize: 300, tableName });
|
14
|
+
};
|
15
|
+
}
|
@@ -1,3 +1,6 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService } from './client';
|
4
|
+
import { DesktopService } from './desktop';
|
2
5
|
|
3
|
-
export const tableViewerService = new ClientService();
|
6
|
+
export const tableViewerService = isDesktop ? new DesktopService() : new ClientService();
|
@@ -1,5 +1,9 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService } from './client';
|
2
4
|
import { ServerService } from './server';
|
3
5
|
|
4
6
|
export const threadService =
|
5
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
7
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
8
|
+
? new ServerService()
|
9
|
+
: new ClientService();
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService as DeprecatedService } from './_deprecated';
|
2
4
|
import { ClientService } from './client';
|
3
5
|
import { ServerService } from './server';
|
@@ -6,4 +8,6 @@ const clientService =
|
|
6
8
|
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
9
|
|
8
10
|
export const topicService =
|
9
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
11
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
12
|
+
? new ServerService()
|
13
|
+
: clientService;
|
@@ -1,3 +1,5 @@
|
|
1
|
+
import { isDesktop } from '@/const/version';
|
2
|
+
|
1
3
|
import { ClientService as DeprecatedService } from './_deprecated';
|
2
4
|
import { ClientService } from './client';
|
3
5
|
import { ServerService } from './server';
|
@@ -6,6 +8,8 @@ const clientService =
|
|
6
8
|
process.env.NEXT_PUBLIC_CLIENT_DB === 'pglite' ? new ClientService() : new DeprecatedService();
|
7
9
|
|
8
10
|
export const userService =
|
9
|
-
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server'
|
11
|
+
process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' || isDesktop
|
12
|
+
? new ServerService()
|
13
|
+
: clientService;
|
10
14
|
|
11
15
|
export const userClientService = clientService;
|
@@ -8,6 +8,7 @@ import { StateCreator } from 'zustand/vanilla';
|
|
8
8
|
import { LOADING_FLAT } from '@/const/message';
|
9
9
|
import { PLUGIN_SCHEMA_API_MD5_PREFIX, PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
|
10
10
|
import { chatService } from '@/services/chat';
|
11
|
+
import { mcpService } from '@/services/mcp';
|
11
12
|
import { messageService } from '@/services/message';
|
12
13
|
import { ChatStore } from '@/store/chat/store';
|
13
14
|
import { useToolStore } from '@/store/tool';
|
@@ -41,6 +42,7 @@ export interface ChatPluginAction {
|
|
41
42
|
invokeBuiltinTool: (id: string, payload: ChatToolPayload) => Promise<void>;
|
42
43
|
invokeDefaultTypePlugin: (id: string, payload: any) => Promise<string | undefined>;
|
43
44
|
invokeMarkdownTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
45
|
+
invokeMCPTypePlugin: (id: string, payload: ChatToolPayload) => Promise<string | undefined>;
|
44
46
|
|
45
47
|
invokeStandaloneTypePlugin: (id: string, payload: ChatToolPayload) => Promise<void>;
|
46
48
|
|
@@ -271,7 +273,7 @@ export const chatPlugin: StateCreator<
|
|
271
273
|
// trigger the plugin call
|
272
274
|
const data = await get().internal_invokeDifferentTypePlugin(id, payload);
|
273
275
|
|
274
|
-
if (
|
276
|
+
if (data && !['markdown', 'standalone'].includes(payload.type)) {
|
275
277
|
shouldCreateMessage = true;
|
276
278
|
latestToolId = id;
|
277
279
|
}
|
@@ -328,7 +330,9 @@ export const chatPlugin: StateCreator<
|
|
328
330
|
|
329
331
|
const updateAssistantMessage = async () => {
|
330
332
|
if (!assistantMessage) return;
|
331
|
-
await messageService.updateMessage(assistantMessage!.id, {
|
333
|
+
await messageService.updateMessage(assistantMessage!.id, {
|
334
|
+
tools: assistantMessage?.tools,
|
335
|
+
});
|
332
336
|
};
|
333
337
|
|
334
338
|
await Promise.all([
|
@@ -438,11 +442,51 @@ export const chatPlugin: StateCreator<
|
|
438
442
|
return await get().invokeBuiltinTool(id, payload);
|
439
443
|
}
|
440
444
|
|
445
|
+
// @ts-ignore
|
446
|
+
case 'mcp': {
|
447
|
+
return await get().invokeMCPTypePlugin(id, payload);
|
448
|
+
}
|
449
|
+
|
441
450
|
default: {
|
442
451
|
return await get().invokeDefaultTypePlugin(id, payload);
|
443
452
|
}
|
444
453
|
}
|
445
454
|
},
|
455
|
+
invokeMCPTypePlugin: async (id, payload) => {
|
456
|
+
const { internal_updateMessageContent, refreshMessages, internal_togglePluginApiCalling } =
|
457
|
+
get();
|
458
|
+
let data: string = '';
|
459
|
+
|
460
|
+
try {
|
461
|
+
const abortController = internal_togglePluginApiCalling(
|
462
|
+
true,
|
463
|
+
id,
|
464
|
+
n('fetchPlugin/start') as string,
|
465
|
+
);
|
466
|
+
|
467
|
+
const result = await mcpService.invokeMcpToolCall(payload, {
|
468
|
+
signal: abortController?.signal,
|
469
|
+
});
|
470
|
+
if (!!result) data = result;
|
471
|
+
} catch (error) {
|
472
|
+
console.log(error);
|
473
|
+
const err = error as Error;
|
474
|
+
|
475
|
+
// ignore the aborted request error
|
476
|
+
if (!err.message.includes('The user aborted a request.')) {
|
477
|
+
await messageService.updateMessageError(id, error as any);
|
478
|
+
await refreshMessages();
|
479
|
+
}
|
480
|
+
}
|
481
|
+
|
482
|
+
internal_togglePluginApiCalling(false, id, n('fetchPlugin/end') as string);
|
483
|
+
// 如果报错则结束了
|
484
|
+
if (!data) return;
|
485
|
+
|
486
|
+
await internal_updateMessageContent(id, data);
|
487
|
+
|
488
|
+
return data;
|
489
|
+
},
|
446
490
|
|
447
491
|
internal_togglePluginApiCalling: (loading, id, action) => {
|
448
492
|
return get().internal_toggleLoadingArrays('pluginApiLoadingIds', loading, id, action);
|