@atlisp/mcp 1.0.15 → 1.0.17
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 +247 -95
- package/src/cad-worker.js +101 -9
- package/src/cad.js +22 -17
- package/src/constants.js +1 -1
- package/src/handlers/cad-handlers.js +10 -0
- 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.17",
|
|
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 = {
|
|
@@ -45,6 +50,7 @@ const TOOL_HANDLERS = {
|
|
|
45
50
|
get_install_code: () => ({ content: [{ type: 'text', text: `复制以下代码到 CAD 命令行安装 @lisp:\n\n(progn(vl-load-com)(setq s strcat h "http" o(vlax-create-object (s"win"h".win"h"request.5.1"))v vlax-invoke e eval r read)(v o'open "get" (s h"://atlisp.""cn/@"):vlax-true)(v o'send)(v o'WaitforResponse 1000)(e(r(vlax-get-property o'ResponseText))))` }] }),
|
|
46
51
|
at_command: (a) => cadHandlers.atCommand(a.command),
|
|
47
52
|
new_document: () => cadHandlers.newDocument(),
|
|
53
|
+
bring_to_front: () => cadHandlers.bringToFront(),
|
|
48
54
|
install_atlisp: () => cadHandlers.installAtlisp(),
|
|
49
55
|
get_function_usage: (a) => functionHandlers.getFunctionUsage(a.name, a.package),
|
|
50
56
|
list_functions: (a) => functionHandlers.listFunctions(a.package),
|
|
@@ -135,6 +141,11 @@ export const tools = [
|
|
|
135
141
|
description: '在 CAD 中新建空白文档',
|
|
136
142
|
inputSchema: { type: 'object', properties: {} }
|
|
137
143
|
},
|
|
144
|
+
{
|
|
145
|
+
name: 'bring_to_front',
|
|
146
|
+
description: '将 CAD 窗口切换到前台(从后台运行状态唤醒)',
|
|
147
|
+
inputSchema: { type: 'object', properties: {} }
|
|
148
|
+
},
|
|
138
149
|
{
|
|
139
150
|
name: 'install_atlisp',
|
|
140
151
|
description: '在 CAD 中安装 @lisp',
|
|
@@ -239,6 +250,83 @@ export function createMcpServer() {
|
|
|
239
250
|
return server;
|
|
240
251
|
}
|
|
241
252
|
|
|
253
|
+
async function handleMcpRequest(session, method, params, id) {
|
|
254
|
+
try {
|
|
255
|
+
if (method === 'initialize') {
|
|
256
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: {
|
|
257
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
258
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
259
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
260
|
+
} });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (method === 'tools/list') {
|
|
265
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { tools } });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (method === 'resources/list') {
|
|
270
|
+
const resources = await listResources(subMgr.list());
|
|
271
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { resources } });
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (method === 'resources/read') {
|
|
276
|
+
const uri = params?.uri;
|
|
277
|
+
if (!uri) {
|
|
278
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const data = await readResource(uri);
|
|
283
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } });
|
|
284
|
+
} catch (e) {
|
|
285
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (method === 'resources/subscribe') {
|
|
291
|
+
const uri = params?.uri;
|
|
292
|
+
if (!uri) {
|
|
293
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
subMgr.subscribe(uri);
|
|
297
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (method === 'resources/unsubscribe') {
|
|
302
|
+
const uri = params?.uri;
|
|
303
|
+
if (!uri) {
|
|
304
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
subMgr.unsubscribe(uri);
|
|
308
|
+
session.sendResponse({ jsonrpc: '2.0', id, result: { success: true } });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (method === 'tools/call') {
|
|
313
|
+
const toolName = params?.name;
|
|
314
|
+
if (!toolName) {
|
|
315
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } });
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const result = await handleToolCall(toolName, params?.arguments || {});
|
|
319
|
+
session.sendResponse({ jsonrpc: '2.0', id, result });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } });
|
|
324
|
+
} catch (e) {
|
|
325
|
+
log('MCP error: ' + e.message);
|
|
326
|
+
session.sendResponse({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
242
330
|
export async function startServer() {
|
|
243
331
|
if (TRANSPORT === 'stdio') {
|
|
244
332
|
await initCadConnection();
|
|
@@ -248,7 +336,24 @@ export async function startServer() {
|
|
|
248
336
|
console.error(`${SERVER_NAME} 运行在 stdio 模式`);
|
|
249
337
|
} else {
|
|
250
338
|
const app = express();
|
|
251
|
-
|
|
339
|
+
|
|
340
|
+
const JSON_CONTENT_TYPES = ['application/json', 'application/json-rpc', 'application/vnd.api+json'];
|
|
341
|
+
app.use(async (req, res, next) => {
|
|
342
|
+
const ct = (req.headers['content-type'] || '').toLowerCase();
|
|
343
|
+
if (!JSON_CONTENT_TYPES.some(t => ct.startsWith(t))) return next();
|
|
344
|
+
try {
|
|
345
|
+
const buf = await rawBody(req, { limit: '10mb' });
|
|
346
|
+
const m = ct.match(/charset\s*=\s*([^\s;]+)/i);
|
|
347
|
+
let charset = m ? m[1].toLowerCase() : 'utf-8';
|
|
348
|
+
const charsetMap = { 'gb2312': 'gbk', 'gb18030': 'gbk', 'gbk': 'gbk', '936': 'gbk' };
|
|
349
|
+
charset = charsetMap[charset] || charset;
|
|
350
|
+
const str = encodingExists(charset) ? charsetDecode(buf, charset) : buf.toString('utf-8');
|
|
351
|
+
req.body = JSON.parse(str);
|
|
352
|
+
next();
|
|
353
|
+
} catch (e) {
|
|
354
|
+
res.status(400).json({ error: 'Invalid JSON body', message: e.message });
|
|
355
|
+
}
|
|
356
|
+
});
|
|
252
357
|
|
|
253
358
|
app.use((req, res, next) => {
|
|
254
359
|
log(`${req.method} ${req.path} - body: ${JSON.stringify(req.body)}`);
|
|
@@ -281,101 +386,148 @@ export async function startServer() {
|
|
|
281
386
|
|
|
282
387
|
createMcpServer();
|
|
283
388
|
|
|
284
|
-
|
|
389
|
+
app.get('/sse', (req, res) => {
|
|
390
|
+
const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
|
|
391
|
+
|
|
392
|
+
const session = new SseSession(res, { clientId });
|
|
393
|
+
|
|
394
|
+
session.sendEndpoint('/mcp');
|
|
395
|
+
session.sendCapabilities({
|
|
396
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
397
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
398
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
sseSessionManager.add(clientId, session);
|
|
402
|
+
|
|
403
|
+
res.on('close', () => {
|
|
404
|
+
sseSessionManager.remove(clientId);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
285
407
|
|
|
286
|
-
['/
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
408
|
+
['/mcp/sse'].forEach(p => app.all(p, (req, res) => {
|
|
409
|
+
const clientId = req.get('mcp-session-id') || req.get('Last-Event-ID') || crypto.randomUUID();
|
|
410
|
+
|
|
411
|
+
const session = new SseSession(res, { clientId });
|
|
412
|
+
|
|
413
|
+
session.sendEndpoint('/mcp');
|
|
414
|
+
session.sendCapabilities({
|
|
415
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
416
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
417
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
sseSessionManager.add(clientId, session);
|
|
421
|
+
|
|
422
|
+
res.on('close', () => {
|
|
423
|
+
sseSessionManager.remove(clientId);
|
|
424
|
+
});
|
|
290
425
|
}));
|
|
291
426
|
|
|
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
|
-
|
|
427
|
+
app.post('/mcp', async (req, res) => {
|
|
428
|
+
const body = req.body || {};
|
|
429
|
+
const { method, params, id } = body;
|
|
430
|
+
const accept = req.get('Accept') || '';
|
|
431
|
+
const wantsSse = accept.includes('text/event-stream');
|
|
432
|
+
const sessionId = req.get('mcp-session-id') || '';
|
|
433
|
+
log(`${req.method} ${req.path} - session: ${sessionId} - method: ${method} - wantsSSE: ${wantsSse}`);
|
|
434
|
+
|
|
435
|
+
if (wantsSse) {
|
|
436
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
437
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
438
|
+
res.setHeader('Connection', 'keep-alive');
|
|
439
|
+
|
|
440
|
+
if (sessionId && sseSessionManager.has(sessionId)) {
|
|
441
|
+
const session = sseSessionManager.get(sessionId);
|
|
442
|
+
if (!id) {
|
|
443
|
+
res.status(204).end();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
await handleMcpRequest(session, method, params, id);
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
res.setHeader('mcp-session-id', sessionId || crypto.randomUUID());
|
|
452
|
+
const { send } = createSseResponseHandler(res, wantsSse);
|
|
453
|
+
|
|
454
|
+
if (!id) return res.status(204).end();
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
if (method === 'initialize') {
|
|
458
|
+
send({ jsonrpc: '2.0', id, result: {
|
|
459
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
460
|
+
capabilities: { tools: {}, resources: { subscribe: true, listChanged: true } },
|
|
461
|
+
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION }
|
|
462
|
+
} }, true);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (method === 'tools/list') {
|
|
467
|
+
send({ jsonrpc: '2.0', id, result: { tools } }, true);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (method === 'resources/list') {
|
|
472
|
+
const resources = await listResources(subMgr.list());
|
|
473
|
+
send({ jsonrpc: '2.0', id, result: { resources } }, true);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (method === 'resources/read') {
|
|
478
|
+
const uri = params?.uri;
|
|
479
|
+
if (!uri) {
|
|
480
|
+
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
const data = await readResource(uri);
|
|
485
|
+
send({ jsonrpc: '2.0', id, result: { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(data) }] } }, true);
|
|
486
|
+
} catch (e) {
|
|
487
|
+
send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (method === 'resources/subscribe') {
|
|
493
|
+
const uri = params?.uri;
|
|
494
|
+
if (!uri) {
|
|
495
|
+
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
subMgr.subscribe(uri);
|
|
499
|
+
send({ jsonrpc: '2.0', id, result: { success: true } }, true);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (method === 'resources/unsubscribe') {
|
|
504
|
+
const uri = params?.uri;
|
|
505
|
+
if (!uri) {
|
|
506
|
+
send({ jsonrpc: '2.0', id, error: { code: -32600, message: '缺少 uri 参数' } }, true);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
subMgr.unsubscribe(uri);
|
|
510
|
+
send({ jsonrpc: '2.0', id, result: { success: true } }, true);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (method === 'tools/call') {
|
|
515
|
+
const toolName = params?.name;
|
|
516
|
+
if (!toolName) {
|
|
517
|
+
send({ jsonrpc: '2.0', id, error: { code: -32602, message: 'Invalid params: missing tool name' } }, true);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const result = await handleToolCall(toolName, params?.arguments || {});
|
|
521
|
+
send({ jsonrpc: '2.0', id, result }, true);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
send({ jsonrpc: '2.0', id, error: { code: -32601, message: 'Method not found' } }, true);
|
|
526
|
+
} catch (e) {
|
|
527
|
+
log('MCP error: ' + e.message);
|
|
528
|
+
send({ jsonrpc: '2.0', id, error: { code: -32603, message: e.message } }, true);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
379
531
|
|
|
380
532
|
app.listen(PORT, HOST, async () => {
|
|
381
533
|
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 {
|
|
@@ -111,19 +120,33 @@ const cadSend = edge.func(function() {/*
|
|
|
111
120
|
const cadSendWithResult = edge.func(function() {/*
|
|
112
121
|
async (input) => {
|
|
113
122
|
string tempFile = null;
|
|
123
|
+
string lispFile = null;
|
|
114
124
|
try {
|
|
115
125
|
dynamic d = input;
|
|
116
126
|
string code = d.code.ToString();
|
|
117
127
|
string platform = d.platform.ToString();
|
|
128
|
+
string encoding = d.encoding != null ? d.encoding.ToString() : "";
|
|
118
129
|
string progId = platform + ".Application";
|
|
119
|
-
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
|
+
|
|
120
133
|
tempFile = System.IO.Path.Combine(tempDir, "atlisp_result_" + System.Guid.NewGuid().ToString("N") + ".txt");
|
|
121
|
-
|
|
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
|
+
|
|
122
143
|
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
|
|
123
144
|
if (cad != null) {
|
|
124
145
|
dynamic doc = cad.ActiveDocument;
|
|
125
146
|
if (doc != null) {
|
|
126
|
-
|
|
147
|
+
// 加载 .lsp 文件执行
|
|
148
|
+
doc.SendCommand("(load \"" + lispFile.Replace("\\", "\\\\") + "\")\n");
|
|
149
|
+
|
|
127
150
|
int retries = 10;
|
|
128
151
|
string result = "";
|
|
129
152
|
while (retries > 0 && !System.IO.File.Exists(tempFile)) {
|
|
@@ -135,8 +158,33 @@ const cadSendWithResult = edge.func(function() {/*
|
|
|
135
158
|
while (retries > 0) {
|
|
136
159
|
try {
|
|
137
160
|
byte[] bytes = System.IO.File.ReadAllBytes(tempFile);
|
|
138
|
-
|
|
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
|
+
|
|
139
186
|
try { System.IO.File.Delete(tempFile); } catch {}
|
|
187
|
+
try { System.IO.File.Delete(lispFile); } catch {}
|
|
140
188
|
break;
|
|
141
189
|
} catch (System.IO.IOException) {
|
|
142
190
|
System.Threading.Thread.Sleep(200);
|
|
@@ -156,11 +204,17 @@ const cadSendWithResult = edge.func(function() {/*
|
|
|
156
204
|
if (tempFile != null) {
|
|
157
205
|
try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
|
|
158
206
|
}
|
|
207
|
+
if (lispFile != null) {
|
|
208
|
+
try { if (System.IO.File.Exists(lispFile)) System.IO.File.Delete(lispFile); } catch {}
|
|
209
|
+
}
|
|
159
210
|
return new { success = false, error = ex.Message };
|
|
160
211
|
}
|
|
161
212
|
if (tempFile != null) {
|
|
162
213
|
try { if (System.IO.File.Exists(tempFile)) System.IO.File.Delete(tempFile); } catch {}
|
|
163
214
|
}
|
|
215
|
+
if (lispFile != null) {
|
|
216
|
+
try { if (System.IO.File.Exists(lispFile)) System.IO.File.Delete(lispFile); } catch {}
|
|
217
|
+
}
|
|
164
218
|
return new { success = false };
|
|
165
219
|
}
|
|
166
220
|
*/});
|
|
@@ -241,6 +295,24 @@ const cadNewDoc = edge.func(function() {/*
|
|
|
241
295
|
}
|
|
242
296
|
*/});
|
|
243
297
|
|
|
298
|
+
const cadBringToFront = edge.func(function() {/*
|
|
299
|
+
async (input) => {
|
|
300
|
+
try {
|
|
301
|
+
string platform = input.ToString();
|
|
302
|
+
string progId = platform + ".Application";
|
|
303
|
+
dynamic cad = System.Runtime.InteropServices.Marshal.GetActiveObject(progId);
|
|
304
|
+
if (cad != null) {
|
|
305
|
+
cad.Visible = true;
|
|
306
|
+
return new { success = true };
|
|
307
|
+
}
|
|
308
|
+
} catch (Exception ex) {
|
|
309
|
+
System.Diagnostics.Debug.WriteLine("cadBringToFront error: " + ex.Message);
|
|
310
|
+
return new { success = false, error = ex.Message };
|
|
311
|
+
}
|
|
312
|
+
return new { success = false, error = "CAD not found" };
|
|
313
|
+
}
|
|
314
|
+
*/});
|
|
315
|
+
|
|
244
316
|
async function waitForIdle(platform) {
|
|
245
317
|
let retries = BUSY_RETRIES;
|
|
246
318
|
while (retries > 0) {
|
|
@@ -326,6 +398,14 @@ async function handleMessage(msg) {
|
|
|
326
398
|
});
|
|
327
399
|
});
|
|
328
400
|
}
|
|
401
|
+
if (msg.type === 'bringToFront') {
|
|
402
|
+
return new Promise((resolve, reject) => {
|
|
403
|
+
cadBringToFront(msg.platform || 'AutoCAD', (e, r) => {
|
|
404
|
+
if (e) reject(e);
|
|
405
|
+
else resolve(r);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
329
409
|
if (msg.type === 'send') {
|
|
330
410
|
const docCheck = await new Promise((resolve, reject) => {
|
|
331
411
|
cadHasDoc(msg.platform || 'AutoCAD', (e, r) => {
|
|
@@ -343,10 +423,16 @@ async function handleMessage(msg) {
|
|
|
343
423
|
if (!(await waitForIdle(msg.platform))) {
|
|
344
424
|
return { success: false, error: 'CAD is busy, timeout' };
|
|
345
425
|
}
|
|
426
|
+
log(`send: ${msg.code}`);
|
|
346
427
|
return new Promise((resolve, reject) => {
|
|
347
428
|
cadSend({ code: msg.code, platform: msg.platform }, (e, r) => {
|
|
348
|
-
if (e)
|
|
349
|
-
|
|
429
|
+
if (e) {
|
|
430
|
+
log(`send error: ${e.message}`);
|
|
431
|
+
reject(e);
|
|
432
|
+
} else {
|
|
433
|
+
log(`send result: ${JSON.stringify(r)}`);
|
|
434
|
+
resolve(r);
|
|
435
|
+
}
|
|
350
436
|
});
|
|
351
437
|
});
|
|
352
438
|
}
|
|
@@ -367,10 +453,16 @@ async function handleMessage(msg) {
|
|
|
367
453
|
if (!(await waitForIdle(msg.platform))) {
|
|
368
454
|
return { success: false, error: 'CAD is busy, timeout' };
|
|
369
455
|
}
|
|
456
|
+
log(`sendResult: ${msg.code}`);
|
|
370
457
|
return new Promise((resolve, reject) => {
|
|
371
|
-
cadSendWithResult({ code: msg.code, platform: msg.platform }, (e, r) => {
|
|
372
|
-
if (e)
|
|
373
|
-
|
|
458
|
+
cadSendWithResult({ code: msg.code, platform: msg.platform, encoding: msg.encoding || '' }, (e, r) => {
|
|
459
|
+
if (e) {
|
|
460
|
+
log(`sendResult error: ${e.message}`);
|
|
461
|
+
reject(e);
|
|
462
|
+
} else {
|
|
463
|
+
log(`sendResult result: ${JSON.stringify(r)}`);
|
|
464
|
+
resolve(r);
|
|
465
|
+
}
|
|
374
466
|
});
|
|
375
467
|
});
|
|
376
468
|
}
|
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; }
|
|
@@ -192,6 +187,16 @@ class CadConnection {
|
|
|
192
187
|
return false;
|
|
193
188
|
}
|
|
194
189
|
}
|
|
190
|
+
|
|
191
|
+
async bringToFront() {
|
|
192
|
+
if (!this.connected) throw new Error('未连接 CAD');
|
|
193
|
+
try {
|
|
194
|
+
const result = await sendMessage({ type: 'bringToFront', platform: _platform });
|
|
195
|
+
return result && result.success;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
195
200
|
|
|
196
201
|
async getInfo() {
|
|
197
202
|
if (!this.connected) return 'CAD 未连接';
|
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.17';
|
|
11
11
|
|
|
12
12
|
export const MOCK_PACKAGES = [
|
|
13
13
|
{ name: 'base', description: '基础函数库', version: '1.0.0' },
|
|
@@ -60,6 +60,16 @@ export async function newDocument() {
|
|
|
60
60
|
return { content: [{ type: 'text', text: '新建文档失败' }], isError: true };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
export async function bringToFront() {
|
|
64
|
+
if (!cad.connected) { await cad.connect(); }
|
|
65
|
+
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
66
|
+
const result = await cad.bringToFront();
|
|
67
|
+
if (result) {
|
|
68
|
+
return { content: [{ type: 'text', text: '已将 CAD 窗口切换到前台' }] };
|
|
69
|
+
}
|
|
70
|
+
return { content: [{ type: 'text', text: '切换前台失败' }], isError: true };
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
export async function installAtlisp() {
|
|
64
74
|
if (!cad.connected) { await cad.connect(); }
|
|
65
75
|
if (!cad.connected) return { content: [{ type: 'text', text: '未连接 CAD' }], isError: true };
|
|
@@ -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
|
-
}
|