@atlisp/mcp 1.0.14 → 1.0.16
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/README.md +0 -4
- package/package.json +4 -2
- package/src/atlisp-mcp.js +241 -95
- package/src/cad-worker.js +124 -10
- package/src/cad.js +12 -17
- package/src/constants.js +1 -1
- package/src/handlers/cad-handlers.js +1 -1
- package/src/sse-session-manager.js +135 -0
- package/src/sse-session.js +204 -0
- package/src/sse-utils.js +0 -32
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlisp/mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "MCP Server for @lisp on CAD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
41
41
|
"edge-js": "^25.0.1",
|
|
42
|
-
"express": "^5.2.1"
|
|
42
|
+
"express": "^5.2.1",
|
|
43
|
+
"iconv-lite": "^0.6.3",
|
|
44
|
+
"raw-body": "^3.0.0"
|
|
43
45
|
},
|
|
44
46
|
"engines": {
|
|
45
47
|
"node": ">=18.0.0"
|
package/src/atlisp-mcp.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import rawBody from 'raw-body';
|
|
3
|
+
import iconv from 'iconv-lite';
|
|
4
|
+
const { decode: charsetDecode, encodingExists } = iconv;
|
|
2
5
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
-
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
4
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
7
|
import {
|
|
6
8
|
ListToolsRequestSchema,
|
|
@@ -18,7 +20,8 @@ import * as packageHandlers from './handlers/package-handlers.js';
|
|
|
18
20
|
import * as functionHandlers from './handlers/function-handlers.js';
|
|
19
21
|
import { listResources, readResource } from './handlers/resource-handlers.js';
|
|
20
22
|
import * as subMgr from './subscription-manager.js';
|
|
21
|
-
import { createSseResponseHandler } from './sse-
|
|
23
|
+
import { SseSession, SSE_EVENTS, createSseResponseHandler, parseLastEventId } from './sse-session.js';
|
|
24
|
+
import { SseSessionManager } from './sse-session-manager.js';
|
|
22
25
|
|
|
23
26
|
const PORT = process.env.PORT || 8110;
|
|
24
27
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
@@ -26,6 +29,8 @@ const TRANSPORT = process.env.TRANSPORT || 'http';
|
|
|
26
29
|
const API_KEY = process.env.MCP_API_KEY || '';
|
|
27
30
|
if (API_KEY) log('API Key authentication enabled');
|
|
28
31
|
|
|
32
|
+
const sseSessionManager = new SseSessionManager();
|
|
33
|
+
|
|
29
34
|
export { CAD_PLATFORMS, FILE_EXTENSIONS, MOCK_PACKAGES } from './constants.js';
|
|
30
35
|
|
|
31
36
|
const TOOL_HANDLERS = {
|
|
@@ -239,6 +244,83 @@ export function createMcpServer() {
|
|
|
239
244
|
return server;
|
|
240
245
|
}
|
|
241
246
|
|
|
247
|
+
async function handleMcpRequest(session, method, params, id) {
|
|
248
|
+
try {
|
|
249
|
+
if (method === 'initialize') {
|
|
250
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: {
|
|
251
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
252
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
253
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
254
|
+
} });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (method === 'tools/list') {
|
|
259
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { tools } });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (method === 'resources/list') {
|
|
264
|
+
const resources = await listResources(subMgr.list());
|
|
265
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { resources } });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (method === 'resources/read') {
|
|
270
|
+
const uri = params?.uri;
|
|
271
|
+
if (!uri) {
|
|
272
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const data = await readResource(uri);
|
|
277
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } });
|
|
278
|
+
} catch (e) {
|
|
279
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (method === 'resources/subscribe') {
|
|
285
|
+
const uri = params?.uri;
|
|
286
|
+
if (!uri) {
|
|
287
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
subMgr.subscribe(uri);
|
|
291
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (method === 'resources/unsubscribe') {
|
|
296
|
+
const uri = params?.uri;
|
|
297
|
+
if (!uri) {
|
|
298
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
subMgr.unsubscribe(uri);
|
|
302
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (method === 'tools/call') {
|
|
307
|
+
const toolName = params?.name;
|
|
308
|
+
if (!toolName) {
|
|
309
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } });
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const result = await handleToolCall(toolName, params?.arguments || {});
|
|
313
|
+
session.sendResponse({ jsonrpc: '2.0', id, result });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
318
|
+
} catch (e) {
|
|
319
|
+
log('MCP error: ' + e.message);
|
|
320
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
242
324
|
export async function startServer() {
|
|
243
325
|
if (TRANSPORT === 'stdio') {
|
|
244
326
|
await initCadConnection();
|
|
@@ -248,7 +330,24 @@ export async function startServer() {
|
|
|
248
330
|
console.error(`${SERVER_NAME} 运行在 stdio 模式`);
|
|
249
331
|
} else {
|
|
250
332
|
const app = express();
|
|
251
|
-
|
|
333
|
+
|
|
334
|
+
const JSON_CONTENT_TYPES = ['application/json', 'application/json-rpc', 'application/vnd.api+json'];
|
|
335
|
+
app.use(async (req, res, next) => {
|
|
336
|
+
const ct = (req.headers['content-type'] || '').toLowerCase();
|
|
337
|
+
if (!JSON_CONTENT_TYPES.some(t => ct.startsWith(t))) return next();
|
|
338
|
+
try {
|
|
339
|
+
const buf = await rawBody(req, { limit: '10mb' });
|
|
340
|
+
const m = ct.match(/charset\s*=\s*([^\s;]+)/i);
|
|
341
|
+
let charset = m ? m[1].toLowerCase() : 'utf-8';
|
|
342
|
+
const charsetMap = { 'gb2312': 'gbk', 'gb18030': 'gbk', 'gbk': 'gbk', '936': 'gbk' };
|
|
343
|
+
charset = charsetMap[charset] || charset;
|
|
344
|
+
const str = encodingExists(charset) ? charsetDecode(buf, charset) : buf.toString('utf-8');
|
|
345
|
+
req.body = JSON.parse(str);
|
|
346
|
+
next();
|
|
347
|
+
} catch (e) {
|
|
348
|
+
res.status(400).json({ error: 'Invalid JSON body', message: e.message });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
252
351
|
|
|
253
352
|
app.use((req, res, next) => {
|
|
254
353
|
log(`${req.method} ${req.path} - body: ${JSON.stringify(req.body)}`);
|
|
@@ -281,101 +380,148 @@ export async function startServer() {
|
|
|
281
380
|
|
|
282
381
|
createMcpServer();
|
|
283
382
|
|
|
284
|
-
|
|
383
|
+
app.get('/sse', (req, res) => {
|
|
384
|
+
const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
|
|
385
|
+
|
|
386
|
+
const session = new SseSession(res, { clientId });
|
|
387
|
+
|
|
388
|
+
session.sendEndpoint('/mcp');
|
|
389
|
+
session.sendCapabilities({
|
|
390
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
391
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
392
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
sseSessionManager.add(clientId, session);
|
|
396
|
+
|
|
397
|
+
res.on('close', () => {
|
|
398
|
+
sseSessionManager.remove(clientId);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
285
401
|
|
|
286
|
-
['/
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
402
|
+
['/mcp/sse'].forEach(p => app.all(p, (req, res) => {
|
|
403
|
+
const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
|
|
404
|
+
|
|
405
|
+
const session = new SseSession(res, { clientId });
|
|
406
|
+
|
|
407
|
+
session.sendEndpoint('/mcp');
|
|
408
|
+
session.sendCapabilities({
|
|
409
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
410
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
411
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
sseSessionManager.add(clientId, session);
|
|
415
|
+
|
|
416
|
+
res.on('close', () => {
|
|
417
|
+
sseSessionManager.remove(clientId);
|
|
418
|
+
});
|
|
290
419
|
}));
|
|
291
420
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
421
|
+
app.post('/mcp', async (req, res) => {
|
|
422
|
+
const body = req.body || {};
|
|
423
|
+
const { method, params, id } = body;
|
|
424
|
+
const accept = req.get('Accept') || '';
|
|
425
|
+
const wantsSse = accept.includes('text/event-stream');
|
|
426
|
+
const sessionId = req.get('mcp-session-id') || '';
|
|
427
|
+
log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
|
|
428
|
+
|
|
429
|
+
if (wantsSse) {
|
|
430
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
431
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
432
|
+
res.setHeader('Connection', 'keep-alive');
|
|
433
|
+
|
|
434
|
+
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
435
|
+
const session = sseSessionManager.get(sessionId);
|
|
436
|
+
if (!id) {
|
|
437
|
+
res.status(204).end();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
await handleMcpRequest(session, method, params, id);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
|
|
446
|
+
const { send } = createSseResponseHandler(res, wantsSse);
|
|
447
|
+
|
|
448
|
+
if (!id) return res.status(204).end();
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
if (method === 'initialize') {
|
|
452
|
+
send({ jsonrpc: '2.0', id, result: {
|
|
453
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
454
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
455
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
456
|
+
} }, true);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (method === 'tools/list') {
|
|
461
|
+
send({ jsonrpc: '2.0', id, result: { tools } }, true);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (method === 'resources/list') {
|
|
466
|
+
const resources = await listResources(subMgr.list());
|
|
467
|
+
send({ jsonrpc: '2.0', id, result: { resources } }, true);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (method === 'resources/read') {
|
|
472
|
+
const uri = params?.uri;
|
|
473
|
+
if (!uri) {
|
|
474
|
+
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const data = await readResource(uri);
|
|
479
|
+
send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (method === 'resources/subscribe') {
|
|
487
|
+
const uri = params?.uri;
|
|
488
|
+
if (!uri) {
|
|
489
|
+
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
subMgr.subscribe(uri);
|
|
493
|
+
send({ jsonrpc: '2.0', id, result: { success: true } }, true);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (method === 'resources/unsubscribe') {
|
|
498
|
+
const uri = params?.uri;
|
|
499
|
+
if (!uri) {
|
|
500
|
+
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
subMgr.unsubscribe(uri);
|
|
504
|
+
send({ jsonrpc: '2.0', id, result: { success: true } }, true);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (method === 'tools/call') {
|
|
509
|
+
const toolName = params?.name;
|
|
510
|
+
if (!toolName) {
|
|
511
|
+
send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } }, true);
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const result = await handleToolCall(toolName, params?.arguments || {});
|
|
515
|
+
send({ jsonrpc: '2.0', id, result }, true);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }, true);
|
|
520
|
+
} catch (e) {
|
|
521
|
+
log('MCP error: ' + e.message);
|
|
522
|
+
send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
379
525
|
|
|
380
526
|
app.listen(PORT, HOST, async () => {
|
|
381
527
|
await initCadConnection();
|
package/src/cad-worker.js
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import edge from 'edge-js';
|
|
3
3
|
import process from 'process';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
4
6
|
|
|
5
7
|
const BUSY_RETRIES = parseInt(process.env.BUSY_RETRIES || '10', 10);
|
|
6
8
|
const BUSY_DELAY = parseInt(process.env.BUSY_DELAY || '500', 10);
|
|
7
9
|
|
|
10
|
+
const DEBUG_FILE = process.env.DEBUG_FILE || path.join(process.env.TEMP || '/tmp', 'cad-worker-debug.log');
|
|
11
|
+
|
|
12
|
+
function log(msg) {
|
|
13
|
+
const entry = `${new Date().toISOString()} ${msg}\n`;
|
|
14
|
+
fs.appendFileSync(DEBUG_FILE, entry);
|
|
15
|
+
}
|
|
16
|
+
|
|
8
17
|
const cadConnect = edge.func(function() {/*
|
|
9
18
|
async (input) => {
|
|
10
19
|
try {
|
|
@@ -47,6 +56,45 @@ const cadConnect = edge.func(function() {/*
|
|
|
47
56
|
}
|
|
48
57
|
*/});
|
|
49
58
|
|
|
59
|
+
const cadLaunch = edge.func(function() {/*
|
|
60
|
+
async (input) => {
|
|
61
|
+
string[] platforms = { "AutoCAD", "ZWCAD", "GStarCAD", "BricsCAD" };
|
|
62
|
+
string[] progIds = { "AutoCAD.Application", "ZWCAD.Application", "GStarCAD.Application", "BricsCAD.Application" };
|
|
63
|
+
|
|
64
|
+
for (int i = 0; i < platforms.Length; i++) {
|
|
65
|
+
try {
|
|
66
|
+
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progIds[i]);
|
|
67
|
+
if (cad != null) {
|
|
68
|
+
string ver = cad.Version.ToString();
|
|
69
|
+
return new { success = true, platform = platforms[i], version = ver, launched = false };
|
|
70
|
+
}
|
|
71
|
+
} catch {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (int i = 0; i < platforms.Length; i++) {
|
|
75
|
+
try {
|
|
76
|
+
var type = System.Type.GetTypeFromProgID(progIds[i]);
|
|
77
|
+
if (type != null) {
|
|
78
|
+
dynamic cad = System.Activator.CreateInstance(type);
|
|
79
|
+
if (cad != null) {
|
|
80
|
+
for (int retry = 0; retry < 60; retry++) {
|
|
81
|
+
await System.Threading.Tasks.Task.Delay(500);
|
|
82
|
+
try {
|
|
83
|
+
string ver = cad.Version.ToString();
|
|
84
|
+
return new { success = true, platform = platforms[i], version = ver, launched = true };
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch (Exception ex) {
|
|
90
|
+
System.Diagnostics.Debug.WriteLine("Create COM " + platforms[i] + " error: " + ex.Message);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return new { success = false, error = "No CAD found" };
|
|
95
|
+
}
|
|
96
|
+
*/});
|
|
97
|
+
|
|
50
98
|
const cadSend = edge.func(function() {/*
|
|
51
99
|
async (input) => {
|
|
52
100
|
try {
|
|
@@ -72,19 +120,33 @@ const cadSend = edge.func(function() {/*
|
|
|
72
120
|
const cadSendWithResult = edge.func(function() {/*
|
|
73
121
|
async (input) => {
|
|
74
122
|
string tempFile = null;
|
|
123
|
+
string lispFile = null;
|
|
75
124
|
try {
|
|
76
125
|
dynamic d = input;
|
|
77
126
|
string code = d.code.ToString();
|
|
78
127
|
string platform = d.platform.ToString();
|
|
128
|
+
string encoding = d.encoding != null ? d.encoding.ToString() : "";
|
|
79
129
|
string progId = platform + ".Application";
|
|
80
|
-
string tempDir = System.IO.Path.
|
|
130
|
+
string tempDir = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "@lisp");
|
|
131
|
+
System.IO.Directory.CreateDirectory(tempDir);
|
|
132
|
+
|
|
81
133
|
tempFile = System.IO.Path.Combine(tempDir, "atlisp_result_" + System.Guid.NewGuid().ToString("N") + ".txt");
|
|
82
|
-
|
|
134
|
+
lispFile = System.IO.Path.Combine(tempDir, "atlisp_code_" + System.Guid.NewGuid().ToString("N") + ".lsp");
|
|
135
|
+
|
|
136
|
+
// 生成 LISP 代码:执行 code,将结果打印到 tempFile
|
|
137
|
+
// 使用 princ 输出原始字符串
|
|
138
|
+
string lispCode = "(progn(setq f(open \"" + tempFile.Replace("\\", "\\\\") + "\" \"w\"))(princ " + code + " f)(close f)(princ))";
|
|
139
|
+
|
|
140
|
+
// 使用系统 ANSI 编码以兼容 GstarCAD / ZWCAD
|
|
141
|
+
System.IO.File.WriteAllText(lispFile, lispCode, System.Text.Encoding.GetEncoding(System.Globalization.CultureInfo.CurrentCulture.TextInfo.ANSICodePage));
|
|
142
|
+
|
|
83
143
|
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
|
|
84
144
|
if (cad != null) {
|
|
85
145
|
dynamic doc = cad.ActiveDocument;
|
|
86
146
|
if (doc != null) {
|
|
87
|
-
|
|
147
|
+
// 加载 .lsp 文件执行
|
|
148
|
+
doc.SendCommand("(load \"" + lispFile.Replace("\\", "\\\\") + "\")\n");
|
|
149
|
+
|
|
88
150
|
int retries = 10;
|
|
89
151
|
string result = "";
|
|
90
152
|
while (retries > 0 && !System.IO.File.Exists(tempFile)) {
|
|
@@ -96,8 +158,33 @@ const cadSendWithResult = edge.func(function() {/*
|
|
|
96
158
|
while (retries > 0) {
|
|
97
159
|
try {
|
|
98
160
|
byte[] bytes = System.IO.File.ReadAllBytes(tempFile);
|
|
99
|
-
|
|
161
|
+
System.Text.Encoding enc;
|
|
162
|
+
|
|
163
|
+
// 优先使用指定的编码
|
|
164
|
+
if (!string.IsNullOrEmpty(encoding)) {
|
|
165
|
+
enc = System.Text.Encoding.GetEncoding(encoding);
|
|
166
|
+
} else {
|
|
167
|
+
// 自动检测编码:先尝试 UTF-8,如果失败则使用 GBK
|
|
168
|
+
try {
|
|
169
|
+
string utf8Result = System.Text.Encoding.UTF8.GetString(bytes);
|
|
170
|
+
if (!utf8Result.Contains("\ufffd")) {
|
|
171
|
+
enc = System.Text.Encoding.UTF8;
|
|
172
|
+
} else {
|
|
173
|
+
enc = System.Text.Encoding.GetEncoding("gbk");
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
try {
|
|
177
|
+
enc = System.Text.Encoding.GetEncoding("gbk");
|
|
178
|
+
} catch {
|
|
179
|
+
enc = System.Text.Encoding.Default;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
result = enc.GetString(bytes).Trim();
|
|
185
|
+
|
|
100
186
|
try { System.IO.File.Delete(tempFile); } catch {}
|
|
187
|
+
try { System.IO.File.Delete(lispFile); } catch {}
|
|
101
188
|
break;
|
|
102
189
|
} catch (System.IO.IOException) {
|
|
103
190
|
System.Threading.Thread.Sleep(200);
|
|
@@ -117,11 +204,17 @@ const cadSendWithResult = edge.func(function() {/*
|
|
|
117
204
|
if (tempFile != null) {
|
|
118
205
|
try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
|
|
119
206
|
}
|
|
207
|
+
if (lispFile != null) {
|
|
208
|
+
try { if (System.IO.File.Exists(lispFile)) System.IO.File.Delete(lispFile); } catch {}
|
|
209
|
+
}
|
|
120
210
|
return new { success = false, error = ex.Message };
|
|
121
211
|
}
|
|
122
212
|
if (tempFile != null) {
|
|
123
213
|
try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
|
|
124
214
|
}
|
|
215
|
+
if (lispFile != null) {
|
|
216
|
+
try { if (System.IO.File.Exists(lispFile)) System.IO.File.Delete(lispFile); } catch {}
|
|
217
|
+
}
|
|
125
218
|
return new { success = false };
|
|
126
219
|
}
|
|
127
220
|
*/});
|
|
@@ -255,8 +348,17 @@ async function handleMessage(msg) {
|
|
|
255
348
|
return { pong: true };
|
|
256
349
|
}
|
|
257
350
|
if (msg.type === 'connect') {
|
|
351
|
+
try {
|
|
352
|
+
const result = await new Promise((resolve, reject) => {
|
|
353
|
+
cadConnect({}, (e, r) => {
|
|
354
|
+
if (e) reject(e);
|
|
355
|
+
else resolve(r);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
if (result && result.success) return result;
|
|
359
|
+
} catch (e) {}
|
|
258
360
|
return new Promise((resolve, reject) => {
|
|
259
|
-
|
|
361
|
+
cadLaunch({}, (e, r) => {
|
|
260
362
|
if (e) reject(e);
|
|
261
363
|
else resolve(r);
|
|
262
364
|
});
|
|
@@ -295,10 +397,16 @@ async function handleMessage(msg) {
|
|
|
295
397
|
if (!(await waitForIdle(msg.platform))) {
|
|
296
398
|
return { success: false, error: 'CAD is busy, timeout' };
|
|
297
399
|
}
|
|
400
|
+
log(`send: ${msg.code}`);
|
|
298
401
|
return new Promise((resolve, reject) => {
|
|
299
402
|
cadSend({ code: msg.code, platform: msg.platform }, (e, r) => {
|
|
300
|
-
if (e)
|
|
301
|
-
|
|
403
|
+
if (e) {
|
|
404
|
+
log(`send error: ${e.message}`);
|
|
405
|
+
reject(e);
|
|
406
|
+
} else {
|
|
407
|
+
log(`send result: ${JSON.stringify(r)}`);
|
|
408
|
+
resolve(r);
|
|
409
|
+
}
|
|
302
410
|
});
|
|
303
411
|
});
|
|
304
412
|
}
|
|
@@ -319,10 +427,16 @@ async function handleMessage(msg) {
|
|
|
319
427
|
if (!(await waitForIdle(msg.platform))) {
|
|
320
428
|
return { success: false, error: 'CAD is busy, timeout' };
|
|
321
429
|
}
|
|
430
|
+
log(`sendResult: ${msg.code}`);
|
|
322
431
|
return new Promise((resolve, reject) => {
|
|
323
|
-
cadSendWithResult({ code: msg.code, platform: msg.platform }, (e, r) => {
|
|
324
|
-
if (e)
|
|
325
|
-
|
|
432
|
+
cadSendWithResult({ code: msg.code, platform: msg.platform, encoding: msg.encoding || '' }, (e, r) => {
|
|
433
|
+
if (e) {
|
|
434
|
+
log(`sendResult error: ${e.message}`);
|
|
435
|
+
reject(e);
|
|
436
|
+
} else {
|
|
437
|
+
log(`sendResult result: ${JSON.stringify(r)}`);
|
|
438
|
+
resolve(r);
|
|
439
|
+
}
|
|
326
440
|
});
|
|
327
441
|
});
|
|
328
442
|
}
|
package/src/cad.js
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { CAD_PLATFORMS, FILE_EXTENSIONS } from './constants.js';
|
|
7
7
|
|
|
8
8
|
const MESSAGE_TIMEOUT = parseInt(process.env.MESSAGE_TIMEOUT || '60000', 10);
|
|
9
|
+
const RESULT_ENCODING = process.env.RESULT_ENCODING || '';
|
|
9
10
|
|
|
10
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const workerPath = path.join(__dirname, 'cad-worker.js');
|
|
@@ -37,9 +38,11 @@ export function getWorker() {
|
|
|
37
38
|
worker.connected = true;
|
|
38
39
|
|
|
39
40
|
worker.stderr.on('data', (d) => console.error('worker stderr:', d.toString()));
|
|
40
|
-
|
|
41
|
+
const w = worker;
|
|
42
|
+
w.on('exit', (code) => {
|
|
41
43
|
console.error('worker exited:', code);
|
|
42
|
-
worker
|
|
44
|
+
if (w !== worker) return;
|
|
45
|
+
w.connected = false;
|
|
43
46
|
if (heartbeatTimer) {
|
|
44
47
|
clearInterval(heartbeatTimer);
|
|
45
48
|
heartbeatTimer = null;
|
|
@@ -138,26 +141,18 @@ class CadConnection {
|
|
|
138
141
|
|
|
139
142
|
async sendCommand(code) {
|
|
140
143
|
if (!this.connected) throw new Error('未连接 CAD');
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
throw new Error(result.error || '发送失败');
|
|
145
|
-
} catch (e) {
|
|
146
|
-
throw e;
|
|
147
|
-
}
|
|
144
|
+
const result = await sendMessage({ type: 'send', code: code, platform: _platform });
|
|
145
|
+
if (result.success) return true;
|
|
146
|
+
throw new Error(result.error || '发送失败');
|
|
148
147
|
}
|
|
149
148
|
|
|
150
149
|
async sendCommandWithResult(code) {
|
|
151
150
|
if (!this.connected) throw new Error('未连接 CAD');
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return result.result || '';
|
|
156
|
-
}
|
|
157
|
-
throw new Error(result.error || '发送失败');
|
|
158
|
-
} catch (e) {
|
|
159
|
-
throw e;
|
|
151
|
+
const result = await sendMessage({ type: 'sendResult', code: code, platform: _platform, encoding: RESULT_ENCODING });
|
|
152
|
+
if (result.success) {
|
|
153
|
+
return result.result || '';
|
|
160
154
|
}
|
|
155
|
+
throw new Error(result.error || '发送失败');
|
|
161
156
|
}
|
|
162
157
|
|
|
163
158
|
getVersion() { return this.version; }
|
package/src/constants.js
CHANGED
|
@@ -7,7 +7,7 @@ export const FILE_EXTENSIONS = {
|
|
|
7
7
|
|
|
8
8
|
export const PROTOCOL_VERSION = '2024-11-05';
|
|
9
9
|
export const SERVER_NAME = 'atlisp-mcp-server';
|
|
10
|
-
export const SERVER_VERSION = '1.0.
|
|
10
|
+
export const SERVER_VERSION = '1.0.16';
|
|
11
11
|
|
|
12
12
|
export const MOCK_PACKAGES = [
|
|
13
13
|
{ name: 'base', description: '基础函数库', version: '1.0.0' },
|
|
@@ -13,7 +13,7 @@ export async function connectCad() {
|
|
|
13
13
|
} catch (e) {
|
|
14
14
|
log('connect_cad error: ' + e.message);
|
|
15
15
|
}
|
|
16
|
-
return { content: [{ type: 'text', text: '
|
|
16
|
+
return { content: [{ type: 'text', text: '未能连接或启动 CAD,请确保 CAD 已安装' }], isError: true };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export async function evalLisp(code, withResult = false) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { SseSession, SSE_EVENTS } from './sse-session.js';
|
|
2
|
+
|
|
3
|
+
export class SseSessionManager {
|
|
4
|
+
#sessions = new Map();
|
|
5
|
+
#eventHandlers = new Map();
|
|
6
|
+
|
|
7
|
+
constructor() {
|
|
8
|
+
this.#sessions = new Map();
|
|
9
|
+
this.#eventHandlers = new Map();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
add(clientId, session) {
|
|
13
|
+
if (!(session instanceof SseSession)) {
|
|
14
|
+
throw new Error('session must be an SseSession instance');
|
|
15
|
+
}
|
|
16
|
+
this.#sessions.set(clientId, session);
|
|
17
|
+
session.startHeartbeat();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
remove(clientId) {
|
|
21
|
+
const session = this.#sessions.get(clientId);
|
|
22
|
+
if (session) {
|
|
23
|
+
session.close();
|
|
24
|
+
this.#sessions.delete(clientId);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get(clientId) {
|
|
31
|
+
return this.#sessions.get(clientId) || null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
has(clientId) {
|
|
35
|
+
return this.#sessions.has(clientId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
list() {
|
|
39
|
+
return Array.from(this.#sessions.keys()).map(id => ({
|
|
40
|
+
clientId: id,
|
|
41
|
+
isActive: this.#sessions.get(id)?.isActive ?? false
|
|
42
|
+
}));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
listActive() {
|
|
46
|
+
return this.list().filter(s => s.isActive);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
count() {
|
|
50
|
+
return this.#sessions.size;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
sendToClient(clientId, event, data, id = null) {
|
|
54
|
+
const session = this.#sessions.get(clientId);
|
|
55
|
+
if (!session || !session.isActive) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return session.sendEvent(event, data, id);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
sendMessageToClient(clientId, data) {
|
|
62
|
+
return this.sendToClient(clientId, SSE_EVENTS.MESSAGE, data);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
sendErrorToClient(clientId, code, message, id = null) {
|
|
66
|
+
return this.sendToClient(clientId, SSE_EVENTS.ERROR, { code, message }, id);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
sendProgressToClient(clientId, current, total, message = '') {
|
|
70
|
+
return this.sendToClient(clientId, SSE_EVENTS.PROGRESS, { current, total, message });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
broadcast(event, data) {
|
|
74
|
+
let successCount = 0;
|
|
75
|
+
for (const [clientId, session] of this.#sessions) {
|
|
76
|
+
if (session.isActive && session.sendEvent(event, data)) {
|
|
77
|
+
successCount++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return successCount;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
broadcastMessage(data) {
|
|
84
|
+
return this.broadcast(SSE_EVENTS.MESSAGE, data);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
broadcastError(code, message) {
|
|
88
|
+
return this.broadcast(SSE_EVENTS.ERROR, { code, message });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
on(event, handler) {
|
|
92
|
+
if (!this.#eventHandlers.has(event)) {
|
|
93
|
+
this.#eventHandlers.set(event, new Set());
|
|
94
|
+
}
|
|
95
|
+
this.#eventHandlers.get(event).add(handler);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
off(event, handler) {
|
|
99
|
+
const handlers = this.#eventHandlers.get(event);
|
|
100
|
+
if (handlers) {
|
|
101
|
+
handlers.delete(handler);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
emit(event, data) {
|
|
106
|
+
const handlers = this.#eventHandlers.get(event);
|
|
107
|
+
if (handlers) {
|
|
108
|
+
for (const handler of handlers) {
|
|
109
|
+
try {
|
|
110
|
+
handler(data);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Ignore handler errors
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
cleanupInactive() {
|
|
119
|
+
for (const [clientId, session] of this.#sessions) {
|
|
120
|
+
if (!session.isActive) {
|
|
121
|
+
session.close();
|
|
122
|
+
this.#sessions.delete(clientId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
closeAll() {
|
|
128
|
+
for (const [clientId, session] of this.#sessions) {
|
|
129
|
+
session.close();
|
|
130
|
+
}
|
|
131
|
+
this.#sessions.clear();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export const globalSessionManager = new SseSessionManager();
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_HEARTBEAT_INTERVAL = 30000;
|
|
4
|
+
const RECONNECT_DELAY = 5000;
|
|
5
|
+
|
|
6
|
+
export const SSE_EVENTS = {
|
|
7
|
+
MESSAGE: 'message',
|
|
8
|
+
ENDPOINT: 'endpoint',
|
|
9
|
+
PING: 'ping',
|
|
10
|
+
ERROR: 'error',
|
|
11
|
+
PROGRESS: 'progress',
|
|
12
|
+
CAPABILITIES: 'capabilities',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class SseSession {
|
|
16
|
+
#res;
|
|
17
|
+
#clientId;
|
|
18
|
+
#active = true;
|
|
19
|
+
#heartbeatTimer = null;
|
|
20
|
+
#lastEventId = 0;
|
|
21
|
+
#heartbeatInterval;
|
|
22
|
+
#sessionData = {};
|
|
23
|
+
|
|
24
|
+
constructor(res, options = {}) {
|
|
25
|
+
this.#res = res;
|
|
26
|
+
this.#clientId = options.clientId || randomUUID();
|
|
27
|
+
this.#heartbeatInterval = options.heartbeatInterval || DEFAULT_HEARTBEAT_INTERVAL;
|
|
28
|
+
this.#sessionData = options.sessionData || {};
|
|
29
|
+
|
|
30
|
+
this._setupSseHeaders();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get clientId() {
|
|
34
|
+
return this.#clientId;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get isActive() {
|
|
38
|
+
return this.#active;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get sessionData() {
|
|
42
|
+
return this.#sessionData;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
setSessionData(key, value) {
|
|
46
|
+
this.#sessionData[key] = value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getSessionData(key) {
|
|
50
|
+
return this.#sessionData[key];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_setupSseHeaders() {
|
|
54
|
+
this.#res.setHeader('Content-Type', 'text/event-stream');
|
|
55
|
+
this.#res.setHeader('Cache-Control', 'no-cache');
|
|
56
|
+
this.#res.setHeader('Connection', 'keep-alive');
|
|
57
|
+
this.#res.setHeader('Transfer-Encoding', 'chunked');
|
|
58
|
+
this.#res.setHeader('X-Accel-Buffering', 'no');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_formatEvent(event, data, id = null) {
|
|
62
|
+
let lines = [];
|
|
63
|
+
if (id !== null) {
|
|
64
|
+
lines.push(`id: ${id}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push(`event: ${event}`);
|
|
67
|
+
|
|
68
|
+
const jsonStr = JSON.stringify(data);
|
|
69
|
+
lines.push(`data: ${jsonStr}`);
|
|
70
|
+
|
|
71
|
+
return lines.join('\n') + '\n\n';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
_write(content) {
|
|
75
|
+
if (!this.#active) return false;
|
|
76
|
+
try {
|
|
77
|
+
const result = this.#res.write(content);
|
|
78
|
+
if (typeof this.#res.flush === 'function') {
|
|
79
|
+
this.#res.flush();
|
|
80
|
+
}
|
|
81
|
+
if (result === false) {
|
|
82
|
+
this.#active = false;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
} catch (e) {
|
|
87
|
+
this.#active = false;
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
sendEvent(event, data, id = null) {
|
|
93
|
+
this.#lastEventId++;
|
|
94
|
+
const eventId = id !== null ? id : this.#lastEventId;
|
|
95
|
+
return this._write(this._formatEvent(event, data, eventId));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
sendMessage(data) {
|
|
99
|
+
return this.sendEvent(SSE_EVENTS.MESSAGE, data);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sendEndpoint(path) {
|
|
103
|
+
return this.sendEvent(SSE_EVENTS.ENDPOINT, { path });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
sendError(code, message, id = null) {
|
|
107
|
+
return this.sendEvent(SSE_EVENTS.ERROR, { code, message }, id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sendPing() {
|
|
111
|
+
return this.sendEvent(SSE_EVENTS.PING, { timestamp: Date.now() });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
sendProgress(current, total, message = '') {
|
|
115
|
+
return this.sendEvent(SSE_EVENTS.PROGRESS, { current, total, message });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
sendCapabilities(capabilities) {
|
|
119
|
+
return this.sendEvent(SSE_EVENTS.CAPABILITIES, capabilities);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
sendResponse(jsonrpcResponse, id = null) {
|
|
123
|
+
const eventId = id !== null ? id : this.#lastEventId;
|
|
124
|
+
return this.sendEvent(SSE_EVENTS.MESSAGE, jsonrpcResponse, eventId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
startHeartbeat(interval = null) {
|
|
128
|
+
this.stopHeartbeat();
|
|
129
|
+
const ms = interval || this.#heartbeatInterval;
|
|
130
|
+
|
|
131
|
+
this.#heartbeatTimer = setInterval(() => {
|
|
132
|
+
if (!this.#active) {
|
|
133
|
+
this.stopHeartbeat();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!this.sendPing()) {
|
|
137
|
+
this.stopHeartbeat();
|
|
138
|
+
this.close();
|
|
139
|
+
}
|
|
140
|
+
}, ms);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
stopHeartbeat() {
|
|
144
|
+
if (this.#heartbeatTimer) {
|
|
145
|
+
clearInterval(this.#heartbeatTimer);
|
|
146
|
+
this.#heartbeatTimer = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
close() {
|
|
151
|
+
this.#active = false;
|
|
152
|
+
this.stopHeartbeat();
|
|
153
|
+
try {
|
|
154
|
+
this.#res.end();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// Ignore errors during close
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function setupSseHeaders(res) {
|
|
162
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
163
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
164
|
+
res.setHeader('Connection', 'keep-alive');
|
|
165
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function sendSSE(res, data, event = SSE_EVENTS.MESSAGE) {
|
|
169
|
+
const lines = [
|
|
170
|
+
`event: ${event}`,
|
|
171
|
+
`data: ${JSON.stringify(data)}`,
|
|
172
|
+
''
|
|
173
|
+
];
|
|
174
|
+
res.write(lines.join('\n') + '\n');
|
|
175
|
+
if (typeof res.flush === 'function') res.flush();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function createSseResponseHandler(res, acceptSse = false) {
|
|
179
|
+
if (acceptSse) {
|
|
180
|
+
setupSseHeaders(res);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
send: (data, done = false) => {
|
|
185
|
+
const json = JSON.stringify(data);
|
|
186
|
+
if (acceptSse) {
|
|
187
|
+
sendSSE(res, data);
|
|
188
|
+
if (done) res.end();
|
|
189
|
+
} else {
|
|
190
|
+
res.setHeader('Content-Type', 'application/json');
|
|
191
|
+
res.end(json);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function parseLastEventId(req) {
|
|
198
|
+
const lastEventId = req.get('Last-Event-ID') || req.get('last-event-id');
|
|
199
|
+
if (lastEventId) {
|
|
200
|
+
const parsed = parseInt(lastEventId, 10);
|
|
201
|
+
return isNaN(parsed) ? null : parsed;
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
package/src/sse-utils.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export function setupSseHeaders(res) {
|
|
2
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
3
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
4
|
-
res.setHeader('Connection', 'keep-alive');
|
|
5
|
-
res.setHeader('Transfer-Encoding', 'chunked');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function sendSSE(res, data, event = 'message') {
|
|
9
|
-
const json = JSON.stringify(data);
|
|
10
|
-
res.write(`event: ${event}\n`);
|
|
11
|
-
res.write(`data: ${json}\n\n`);
|
|
12
|
-
if (typeof res.flush === 'function') res.flush();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function createSseResponseHandler(res, acceptSse = false) {
|
|
16
|
-
if (acceptSse) {
|
|
17
|
-
setupSseHeaders(res);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
send: (data, done = false) => {
|
|
22
|
-
const json = JSON.stringify(data);
|
|
23
|
-
if (acceptSse) {
|
|
24
|
-
sendSSE(res, data);
|
|
25
|
-
if (done) res.end();
|
|
26
|
-
} else {
|
|
27
|
-
res.setHeader('Content-Type', 'application/json');
|
|
28
|
-
res.end(json);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
}
|