zwcad-mcp 0.2.0 → 0.2.2
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 +21 -2
- package/bridge.js +49 -1
- package/lib/errorlog.js +160 -0
- package/lib/proxy.js +157 -4
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
中望CAD(ZWCAD)MCP 服务,让大模型通过 [MCP 协议](https://modelcontextprotocol.io)直接操控中望CAD 平台与中望机械CAD,完成绘图、标注、图层/块/样式管理,以及图框、标题栏、明细表(BOM)等机械操作。
|
|
4
4
|
|
|
5
|
-
**共计
|
|
5
|
+
**共计 41 个工具**:平台 26 个(不依赖机械环境)+ 机械 11 个(需中望机械CAD)+ 诊断 2 个 + 启动器侧 2 个。
|
|
6
|
+
|
|
7
|
+
> 启动器侧的两个工具(`zwcad_wait_ready`、`zwcad_launcher_diagnose`)在 Python 运行时尚未就绪时也可用,
|
|
8
|
+
> 用于等待初始化完成、以及在首次安装失败时自助排查。
|
|
6
9
|
|
|
7
10
|
## 为什么需要 Node 启动器
|
|
8
11
|
|
|
@@ -78,12 +81,27 @@ npx -y zwcad-mcp --prepare # 只准备运行时后退出
|
|
|
78
81
|
|
|
79
82
|
| 现象 | 处理 |
|
|
80
83
|
|---|---|
|
|
81
|
-
|
|
|
84
|
+
| 首次使用提示「正在首次初始化」 | **正常**,正在后台安装依赖(约 40 秒),握手本身只需约 120 ms |
|
|
82
85
|
| 报「未找到可用的 Python」 | 装 Python 3.10+,或设 `ZWCAD_MCP_PYTHON` 指向已有解释器 |
|
|
83
86
|
| 装依赖失败(网络) | 设 `ZWCAD_MCP_PIP_INDEX=https://pypi.tuna.tsinghua.edu.cn/simple` |
|
|
84
87
|
| 运行时损坏 | 删 `%USERPROFILE%\.workbuddy\zwcad-mcp-runtime\` 后重连,自动重建 |
|
|
85
88
|
| 机械工具报 `TYPELIB_NOT_LOADED` | 需中望机械CAD 已启动;平台工具不受影响 |
|
|
86
89
|
|
|
90
|
+
### 错误日志
|
|
91
|
+
|
|
92
|
+
启动器会把**失败事件**写入本地文件,便于排查与反馈:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
%USERPROFILE%\.workbuddy\zwcad-mcp-runtime\logs\error.log
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- 每行一条 JSON:时间、事件名、Node 版本、诊断细节
|
|
99
|
+
- **只记失败**,不记成功的工具调用,不含图纸内容
|
|
100
|
+
- 单文件上限 1 MB,自动轮转为 `.1`~`.3`,总量 ≤ 4 MB
|
|
101
|
+
- `--doctor` 末尾会直接打印最近 10 条摘要,多数情况无需打开文件
|
|
102
|
+
|
|
103
|
+
可用 `ZWCAD_MCP_LOG_DIR` 改变存放位置。
|
|
104
|
+
|
|
87
105
|
### 关于 `-2147312566`(加载类型库/DLL 时出错)
|
|
88
106
|
|
|
89
107
|
这是机械类型库的已知情况,**不是文件缺失或权限问题**。实测根因:`ZwmToolKit.tlb` 内部引用了另一个仅在 ZWCAD 进程运行时才可解析的类型库。
|
|
@@ -98,6 +116,7 @@ npx -y zwcad-mcp --prepare # 只准备运行时后退出
|
|
|
98
116
|
|---|---|
|
|
99
117
|
| `ZWCAD_MCP_PYTHON` | 指定 Python 解释器路径,跳过自动探测(无效时报错而非静默回退) |
|
|
100
118
|
| `ZWCAD_MCP_PIP_INDEX` | 指定 pip 镜像源 |
|
|
119
|
+
| `ZWCAD_MCP_LOG_DIR` | 指定错误日志目录,默认在 `zwcad-mcp-runtime/logs` |
|
|
101
120
|
|
|
102
121
|
## 仓库结构
|
|
103
122
|
|
package/bridge.js
CHANGED
|
@@ -79,9 +79,13 @@ function log(...parts) {
|
|
|
79
79
|
process.stderr.write('[zwcad-mcp] ' + parts.join(' ') + '\n');
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** 错误日志(落盘),失败不影响主流程 */
|
|
83
|
+
const { logError, logFilePath } = require('./lib/errorlog');
|
|
84
|
+
|
|
82
85
|
function fatal(err) {
|
|
83
86
|
const msg = err && err.stack ? err.stack : String(err);
|
|
84
87
|
log('启动失败:' + msg);
|
|
88
|
+
logError('launcher.fatal', { error: err });
|
|
85
89
|
process.exit(1);
|
|
86
90
|
}
|
|
87
91
|
|
|
@@ -260,6 +264,12 @@ function resolvePython() {
|
|
|
260
264
|
log('使用 Python ' + ver.join('.') + ':' + exe);
|
|
261
265
|
return { exe, version: ver };
|
|
262
266
|
}
|
|
267
|
+
logError('runtime.python.not_found', {
|
|
268
|
+
candidates: pythonCandidates(),
|
|
269
|
+
tried,
|
|
270
|
+
minVersion: MIN_PYTHON.join('.'),
|
|
271
|
+
envOverride: process.env.ZWCAD_MCP_PYTHON || null,
|
|
272
|
+
});
|
|
263
273
|
throw new Error(
|
|
264
274
|
'未找到可用的 Python(需要 >= ' +
|
|
265
275
|
MIN_PYTHON.join('.') +
|
|
@@ -384,7 +394,20 @@ async function installDeps(venvPython) {
|
|
|
384
394
|
args.push(...requiredPackages());
|
|
385
395
|
|
|
386
396
|
log('首次运行,安装依赖(约 30~60 秒,请稍候)…');
|
|
387
|
-
|
|
397
|
+
const startedAt = Date.now();
|
|
398
|
+
try {
|
|
399
|
+
await runStreaming(venvPython, args, '安装依赖');
|
|
400
|
+
} catch (e) {
|
|
401
|
+
logError('runtime.install.failed', {
|
|
402
|
+
error: e,
|
|
403
|
+
packageCount: requiredPackages().length,
|
|
404
|
+
packages: requiredPackages(),
|
|
405
|
+
pipIndex: index || null,
|
|
406
|
+
elapsedMs: Date.now() - startedAt,
|
|
407
|
+
venvPython,
|
|
408
|
+
});
|
|
409
|
+
throw e;
|
|
410
|
+
}
|
|
388
411
|
}
|
|
389
412
|
|
|
390
413
|
/**
|
|
@@ -490,6 +513,31 @@ function doctor() {
|
|
|
490
513
|
} else {
|
|
491
514
|
log('--- 虚拟环境 --- 尚未创建(首次启动时会自动创建)');
|
|
492
515
|
}
|
|
516
|
+
|
|
517
|
+
log('--- 错误日志 ---');
|
|
518
|
+
const lp = logFilePath();
|
|
519
|
+
if (fs.existsSync(lp)) {
|
|
520
|
+
const st = fs.statSync(lp);
|
|
521
|
+
log(' 路径:' + lp);
|
|
522
|
+
log(' 大小:' + (st.size / 1024).toFixed(1) + ' KB,最近写入 ' + st.mtime.toISOString());
|
|
523
|
+
log(' 查看最近 10 条:');
|
|
524
|
+
try {
|
|
525
|
+
const lines = fs.readFileSync(lp, 'utf8').trim().split('\n').slice(-10);
|
|
526
|
+
for (const line of lines) {
|
|
527
|
+
try {
|
|
528
|
+
const r = JSON.parse(line);
|
|
529
|
+
log(' [' + r.time + '] ' + r.event);
|
|
530
|
+
} catch {
|
|
531
|
+
log(' ' + line.slice(0, 120));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
log(' (读取失败)');
|
|
536
|
+
}
|
|
537
|
+
} else {
|
|
538
|
+
log(' 暂无错误记录(这是好事)');
|
|
539
|
+
log(' 路径:' + lp + '(首次出错时自动创建)');
|
|
540
|
+
}
|
|
493
541
|
}
|
|
494
542
|
|
|
495
543
|
/* ------------------------------------------------------------------ *
|
package/lib/errorlog.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 错误日志:把启动器与 Python 侧的失败事件写到本地文件,便于事后排查。
|
|
4
|
+
*
|
|
5
|
+
* 设计取舍
|
|
6
|
+
* --------
|
|
7
|
+
* 1. **只记失败**,不记成功。成功事件写日志只会淹没真正有用的信息,
|
|
8
|
+
* 而且调用量上来后文件会迅速膨胀。
|
|
9
|
+
* 2. **只记本地,不联网**。图纸内容、文件路径都可能涉密,
|
|
10
|
+
* 用户遇到问题时可以把文件发给我们,由人工判断是否适合上报。
|
|
11
|
+
* 3. **写入绝不抛异常**。日志本身出问题不能影响 MCP 服务运行——
|
|
12
|
+
* 这是日志系统最常见的自伤方式。
|
|
13
|
+
* 4. **自动轮转**。单文件上限 1 MB,超过后转成 `.1`,最多保留 3 个历史文件,
|
|
14
|
+
* 总量控制在 4 MB 以内,不会占满用户磁盘。
|
|
15
|
+
*
|
|
16
|
+
* 存放位置
|
|
17
|
+
* --------
|
|
18
|
+
* %USERPROFILE%\.workbuddy\zwcad-mcp-runtime\logs\
|
|
19
|
+
*
|
|
20
|
+
* 放在 runtime 目录下,用户执行「删掉 runtime 目录重置」时会一并清理,
|
|
21
|
+
* 不会留下孤儿文件。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const os = require('os');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
|
|
30
|
+
/** 单文件上限:1 MB */
|
|
31
|
+
const MAX_BYTES = 1024 * 1024;
|
|
32
|
+
/** 保留的历史文件数 */
|
|
33
|
+
const MAX_BACKUPS = 3;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 解析日志目录。
|
|
37
|
+
*
|
|
38
|
+
* 允许用环境变量覆盖,便于排查时把日志指到更方便的位置。
|
|
39
|
+
*/
|
|
40
|
+
function resolveLogDir() {
|
|
41
|
+
if (process.env.ZWCAD_MCP_LOG_DIR) {
|
|
42
|
+
return process.env.ZWCAD_MCP_LOG_DIR;
|
|
43
|
+
}
|
|
44
|
+
return path.join(os.homedir(), '.workbuddy', 'zwcad-mcp-runtime', 'logs');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 轮转:当前文件超过上限时,依次改名 .1 / .2 / .3,最老的被丢弃。
|
|
49
|
+
*
|
|
50
|
+
* 用 rename 而不是复制,避免大文件复制带来的 IO 抖动。
|
|
51
|
+
*/
|
|
52
|
+
function rotateIfNeeded(file) {
|
|
53
|
+
let size;
|
|
54
|
+
try {
|
|
55
|
+
size = fs.statSync(file).size;
|
|
56
|
+
} catch {
|
|
57
|
+
return; // 文件不存在,无需轮转
|
|
58
|
+
}
|
|
59
|
+
if (size < MAX_BYTES) return;
|
|
60
|
+
|
|
61
|
+
// 从最老的开始往后挪,腾出 .1 的位置
|
|
62
|
+
for (let i = MAX_BACKUPS; i >= 1; i--) {
|
|
63
|
+
const src = i === 1 ? file : file + '.' + (i - 1);
|
|
64
|
+
const dst = file + '.' + i;
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(src)) fs.renameSync(src, dst);
|
|
67
|
+
} catch {
|
|
68
|
+
/* 轮转失败不影响主流程 */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 把值安全地转成可 JSON 序列化的形式 */
|
|
74
|
+
function sanitize(value, depth = 0) {
|
|
75
|
+
if (value === null || value === undefined) return value;
|
|
76
|
+
if (depth > 4) return '[深度超限]';
|
|
77
|
+
|
|
78
|
+
const t = typeof value;
|
|
79
|
+
if (t === 'string') {
|
|
80
|
+
// 抑制超长字符串(例如整段 Python 堆栈)
|
|
81
|
+
return value.length > 4000 ? value.slice(0, 4000) + '…[截断]' : value;
|
|
82
|
+
}
|
|
83
|
+
if (t === 'number' || t === 'boolean') return value;
|
|
84
|
+
if (t === 'function') return '[Function]';
|
|
85
|
+
|
|
86
|
+
if (value instanceof Error) {
|
|
87
|
+
return {
|
|
88
|
+
name: value.name,
|
|
89
|
+
message: value.message,
|
|
90
|
+
stack: value.stack ? value.stack.slice(0, 4000) : undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (Array.isArray(value)) {
|
|
95
|
+
return value.slice(0, 50).map((v) => sanitize(v, depth + 1));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (t === 'object') {
|
|
99
|
+
const out = {};
|
|
100
|
+
let n = 0;
|
|
101
|
+
for (const k of Object.keys(value)) {
|
|
102
|
+
if (n++ >= 50) {
|
|
103
|
+
out['…'] = '[字段过多,已截断]';
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
out[k] = sanitize(value[k], depth + 1);
|
|
108
|
+
} catch {
|
|
109
|
+
out[k] = '[无法序列化]';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return String(value);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** 进程级上下文,每条日志都会带上 */
|
|
119
|
+
const baseContext = {
|
|
120
|
+
pid: process.pid,
|
|
121
|
+
node: process.version,
|
|
122
|
+
platform: process.platform + '/' + process.arch,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 记录一条错误事件。
|
|
127
|
+
*
|
|
128
|
+
* @param {string} event 事件名,用点号分层,如 `runtime.install.failed`
|
|
129
|
+
* @param {object} detail 事件细节,任意可序列化对象
|
|
130
|
+
*/
|
|
131
|
+
function logError(event, detail) {
|
|
132
|
+
try {
|
|
133
|
+
const dir = resolveLogDir();
|
|
134
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
135
|
+
|
|
136
|
+
const file = path.join(dir, 'error.log');
|
|
137
|
+
rotateIfNeeded(file);
|
|
138
|
+
|
|
139
|
+
const record = {
|
|
140
|
+
time: new Date().toISOString(),
|
|
141
|
+
event: String(event),
|
|
142
|
+
...baseContext,
|
|
143
|
+
detail: sanitize(detail || {}),
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
fs.appendFileSync(file, JSON.stringify(record) + '\n', 'utf8');
|
|
147
|
+
|
|
148
|
+
// 同时打印一行到 stderr,让开发者在终端也能立刻看到
|
|
149
|
+
process.stderr.write(`[zwcad-mcp][错误] ${record.event}\n`);
|
|
150
|
+
} catch {
|
|
151
|
+
// 日志失败绝不能影响主流程
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 返回当前日志文件路径,供 --doctor 展示 */
|
|
156
|
+
function logFilePath() {
|
|
157
|
+
return path.join(resolveLogDir(), 'error.log');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { logError, logFilePath, resolveLogDir };
|
package/lib/proxy.js
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
const fs = require('fs');
|
|
31
31
|
const path = require('path');
|
|
32
32
|
const { spawn } = require('child_process');
|
|
33
|
+
const { logError, logFilePath } = require('./errorlog');
|
|
33
34
|
|
|
34
35
|
const PROTOCOL_VERSION = '2024-11-05';
|
|
35
36
|
|
|
@@ -71,6 +72,26 @@ const PROXY_TOOLS = [
|
|
|
71
72
|
additionalProperties: false,
|
|
72
73
|
},
|
|
73
74
|
},
|
|
75
|
+
{
|
|
76
|
+
name: 'zwcad_launcher_diagnose',
|
|
77
|
+
description:
|
|
78
|
+
'诊断启动器与 Python 运行时的状态,并返回最近的错误记录。\n\n' +
|
|
79
|
+
'与 zwcad_diagnose 的区别:本工具由 JS 启动器实现,**在 Python 尚未就绪时也可用**,' +
|
|
80
|
+
'因此是排查「首次安装失败 / 一直停在初始化」的首选;' +
|
|
81
|
+
'zwcad_diagnose 依赖 Python 侧,只能在其就绪后调用。\n\n' +
|
|
82
|
+
'返回内容包括:运行时状态、Python 解释器路径与版本、虚拟环境状态、' +
|
|
83
|
+
'最近若干条错误日志(含事件名与诊断细节)。',
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
limit: {
|
|
88
|
+
type: 'number',
|
|
89
|
+
description: '返回最近多少条错误日志,默认 10,上限 50。',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
74
95
|
];
|
|
75
96
|
|
|
76
97
|
/**
|
|
@@ -109,6 +130,13 @@ function createProxy(opts) {
|
|
|
109
130
|
let inputBuffer = '';
|
|
110
131
|
/** 子进程 → 我们的待处理缓冲 */
|
|
111
132
|
let childBuffer = '';
|
|
133
|
+
/**
|
|
134
|
+
* 子进程 stderr 的尾部。
|
|
135
|
+
*
|
|
136
|
+
* Python 崩溃时异常堆栈在这里,是事后定位问题的关键依据,
|
|
137
|
+
* 所以即使已经转发到自己的 stderr,也要留一份用于写错误日志。
|
|
138
|
+
*/
|
|
139
|
+
let stderrTail = '';
|
|
112
140
|
|
|
113
141
|
/** 子进程是否已就绪并接管 */
|
|
114
142
|
let child = null;
|
|
@@ -196,6 +224,7 @@ function createProxy(opts) {
|
|
|
196
224
|
runtimeState = 'failed';
|
|
197
225
|
runtimeError = e && e.message ? e.message : String(e);
|
|
198
226
|
log('运行时准备失败:' + runtimeError);
|
|
227
|
+
logError('runtime.ready.failed', { error: e });
|
|
199
228
|
throw e;
|
|
200
229
|
}
|
|
201
230
|
})();
|
|
@@ -223,7 +252,11 @@ function createProxy(opts) {
|
|
|
223
252
|
log('启动 MCP Server…');
|
|
224
253
|
child = spawn(py, opts.childArgs, { stdio: ['pipe', 'pipe', 'pipe'], env, windowsHide: true });
|
|
225
254
|
|
|
226
|
-
|
|
255
|
+
// 保留 stderr 尾部:Python 若崩溃,异常堆栈在这里,是定位问题的关键
|
|
256
|
+
child.stderr.on('data', (d) => {
|
|
257
|
+
stderrTail = (stderrTail + d.toString('utf8')).slice(-8000);
|
|
258
|
+
process.stderr.write(d);
|
|
259
|
+
});
|
|
227
260
|
|
|
228
261
|
child.stdout.on('data', (chunk) => {
|
|
229
262
|
childBuffer += chunk.toString('utf8');
|
|
@@ -239,10 +272,19 @@ function createProxy(opts) {
|
|
|
239
272
|
log('子进程启动失败:' + e.message);
|
|
240
273
|
runtimeState = 'failed';
|
|
241
274
|
runtimeError = e.message;
|
|
275
|
+
logError('python.spawn.failed', { error: e, python: py, args: opts.childArgs });
|
|
242
276
|
});
|
|
243
277
|
|
|
244
278
|
child.on('close', (code) => {
|
|
245
279
|
log('MCP Server 退出,code=' + code);
|
|
280
|
+
// 非 0 退出往往是 Python 侧崩了,值得留痕;0 是正常结束
|
|
281
|
+
if (code !== 0) {
|
|
282
|
+
logError('python.exited', {
|
|
283
|
+
code,
|
|
284
|
+
initialized: childInitialized,
|
|
285
|
+
stderrTail: stderrTail.slice(-3000),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
246
288
|
process.exit(code == null ? 1 : code);
|
|
247
289
|
});
|
|
248
290
|
|
|
@@ -472,13 +514,14 @@ function createProxy(opts) {
|
|
|
472
514
|
} else if (r.state === 'failed') {
|
|
473
515
|
text =
|
|
474
516
|
`服务初始化失败:${r.error}\n\n` +
|
|
475
|
-
|
|
517
|
+
`下一步:调用 zwcad_launcher_diagnose 获取完整的错误记录与运行环境信息,` +
|
|
518
|
+
`据此判断原因并告知用户具体怎么修。\n\n` +
|
|
476
519
|
`常见原因:未安装 Python 3.10+、网络无法访问 PyPI、磁盘空间不足。`;
|
|
477
520
|
} else {
|
|
478
521
|
text =
|
|
479
522
|
`已等待 ${seconds} 秒,初始化仍未完成,仍在安装依赖。\n\n` +
|
|
480
|
-
|
|
481
|
-
|
|
523
|
+
`下一步:可再调用一次本工具继续等待;若累计超过 2 分钟仍无进展,` +
|
|
524
|
+
`调用 zwcad_launcher_diagnose 查看错误记录。`;
|
|
482
525
|
}
|
|
483
526
|
|
|
484
527
|
writeJson({
|
|
@@ -492,6 +535,19 @@ function createProxy(opts) {
|
|
|
492
535
|
return;
|
|
493
536
|
}
|
|
494
537
|
|
|
538
|
+
if (name === 'zwcad_launcher_diagnose') {
|
|
539
|
+
let limit = Number(args.limit);
|
|
540
|
+
if (!Number.isFinite(limit) || limit <= 0) limit = 10;
|
|
541
|
+
limit = Math.min(limit, 50);
|
|
542
|
+
|
|
543
|
+
writeJson({
|
|
544
|
+
jsonrpc: '2.0',
|
|
545
|
+
id: msg.id,
|
|
546
|
+
result: { content: [{ type: 'text', text: buildDiagnosisReport(limit) }] },
|
|
547
|
+
});
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
495
551
|
writeJson({
|
|
496
552
|
jsonrpc: '2.0',
|
|
497
553
|
id: msg.id,
|
|
@@ -502,6 +558,103 @@ function createProxy(opts) {
|
|
|
502
558
|
});
|
|
503
559
|
}
|
|
504
560
|
|
|
561
|
+
/**
|
|
562
|
+
* 生成诊断报告。
|
|
563
|
+
*
|
|
564
|
+
* 目标是让 AI 读完就能判断问题出在哪,并把结论用自然语言转述给用户,
|
|
565
|
+
* 而不是把一坨技术细节甩给用户让他自己去跑命令。
|
|
566
|
+
*/
|
|
567
|
+
function buildDiagnosisReport(limit) {
|
|
568
|
+
const lines = [];
|
|
569
|
+
const stateText =
|
|
570
|
+
runtimeState === 'ready' && childReady
|
|
571
|
+
? '已就绪'
|
|
572
|
+
: runtimeState === 'failed'
|
|
573
|
+
? '失败'
|
|
574
|
+
: '正在准备';
|
|
575
|
+
|
|
576
|
+
lines.push('## 运行时状态');
|
|
577
|
+
lines.push(`状态:${stateText}`);
|
|
578
|
+
if (runtimeState === 'failed' && runtimeError) {
|
|
579
|
+
lines.push(`错误:${runtimeError}`);
|
|
580
|
+
}
|
|
581
|
+
if (childReady) {
|
|
582
|
+
lines.push('Python 侧已完成握手,可以正常接受请求。');
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
lines.push('');
|
|
586
|
+
lines.push('## 运行环境');
|
|
587
|
+
lines.push(`Node:${process.version}(${process.platform}/${process.arch})`);
|
|
588
|
+
lines.push(`启动器版本:${opts.version || '未知'}`);
|
|
589
|
+
lines.push(`工作目录:${cwd}`);
|
|
590
|
+
lines.push(
|
|
591
|
+
`环境变量:ZWCAD_MCP_PYTHON=${process.env.ZWCAD_MCP_PYTHON || '(未设置)'},` +
|
|
592
|
+
`ZWCAD_MCP_PIP_INDEX=${process.env.ZWCAD_MCP_PIP_INDEX || '(未设置)'}`
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
lines.push('');
|
|
596
|
+
lines.push('## 错误记录');
|
|
597
|
+
const entries = readRecentErrors(limit);
|
|
598
|
+
if (entries.length === 0) {
|
|
599
|
+
lines.push('无错误记录。若问题仍存在,可能是尚在安装中,或故障未落到日志点。');
|
|
600
|
+
} else {
|
|
601
|
+
lines.push(`日志文件:${logFilePath()}`);
|
|
602
|
+
lines.push('');
|
|
603
|
+
for (const e of entries) {
|
|
604
|
+
lines.push(`- [${e.time}] **${e.event}**`);
|
|
605
|
+
if (e.detail) {
|
|
606
|
+
lines.push(' ' + e.detail.replace(/\n/g, '\n '));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
lines.push('');
|
|
612
|
+
lines.push('## 如何判读');
|
|
613
|
+
lines.push('- `runtime.python.not_found`:本机没有 3.10+ 的 Python。');
|
|
614
|
+
lines.push(' 让用户装 Python(建议 3.13),或设 `ZWCAD_MCP_PYTHON` 指向已有解释器。');
|
|
615
|
+
lines.push('- `runtime.install.failed`:装依赖失败。看 detail 里的 `pipIndex` 与 `error`,');
|
|
616
|
+
lines.push(' 多为网络问题,让用户设 `ZWCAD_MCP_PIP_INDEX` 为国内镜像后重连。');
|
|
617
|
+
lines.push('- `python.spawn.failed` / `python.exited`:Python 进程起不来或崩溃,');
|
|
618
|
+
lines.push(' 看 detail 里的 `stderrTail`(Python 自身的异常堆栈)。');
|
|
619
|
+
lines.push('- 无任何错误记录但仍不可用:多半仍在安装,让其再等或重连。');
|
|
620
|
+
|
|
621
|
+
return lines.join('\n');
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* 读取最近 N 条错误日志,转成易读文本。
|
|
626
|
+
*
|
|
627
|
+
* 读取失败一律返回空数组——诊断工具自己不能因为读不到日志而报错。
|
|
628
|
+
*/
|
|
629
|
+
function readRecentErrors(limit) {
|
|
630
|
+
let raw;
|
|
631
|
+
try {
|
|
632
|
+
raw = fs.readFileSync(logFilePath(), 'utf8');
|
|
633
|
+
} catch {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const out = [];
|
|
638
|
+
const all = raw.trim().split('\n').slice(-limit);
|
|
639
|
+
for (const line of all) {
|
|
640
|
+
try {
|
|
641
|
+
const r = JSON.parse(line);
|
|
642
|
+
let detail = '';
|
|
643
|
+
if (r.detail && Object.keys(r.detail).length > 0) {
|
|
644
|
+
try {
|
|
645
|
+
detail = JSON.stringify(r.detail, null, 1);
|
|
646
|
+
} catch {
|
|
647
|
+
detail = '[无法序列化]';
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
out.push({ time: r.time, event: r.event, detail });
|
|
651
|
+
} catch {
|
|
652
|
+
/* 跳过损坏行 */
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return out;
|
|
656
|
+
}
|
|
657
|
+
|
|
505
658
|
/**
|
|
506
659
|
* 未就绪时的工具调用:返回结构化错误,让 AI 知道该等还是该修环境。
|
|
507
660
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zwcad-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "中望CAD(ZWCAD)MCP 服务:用自然语言驱动 ZWCAD 二维平台与中望机械 CAD,完成绘图、标注、图层管理与机械图框/BOM 操作",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
"prepare-runtime": "node bridge.js --prepare",
|
|
24
24
|
"build:connector": "node scripts/build-connector.js",
|
|
25
25
|
"check:connector": "node scripts/build-connector.js --check",
|
|
26
|
+
"test:errorlog": "node scripts/test-errorlog.js",
|
|
27
|
+
"test:rotation": "node scripts/test-rotation.js",
|
|
28
|
+
"test": "node scripts/test-errorlog.js && node scripts/test-rotation.js",
|
|
26
29
|
"prepack": "node scripts/build-connector.js --check"
|
|
27
30
|
},
|
|
28
31
|
"engines": {
|