sloth-d2c-mcp 1.0.4-beta65

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.
@@ -0,0 +1,539 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import express from 'express';
3
+ // 流式 HTTP 传输实现
4
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5
+ // 判断是否为初始化请求
6
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
7
+ // 日志工具类,带时间戳输出日志
8
+ import { Logger } from './utils/logger.js';
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { v4 as uuidv4 } from 'uuid';
13
+ import open from 'open';
14
+ import FileManager from './utils/file-manager.js';
15
+ import * as flatted from 'flatted';
16
+ // 导入默认提示词
17
+ import { chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt } from 'sloth-d2c-node/convert';
18
+ // 保存 HTTP 服务器实例
19
+ let httpServer = null;
20
+ // 管理所有活跃的传输对象,按 sessionId 分类
21
+ const transports = {
22
+ streamable: {}, // 流式 HTTP 传输
23
+ sse: {}, // SSE 传输
24
+ };
25
+ // 认证功能存储
26
+ const pendingRequests = new Map(); // 等待中的认证请求
27
+ let configStorage = {}; // 配置缓存
28
+ let configManager = null; // 配置管理器实例
29
+ const __filename = fileURLToPath(import.meta.url);
30
+ const __dirname = path.dirname(__filename);
31
+ // 获取端口号
32
+ let PORT = 0;
33
+ export async function loadConfig(mcpServer, configManagerInstance) {
34
+ if (configManagerInstance) {
35
+ configManager = configManagerInstance;
36
+ // 打印配置文件路径
37
+ try {
38
+ const configPath = configManager.getConfigPath();
39
+ Logger.log(`配置文件路径: ${configPath}`);
40
+ // 检查配置文件是否存在
41
+ const configExists = await configManager.exists();
42
+ Logger.log(`配置文件存在: ${configExists ? '是' : '否'}`);
43
+ if (configExists) {
44
+ // 如果配置文件存在,加载并显示配置信息
45
+ try {
46
+ const config = await configManager.load();
47
+ mcpServer.setConfig(config.mcp);
48
+ Logger.log(`当前配置内容: ${JSON.stringify(config, null, 2)}`);
49
+ }
50
+ catch (error) {
51
+ Logger.warn(`读取配置文件失败: ${error}`);
52
+ }
53
+ }
54
+ }
55
+ catch (error) {
56
+ Logger.error(`获取配置信息时出错: ${error}`);
57
+ }
58
+ }
59
+ }
60
+ // 启动 HTTP 服务器,监听指定端口,注册各类路由
61
+ export async function startHttpServer(port = PORT, mcpServer, configManagerInstance, isWeb) {
62
+ const app = express();
63
+ PORT = port;
64
+ // 存储配置管理器实例(如果提供)
65
+ loadConfig(mcpServer, configManagerInstance);
66
+ // if (configManagerInstance) {
67
+ // configManager = configManagerInstance;
68
+ // // 打印配置文件路径
69
+ // try {
70
+ // const configPath = configManager.getConfigPath();
71
+ // Logger.log(`配置文件路径: ${configPath}`);
72
+ // // 检查配置文件是否存在
73
+ // const configExists = await configManager.exists();
74
+ // Logger.log(`配置文件存在: ${configExists ? '是' : '否'}`);
75
+ // if (configExists) {
76
+ // // 如果配置文件存在,加载并显示配置信息
77
+ // try {
78
+ // const config = await configManager.load();
79
+ // mcpServer.setConfig(config.mcp);
80
+ // Logger.log(`当前配置内容: ${JSON.stringify(config, null, 2)}`);
81
+ // } catch (error) {
82
+ // Logger.warn(`读取配置文件失败: ${error}`);
83
+ // }
84
+ // }
85
+ // } catch (error) {
86
+ // Logger.error(`获取配置信息时出错: ${error}`);
87
+ // }
88
+ // }
89
+ // 仅对 /mcp /submit 路径解析 JSON 请求体,避免影响 SSE
90
+ app.use('/mcp', express.json());
91
+ app.use('/submit', express.json());
92
+ app.use('/saveNodes', express.json({
93
+ limit: '100mb'
94
+ }));
95
+ // 为认证端点添加中间件 - 需要同时支持 JSON 和表单数据
96
+ app.use(express.urlencoded({ extended: true }));
97
+ // 设置跨域
98
+ app.use((req, res, next) => {
99
+ res.header('Access-Control-Allow-Origin', '*');
100
+ res.header('Access-Control-Allow-Methods', 'GET, POST');
101
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content-Length, X-Requested-With');
102
+ next();
103
+ });
104
+ // app.use(express.json());
105
+ // 处理流式 HTTP 的 POST 请求,支持会话复用和初始化
106
+ app.post('/mcp', async (req, res) => {
107
+ Logger.log('Received StreamableHTTP request');
108
+ const sessionId = req.headers['mcp-session-id']; // 获取会话ID
109
+ let transport;
110
+ if (sessionId && transports.streamable[sessionId]) {
111
+ // 已有会话,复用传输对象
112
+ Logger.log('Reusing existing StreamableHTTP transport for sessionId', sessionId);
113
+ transport = transports.streamable[sessionId];
114
+ }
115
+ else if (!sessionId && isInitializeRequest(req.body)) {
116
+ // 新会话初始化请求
117
+ Logger.log('New initialization request for StreamableHTTP sessionId', sessionId);
118
+ transport = new StreamableHTTPServerTransport({
119
+ sessionIdGenerator: () => randomUUID(), // 生成唯一 sessionId
120
+ onsessioninitialized: (sessionId) => {
121
+ // 会话初始化后,存储传输对象
122
+ transports.streamable[sessionId] = transport;
123
+ },
124
+ });
125
+ // 会话关闭时清理资源
126
+ transport.onclose = () => {
127
+ if (transport.sessionId) {
128
+ delete transports.streamable[transport.sessionId];
129
+ }
130
+ };
131
+ // 连接 MCP 服务器,建立上下文
132
+ if (isWeb === true) {
133
+ await mcpServer.connect(transport);
134
+ }
135
+ }
136
+ else {
137
+ // 非法请求,缺少有效 sessionId
138
+ Logger.log('Invalid request:', req.body);
139
+ res.status(400).json({
140
+ jsonrpc: '2.0',
141
+ error: {
142
+ code: -32000,
143
+ message: 'Bad Request: No valid session ID provided',
144
+ },
145
+ id: null,
146
+ });
147
+ return;
148
+ }
149
+ // 进度通知相关逻辑
150
+ let progressInterval = null;
151
+ const progressToken = req.body.params?._meta?.progressToken; // 获取进度令牌
152
+ let progress = 0;
153
+ Logger.log(`progressToken:${progressToken}`);
154
+ if (progressToken) {
155
+ Logger.log(`Setting up progress notifications for token ${progressToken} on session ${sessionId} 1112222222`);
156
+ // 定时发送进度通知
157
+ progressInterval = setInterval(async () => {
158
+ // Logger.log("Sending progress notification", progress);
159
+ await mcpServer.server.notification({
160
+ method: 'notifications/progress',
161
+ params: {
162
+ progress,
163
+ progressToken,
164
+ },
165
+ });
166
+ progress++;
167
+ }, 1000);
168
+ }
169
+ Logger.log('Handling StreamableHTTP request');
170
+ // 处理请求,响应客户端
171
+ await transport.handleRequest(req, res, req.body);
172
+ // 清理进度通知定时器
173
+ // if (progressInterval) {
174
+ // clearInterval(progressInterval);
175
+ // }
176
+ Logger.log('StreamableHTTP request handled');
177
+ });
178
+ // 处理 GET/DELETE 请求,用于会话终止或获取会话状态
179
+ const handleSessionRequest = async (req, res) => {
180
+ const sessionId = req.headers['mcp-session-id'];
181
+ if (!sessionId || !transports.streamable[sessionId]) {
182
+ res.status(400).send('Invalid or missing session ID');
183
+ return;
184
+ }
185
+ Logger.log(`Received session termination request for session ${sessionId}`);
186
+ try {
187
+ const transport = transports.streamable[sessionId];
188
+ await transport.handleRequest(req, res);
189
+ }
190
+ catch (error) {
191
+ console.error('Error handling session termination:', error);
192
+ if (!res.headersSent) {
193
+ res.status(500).send('Error processing session termination');
194
+ }
195
+ }
196
+ };
197
+ // GET 请求:获取会话状态
198
+ app.get('/mcp', handleSessionRequest);
199
+ // DELETE 请求:终止会话
200
+ app.delete('/mcp', handleSessionRequest);
201
+ // SSE 连接建立,分配 sessionId 并注册到 transports.sse
202
+ // app.get("/sse", async (req: any, res: any) => {
203
+ // Logger.log("Establishing new SSE connection");
204
+ // const transport = new SSEServerTransport("/messages", res);
205
+ // Logger.log(`New SSE connection established for sessionId ${transport.sessionId}`);
206
+ // Logger.log("/sse request headers:", req.headers);
207
+ // Logger.log("/sse request body:", req.body);
208
+ // transports.sse[transport.sessionId] = transport;
209
+ // // 连接关闭时清理资源
210
+ // res.on("close", () => {
211
+ // delete transports.sse[transport.sessionId];
212
+ // });
213
+ // // 连接 MCP 服务器,建立 SSE 通道
214
+ // await mcpServer.connect(transport);
215
+ // });
216
+ // SSE 消息推送接口,根据 sessionId 找到对应传输对象
217
+ app.post('/messages', async (req, res) => {
218
+ const sessionId = req.query.sessionId;
219
+ const transport = transports.sse[sessionId];
220
+ Logger.log('/messages request headers:', req.headers);
221
+ Logger.log('/messages request body:', req.body);
222
+ if (transport) {
223
+ Logger.log(`Received SSE message for sessionId ${sessionId}`);
224
+ await transport.handlePostMessage(req, res);
225
+ }
226
+ else {
227
+ res.status(400).send(`No transport found for sessionId ${sessionId}`);
228
+ return;
229
+ }
230
+ });
231
+ // 认证相关端点
232
+ // 获取配置接口,支持 fileKey 和 nodeId 参数
233
+ app.get('/getConfig', async (req, res) => {
234
+ try {
235
+ const fileKey = req.query.fileKey;
236
+ const nodeId = req.query.nodeId;
237
+ if (fileKey) {
238
+ // 如果提供了 fileKey,返回该 fileKey 的特定配置
239
+ const globalConfig = await configManager.load();
240
+ const fileConfig = globalConfig.fileConfigs?.[fileKey] || {};
241
+ // 从 fileManager 按 nodeId 加载 groupsData 和 promptSetting
242
+ const fileManager = new FileManager('d2c-mcp');
243
+ const groupsData = await fileManager.loadGroupsData(fileKey, nodeId);
244
+ const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
245
+ // 合并用户自定义提示词和默认提示词
246
+ const promptSetting = {
247
+ chunkOptimizePrompt: savedPromptSetting?.chunkOptimizePrompt || chunkOptimizeCodePrompt,
248
+ aggregationOptimizePrompt: savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt,
249
+ finalOptimizePrompt: savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt,
250
+ };
251
+ res.json({
252
+ success: true,
253
+ data: {
254
+ mcp: globalConfig.mcp || {},
255
+ convertSetting: fileConfig.convertSetting,
256
+ imageSetting: fileConfig.imageSetting,
257
+ promptSetting: promptSetting,
258
+ fileKey: fileKey,
259
+ groupsData: groupsData,
260
+ },
261
+ });
262
+ }
263
+ else {
264
+ // 如果没有提供 fileKey,返回默认配置
265
+ if (await configManager.exists()) {
266
+ configStorage = await configManager.load();
267
+ }
268
+ // 返回默认提示词
269
+ const promptSetting = {
270
+ chunkOptimizePrompt: chunkOptimizeCodePrompt,
271
+ aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
272
+ finalOptimizePrompt: finalOptimizeCodePrompt,
273
+ };
274
+ res.json({
275
+ success: true,
276
+ data: {
277
+ mcp: configStorage.mcp || {},
278
+ convertSetting: configStorage.convertSetting || {},
279
+ imageSetting: configStorage.imageSetting || {},
280
+ promptSetting: promptSetting,
281
+ },
282
+ });
283
+ }
284
+ }
285
+ catch (error) {
286
+ Logger.error('获取配置失败:', error);
287
+ res.status(500).json({
288
+ success: false,
289
+ message: '获取配置失败',
290
+ error: error instanceof Error ? error.message : String(error),
291
+ });
292
+ }
293
+ });
294
+ app.get('/getHtml', async (req, res) => {
295
+ // 加载现有配置
296
+ const fileManager = new FileManager('d2c-mcp');
297
+ const html = await fileManager.loadFile(req.query.fileKey, req.query.nodeId, 'absolute.html');
298
+ res.json({
299
+ success: true,
300
+ data: html,
301
+ });
302
+ });
303
+ // 认证页面
304
+ app.get('/auth-page', (req, res) => {
305
+ try {
306
+ // 尝试找到相对于当前位置的模板路径
307
+ const isStdioMode = process.env.NODE_ENV === 'cli' || process.argv.includes('--stdio');
308
+ const isDevMode = process.argv.includes('--dev');
309
+ const templatePath = path.join(__dirname, isStdioMode && !isDevMode ? '../interceptor-web/dist/index.html' : '../../interceptor-web/dist/index.html');
310
+ Logger.log('templatePath', templatePath);
311
+ if (fs.existsSync(templatePath)) {
312
+ const htmlContent = fs.readFileSync(templatePath, 'utf8');
313
+ res.send(htmlContent);
314
+ }
315
+ else {
316
+ // 备用方案:简单的 HTML 表单
317
+ const fallbackHtml = `
318
+ <!DOCTYPE html>
319
+ <html>
320
+ <head>
321
+ <title>身份验证</title>
322
+ <meta charset="utf-8">
323
+ <meta name="viewport" content="width=device-width, initial-scale=1">
324
+ </head>
325
+ <body>
326
+ <div style="max-width: 500px; margin: 50px auto; padding: 20px; font-family: Arial, sans-serif;">
327
+ <h2>需要身份验证</h2>
328
+ <form id="authForm" method="POST" action="/submit">
329
+ <input type="hidden" name="token" value="${req.query.token || ''}" />
330
+ <div style="margin-bottom: 15px;">
331
+ <label for="value" style="display: block; margin-bottom: 5px;">请输入您的信息:</label>
332
+ <input type="text" id="value" name="value" required style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;" />
333
+ </div>
334
+ <button type="submit" style="background: #007cba; color: white; padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer;">提交</button>
335
+ </form>
336
+ </div>
337
+ </body>
338
+ </html>`;
339
+ res.send(fallbackHtml);
340
+ }
341
+ }
342
+ catch (error) {
343
+ res.status(500).send('加载认证页面时出错');
344
+ }
345
+ });
346
+ // 提交认证信息,支持 fileKey 和 nodeId 参数
347
+ app.post('/submit', async (req, res) => {
348
+ try {
349
+ // 安全地解构请求体,处理可能为空的情况
350
+ const { token, value, fileKey, nodeId } = req.body || {};
351
+ if (!token || !value) {
352
+ res.status(400).send('请求体中缺少 token 或 value');
353
+ return;
354
+ }
355
+ // 保存配置到对应的 fileKey 或默认配置
356
+ if (fileKey) {
357
+ // 保存到特定 fileKey 的配置
358
+ const globalConfig = await configManager.load();
359
+ if (!globalConfig.fileConfigs) {
360
+ globalConfig.fileConfigs = {};
361
+ }
362
+ if (!globalConfig.fileConfigs[fileKey]) {
363
+ globalConfig.fileConfigs[fileKey] = {};
364
+ }
365
+ if (value.convertSetting) {
366
+ globalConfig.fileConfigs[fileKey].convertSetting = value.convertSetting;
367
+ }
368
+ if (value.imageSetting) {
369
+ globalConfig.fileConfigs[fileKey].imageSetting = value.imageSetting;
370
+ }
371
+ await configManager.save(globalConfig);
372
+ Logger.log(`已保存 fileKey "${fileKey}" 的配置:`, value);
373
+ }
374
+ // 使用 fileManager 按 nodeId 保存 groupsData 和 promptSetting
375
+ const fileManager = new FileManager('d2c-mcp');
376
+ if (fileKey && value.groupsData && Array.isArray(value.groupsData)) {
377
+ await fileManager.saveGroupsData(fileKey, nodeId, value.groupsData);
378
+ Logger.log(`已保存 groupsData 到 fileKey "${fileKey}", nodeId "${nodeId}":`, value.groupsData.length, '个分组');
379
+ }
380
+ if (fileKey && value.promptSetting) {
381
+ await fileManager.savePromptSetting(fileKey, nodeId, value.promptSetting);
382
+ Logger.log(`已保存 promptSetting 到 fileKey "${fileKey}", nodeId "${nodeId}"`);
383
+ }
384
+ // 如果有 MCP 配置,更新全局配置
385
+ if (value.mcp) {
386
+ const globalConfig = await configManager.load();
387
+ globalConfig.mcp = { ...globalConfig.mcp, ...value.mcp };
388
+ await configManager.save(globalConfig);
389
+ Logger.log('已更新全局 MCP 配置');
390
+ }
391
+ const request = pendingRequests.get(token);
392
+ if (request && request.resolve) {
393
+ request.resolve(JSON.stringify(value));
394
+ res.send('提交成功!您可以关闭此窗口。');
395
+ }
396
+ else {
397
+ res.status(404).send('无效或已过期的 token');
398
+ }
399
+ }
400
+ catch (error) {
401
+ Logger.error('保存配置失败:', error);
402
+ res.status(500).send('保存配置失败: ' + (error instanceof Error ? error.message : String(error)));
403
+ }
404
+ });
405
+ // 保存节点数据端点
406
+ app.post('/saveNodes', async (req, res) => {
407
+ try {
408
+ console.log('req.body', req);
409
+ const { nodeList, fileKey, nodeId, imageMap, absoluteHtml } = req.body;
410
+ if (!nodeList || !fileKey || !nodeId) {
411
+ res.status(400).json({
412
+ success: false,
413
+ message: '缺少必要参数: nodeList, fileKey, nodeId',
414
+ });
415
+ return;
416
+ }
417
+ const fileManager = new FileManager('d2c-mcp');
418
+ // 保存节点列表
419
+ await fileManager.saveFile(fileKey, nodeId, 'nodeList.json', (nodeList));
420
+ // 保存图片映射
421
+ if (imageMap) {
422
+ await fileManager.saveFile(fileKey, nodeId, 'imageMap.json', flatted.stringify(imageMap));
423
+ }
424
+ // 保存绝对布局HTML
425
+ if (absoluteHtml) {
426
+ await fileManager.saveFile(fileKey, nodeId, 'absolute.html', absoluteHtml);
427
+ }
428
+ Logger.log(`成功保存节点数据: fileKey=${fileKey}, nodeId=${nodeId}`);
429
+ res.json({
430
+ success: true,
431
+ message: '节点数据保存成功',
432
+ });
433
+ }
434
+ catch (error) {
435
+ Logger.error('保存节点数据失败:', error);
436
+ res.status(500).json({
437
+ success: false,
438
+ message: '保存节点数据失败',
439
+ error: error instanceof Error ? error.message : String(error),
440
+ });
441
+ }
442
+ });
443
+ // 日志重连端点
444
+ app.post('/reconnect-logging', (req, res) => {
445
+ Logger.log('收到来自 VSCode 扩展的日志重连请求');
446
+ // 重新初始化日志连接
447
+ Logger.reconnectVSCodeLogging();
448
+ res.status(200).json({
449
+ success: true,
450
+ message: '日志重连已启动',
451
+ });
452
+ });
453
+ // 启动 HTTP 服务器,监听端口
454
+ httpServer = app.listen(port, () => {
455
+ Logger.log(`HTTP server listening on port ${port}`);
456
+ Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
457
+ Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
458
+ Logger.log(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`);
459
+ Logger.log(`Auth endpoints available at http://localhost:${port}/auth-page`);
460
+ Logger.log(`MCP Config at http://localhost:${port}/getConfig`);
461
+ Logger.log(`Save Nodes endpoint available at http://localhost:${port}/saveNodes`);
462
+ Logger.log(`Logging reconnect endpoint available at http://localhost:${port}/reconnect-logging`);
463
+ });
464
+ // 监听 SIGINT 信号,优雅关闭服务器和所有会话
465
+ process.on('SIGINT', async () => {
466
+ Logger.log('SERVER: 收到SIGINT信号');
467
+ Logger.log('正在关闭服务器...');
468
+ // 清理等待中的认证请求
469
+ pendingRequests.clear();
470
+ // 关闭所有活跃的 SSE 和 streamable 传输,释放资源
471
+ await closeTransports(transports.sse);
472
+ await closeTransports(transports.streamable);
473
+ Logger.log('服务器关闭完成');
474
+ process.exit(0);
475
+ });
476
+ }
477
+ // 批量关闭所有传输对象,确保资源清理
478
+ async function closeTransports(transports) {
479
+ for (const sessionId in transports) {
480
+ try {
481
+ await transports[sessionId].close(); // 调用传输对象的关闭方法
482
+ delete transports[sessionId]; // 删除会话
483
+ }
484
+ catch (error) {
485
+ console.error(`Error closing transport for session ${sessionId}:`, error);
486
+ }
487
+ }
488
+ }
489
+ // 认证功能 - 获取用户输入函数
490
+ export async function getUserInput(payload) {
491
+ return new Promise(async (resolve, reject) => {
492
+ if (!httpServer) {
493
+ reject(new Error('HTTP 服务器未运行。请先启动服务器。'));
494
+ return;
495
+ }
496
+ const token = uuidv4();
497
+ const authUrl = `http://localhost:${PORT}/auth-page?token=${token}&fileKey=${payload.fileKey}&nodeId=${payload.nodeId}`;
498
+ // 存储解析函数 - 无超时限制
499
+ pendingRequests.set(token, {
500
+ resolve: (value) => {
501
+ pendingRequests.delete(token);
502
+ resolve(value);
503
+ },
504
+ timeout: null, // 不再使用超时
505
+ });
506
+ // 打开浏览器
507
+ try {
508
+ await open(authUrl);
509
+ }
510
+ catch (err) {
511
+ pendingRequests.delete(token);
512
+ reject(new Error(`打开浏览器失败: ${err.message}`));
513
+ }
514
+ });
515
+ }
516
+ // 停止 HTTP 服务器,并关闭所有 SSE 传输
517
+ export async function stopHttpServer() {
518
+ if (!httpServer) {
519
+ throw new Error('HTTP 服务器未运行');
520
+ }
521
+ return new Promise((resolve, reject) => {
522
+ // 清理等待中的认证请求
523
+ pendingRequests.clear();
524
+ httpServer.close((err) => {
525
+ if (err) {
526
+ reject(err);
527
+ return;
528
+ }
529
+ httpServer = null;
530
+ // 关闭所有 SSE 传输
531
+ const closing = Object.values(transports.sse).map((transport) => {
532
+ return transport.close();
533
+ });
534
+ Promise.all(closing).then(() => {
535
+ resolve();
536
+ });
537
+ });
538
+ });
539
+ }