zwcad-mcp 0.2.1 → 0.2.3
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 +5 -1
- package/lib/proxy.js +147 -4
- package/package.json +1 -1
- package/tools.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
中望CAD(ZWCAD)MCP 服务,让大模型通过 [MCP 协议](https://modelcontextprotocol.io)直接操控中望CAD 平台与中望机械CAD,完成绘图、标注、图层/块/样式管理,以及图框、标题栏、明细表(BOM)等机械操作。
|
|
4
4
|
|
|
5
|
-
**共计
|
|
5
|
+
**共计 40 个工具**:平台 26 个(不依赖机械环境)+ 机械 11 个(需中望机械CAD)+ 诊断 2 个 + 启动器侧 1 个。
|
|
6
|
+
|
|
7
|
+
> 启动器侧的 `zwcad_wait_ready` 在 Python 运行时尚未就绪时也可用,用于等待初始化完成。
|
|
8
|
+
> 诊断统一走 `zwcad_diagnose`:Python 已就绪时返回完整诊断,
|
|
9
|
+
> 未就绪时自动降级为启动器视角(含错误日志与环境信息)并标注未检测项。
|
|
6
10
|
|
|
7
11
|
## 为什么需要 Node 启动器
|
|
8
12
|
|
package/lib/proxy.js
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
const fs = require('fs');
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const { spawn } = require('child_process');
|
|
33
|
-
const { logError } = require('./errorlog');
|
|
33
|
+
const { logError, logFilePath } = require('./errorlog');
|
|
34
34
|
|
|
35
35
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
36
36
|
|
|
@@ -386,6 +386,15 @@ function createProxy(opts) {
|
|
|
386
386
|
handleProxyTool(msg);
|
|
387
387
|
return;
|
|
388
388
|
}
|
|
389
|
+
|
|
390
|
+
// zwcad_diagnose 是 Python 侧的主诊断工具,但它本身跑在 Python 里。
|
|
391
|
+
// Python 未就绪时它必然报「正在初始化」,而这恰恰是最需要诊断的时刻。
|
|
392
|
+
// 所以这里做一次兜底:未就绪时直接由代理层给出启动器视角的诊断,
|
|
393
|
+
// 让 AI 只需认准「一个诊断入口」。
|
|
394
|
+
if (toolName === 'zwcad_diagnose' && !childReady) {
|
|
395
|
+
handleDiagnoseFallback(msg);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
389
398
|
}
|
|
390
399
|
|
|
391
400
|
// tools/list 永远由本地静态清单应答,不受 childReady 影响。
|
|
@@ -494,13 +503,14 @@ function createProxy(opts) {
|
|
|
494
503
|
} else if (r.state === 'failed') {
|
|
495
504
|
text =
|
|
496
505
|
`服务初始化失败:${r.error}\n\n` +
|
|
497
|
-
|
|
506
|
+
`下一步:调用 zwcad_diagnose 获取完整的错误记录与运行环境信息,` +
|
|
507
|
+
`据此判断原因并告知用户具体怎么修。\n\n` +
|
|
498
508
|
`常见原因:未安装 Python 3.10+、网络无法访问 PyPI、磁盘空间不足。`;
|
|
499
509
|
} else {
|
|
500
510
|
text =
|
|
501
511
|
`已等待 ${seconds} 秒,初始化仍未完成,仍在安装依赖。\n\n` +
|
|
502
|
-
|
|
503
|
-
|
|
512
|
+
`下一步:可再调用一次本工具继续等待;若累计超过 2 分钟仍无进展,` +
|
|
513
|
+
`调用 zwcad_diagnose 查看错误记录。`;
|
|
504
514
|
}
|
|
505
515
|
|
|
506
516
|
writeJson({
|
|
@@ -524,6 +534,139 @@ function createProxy(opts) {
|
|
|
524
534
|
});
|
|
525
535
|
}
|
|
526
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Python 未就绪时,zwcad_diagnose 的兜底应答。
|
|
539
|
+
*
|
|
540
|
+
* 设计要点:对 AI 来说诊断入口始终是 zwcad_diagnose,不必记两个工具名。
|
|
541
|
+
* 这里返回启动器视角的诊断,并**明确标注哪些项目因 Python 未就绪而无法检测**,
|
|
542
|
+
* 避免 AI 把「未检测」误读成「正常」或「故障」。
|
|
543
|
+
*/
|
|
544
|
+
function handleDiagnoseFallback(msg) {
|
|
545
|
+
const report = buildDiagnosisReport(10);
|
|
546
|
+
|
|
547
|
+
const header =
|
|
548
|
+
'## 诊断来源\n' +
|
|
549
|
+
'Python 运行时尚未就绪,本次为**启动器视角**的诊断,' +
|
|
550
|
+
'无法检测 ZWCAD 连接状态、机械类型库等 Python 侧项目。\n' +
|
|
551
|
+
'待初始化完成后再次调用本工具,可获得完整诊断。\n\n';
|
|
552
|
+
|
|
553
|
+
const footer =
|
|
554
|
+
'\n\n## 本次未检测的项目\n' +
|
|
555
|
+
'- ZWCAD 是否已启动、是否打开了 DWG\n' +
|
|
556
|
+
'- 机械扩展(ZwmToolKit)类型库加载状态\n' +
|
|
557
|
+
'- 平台/机械 COM 连接可用性\n\n' +
|
|
558
|
+
'这些需要 Python 运行时就绪后才能探测。在此之前,' +
|
|
559
|
+
'如需了解 ZWCAD 侧的情况,请直接查看用户屏幕上是否打开了图纸。';
|
|
560
|
+
|
|
561
|
+
writeJson({
|
|
562
|
+
jsonrpc: '2.0',
|
|
563
|
+
id: msg.id,
|
|
564
|
+
result: {
|
|
565
|
+
content: [{ type: 'text', text: header + report + footer }],
|
|
566
|
+
// 这不是工具失败,而是「部分信息暂时不可得」,用 isError 会让 AI
|
|
567
|
+
// 误判为故障,所以按正常结果返回,把局限性写在正文里。
|
|
568
|
+
isError: false,
|
|
569
|
+
},
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* 生成诊断报告。
|
|
575
|
+
*
|
|
576
|
+
* 目标是让 AI 读完就能判断问题出在哪,并把结论用自然语言转述给用户,
|
|
577
|
+
* 而不是把一坨技术细节甩给用户让他自己去跑命令。
|
|
578
|
+
*/
|
|
579
|
+
function buildDiagnosisReport(limit) {
|
|
580
|
+
const lines = [];
|
|
581
|
+
const stateText =
|
|
582
|
+
runtimeState === 'ready' && childReady
|
|
583
|
+
? '已就绪'
|
|
584
|
+
: runtimeState === 'failed'
|
|
585
|
+
? '失败'
|
|
586
|
+
: '正在准备';
|
|
587
|
+
|
|
588
|
+
lines.push('## 运行时状态');
|
|
589
|
+
lines.push(`状态:${stateText}`);
|
|
590
|
+
if (runtimeState === 'failed' && runtimeError) {
|
|
591
|
+
lines.push(`错误:${runtimeError}`);
|
|
592
|
+
}
|
|
593
|
+
if (childReady) {
|
|
594
|
+
lines.push('Python 侧已完成握手,可以正常接受请求。');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
lines.push('');
|
|
598
|
+
lines.push('## 运行环境');
|
|
599
|
+
lines.push(`Node:${process.version}(${process.platform}/${process.arch})`);
|
|
600
|
+
lines.push(`启动器版本:${opts.version || '未知'}`);
|
|
601
|
+
lines.push(`工作目录:${cwd}`);
|
|
602
|
+
lines.push(
|
|
603
|
+
`环境变量:ZWCAD_MCP_PYTHON=${process.env.ZWCAD_MCP_PYTHON || '(未设置)'},` +
|
|
604
|
+
`ZWCAD_MCP_PIP_INDEX=${process.env.ZWCAD_MCP_PIP_INDEX || '(未设置)'}`
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
lines.push('');
|
|
608
|
+
lines.push('## 错误记录');
|
|
609
|
+
const entries = readRecentErrors(limit);
|
|
610
|
+
if (entries.length === 0) {
|
|
611
|
+
lines.push('无错误记录。若问题仍存在,可能是尚在安装中,或故障未落到日志点。');
|
|
612
|
+
} else {
|
|
613
|
+
lines.push(`日志文件:${logFilePath()}`);
|
|
614
|
+
lines.push('');
|
|
615
|
+
for (const e of entries) {
|
|
616
|
+
lines.push(`- [${e.time}] **${e.event}**`);
|
|
617
|
+
if (e.detail) {
|
|
618
|
+
lines.push(' ' + e.detail.replace(/\n/g, '\n '));
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
lines.push('');
|
|
624
|
+
lines.push('## 如何判读');
|
|
625
|
+
lines.push('- `runtime.python.not_found`:本机没有 3.10+ 的 Python。');
|
|
626
|
+
lines.push(' 让用户装 Python(建议 3.13),或设 `ZWCAD_MCP_PYTHON` 指向已有解释器。');
|
|
627
|
+
lines.push('- `runtime.install.failed`:装依赖失败。看 detail 里的 `pipIndex` 与 `error`,');
|
|
628
|
+
lines.push(' 多为网络问题,让用户设 `ZWCAD_MCP_PIP_INDEX` 为国内镜像后重连。');
|
|
629
|
+
lines.push('- `python.spawn.failed` / `python.exited`:Python 进程起不来或崩溃,');
|
|
630
|
+
lines.push(' 看 detail 里的 `stderrTail`(Python 自身的异常堆栈)。');
|
|
631
|
+
lines.push('- 无任何错误记录但仍不可用:多半仍在安装,让其再等或重连。');
|
|
632
|
+
|
|
633
|
+
return lines.join('\n');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* 读取最近 N 条错误日志,转成易读文本。
|
|
638
|
+
*
|
|
639
|
+
* 读取失败一律返回空数组——诊断工具自己不能因为读不到日志而报错。
|
|
640
|
+
*/
|
|
641
|
+
function readRecentErrors(limit) {
|
|
642
|
+
let raw;
|
|
643
|
+
try {
|
|
644
|
+
raw = fs.readFileSync(logFilePath(), 'utf8');
|
|
645
|
+
} catch {
|
|
646
|
+
return [];
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const out = [];
|
|
650
|
+
const all = raw.trim().split('\n').slice(-limit);
|
|
651
|
+
for (const line of all) {
|
|
652
|
+
try {
|
|
653
|
+
const r = JSON.parse(line);
|
|
654
|
+
let detail = '';
|
|
655
|
+
if (r.detail && Object.keys(r.detail).length > 0) {
|
|
656
|
+
try {
|
|
657
|
+
detail = JSON.stringify(r.detail, null, 1);
|
|
658
|
+
} catch {
|
|
659
|
+
detail = '[无法序列化]';
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
out.push({ time: r.time, event: r.event, detail });
|
|
663
|
+
} catch {
|
|
664
|
+
/* 跳过损坏行 */
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
|
|
527
670
|
/**
|
|
528
671
|
* 未就绪时的工具调用:返回结构化错误,让 AI 知道该等还是该修环境。
|
|
529
672
|
*
|
package/package.json
CHANGED
package/tools.json
CHANGED
|
@@ -1300,7 +1300,7 @@
|
|
|
1300
1300
|
},
|
|
1301
1301
|
{
|
|
1302
1302
|
"name": "zwcad_diagnose",
|
|
1303
|
-
"description": "
|
|
1303
|
+
"description": "诊断 MCP 服务的运行状态,并按需给出修复建议。\n\n分两种情形,工具名不变:\n- Python 运行时已就绪:返回完整诊断(平台 COM 可用性、活动图纸、机械扩展与 ZwmToolKit 类型库状态)。\n- Python 运行时未就绪:自动降级为启动器视角,返回运行时状态、Python 解释器路径与版本、最近错误记录(含 pip 镜像源、依赖清单、耗时),并明确标注哪些项目本次未检测。\n\n因此排查任何问题都从这里开始,无需另找工具。",
|
|
1304
1304
|
"inputSchema": {
|
|
1305
1305
|
"properties": {
|
|
1306
1306
|
"probe_cad": {
|
|
@@ -1320,4 +1320,4 @@
|
|
|
1320
1320
|
}
|
|
1321
1321
|
}
|
|
1322
1322
|
}
|
|
1323
|
-
]
|
|
1323
|
+
]
|