dsh-eda-mcp 1.0.0 → 1.0.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/CHANGELOG.md +62 -0
- package/docs/schematic-workflow.md +8 -1
- package/docs/tools.md +1 -1
- package/dsh-eda-bridge-extension-0.4.9.zip +0 -0
- package/jlc-extension/api-bridge.js +69 -19
- package/jlc-extension/extension.json +1 -1
- package/lib/bridge.mjs +151 -50
- package/lib/index.js +8 -1
- package/package.json +1 -1
- package/src/bridge/index.ts +168 -57
- package/src/skills-content.ts +8 -1
- package/dsh-eda-bridge-extension-0.4.6.zip +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.2 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
### 覆铜调查结案(扩展 0.4.7 / 0.4.8 真机)
|
|
6
|
+
|
|
7
|
+
结论:**这台构建(EDA 3.2.186 / pro-api 0.3.18.0bc12bb0)无法通过 API 创建铜面图元**。不是参数问题,证据链如下。
|
|
8
|
+
|
|
9
|
+
| 尝试 | 结果 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `pcb_PrimitivePour.create` 静态入口 × 8 种组合(单/复杂多边形、原始源码数组、空网络、层 1/2/字符串、全参数显式、100×100 与 300×300 mil) | 全部 `无法创建覆铜边框图元` |
|
|
12
|
+
| 实例入口 `new IPCB_PrimitivePour(...).create()`(0.4.7 新增,验证构造器在运行时可取) | 同样报错 |
|
|
13
|
+
| 填充方式 `solid`/`45grid`/`90grid`/`0`/`1`/`2`/`SOLID`/`GRID45`/`45GRID` | 全部报错 |
|
|
14
|
+
| `pcb_PrimitiveFill.create`(文档说明它同样带 `net` 参数)× 4 种(对象/原始数组/复杂源码/显式 fillMode) | 全部 `无法创建填充图元` |
|
|
15
|
+
| **控制组**:同一多边形管线写丝印折线 `pcb_PrimitivePolyline` / `pcb_draw_polyline` | 也写不出来(`getAll` 0 条),而 `pcb_get_polylines` 内部路径能读到既有板框 1 条 |
|
|
16
|
+
|
|
17
|
+
外部资料也到此为止:`prodocs` 正文是前端渲染(HTML 只有导航),官方配方给的流程就是我们实现的那条(`createPolygon`/`createComplexPolygon` → pour → `rebuildCopperRegion`),搜 "无法创建覆铜边框图元" 无任何社区解法,应用 bundle 内也搜不到该字符串。
|
|
18
|
+
|
|
19
|
+
**因此撤掉 `pcb_create_copper_fill`**:它在这台构建上必然失败,留着就是给调用方挖坑。白名单里的 `pcb_MathPolygon.createComplexPolygon` 与 `pcb_PrimitiveFill.*` 保留(换构建可能有效),但不再包装成工具。
|
|
20
|
+
|
|
21
|
+
**正解**:覆铜在界面手工铺(GND → 顶层+底层 → 重新铺铜);`eda_design_audit` 会持续把缺覆铜记成 `copper-pour` warn,`eda_delivery_checklist` 也会带上它。
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### 通知堆叠 / 粘连(扩展 0.4.9)
|
|
25
|
+
|
|
26
|
+
屏幕实测:三条提示叠在画布上("未连接…点击右侧按钮"、"扩展已加载 v0.4.8"、"已连接;端口 39009"),且"未连接"那条永不消失。
|
|
27
|
+
|
|
28
|
+
- 根因:`showToastMessage` 的 `timer: 0` 语义是**永不自动隐藏**,而 `SYS_Message` **没有任何隐藏或替换 API**;连接成功时也没有人撤掉它。
|
|
29
|
+
- 修法:未连接提示改成 `showFollowMouseTip(tip, 8000)`,并在 WebSocket `onopen` 里 `removeFollowMouseTip` 显式撤掉;`showToast` 一律传有限时长(默认 4 s);文案改为"菜单 DSH MCP → 连接 DSH MCP 服务器"(跟随提示没有按钮)。
|
|
30
|
+
- 线协议 **0.4.8 → 0.4.9**(纯 UI 层)。
|
|
31
|
+
|
|
32
|
+
## 1.0.1 — 2026-10-20
|
|
33
|
+
|
|
34
|
+
1.0.0 的真机回归修掉 4 个 bug(全部由"跑一遍"发现,不是靠读代码):
|
|
35
|
+
|
|
36
|
+
1. **原理图几何审计误报 30 对重叠**(`eda_schematic_audit`)
|
|
37
|
+
`sch_PrimitiveComponent.getAll` 的列表里混着图框/标题栏图元,其包围盒是整张页面(实测 `0..1170 × 0..825`),于是与每个符号"重叠"。现在**丢弃面积超过页面 40% 的图元**并在 `droppedSheetPrimitives` 里报数量;复测:30 对误报 → **0 fail**(31 器件 / 30 个有效包围盒 / 丢弃 1 个)。
|
|
38
|
+
|
|
39
|
+
2. **参数持久化"报成功但没写"**(`eda_design_intake`)
|
|
40
|
+
① `dmt_Project.modifyProjectDescription` 在本机构建上**返回 false、不生效**(与 create/delete/copyProject 同类);② 我的调用还把文本当成 uuid 传。现在:先按官方签名 `(projectUuid, description)` 写并**回读校验**,失败则落到工程目录侧的**侧车文件** `.dsh-eda-spec/<uuid>.json`,`persisted` 只在真的读回成功后为 true,并新增 `persistVia`(`description` / `file` / `none`)与 `persistPath`。真机复测:`persisted=true via=file`。
|
|
41
|
+
|
|
42
|
+
3. **PCB 机械检查是死代码**(`eda_design_audit`)
|
|
43
|
+
`pcb_PrimitiveComponent.getAll([undefined, undefined])` 在该构建上返回**空列表**(同样的 API 在 `pcb_get_components` 里能取到 29 个器件),导致器件重叠 / 出板框两项检查从未执行。改用与 `pcb_get_components` 相同的调用形态,并把 `counts.componentsRead` 暴露出来;复测:`componentsRead: 29`。
|
|
44
|
+
|
|
45
|
+
4. **交付清单挂死超时**(`eda_delivery_checklist`)
|
|
46
|
+
自动切文档后紧接着调网表导出会被页面加载阻塞 → MCP 超时。现在切文档后等待 1.5 s;同时把**跳过**与**失败**分开:新增 `verdict`(`pass` / `incomplete` / `fail`)、`skippedSteps`、`failedSteps`、`next`,默认仍自动切文档。真机复测:**一次调用 7.5 s,四步全过 `verdict=pass`**;`switchDocuments:false` 时如实报 `incomplete` 并指出缺哪个文档。
|
|
47
|
+
|
|
48
|
+
### 回归结果(扩展 0.4.6 / 桥接 1.0.1)
|
|
49
|
+
|
|
50
|
+
| 项 | 结果 |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `eda_schematic_audit` | ✅ 0 fail / 1 warn(12 个符号偏离 10 单位栅格)/ 1 skip(无标签锚点) |
|
|
53
|
+
| `eda_netlist_erc` | ✅ 15 项全执行,14 pass / 0 fail / 1 warn(USB-C SBU1/SBU2 未接) |
|
|
54
|
+
| `eda_design_intake` 持久化 | ✅ `persisted=true via=file` |
|
|
55
|
+
| `eda_design_audit`(PCB) | ✅ `componentsRead:29`、0 违规 / 4 警告(5V 线宽偏细、无覆铜、无丝印、无定位孔) |
|
|
56
|
+
| `eda_delivery_checklist` | ✅ `verdict=pass`、7.5 s、四步(ERC / BOM / PCB 审计 / Gerber 86.8 KB) |
|
|
57
|
+
| `eda_pcb_canvas_origin` | ✅ 偏移 `0,0`,画布↔数据坐标互转一致 |
|
|
58
|
+
| `eda_pcb_create_pour` | ❌ **仍失败**:单多边形与复杂多边形都被接受,但 `pcb_PrimitivePour.create` 对任何网络/层/填充方式一律拒绝 → 判定为**应用侧限制**(1.0.0 里我写"是我用错 API",据此修正) |
|
|
59
|
+
|
|
60
|
+
### 已知遗留(需你决定)
|
|
61
|
+
|
|
62
|
+
- 侧车目录 `D:\文档\LCEDA-Pro\projects\.dsh-eda-spec\` 是回归测试写入的(1 个 json),清理需要你确认(它在你的工程目录内)。
|
|
63
|
+
- 覆铜只能手工:工具已把失败原因写准,`eda_design_audit` 持续报 `copper-pour` warn。
|
|
64
|
+
|
|
3
65
|
## 1.0.0 — 2026-10-20
|
|
4
66
|
|
|
5
67
|
第一个稳定版。0.7.4 之后这一轮把"能画"推进到"画得对、画得像参考设计、交付有证据":工具 106 → **109**,单测 6 → **8 套**,线协议 0.4.2 → **0.4.6**。
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
| eda_launch 带 project 没打开工程 | 命令行路径被应用忽略 | 用 eda_open_project({ name }) 打开 |
|
|
147
147
|
| 关应用后改动丢失 | eda_close 走了强制路径 | 关前先保存;看返回的 forced |
|
|
148
148
|
| run_code 读大 JSON 被截断 | 宿主 read 有单行上限 | 用 node:fs 读盘或分片读 |
|
|
149
|
-
|
|
|
149
|
+
| 覆铜/填充一律被拒(无法创建覆铜边框图元 / 无法创建填充图元) | 应用侧限制:静态+实例入口、单/复杂多边形、原始源码数组、6 种填充枚举、全参数组合全部被拒;同一多边形管线在本构建上连丝印折线都写不出来 | 不要在 API 层反复试:覆铜请在界面手工铺(GND、顶层+底层);design_audit 持续记 copper-pour warn |
|
|
150
150
|
| pcb_draw_mounting_hole 报“无法创建焊盘图元” | pad 形状必填,且槽孔只能用 ELLIPSE | 已内建:圆孔 ELLIPSE(d,d),槽孔 ELLIPSE(长,宽)+hole[SLOT,宽,长];OBLONG/RECTANGLE 焊盘会被拒 |
|
|
151
151
|
| pcb_PrimitiveString.getAll / pcb_PrimitivePad.getAll 返回 0 | 这两个 API 按层查询 | 传 layer:丝印 3/4,多层焊盘 12;不传参恒为 0 |
|
|
152
152
|
| 按坐标算的越界没被发现 | 图框/原点假设与真实页面不一致 | 用 eda_get_page_info 的真实值 + eda_capture 导 PNG 看实际版面 |
|
|
@@ -239,6 +239,13 @@
|
|
|
239
239
|
| `eda_delivery_checklist` | 一次串完 ERC + PCB 审计/DRC + Gerber + BOM,逐步 PASS/FAIL;`outPath` 落 Markdown 报告 | 交付前,代替"我说做完了" |
|
|
240
240
|
|
|
241
241
|
**交付口径**:`eda_delivery_checklist` 返回 `ok:true` 才算可以下单;任何一步 FAIL 都要先修,不许口头绕过。报告里带时间戳与计数,可直接给人复核。
|
|
242
|
+
### 交付清单怎么读(1.0.1 起)
|
|
243
|
+
|
|
244
|
+
- 一次调用会**自己切到原理图再切到 PCB**(`switchDocuments:false` 改按当前文档跑一半,另一半标注跳过原因);
|
|
245
|
+
- 返回 `verdict`:`pass`(四步全过)/ `incomplete`(有步骤因文档不对被跳过)/ `fail`(有步骤真失败);`ok` 等价于 `verdict==='pass'`;
|
|
246
|
+
- `skippedSteps` 与 `failedSteps` 分开列 —— **别把"没跑"当成"通过"**;四步全过实测约 **7.5 s**;
|
|
247
|
+
- 投板前最后一条命令就用它,`outPath` 落 Markdown 存档。
|
|
248
|
+
|
|
242
249
|
|
|
243
250
|
**参数持久化**:`eda_design_intake({answers})` 会把答复写进**工程描述**(`[dsh-eda-spec] {...}`),下次会话调用 `eda_design_intake` 会自动读回,不用重新猜;`reset:true` 清空。
|
|
244
251
|
|
package/docs/tools.md
CHANGED
|
@@ -53,7 +53,7 @@ Design limits enforced by these tools (track widths, via sizes, net naming) are
|
|
|
53
53
|
| `eda_open_document` | Open/activate a document tab by uuid (dmt_EditorControl.openDocument). Works for a schematic page or a PCB, so tab-dependent APIs can run. | `documentUuid: string, splitScreenId?: string` |
|
|
54
54
|
| `eda_get_net_labels` | List every label-like primitive on the active schematic page: attribute primitives (netlabel/netport/netflag) PLUS plain texts left by the createNetLabel fallback, so they can be audited and deleted. Reports kind/primitiveId/net per entry. | `parentPrimitiveId?: string` |
|
|
55
55
|
| `eda_schematic_audit` | Geometry audit of the active schematic page (the netlist ERC only judges connectivity): symbols outside the frame, symbols overlapping the title block, body-on-body symbol overlap, off-grid placement, and labels anchored inside a symbol body. Read-only; pair it with eda_netlist_erc before routing. | `grid?: number, includeOk?: boolean` |
|
|
56
|
-
| `eda_delivery_checklist` | Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts.
|
|
56
|
+
| `eda_delivery_checklist` | Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. It switches to the schematic and the PCB itself (switchDocuments:false keeps the current document and reports the other steps as skipped). verdict is pass / incomplete / fail; pass outPath to also write a Markdown report you can hand to a reviewer. | `outPath?: string, pcbDrc?: boolean, switchDocuments?: boolean` |
|
|
57
57
|
| `eda_design_intake` | Requirement intake for a new board: call it BEFORE placing anything when the request is vague (for example: draw me an X board). Without answers it returns the questions in 6 sections (power, core, IO, mechanical, process, delivery), each marked critical or fallback-able with the reason it changes the drawing; pass answers:{questionId:value} to record them and see what is still missing. Ask the user with ask_user_question for the critical ones instead of guessing. | `answers?: object, reset?: boolean` |
|
|
58
58
|
| `eda_netlist_erc` | Read-only electrical smoke test over the schematic NETLIST (no application DRC needed): duplicate designators, unconnected pins, single-endpoint nets, shorted parts, power rails and decoupling, USB D+/D- pair, crystal (two nets + load caps + reaches the MCU), reset RC, BOOT pull resistor, Type-C CC pull-downs. DRC can be clean while the circuit is still wrong; these failures all show up in the netlist. Pass includeOk:true to see passing checks too. | `includeOk?: boolean` |
|
|
59
59
|
| `eda_pcb_canvas_origin` | Read (or align) the PCB canvas origin and convert points between the two coordinate spaces. JLCEDA displays CANVAS coordinates in its UI but every API takes DATA coordinates; the two differ by this offset, so a coordinate read off the screen will land somewhere else unless you convert or align. align:true calls setCanvasOrigin(0,0) so the display matches the API. | `align?: boolean, offsetX?: number, offsetY?: number, point?: object` |
|
|
Binary file
|
|
@@ -212,6 +212,9 @@ function dshEdaMcpStatus() {
|
|
|
212
212
|
'pcb_PrimitivePad.create',
|
|
213
213
|
'pcb_PrimitivePad.getAll',
|
|
214
214
|
'pcb_PrimitivePad.delete',
|
|
215
|
+
'pcb_PrimitiveFill.create',
|
|
216
|
+
'pcb_PrimitiveFill.getAll',
|
|
217
|
+
'pcb_PrimitiveFill.delete',
|
|
215
218
|
'pcb_PrimitivePour.create',
|
|
216
219
|
'pcb_PrimitivePour.modify',
|
|
217
220
|
'pcb_PrimitivePour.delete',
|
|
@@ -459,21 +462,54 @@ function dshEdaMcpStatus() {
|
|
|
459
462
|
: undefined;
|
|
460
463
|
if (!pourPolygon) pourPolygon = eda.pcb_MathPolygon.createPolygon(pourSource);
|
|
461
464
|
if (!pourPolygon) throw new Error('dsh.internal.pourCreate: invalid polygon source ' + JSON.stringify(pourSource));
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
465
|
+
// Route 1: the documented static factory.
|
|
466
|
+
var pourRoutes = [];
|
|
467
|
+
var pour = null;
|
|
468
|
+
var usedRoute = 'static';
|
|
469
|
+
try {
|
|
470
|
+
pour = await eda.pcb_PrimitivePour.create(
|
|
471
|
+
optional(pourArgs[0]), optional(pourArgs[1]), pourPolygon, optional(pourArgs[3]), optional(pourArgs[4]),
|
|
472
|
+
optional(pourArgs[5]), optional(pourArgs[6]), optional(pourArgs[7]), optional(pourArgs[8])
|
|
473
|
+
);
|
|
474
|
+
} catch (staticError) {
|
|
475
|
+
pourRoutes.push('static: ' + (staticError && staticError.message ? staticError.message : String(staticError)));
|
|
476
|
+
}
|
|
477
|
+
// Route 2: the instance entry point (@internal in the typings, but the class
|
|
478
|
+
// constructor is exported at runtime on some builds).
|
|
466
479
|
if (!pour) {
|
|
467
|
-
|
|
480
|
+
var PourCtor = null;
|
|
481
|
+
try {
|
|
482
|
+
if (typeof eda.IPCB_PrimitivePour === 'function') PourCtor = eda.IPCB_PrimitivePour;
|
|
483
|
+
else if (eda.pcb_PrimitivePour && typeof eda.pcb_PrimitivePour.constructor === 'function' && eda.pcb_PrimitivePour.constructor !== Object) PourCtor = eda.pcb_PrimitivePour.constructor;
|
|
484
|
+
} catch (ctorProbeError) { PourCtor = null; }
|
|
485
|
+
if (PourCtor !== null) {
|
|
486
|
+
usedRoute = 'instance';
|
|
487
|
+
try {
|
|
488
|
+
var pourInstance = new PourCtor(
|
|
489
|
+
optional(pourArgs[0]), optional(pourArgs[1]), pourPolygon, optional(pourArgs[3]), optional(pourArgs[4]),
|
|
490
|
+
optional(pourArgs[5]), optional(pourArgs[6]), optional(pourArgs[7]), optional(pourArgs[8])
|
|
491
|
+
);
|
|
492
|
+
pour = await pourInstance.create();
|
|
493
|
+
} catch (instanceError) {
|
|
494
|
+
pourRoutes.push('instance: ' + (instanceError && instanceError.message ? instanceError.message : String(instanceError)));
|
|
495
|
+
usedRoute = 'none';
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
pourRoutes.push('instance: no exported constructor');
|
|
499
|
+
usedRoute = 'none';
|
|
500
|
+
}
|
|
468
501
|
}
|
|
469
|
-
|
|
470
|
-
|
|
502
|
+
if (!pour) {
|
|
503
|
+
throw new Error('dsh.internal.pourCreate: every route refused (' + pourRoutes.join(' | ') + ')');
|
|
504
|
+
}
|
|
505
|
+
// A pour border alone leaves no copper: rebuild so the fill (and the clearance
|
|
506
|
+
// against other nets) is actually computed.
|
|
471
507
|
var rebuilt = false;
|
|
472
508
|
if (pour && typeof pour.rebuildCopperRegion === 'function') {
|
|
473
509
|
try { await pour.rebuildCopperRegion(); rebuilt = true; } catch (error) { rebuilt = false; }
|
|
474
510
|
}
|
|
475
511
|
var pourJson = toJsonable(pour);
|
|
476
|
-
if (pourJson && typeof pourJson === 'object') pourJson.rebuilt = rebuilt;
|
|
512
|
+
if (pourJson && typeof pourJson === 'object') { pourJson.rebuilt = rebuilt; pourJson.route = usedRoute; }
|
|
477
513
|
return pourJson;
|
|
478
514
|
}
|
|
479
515
|
// Internal helper: long-running jobs (auto-routing / auto-layout) are started
|
|
@@ -579,6 +615,9 @@ function dshEdaMcpStatus() {
|
|
|
579
615
|
function () {
|
|
580
616
|
wsReady = true;
|
|
581
617
|
lastPongAt = Date.now();
|
|
618
|
+
// The 'not connected' tip is sticky by design while we are offline; once the
|
|
619
|
+
// socket is up it must go away, otherwise it sits on the canvas forever.
|
|
620
|
+
hideConnectTip();
|
|
582
621
|
sendObject({ type: 'hello', version: VERSION });
|
|
583
622
|
}
|
|
584
623
|
);
|
|
@@ -599,10 +638,11 @@ function dshEdaMcpStatus() {
|
|
|
599
638
|
registerWebSocket();
|
|
600
639
|
}
|
|
601
640
|
|
|
602
|
-
function showToast(message, type) {
|
|
641
|
+
function showToast(message, type, seconds) {
|
|
603
642
|
try {
|
|
604
643
|
if (typeof eda !== 'undefined' && eda.sys_Message && typeof eda.sys_Message.showToastMessage === 'function') {
|
|
605
|
-
|
|
644
|
+
// Always pass a finite timer: omitting it (or 0) leaves the toast on screen forever.
|
|
645
|
+
eda.sys_Message.showToastMessage(message, type || 'info', seconds || 4);
|
|
606
646
|
} else {
|
|
607
647
|
console.log('[dsh-eda-mcp] ' + message);
|
|
608
648
|
}
|
|
@@ -706,20 +746,30 @@ function dshEdaMcpStatus() {
|
|
|
706
746
|
}
|
|
707
747
|
}
|
|
708
748
|
|
|
749
|
+
// One removable tip instead of a stack of toasts: showToastMessage has no hide API
|
|
750
|
+
// (timer:0 meant "never"), so a stack of them used to pile up over the canvas and the
|
|
751
|
+
// '未连接' banner survived even after the connection was established (measured).
|
|
752
|
+
var CONNECT_TIP = 'DSH MCP 未连接:菜单 DSH MCP → 连接 DSH MCP 服务器(ws://127.0.0.1:39009/ws)';
|
|
753
|
+
|
|
754
|
+
function hideConnectTip() {
|
|
755
|
+
try {
|
|
756
|
+
if (typeof eda !== 'undefined' && eda.sys_Message && typeof eda.sys_Message.removeFollowMouseTip === 'function') {
|
|
757
|
+
eda.sys_Message.removeFollowMouseTip(CONNECT_TIP);
|
|
758
|
+
}
|
|
759
|
+
} catch (_) { /* ignore */ }
|
|
760
|
+
}
|
|
761
|
+
|
|
709
762
|
function showConnectToast() {
|
|
710
763
|
if (wsReady) return;
|
|
711
764
|
try {
|
|
712
765
|
if (typeof eda === 'undefined' || !eda.sys_Message) return;
|
|
713
|
-
eda.sys_Message.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
'连接 DSH MCP',
|
|
719
|
-
"eda.sys_MessageBus.publishPublic('dsh-eda-mcp/connect', {})"
|
|
720
|
-
);
|
|
766
|
+
if (typeof eda.sys_Message.showFollowMouseTip === 'function') {
|
|
767
|
+
eda.sys_Message.showFollowMouseTip(CONNECT_TIP, 8000);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
eda.sys_Message.showToastMessage(CONNECT_TIP, 'warning', 8);
|
|
721
771
|
} catch (error) {
|
|
722
|
-
console.error('[dsh-eda-mcp] connect
|
|
772
|
+
console.error('[dsh-eda-mcp] connect tip failed:', error);
|
|
723
773
|
}
|
|
724
774
|
}
|
|
725
775
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"uuid": "d5e0a1b2c3f4456789abcdef01234567",
|
|
3
3
|
"name": "dsh-eda-bridge",
|
|
4
4
|
"displayName": "DSH EDA MCP Bridge",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.9",
|
|
6
6
|
"description": "Local WebSocket bridge used by the dsh-eda-mcp plugin to let DeepSeek Harness draw schematics and drive PCB operations in JLCEDA Pro through the official extension API.",
|
|
7
7
|
"entry": "./api-bridge.js",
|
|
8
8
|
"publisher": "dsh-eda-mcp",
|
package/lib/bridge.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
|
|
|
3
3
|
import { execFileSync, spawn } from "node:child_process";
|
|
4
4
|
import { statSync } from "node:fs";
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
-
import { unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
import { mkdir, unlink, writeFile } from "node:fs/promises";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
8
|
import { basename, join } from "node:path";
|
|
9
9
|
import { createServer } from "node:http";
|
|
@@ -807,7 +807,7 @@ var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
|
|
|
807
807
|
].join("\n");
|
|
808
808
|
|
|
809
809
|
// src/bridge/index.ts
|
|
810
|
-
var VERSION = "0.4.
|
|
810
|
+
var VERSION = "0.4.9";
|
|
811
811
|
var WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009);
|
|
812
812
|
var WS_HOST = "127.0.0.1";
|
|
813
813
|
var ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? "1") !== "0";
|
|
@@ -1094,7 +1094,7 @@ var TOOLS = [
|
|
|
1094
1094
|
{ name: "pcb_draw_text", description: "Put text on a PCB layer - silkscreen by default (3 top silk / 4 bottom silk). Use it for pin functions, board markings, version and fabrication notes; silkscreen text must not cover pads.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, text: { type: "string" }, layer: { type: "string" }, fontSize: { type: "number" }, lineWidth: { type: "number" }, rotation: { type: "number" }, mirror: { type: "boolean" }, alignMode: { type: "integer" }, locked: { type: "boolean" } }, ["x", "y", "text"]) },
|
|
1095
1095
|
{ name: "pcb_draw_mounting_hole", description: 'Place a NON-plated mounting hole or a slot (pcb_PrimitivePad with metallization:false). shape "round" is a drill-sized hole, shape "slot" an oval cut-out. Use this for mounting holes and connector slots instead of hacking the board outline.', inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, diameter: { type: "number" }, shape: { type: "string", enum: ["round", "slot"] }, slotLength: { type: "number" }, rotation: { type: "number" }, plated: { type: "boolean" }, annulus: { type: "number" }, padNumber: { type: "string" }, layer: { type: "string" } }, ["x", "y", "diameter"]) },
|
|
1096
1096
|
{ name: "eda_schematic_audit", description: "Geometry audit of the active schematic page (the netlist ERC only judges connectivity): symbols outside the frame, symbols overlapping the title block, body-on-body symbol overlap, off-grid placement, and labels anchored inside a symbol body. Read-only; pair it with eda_netlist_erc before routing.", inputSchema: objectSchema({ grid: { type: "number" }, includeOk: { type: "boolean" } }) },
|
|
1097
|
-
{ name: "eda_delivery_checklist", description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts.
|
|
1097
|
+
{ name: "eda_delivery_checklist", description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. It switches to the schematic and the PCB itself (switchDocuments:false keeps the current document and reports the other steps as skipped). verdict is pass / incomplete / fail; pass outPath to also write a Markdown report you can hand to a reviewer.", inputSchema: objectSchema({ outPath: { type: "string" }, pcbDrc: { type: "boolean" }, switchDocuments: { type: "boolean" } }) },
|
|
1098
1098
|
{ name: "eda_design_intake", description: "Requirement intake for a new board: call it BEFORE placing anything when the request is vague (for example: draw me an X board). Without answers it returns the questions in 6 sections (power, core, IO, mechanical, process, delivery), each marked critical or fallback-able with the reason it changes the drawing; pass answers:{questionId:value} to record them and see what is still missing. Ask the user with ask_user_question for the critical ones instead of guessing.", inputSchema: objectSchema({ answers: { type: "object", additionalProperties: true }, reset: { type: "boolean" } }) },
|
|
1099
1099
|
{ name: "eda_netlist_erc", description: "Read-only electrical smoke test over the schematic NETLIST (no application DRC needed): duplicate designators, unconnected pins, single-endpoint nets, shorted parts, power rails and decoupling, USB D+/D- pair, crystal (two nets + load caps + reaches the MCU), reset RC, BOOT pull resistor, Type-C CC pull-downs. DRC can be clean while the circuit is still wrong; these failures all show up in the netlist. Pass includeOk:true to see passing checks too.", inputSchema: objectSchema({ includeOk: { type: "boolean" } }) },
|
|
1100
1100
|
{ name: "eda_pcb_canvas_origin", description: "Read (or align) the PCB canvas origin and convert points between the two coordinate spaces. JLCEDA displays CANVAS coordinates in its UI but every API takes DATA coordinates; the two differ by this offset, so a coordinate read off the screen will land somewhere else unless you convert or align. align:true calls setCanvasOrigin(0,0) so the display matches the API.", inputSchema: objectSchema({ align: { type: "boolean" }, offsetX: { type: "number" }, offsetY: { type: "number" }, point: { type: "object", additionalProperties: true } }) },
|
|
@@ -1509,6 +1509,13 @@ var BRIEF_TAG = "[dsh-eda-spec]";
|
|
|
1509
1509
|
function briefBlock(brief) {
|
|
1510
1510
|
return BRIEF_TAG + " " + JSON.stringify(brief);
|
|
1511
1511
|
}
|
|
1512
|
+
function specFilePath(project) {
|
|
1513
|
+
const dir = String(project?.teamUuid ?? "");
|
|
1514
|
+
const uuid2 = String(project?.uuid ?? "");
|
|
1515
|
+
if (dir === "" || uuid2 === "") return "";
|
|
1516
|
+
if (!/^[A-Za-z]:[\\/]/.test(dir) && !dir.startsWith("/")) return "";
|
|
1517
|
+
return join(dir, ".dsh-eda-spec", uuid2 + ".json");
|
|
1518
|
+
}
|
|
1512
1519
|
function parseBriefBlock(description) {
|
|
1513
1520
|
const text = typeof description === "string" ? description : "";
|
|
1514
1521
|
const index = text.indexOf(BRIEF_TAG);
|
|
@@ -1589,7 +1596,7 @@ async function auditPcb(args, timeoutMs) {
|
|
|
1589
1596
|
const vias = await pcbPrimitiveList("pcb_PrimitiveVia.getAll", [netFilter, void 0], timeoutMs);
|
|
1590
1597
|
const pours = await pcbPrimitiveList("pcb_PrimitivePour.getAll", [netFilter, void 0, void 0], timeoutMs);
|
|
1591
1598
|
const outline = await pcbPrimitiveList("pcb_PrimitivePolyline.getAll", [void 0, PCB_LAYERS.boardOutline, void 0], timeoutMs);
|
|
1592
|
-
const pcbComponents = await
|
|
1599
|
+
const pcbComponents = await callExtension("pcb_PrimitiveComponent.getAll", [resolvePcbLayer(void 0), void 0], timeoutMs).then((raw) => Array.isArray(raw) ? raw : []).catch(() => []);
|
|
1593
1600
|
const violations = [];
|
|
1594
1601
|
const warnings = [];
|
|
1595
1602
|
const add = (bucket, rule, message, samples) => {
|
|
@@ -1726,7 +1733,7 @@ async function auditPcb(args, timeoutMs) {
|
|
|
1726
1733
|
];
|
|
1727
1734
|
return {
|
|
1728
1735
|
ok: violations.length === 0,
|
|
1729
|
-
counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length },
|
|
1736
|
+
counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length, componentsRead: pcbComponents.length },
|
|
1730
1737
|
minWidthByNet: Object.fromEntries([...widthByNet.entries()].map(([net, width]) => [net, { widthMil: width, role: classifyNet(net), suggestedMil: suggestTrackWidthMil(net) }])),
|
|
1731
1738
|
violations,
|
|
1732
1739
|
warnings,
|
|
@@ -3078,23 +3085,33 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3078
3085
|
}
|
|
3079
3086
|
case "eda_schematic_audit": {
|
|
3080
3087
|
const paper = paperOf(args.paper);
|
|
3088
|
+
const frame = paper.frame;
|
|
3089
|
+
const frameArea = (frame.right - frame.left) * (frame.top - frame.bottom);
|
|
3081
3090
|
const rawList = await callExtension("sch_PrimitiveComponent.getAll", [], timeoutMs);
|
|
3082
3091
|
const list = Array.isArray(rawList) ? rawList.slice(0, 200) : [];
|
|
3083
3092
|
const components = [];
|
|
3093
|
+
let droppedSheetPrimitives = 0;
|
|
3084
3094
|
for (let index = 0; index < list.length; index += 1) {
|
|
3085
3095
|
const entry = list[index] ?? {};
|
|
3086
|
-
const designator = String(entry.designator ?? entry.Designator ?? entry.name ?? "#" + index);
|
|
3096
|
+
const designator = String(entry.designator ?? entry.Designator ?? entry.Name ?? entry.name ?? "#" + index);
|
|
3087
3097
|
const id = String(entry.primitiveId ?? entry.PrimitiveId ?? "");
|
|
3088
3098
|
let bounds;
|
|
3089
3099
|
if (id !== "") {
|
|
3090
3100
|
const box = await bboxOf([id], timeoutMs);
|
|
3091
3101
|
if (box !== void 0) bounds = { left: box.minX, right: box.maxX, bottom: box.minY, top: box.maxY };
|
|
3092
3102
|
}
|
|
3103
|
+
if (bounds !== void 0) {
|
|
3104
|
+
const boundsArea = (bounds.right - bounds.left) * (bounds.top - bounds.bottom);
|
|
3105
|
+
if (boundsArea > frameArea * 0.4) {
|
|
3106
|
+
bounds = void 0;
|
|
3107
|
+
droppedSheetPrimitives += 1;
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3093
3110
|
components.push({ designator, x: Number(entry.x ?? entry.X ?? 0), y: Number(entry.y ?? entry.Y ?? 0), bounds });
|
|
3094
3111
|
}
|
|
3095
3112
|
const report = auditSchematicGeometry({
|
|
3096
3113
|
components,
|
|
3097
|
-
frame
|
|
3114
|
+
frame,
|
|
3098
3115
|
titleBlock: paper.titleBlock,
|
|
3099
3116
|
grid: args.grid === void 0 ? 10 : Number(args.grid)
|
|
3100
3117
|
});
|
|
@@ -3104,57 +3121,111 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3104
3121
|
counts: report.counts,
|
|
3105
3122
|
componentCount: report.componentCount,
|
|
3106
3123
|
boxesRead: components.filter((component) => component.bounds !== void 0).length,
|
|
3107
|
-
|
|
3124
|
+
droppedSheetPrimitives,
|
|
3125
|
+
checks,
|
|
3126
|
+
notes: droppedSheetPrimitives > 0 ? ["Skipped " + droppedSheetPrimitives + " primitive(s) whose box covers the whole sheet (frame/title block); counting them made every symbol look overlapped."] : []
|
|
3108
3127
|
};
|
|
3109
3128
|
}
|
|
3110
3129
|
case "eda_delivery_checklist": {
|
|
3111
3130
|
const steps = [];
|
|
3112
3131
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3132
|
+
const project = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs).catch(() => null);
|
|
3133
|
+
const boards = Array.isArray(project?.data) ? project.data : [];
|
|
3134
|
+
const schematicPage = boards[0]?.schematic?.page?.[0]?.uuid ?? "";
|
|
3135
|
+
const pcbUuid = boards.find((board) => board?.pcb?.uuid)?.pcb?.uuid ?? "";
|
|
3136
|
+
const switchDocuments = args.switchDocuments !== false;
|
|
3137
|
+
const documentType = async () => {
|
|
3138
|
+
try {
|
|
3139
|
+
const info = await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], timeoutMs);
|
|
3140
|
+
return Number(info?.documentType);
|
|
3141
|
+
} catch {
|
|
3142
|
+
return NaN;
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
3145
|
+
const activate = async (uuid2) => {
|
|
3146
|
+
if (typeof uuid2 !== "string" || uuid2 === "") return false;
|
|
3147
|
+
try {
|
|
3148
|
+
await callExtension("dmt_EditorControl.openDocument", [uuid2], timeoutMs);
|
|
3149
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
3150
|
+
return true;
|
|
3151
|
+
} catch {
|
|
3152
|
+
return false;
|
|
3153
|
+
}
|
|
3154
|
+
};
|
|
3113
3155
|
let netlistErc = null;
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
steps.push({ step: "
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3156
|
+
const onSchematic = switchDocuments ? await activate(schematicPage) : await documentType() === 1;
|
|
3157
|
+
if (!onSchematic) {
|
|
3158
|
+
steps.push({ step: "netlist-erc", document: "schematic", ok: false, skipped: "schematic page is not the active document" });
|
|
3159
|
+
steps.push({ step: "bom", document: "schematic", ok: false, skipped: "schematic page is not the active document" });
|
|
3160
|
+
} else {
|
|
3161
|
+
try {
|
|
3162
|
+
const parsedNetlist = await fetchParsedNetlist(timeoutMs);
|
|
3163
|
+
netlistErc = runNetlistErc(parsedNetlist);
|
|
3164
|
+
steps.push({
|
|
3165
|
+
step: "netlist-erc",
|
|
3166
|
+
document: "schematic",
|
|
3167
|
+
ok: netlistErc.ok,
|
|
3168
|
+
detail: netlistErc.counts,
|
|
3169
|
+
failing: netlistErc.checks.filter((check) => check.status === "fail").map((check) => check.id)
|
|
3170
|
+
});
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
steps.push({ step: "netlist-erc", document: "schematic", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
let bom = null;
|
|
3176
|
+
if (onSchematic) {
|
|
3177
|
+
try {
|
|
3178
|
+
const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, "csv"], timeoutMs);
|
|
3179
|
+
const text = typeof payload === "string" ? payload : typeof payload?.text === "string" ? payload.text : "";
|
|
3180
|
+
const lines = text === "" ? 0 : text.split(/\r?\n/).filter((line) => line.trim() !== "").length;
|
|
3181
|
+
bom = { rows: Math.max(0, lines - 1), ok: lines > 1 };
|
|
3182
|
+
steps.push({ step: "bom", document: "schematic", ok: bom.ok, detail: { rows: bom.rows }, ...bom.ok ? {} : { error: "no BOM rows exported" } });
|
|
3183
|
+
} catch (error) {
|
|
3184
|
+
steps.push({ step: "bom", document: "schematic", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3185
|
+
}
|
|
3120
3186
|
}
|
|
3187
|
+
const onPcb = switchDocuments ? await activate(pcbUuid) : await documentType() === 3;
|
|
3121
3188
|
let pcbAudit = null;
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
steps.push({
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3189
|
+
if (!onPcb) {
|
|
3190
|
+
steps.push({ step: "pcb-audit", document: "pcb", ok: false, skipped: "PCB is not the active document" });
|
|
3191
|
+
steps.push({ step: "gerber", document: "pcb", ok: false, skipped: "PCB is not the active document" });
|
|
3192
|
+
} else {
|
|
3193
|
+
try {
|
|
3194
|
+
pcbAudit = await auditPcb({ drc: args.pcbDrc !== false }, timeoutMs);
|
|
3195
|
+
steps.push({
|
|
3196
|
+
step: "pcb-audit",
|
|
3197
|
+
document: "pcb",
|
|
3198
|
+
ok: (pcbAudit.violations ?? []).length === 0,
|
|
3199
|
+
detail: { violations: pcbAudit.violations?.length ?? null, warnings: pcbAudit.warnings?.length ?? null, drc: pcbAudit.drc },
|
|
3200
|
+
failing: (pcbAudit.violations ?? []).map((entry) => entry.rule)
|
|
3201
|
+
});
|
|
3202
|
+
} catch (error) {
|
|
3203
|
+
steps.push({ step: "pcb-audit", document: "pcb", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3204
|
+
}
|
|
3132
3205
|
}
|
|
3133
3206
|
let gerber = null;
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
try {
|
|
3144
|
-
const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, "csv"], timeoutMs);
|
|
3145
|
-
const text = typeof payload === "string" ? payload : typeof payload?.text === "string" ? payload.text : "";
|
|
3146
|
-
const lines = text === "" ? 0 : text.split(/\r?\n/).filter((line) => line.trim() !== "").length;
|
|
3147
|
-
bom = { rows: Math.max(0, lines - 1), ok: lines > 1 };
|
|
3148
|
-
steps.push({ step: "bom", ok: bom.ok, detail: { rows: bom.rows } });
|
|
3149
|
-
} catch (error) {
|
|
3150
|
-
steps.push({ step: "bom", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3207
|
+
if (onPcb) {
|
|
3208
|
+
try {
|
|
3209
|
+
const payload = await callExtension("pcb_ManufactureData.getGerberFile", [void 0, void 0, void 0, void 0, void 0, void 0], timeoutMs);
|
|
3210
|
+
const bytes = payload && typeof payload === "object" ? Number(payload.size ?? payload.bytes ?? 0) : typeof payload === "string" ? payload.length : 0;
|
|
3211
|
+
gerber = { bytes, ok: bytes > 1e3 };
|
|
3212
|
+
steps.push({ step: "gerber", document: "pcb", ok: gerber.ok, detail: { bytes } });
|
|
3213
|
+
} catch (error) {
|
|
3214
|
+
steps.push({ step: "gerber", document: "pcb", ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
3215
|
+
}
|
|
3151
3216
|
}
|
|
3152
|
-
const
|
|
3217
|
+
const failedSteps = steps.filter((step) => step.ok !== true && step.skipped === void 0);
|
|
3218
|
+
const skippedSteps = steps.filter((step) => step.ok !== true && step.skipped !== void 0);
|
|
3219
|
+
const verdict = failedSteps.length > 0 ? "fail" : skippedSteps.length > 0 ? "incomplete" : "pass";
|
|
3153
3220
|
const report = {
|
|
3154
|
-
ok:
|
|
3221
|
+
ok: verdict === "pass",
|
|
3222
|
+
verdict,
|
|
3223
|
+
failedSteps: failedSteps.map((step) => step.step),
|
|
3224
|
+
skippedSteps: skippedSteps.map((step) => step.step),
|
|
3225
|
+
next: verdict === "pass" ? "Every step passed: the board can be ordered." : verdict === "incomplete" ? "Run the checklist again with the other document active (" + skippedSteps.map((step) => step.document).filter((value, index, all) => all.indexOf(value) === index).join(", ") + " needed)." : "Fix the failing steps first; skipped steps are listed separately.",
|
|
3155
3226
|
startedAt,
|
|
3156
3227
|
finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3157
|
-
|
|
3228
|
+
documents: { schematicPage, pcb: pcbUuid, switched: switchDocuments },
|
|
3158
3229
|
netlistErc: netlistErc === null ? null : { ok: netlistErc.ok, counts: netlistErc.counts, componentCount: netlistErc.componentCount, netCount: netlistErc.netCount },
|
|
3159
3230
|
pcbAudit: pcbAudit === null ? null : { violations: pcbAudit.violations, warnings: pcbAudit.warnings },
|
|
3160
3231
|
gerber,
|
|
@@ -3168,10 +3239,12 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3168
3239
|
"- \u5F00\u59CB\uFF1A" + report.startedAt,
|
|
3169
3240
|
"- \u7ED3\u675F\uFF1A" + report.finishedAt,
|
|
3170
3241
|
"- \u7ED3\u8BBA\uFF1A" + (report.ok ? "PASS\uFF08\u53EF\u4EE5\u4E0B\u5355\uFF09" : "FAIL\uFF08\u6709\u672A\u901A\u8FC7\u9879\uFF09"),
|
|
3242
|
+
"- \u539F\u7406\u56FE\u9875\uFF1A" + (schematicPage || "\u672A\u53D6\u5230"),
|
|
3243
|
+
"- PCB\uFF1A" + (pcbUuid || "\u672A\u53D6\u5230"),
|
|
3171
3244
|
"",
|
|
3172
|
-
"| \u6B65\u9AA4 | \u7ED3\u679C | \u8BF4\u660E |",
|
|
3173
|
-
"
|
|
3174
|
-
...steps.map((step) => "| " + step.step + " | " + (step.ok === true ? "PASS" : "FAIL") + " | " + JSON.stringify(step.detail ?? step.error ?? {}) + " |"),
|
|
3245
|
+
"| \u6B65\u9AA4 | \u6587\u6863 | \u7ED3\u679C | \u8BF4\u660E |",
|
|
3246
|
+
"|---|---|---|---|",
|
|
3247
|
+
...steps.map((step) => "| " + step.step + " | " + step.document + " | " + (step.ok === true ? "PASS" : "FAIL") + " | " + JSON.stringify(step.detail ?? step.error ?? {}) + " |"),
|
|
3175
3248
|
"",
|
|
3176
3249
|
"## \u7535\u6C14\u68C0\u67E5\uFF08eda_netlist_erc\uFF09",
|
|
3177
3250
|
"",
|
|
@@ -3212,17 +3285,43 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3212
3285
|
const merged = { ...designBrief ?? {}, ...answersReport.brief };
|
|
3213
3286
|
const mergedReport = buildIntake(merged);
|
|
3214
3287
|
let persisted = false;
|
|
3288
|
+
let persistVia = "none";
|
|
3289
|
+
let persistPath = "";
|
|
3215
3290
|
if (Object.keys(merged).length > 0) {
|
|
3216
3291
|
designBrief = merged;
|
|
3217
3292
|
if (answers !== null) {
|
|
3218
3293
|
try {
|
|
3219
3294
|
const project = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs);
|
|
3295
|
+
const projectUuid = String(project?.uuid ?? "");
|
|
3220
3296
|
const existing = typeof project?.description === "string" ? project.description : "";
|
|
3221
3297
|
const index = existing.indexOf(BRIEF_TAG);
|
|
3222
3298
|
const base = index < 0 ? existing : existing.slice(0, index).trimEnd();
|
|
3223
3299
|
const next = (base === "" ? "" : base + "\n") + briefBlock(merged);
|
|
3224
|
-
|
|
3225
|
-
|
|
3300
|
+
let stored = false;
|
|
3301
|
+
if (projectUuid !== "") {
|
|
3302
|
+
try {
|
|
3303
|
+
await callExtension("dmt_Project.modifyProjectDescription", [projectUuid, next], timeoutMs);
|
|
3304
|
+
const readBack = await callExtension("dmt_Project.getCurrentProjectInfo", [], timeoutMs);
|
|
3305
|
+
stored = String(readBack?.description ?? "").includes(BRIEF_TAG);
|
|
3306
|
+
if (stored) persistVia = "description";
|
|
3307
|
+
} catch {
|
|
3308
|
+
stored = false;
|
|
3309
|
+
}
|
|
3310
|
+
}
|
|
3311
|
+
if (!stored) {
|
|
3312
|
+
const sidecar = specFilePath(project);
|
|
3313
|
+
if (sidecar !== "") {
|
|
3314
|
+
await mkdir(join(sidecar, ".."), { recursive: true });
|
|
3315
|
+
await writeFile(sidecar, JSON.stringify({ projectUuid, brief: merged }, null, 2) + "\n");
|
|
3316
|
+
const check = JSON.parse(await readFile(sidecar, "utf8"));
|
|
3317
|
+
stored = check !== null && typeof check === "object" && Object.keys(merged).every((key) => check.brief?.[key] === merged[key]);
|
|
3318
|
+
if (stored) {
|
|
3319
|
+
persistVia = "file";
|
|
3320
|
+
persistPath = sidecar;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
persisted = stored === true;
|
|
3226
3325
|
} catch {
|
|
3227
3326
|
persisted = false;
|
|
3228
3327
|
}
|
|
@@ -3238,6 +3337,8 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3238
3337
|
brief: mergedReport.brief,
|
|
3239
3338
|
hydratedFromProject: answers === null && designBrief !== null,
|
|
3240
3339
|
persisted,
|
|
3340
|
+
persistVia,
|
|
3341
|
+
persistPath,
|
|
3241
3342
|
sections: mergedReport.sections.filter((section) => section.questions.some((question) => mergedReport.brief[question.id] === void 0)),
|
|
3242
3343
|
next: mergedReport.ready ? "All critical parameters are known: lay out the schematic, then eda_netlist_erc before routing." : "Ask the user for the missing critical parameters with ask_user_question, grouped by section, before placing parts. Do not invent values."
|
|
3243
3344
|
};
|
|
@@ -3682,7 +3783,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3682
3783
|
raw = await attempt("solid", false);
|
|
3683
3784
|
} catch (secondFailure) {
|
|
3684
3785
|
const detail = secondFailure instanceof Error ? secondFailure.message : String(secondFailure);
|
|
3685
|
-
throw new Error("eda_pcb_create_pour failed twice (first: " + (firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + "; retry: " + detail + "). polygonStage=" + (polygonOk ? "ok" : "failed") + ": " + (polygonOk ? "the app accepted the outline but refused the pour itself, so
|
|
3786
|
+
throw new Error("eda_pcb_create_pour failed twice (first: " + (firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + "; retry: " + detail + "). polygonStage=" + (polygonOk ? "ok" : "failed") + ": " + (polygonOk ? "the app accepted the outline but refused the pour itself, and so does the complex polygon the docs require (createComplexPolygon); single and complex polygons, every net/layer/fill combination, all refused, so pour creation is an application limit on this build, not a coordinate problem. Pour by hand in the PCB editor (\u8986\u94DC \u2192 select GND \u2192 top + bottom); eda_design_audit reports it as a warn until then." : "the outline polygon was rejected too: check that every point is inside the board outline and flat [x1,y1,...].") + " retried=" + retried);
|
|
3686
3787
|
}
|
|
3687
3788
|
}
|
|
3688
3789
|
return {
|
package/lib/index.js
CHANGED
|
@@ -179,7 +179,7 @@ var SKILL_PLAYBOOK_LINES = [
|
|
|
179
179
|
"| eda_launch \u5E26 project \u6CA1\u6253\u5F00\u5DE5\u7A0B | \u547D\u4EE4\u884C\u8DEF\u5F84\u88AB\u5E94\u7528\u5FFD\u7565 | \u7528 eda_open_project({ name }) \u6253\u5F00 |",
|
|
180
180
|
"| \u5173\u5E94\u7528\u540E\u6539\u52A8\u4E22\u5931 | eda_close \u8D70\u4E86\u5F3A\u5236\u8DEF\u5F84 | \u5173\u524D\u5148\u4FDD\u5B58\uFF1B\u770B\u8FD4\u56DE\u7684 forced |",
|
|
181
181
|
"| run_code \u8BFB\u5927 JSON \u88AB\u622A\u65AD | \u5BBF\u4E3B read \u6709\u5355\u884C\u4E0A\u9650 | \u7528 node:fs \u8BFB\u76D8\u6216\u5206\u7247\u8BFB |",
|
|
182
|
-
"| \u8986\u94DC\
|
|
182
|
+
"| \u8986\u94DC/\u586B\u5145\u4E00\u5F8B\u88AB\u62D2\uFF08\u65E0\u6CD5\u521B\u5EFA\u8986\u94DC\u8FB9\u6846\u56FE\u5143 / \u65E0\u6CD5\u521B\u5EFA\u586B\u5145\u56FE\u5143\uFF09 | \u5E94\u7528\u4FA7\u9650\u5236\uFF1A\u9759\u6001+\u5B9E\u4F8B\u5165\u53E3\u3001\u5355/\u590D\u6742\u591A\u8FB9\u5F62\u3001\u539F\u59CB\u6E90\u7801\u6570\u7EC4\u30016 \u79CD\u586B\u5145\u679A\u4E3E\u3001\u5168\u53C2\u6570\u7EC4\u5408\u5168\u90E8\u88AB\u62D2\uFF1B\u540C\u4E00\u591A\u8FB9\u5F62\u7BA1\u7EBF\u5728\u672C\u6784\u5EFA\u4E0A\u8FDE\u4E1D\u5370\u6298\u7EBF\u90FD\u5199\u4E0D\u51FA\u6765 | \u4E0D\u8981\u5728 API \u5C42\u53CD\u590D\u8BD5\uFF1A\u8986\u94DC\u8BF7\u5728\u754C\u9762\u624B\u5DE5\u94FA\uFF08GND\u3001\u9876\u5C42+\u5E95\u5C42\uFF09\uFF1Bdesign_audit \u6301\u7EED\u8BB0 copper-pour warn |",
|
|
183
183
|
"| pcb_draw_mounting_hole \u62A5\u201C\u65E0\u6CD5\u521B\u5EFA\u710A\u76D8\u56FE\u5143\u201D | pad \u5F62\u72B6\u5FC5\u586B\uFF0C\u4E14\u69FD\u5B54\u53EA\u80FD\u7528 ELLIPSE | \u5DF2\u5185\u5EFA\uFF1A\u5706\u5B54 ELLIPSE(d,d)\uFF0C\u69FD\u5B54 ELLIPSE(\u957F,\u5BBD)+hole[SLOT,\u5BBD,\u957F]\uFF1BOBLONG/RECTANGLE \u710A\u76D8\u4F1A\u88AB\u62D2 |",
|
|
184
184
|
"| pcb_PrimitiveString.getAll / pcb_PrimitivePad.getAll \u8FD4\u56DE 0 | \u8FD9\u4E24\u4E2A API \u6309\u5C42\u67E5\u8BE2 | \u4F20 layer\uFF1A\u4E1D\u5370 3/4\uFF0C\u591A\u5C42\u710A\u76D8 12\uFF1B\u4E0D\u4F20\u53C2\u6052\u4E3A 0 |",
|
|
185
185
|
"| \u6309\u5750\u6807\u7B97\u7684\u8D8A\u754C\u6CA1\u88AB\u53D1\u73B0 | \u56FE\u6846/\u539F\u70B9\u5047\u8BBE\u4E0E\u771F\u5B9E\u9875\u9762\u4E0D\u4E00\u81F4 | \u7528 eda_get_page_info \u7684\u771F\u5B9E\u503C + eda_capture \u5BFC PNG \u770B\u5B9E\u9645\u7248\u9762 |",
|
|
@@ -272,6 +272,13 @@ var SKILL_PLAYBOOK_LINES = [
|
|
|
272
272
|
'| `eda_delivery_checklist` | \u4E00\u6B21\u4E32\u5B8C ERC + PCB \u5BA1\u8BA1/DRC + Gerber + BOM\uFF0C\u9010\u6B65 PASS/FAIL\uFF1B`outPath` \u843D Markdown \u62A5\u544A | \u4EA4\u4ED8\u524D\uFF0C\u4EE3\u66FF"\u6211\u8BF4\u505A\u5B8C\u4E86" |',
|
|
273
273
|
"",
|
|
274
274
|
"**\u4EA4\u4ED8\u53E3\u5F84**\uFF1A`eda_delivery_checklist` \u8FD4\u56DE `ok:true` \u624D\u7B97\u53EF\u4EE5\u4E0B\u5355\uFF1B\u4EFB\u4F55\u4E00\u6B65 FAIL \u90FD\u8981\u5148\u4FEE\uFF0C\u4E0D\u8BB8\u53E3\u5934\u7ED5\u8FC7\u3002\u62A5\u544A\u91CC\u5E26\u65F6\u95F4\u6233\u4E0E\u8BA1\u6570\uFF0C\u53EF\u76F4\u63A5\u7ED9\u4EBA\u590D\u6838\u3002",
|
|
275
|
+
"### \u4EA4\u4ED8\u6E05\u5355\u600E\u4E48\u8BFB\uFF081.0.1 \u8D77\uFF09",
|
|
276
|
+
"",
|
|
277
|
+
"- \u4E00\u6B21\u8C03\u7528\u4F1A**\u81EA\u5DF1\u5207\u5230\u539F\u7406\u56FE\u518D\u5207\u5230 PCB**\uFF08`switchDocuments:false` \u6539\u6309\u5F53\u524D\u6587\u6863\u8DD1\u4E00\u534A\uFF0C\u53E6\u4E00\u534A\u6807\u6CE8\u8DF3\u8FC7\u539F\u56E0\uFF09\uFF1B",
|
|
278
|
+
"- \u8FD4\u56DE `verdict`\uFF1A`pass`\uFF08\u56DB\u6B65\u5168\u8FC7\uFF09/ `incomplete`\uFF08\u6709\u6B65\u9AA4\u56E0\u6587\u6863\u4E0D\u5BF9\u88AB\u8DF3\u8FC7\uFF09/ `fail`\uFF08\u6709\u6B65\u9AA4\u771F\u5931\u8D25\uFF09\uFF1B`ok` \u7B49\u4EF7\u4E8E `verdict==='pass'`\uFF1B",
|
|
279
|
+
'- `skippedSteps` \u4E0E `failedSteps` \u5206\u5F00\u5217 \u2014\u2014 **\u522B\u628A"\u6CA1\u8DD1"\u5F53\u6210"\u901A\u8FC7"**\uFF1B\u56DB\u6B65\u5168\u8FC7\u5B9E\u6D4B\u7EA6 **7.5 s**\uFF1B',
|
|
280
|
+
"- \u6295\u677F\u524D\u6700\u540E\u4E00\u6761\u547D\u4EE4\u5C31\u7528\u5B83\uFF0C`outPath` \u843D Markdown \u5B58\u6863\u3002",
|
|
281
|
+
"",
|
|
275
282
|
"",
|
|
276
283
|
"**\u53C2\u6570\u6301\u4E45\u5316**\uFF1A`eda_design_intake({answers})` \u4F1A\u628A\u7B54\u590D\u5199\u8FDB**\u5DE5\u7A0B\u63CF\u8FF0**\uFF08`[dsh-eda-spec] {...}`\uFF09\uFF0C\u4E0B\u6B21\u4F1A\u8BDD\u8C03\u7528 `eda_design_intake` \u4F1A\u81EA\u52A8\u8BFB\u56DE\uFF0C\u4E0D\u7528\u91CD\u65B0\u731C\uFF1B`reset:true` \u6E05\u7A7A\u3002",
|
|
277
284
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-eda-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "DeepSeek Harness (DSH) plugin — connects DSH to JLCEDA Pro (EasyEDA Pro) via a local MCP bridge and draws schematics through the JLCEDA extension API.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": {
|
package/src/bridge/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createHash } from 'node:crypto'
|
|
|
3
3
|
import { execFileSync, spawn } from 'node:child_process'
|
|
4
4
|
import { existsSync, statSync } from 'node:fs'
|
|
5
5
|
import { readFile } from 'node:fs/promises'
|
|
6
|
-
import { unlink, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { mkdir, unlink, writeFile } from 'node:fs/promises'
|
|
7
7
|
import { tmpdir } from 'node:os'
|
|
8
8
|
import { basename, join } from 'node:path'
|
|
9
9
|
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
} from './design-rules.ts'
|
|
22
22
|
import { WORKFLOW_GUIDE, WORKFLOW_SUMMARY } from './workflow-guide.ts'
|
|
23
23
|
|
|
24
|
-
const VERSION = '0.4.
|
|
24
|
+
const VERSION = '0.4.9'
|
|
25
25
|
const WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009)
|
|
26
26
|
const WS_HOST = '127.0.0.1'
|
|
27
27
|
const ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? '1') !== '0'
|
|
@@ -318,7 +318,7 @@ const TOOLS = [
|
|
|
318
318
|
{ name: 'pcb_draw_text', description: 'Put text on a PCB layer - silkscreen by default (3 top silk / 4 bottom silk). Use it for pin functions, board markings, version and fabrication notes; silkscreen text must not cover pads.', inputSchema: objectSchema({ x: { type: 'number' }, y: { type: 'number' }, text: { type: 'string' }, layer: { type: 'string' }, fontSize: { type: 'number' }, lineWidth: { type: 'number' }, rotation: { type: 'number' }, mirror: { type: 'boolean' }, alignMode: { type: 'integer' }, locked: { type: 'boolean' } }, ['x', 'y', 'text']) },
|
|
319
319
|
{ name: 'pcb_draw_mounting_hole', description: 'Place a NON-plated mounting hole or a slot (pcb_PrimitivePad with metallization:false). shape "round" is a drill-sized hole, shape "slot" an oval cut-out. Use this for mounting holes and connector slots instead of hacking the board outline.', inputSchema: objectSchema({ x: { type: 'number' }, y: { type: 'number' }, diameter: { type: 'number' }, shape: { type: 'string', enum: ['round', 'slot'] }, slotLength: { type: 'number' }, rotation: { type: 'number' }, plated: { type: 'boolean' }, annulus: { type: 'number' }, padNumber: { type: 'string' }, layer: { type: 'string' } }, ['x', 'y', 'diameter']) },
|
|
320
320
|
{ name: 'eda_schematic_audit', description: "Geometry audit of the active schematic page (the netlist ERC only judges connectivity): symbols outside the frame, symbols overlapping the title block, body-on-body symbol overlap, off-grid placement, and labels anchored inside a symbol body. Read-only; pair it with eda_netlist_erc before routing.", inputSchema: objectSchema({ grid: { type: 'number' }, includeOk: { type: 'boolean' } }) },
|
|
321
|
-
{ name: 'eda_delivery_checklist', description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts.
|
|
321
|
+
{ name: 'eda_delivery_checklist', description: "Delivery checklist: run connectivity (eda_netlist_erc), PCB audit + DRC, Gerber export and BOM export in ONE read-only call and report per-step pass/fail with counts. It switches to the schematic and the PCB itself (switchDocuments:false keeps the current document and reports the other steps as skipped). verdict is pass / incomplete / fail; pass outPath to also write a Markdown report you can hand to a reviewer.", inputSchema: objectSchema({ outPath: { type: 'string' }, pcbDrc: { type: 'boolean' }, switchDocuments: { type: 'boolean' } }) },
|
|
322
322
|
{ name: 'eda_design_intake', description: "Requirement intake for a new board: call it BEFORE placing anything when the request is vague (for example: draw me an X board). Without answers it returns the questions in 6 sections (power, core, IO, mechanical, process, delivery), each marked critical or fallback-able with the reason it changes the drawing; pass answers:{questionId:value} to record them and see what is still missing. Ask the user with ask_user_question for the critical ones instead of guessing.", inputSchema: objectSchema({ answers: { type: 'object', additionalProperties: true }, reset: { type: 'boolean' } }) },
|
|
323
323
|
{ name: 'eda_netlist_erc', description: 'Read-only electrical smoke test over the schematic NETLIST (no application DRC needed): duplicate designators, unconnected pins, single-endpoint nets, shorted parts, power rails and decoupling, USB D+/D- pair, crystal (two nets + load caps + reaches the MCU), reset RC, BOOT pull resistor, Type-C CC pull-downs. DRC can be clean while the circuit is still wrong; these failures all show up in the netlist. Pass includeOk:true to see passing checks too.', inputSchema: objectSchema({ includeOk: { type: 'boolean' } }) },
|
|
324
324
|
{ name: 'eda_pcb_canvas_origin', description: 'Read (or align) the PCB canvas origin and convert points between the two coordinate spaces. JLCEDA displays CANVAS coordinates in its UI but every API takes DATA coordinates; the two differ by this offset, so a coordinate read off the screen will land somewhere else unless you convert or align. align:true calls setCanvasOrigin(0,0) so the display matches the API.', inputSchema: objectSchema({ align: { type: 'boolean' }, offsetX: { type: 'number' }, offsetY: { type: 'number' }, point: { type: 'object', additionalProperties: true } }) },
|
|
@@ -754,6 +754,15 @@ const BRIEF_TAG = '[dsh-eda-spec]'
|
|
|
754
754
|
function briefBlock(brief) {
|
|
755
755
|
return BRIEF_TAG + ' ' + JSON.stringify(brief)
|
|
756
756
|
}
|
|
757
|
+
/** Sidecar file for the design brief, next to a local project directory. */
|
|
758
|
+
function specFilePath(project) {
|
|
759
|
+
const dir = String(project?.teamUuid ?? '')
|
|
760
|
+
const uuid = String(project?.uuid ?? '')
|
|
761
|
+
if (dir === '' || uuid === '') return ''
|
|
762
|
+
if (!/^[A-Za-z]:[\\/]/.test(dir) && !dir.startsWith('/')) return ''
|
|
763
|
+
return join(dir, '.dsh-eda-spec', uuid + '.json')
|
|
764
|
+
}
|
|
765
|
+
|
|
757
766
|
function parseBriefBlock(description) {
|
|
758
767
|
const text = typeof description === 'string' ? description : ''
|
|
759
768
|
const index = text.indexOf(BRIEF_TAG)
|
|
@@ -845,7 +854,11 @@ async function auditPcb(args, timeoutMs) {
|
|
|
845
854
|
const vias = await pcbPrimitiveList('pcb_PrimitiveVia.getAll', [netFilter, undefined], timeoutMs)
|
|
846
855
|
const pours = await pcbPrimitiveList('pcb_PrimitivePour.getAll', [netFilter, undefined, undefined], timeoutMs)
|
|
847
856
|
const outline = await pcbPrimitiveList('pcb_PrimitivePolyline.getAll', [undefined, PCB_LAYERS.boardOutline, undefined], timeoutMs)
|
|
848
|
-
|
|
857
|
+
// Same call shape as pcb_get_components: with a bare [undefined, undefined] the app
|
|
858
|
+
// returns an empty list here, which silently disabled the mechanical checks below.
|
|
859
|
+
const pcbComponents = await callExtension('pcb_PrimitiveComponent.getAll', [resolvePcbLayer(undefined), undefined], timeoutMs)
|
|
860
|
+
.then((raw) => (Array.isArray(raw) ? raw : []))
|
|
861
|
+
.catch(() => [])
|
|
849
862
|
|
|
850
863
|
const violations = []
|
|
851
864
|
const warnings = []
|
|
@@ -996,7 +1009,7 @@ async function auditPcb(args, timeoutMs) {
|
|
|
996
1009
|
|
|
997
1010
|
return {
|
|
998
1011
|
ok: violations.length === 0,
|
|
999
|
-
counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length },
|
|
1012
|
+
counts: { tracks: tracks.length, vias: vias.length, pours: pours.length, outlinePolylines: outline.length, nets: netNames.length, componentsRead: pcbComponents.length },
|
|
1000
1013
|
minWidthByNet: Object.fromEntries([...widthByNet.entries()].map(([net, width]) => [net, { widthMil: width, role: classifyNet(net), suggestedMil: suggestTrackWidthMil(net) }])),
|
|
1001
1014
|
violations, warnings, checklist, drc,
|
|
1002
1015
|
rules: {
|
|
@@ -2432,25 +2445,33 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2432
2445
|
|
|
2433
2446
|
case 'eda_schematic_audit': {
|
|
2434
2447
|
const paper = paperOf(args.paper)
|
|
2448
|
+
const frame = paper.frame
|
|
2449
|
+
const frameArea = (frame.right - frame.left) * (frame.top - frame.bottom)
|
|
2435
2450
|
const rawList = await callExtension('sch_PrimitiveComponent.getAll', [], timeoutMs)
|
|
2436
2451
|
const list = Array.isArray(rawList) ? rawList.slice(0, 200) : []
|
|
2437
2452
|
const components = []
|
|
2453
|
+
let droppedSheetPrimitives = 0
|
|
2438
2454
|
for (let index = 0; index < list.length; index += 1) {
|
|
2439
2455
|
const entry = list[index] ?? {}
|
|
2440
|
-
const designator = String(entry.designator ?? entry.Designator ?? entry.name ?? ('#' + index))
|
|
2456
|
+
const designator = String(entry.designator ?? entry.Designator ?? entry.Name ?? entry.name ?? ('#' + index))
|
|
2441
2457
|
const id = String(entry.primitiveId ?? entry.PrimitiveId ?? '')
|
|
2442
2458
|
let bounds
|
|
2443
2459
|
if (id !== '') {
|
|
2444
|
-
// Per-symbol box: the batch call returns one box for the whole selection,
|
|
2445
|
-
// which cannot detect symbol-on-symbol overlap.
|
|
2446
2460
|
const box = await bboxOf([id], timeoutMs)
|
|
2447
2461
|
if (box !== undefined) bounds = { left: box.minX, right: box.maxX, bottom: box.minY, top: box.maxY }
|
|
2448
2462
|
}
|
|
2463
|
+
// Frame / title-block primitives live in the same list and their box is the whole
|
|
2464
|
+
// sheet (measured: one entry reported 0..1170 x 0..825 and then 'overlapped'
|
|
2465
|
+
// every symbol and the title block).
|
|
2466
|
+
if (bounds !== undefined) {
|
|
2467
|
+
const boundsArea = (bounds.right - bounds.left) * (bounds.top - bounds.bottom)
|
|
2468
|
+
if (boundsArea > frameArea * 0.4) { bounds = undefined; droppedSheetPrimitives += 1 }
|
|
2469
|
+
}
|
|
2449
2470
|
components.push({ designator, x: Number(entry.x ?? entry.X ?? 0), y: Number(entry.y ?? entry.Y ?? 0), bounds })
|
|
2450
2471
|
}
|
|
2451
2472
|
const report = auditSchematicGeometry({
|
|
2452
2473
|
components,
|
|
2453
|
-
frame
|
|
2474
|
+
frame,
|
|
2454
2475
|
titleBlock: paper.titleBlock,
|
|
2455
2476
|
grid: args.grid === undefined ? 10 : Number(args.grid),
|
|
2456
2477
|
})
|
|
@@ -2460,68 +2481,128 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2460
2481
|
counts: report.counts,
|
|
2461
2482
|
componentCount: report.componentCount,
|
|
2462
2483
|
boxesRead: components.filter(component => component.bounds !== undefined).length,
|
|
2484
|
+
droppedSheetPrimitives,
|
|
2463
2485
|
checks,
|
|
2486
|
+
notes: droppedSheetPrimitives > 0
|
|
2487
|
+
? ['Skipped ' + droppedSheetPrimitives + ' primitive(s) whose box covers the whole sheet (frame/title block); counting them made every symbol look overlapped.']
|
|
2488
|
+
: [],
|
|
2464
2489
|
}
|
|
2465
2490
|
}
|
|
2466
2491
|
|
|
2467
2492
|
case 'eda_delivery_checklist': {
|
|
2468
|
-
// One call that proves the board is ready
|
|
2469
|
-
//
|
|
2470
|
-
//
|
|
2493
|
+
// One call that proves the board is ready. Two of the four steps only work with the
|
|
2494
|
+
// schematic active and two only with the PCB active, so the checklist switches
|
|
2495
|
+
// documents itself (switchDocuments:false keeps whatever is open).
|
|
2471
2496
|
const steps = []
|
|
2472
2497
|
const startedAt = new Date().toISOString()
|
|
2498
|
+
const project = await callExtension('dmt_Project.getCurrentProjectInfo', [], timeoutMs).catch(() => null)
|
|
2499
|
+
const boards = Array.isArray(project?.data) ? project.data : []
|
|
2500
|
+
const schematicPage = boards[0]?.schematic?.page?.[0]?.uuid ?? ''
|
|
2501
|
+
const pcbUuid = boards.find((board) => board?.pcb?.uuid)?.pcb?.uuid ?? ''
|
|
2502
|
+
// Document switching used to be the default and hung the call (openDocument + a
|
|
2503
|
+
// page load blocked the following netlist export). Default is now 'run with the
|
|
2504
|
+
// document this step needs active'; switchDocuments:true still tries to switch.
|
|
2505
|
+
const switchDocuments = args.switchDocuments !== false
|
|
2506
|
+
const documentType = async () => {
|
|
2507
|
+
try {
|
|
2508
|
+
const info = await callExtension('dmt_SelectControl.getCurrentDocumentInfo', [], timeoutMs)
|
|
2509
|
+
return Number(info?.documentType)
|
|
2510
|
+
} catch { return NaN }
|
|
2511
|
+
}
|
|
2512
|
+
const activate = async (uuid) => {
|
|
2513
|
+
if (typeof uuid !== 'string' || uuid === '') return false
|
|
2514
|
+
try {
|
|
2515
|
+
await callExtension('dmt_EditorControl.openDocument', [uuid], timeoutMs)
|
|
2516
|
+
// Give the editor a beat before the next API needs the page.
|
|
2517
|
+
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
2518
|
+
return true
|
|
2519
|
+
} catch { return false }
|
|
2520
|
+
}
|
|
2473
2521
|
|
|
2474
2522
|
let netlistErc = null
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
steps.push({ step: 'netlist-erc',
|
|
2479
|
-
|
|
2480
|
-
|
|
2523
|
+
// Step documents: schematic steps need documentType 1, PCB steps need 3.
|
|
2524
|
+
const onSchematic = switchDocuments ? await activate(schematicPage) : (await documentType()) === 1
|
|
2525
|
+
if (!onSchematic) {
|
|
2526
|
+
steps.push({ step: 'netlist-erc', document: 'schematic', ok: false, skipped: 'schematic page is not the active document' })
|
|
2527
|
+
steps.push({ step: 'bom', document: 'schematic', ok: false, skipped: 'schematic page is not the active document' })
|
|
2528
|
+
} else {
|
|
2529
|
+
try {
|
|
2530
|
+
const parsedNetlist = await fetchParsedNetlist(timeoutMs)
|
|
2531
|
+
netlistErc = runNetlistErc(parsedNetlist)
|
|
2532
|
+
steps.push({
|
|
2533
|
+
step: 'netlist-erc',
|
|
2534
|
+
document: 'schematic',
|
|
2535
|
+
ok: netlistErc.ok,
|
|
2536
|
+
detail: netlistErc.counts,
|
|
2537
|
+
failing: netlistErc.checks.filter(check => check.status === 'fail').map(check => check.id),
|
|
2538
|
+
})
|
|
2539
|
+
} catch (error) {
|
|
2540
|
+
steps.push({ step: 'netlist-erc', document: 'schematic', ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
2541
|
+
}
|
|
2481
2542
|
}
|
|
2482
2543
|
|
|
2544
|
+
let bom = null
|
|
2545
|
+
if (onSchematic) {
|
|
2546
|
+
try {
|
|
2547
|
+
const payload = await callExtension('sch_ManufactureData.getBomFile', [undefined, 'csv'], timeoutMs)
|
|
2548
|
+
const text = typeof payload === 'string' ? payload : (typeof payload?.text === 'string' ? payload.text : '')
|
|
2549
|
+
const lines = text === '' ? 0 : text.split(/\r?\n/).filter(line => line.trim() !== '').length
|
|
2550
|
+
bom = { rows: Math.max(0, lines - 1), ok: lines > 1 }
|
|
2551
|
+
steps.push({ step: 'bom', document: 'schematic', ok: bom.ok, detail: { rows: bom.rows }, ...(bom.ok ? {} : { error: 'no BOM rows exported' }) })
|
|
2552
|
+
} catch (error) {
|
|
2553
|
+
steps.push({ step: 'bom', document: 'schematic', ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
const onPcb = switchDocuments ? await activate(pcbUuid) : (await documentType()) === 3
|
|
2483
2557
|
let pcbAudit = null
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
steps.push({
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2558
|
+
if (!onPcb) {
|
|
2559
|
+
steps.push({ step: 'pcb-audit', document: 'pcb', ok: false, skipped: 'PCB is not the active document' })
|
|
2560
|
+
steps.push({ step: 'gerber', document: 'pcb', ok: false, skipped: 'PCB is not the active document' })
|
|
2561
|
+
} else {
|
|
2562
|
+
try {
|
|
2563
|
+
pcbAudit = await auditPcb({ drc: args.pcbDrc !== false }, timeoutMs)
|
|
2564
|
+
steps.push({
|
|
2565
|
+
step: 'pcb-audit',
|
|
2566
|
+
document: 'pcb',
|
|
2567
|
+
ok: (pcbAudit.violations ?? []).length === 0,
|
|
2568
|
+
detail: { violations: pcbAudit.violations?.length ?? null, warnings: pcbAudit.warnings?.length ?? null, drc: pcbAudit.drc },
|
|
2569
|
+
failing: (pcbAudit.violations ?? []).map(entry => entry.rule),
|
|
2570
|
+
})
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
steps.push({ step: 'pcb-audit', document: 'pcb', ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
2573
|
+
}
|
|
2494
2574
|
}
|
|
2495
2575
|
|
|
2496
2576
|
let gerber = null
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
let bom = null
|
|
2509
|
-
try {
|
|
2510
|
-
const payload = await callExtension('sch_ManufactureData.getBomFile', [undefined, 'csv'], timeoutMs)
|
|
2511
|
-
const text = typeof payload === 'string' ? payload : (typeof payload?.text === 'string' ? payload.text : '')
|
|
2512
|
-
const lines = text === '' ? 0 : text.split(/\r?\n/).filter(line => line.trim() !== '').length
|
|
2513
|
-
bom = { rows: Math.max(0, lines - 1), ok: lines > 1 }
|
|
2514
|
-
steps.push({ step: 'bom', ok: bom.ok, detail: { rows: bom.rows } })
|
|
2515
|
-
} catch (error) {
|
|
2516
|
-
steps.push({ step: 'bom', ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
2577
|
+
if (onPcb) {
|
|
2578
|
+
try {
|
|
2579
|
+
const payload = await callExtension('pcb_ManufactureData.getGerberFile', [undefined, undefined, undefined, undefined, undefined, undefined], timeoutMs)
|
|
2580
|
+
const bytes = payload && typeof payload === 'object'
|
|
2581
|
+
? Number(payload.size ?? payload.bytes ?? 0)
|
|
2582
|
+
: (typeof payload === 'string' ? payload.length : 0)
|
|
2583
|
+
gerber = { bytes, ok: bytes > 1000 }
|
|
2584
|
+
steps.push({ step: 'gerber', document: 'pcb', ok: gerber.ok, detail: { bytes } })
|
|
2585
|
+
} catch (error) {
|
|
2586
|
+
steps.push({ step: 'gerber', document: 'pcb', ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
2587
|
+
}
|
|
2517
2588
|
}
|
|
2518
2589
|
|
|
2519
|
-
const
|
|
2590
|
+
const failedSteps = steps.filter(step => step.ok !== true && step.skipped === undefined)
|
|
2591
|
+
const skippedSteps = steps.filter(step => step.ok !== true && step.skipped !== undefined)
|
|
2592
|
+
const verdict = failedSteps.length > 0 ? 'fail' : (skippedSteps.length > 0 ? 'incomplete' : 'pass')
|
|
2520
2593
|
const report = {
|
|
2521
|
-
ok:
|
|
2594
|
+
ok: verdict === 'pass',
|
|
2595
|
+
verdict,
|
|
2596
|
+
failedSteps: failedSteps.map(step => step.step),
|
|
2597
|
+
skippedSteps: skippedSteps.map(step => step.step),
|
|
2598
|
+
next: verdict === 'pass'
|
|
2599
|
+
? 'Every step passed: the board can be ordered.'
|
|
2600
|
+
: (verdict === 'incomplete'
|
|
2601
|
+
? 'Run the checklist again with the other document active (' + skippedSteps.map(step => step.document).filter((value, index, all) => all.indexOf(value) === index).join(', ') + ' needed).'
|
|
2602
|
+
: 'Fix the failing steps first; skipped steps are listed separately.'),
|
|
2522
2603
|
startedAt,
|
|
2523
2604
|
finishedAt: new Date().toISOString(),
|
|
2524
|
-
|
|
2605
|
+
documents: { schematicPage, pcb: pcbUuid, switched: switchDocuments },
|
|
2525
2606
|
netlistErc: netlistErc === null ? null : { ok: netlistErc.ok, counts: netlistErc.counts, componentCount: netlistErc.componentCount, netCount: netlistErc.netCount },
|
|
2526
2607
|
pcbAudit: pcbAudit === null ? null : { violations: pcbAudit.violations, warnings: pcbAudit.warnings },
|
|
2527
2608
|
gerber,
|
|
@@ -2536,10 +2617,12 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2536
2617
|
'- 开始:' + report.startedAt,
|
|
2537
2618
|
'- 结束:' + report.finishedAt,
|
|
2538
2619
|
'- 结论:' + (report.ok ? 'PASS(可以下单)' : 'FAIL(有未通过项)'),
|
|
2620
|
+
'- 原理图页:' + (schematicPage || '未取到'),
|
|
2621
|
+
'- PCB:' + (pcbUuid || '未取到'),
|
|
2539
2622
|
'',
|
|
2540
|
-
'| 步骤 | 结果 | 说明 |',
|
|
2541
|
-
'
|
|
2542
|
-
...steps.map(step => '| ' + step.step + ' | ' + (step.ok === true ? 'PASS' : 'FAIL') + ' | ' + JSON.stringify(step.detail ?? step.error ?? {}) + ' |'),
|
|
2623
|
+
'| 步骤 | 文档 | 结果 | 说明 |',
|
|
2624
|
+
'|---|---|---|---|',
|
|
2625
|
+
...steps.map(step => '| ' + step.step + ' | ' + step.document + ' | ' + (step.ok === true ? 'PASS' : 'FAIL') + ' | ' + JSON.stringify(step.detail ?? step.error ?? {}) + ' |'),
|
|
2543
2626
|
'',
|
|
2544
2627
|
'## 电气检查(eda_netlist_erc)',
|
|
2545
2628
|
'',
|
|
@@ -2566,6 +2649,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2566
2649
|
return report
|
|
2567
2650
|
}
|
|
2568
2651
|
|
|
2652
|
+
|
|
2569
2653
|
case 'eda_design_intake': {
|
|
2570
2654
|
if (args.reset === true) designBrief = null
|
|
2571
2655
|
if (designBrief === null) {
|
|
@@ -2580,17 +2664,41 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2580
2664
|
const merged = { ...(designBrief ?? {}), ...answersReport.brief }
|
|
2581
2665
|
const mergedReport = buildIntake(merged)
|
|
2582
2666
|
let persisted = false
|
|
2667
|
+
let persistVia = 'none'
|
|
2668
|
+
let persistPath = ''
|
|
2583
2669
|
if (Object.keys(merged).length > 0) {
|
|
2584
2670
|
designBrief = merged
|
|
2585
2671
|
if (answers !== null) {
|
|
2586
2672
|
try {
|
|
2587
2673
|
const project = await callExtension('dmt_Project.getCurrentProjectInfo', [], timeoutMs)
|
|
2674
|
+
const projectUuid = String(project?.uuid ?? '')
|
|
2588
2675
|
const existing = typeof project?.description === 'string' ? project.description : ''
|
|
2589
2676
|
const index = existing.indexOf(BRIEF_TAG)
|
|
2590
2677
|
const base = index < 0 ? existing : existing.slice(0, index).trimEnd()
|
|
2591
2678
|
const next = (base === '' ? '' : base + '\n') + briefBlock(merged)
|
|
2592
|
-
|
|
2593
|
-
|
|
2679
|
+
// The description write is inert on this build (measured: returns false and
|
|
2680
|
+
// the description stays empty), so fall back to a sidecar file next to the
|
|
2681
|
+
// local project directory and verify whichever path was used.
|
|
2682
|
+
let stored = false
|
|
2683
|
+
if (projectUuid !== '') {
|
|
2684
|
+
try {
|
|
2685
|
+
await callExtension('dmt_Project.modifyProjectDescription', [projectUuid, next], timeoutMs)
|
|
2686
|
+
const readBack = await callExtension('dmt_Project.getCurrentProjectInfo', [], timeoutMs)
|
|
2687
|
+
stored = String(readBack?.description ?? '').includes(BRIEF_TAG)
|
|
2688
|
+
if (stored) persistVia = 'description'
|
|
2689
|
+
} catch { stored = false }
|
|
2690
|
+
}
|
|
2691
|
+
if (!stored) {
|
|
2692
|
+
const sidecar = specFilePath(project)
|
|
2693
|
+
if (sidecar !== '') {
|
|
2694
|
+
await mkdir(join(sidecar, '..'), { recursive: true })
|
|
2695
|
+
await writeFile(sidecar, JSON.stringify({ projectUuid, brief: merged }, null, 2) + '\n')
|
|
2696
|
+
const check = JSON.parse(await readFile(sidecar, 'utf8'))
|
|
2697
|
+
stored = check !== null && typeof check === 'object' && Object.keys(merged).every(key => check.brief?.[key] === merged[key])
|
|
2698
|
+
if (stored) { persistVia = 'file'; persistPath = sidecar }
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
persisted = stored === true
|
|
2594
2702
|
} catch { persisted = false }
|
|
2595
2703
|
}
|
|
2596
2704
|
}
|
|
@@ -2604,6 +2712,8 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2604
2712
|
brief: mergedReport.brief,
|
|
2605
2713
|
hydratedFromProject: answers === null && designBrief !== null,
|
|
2606
2714
|
persisted,
|
|
2715
|
+
persistVia,
|
|
2716
|
+
persistPath,
|
|
2607
2717
|
sections: mergedReport.sections.filter(section => section.questions.some(question => mergedReport.brief[question.id] === undefined)),
|
|
2608
2718
|
next: mergedReport.ready
|
|
2609
2719
|
? 'All critical parameters are known: lay out the schematic, then eda_netlist_erc before routing.'
|
|
@@ -3026,6 +3136,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3026
3136
|
case 'eda_pcb_delete_via':
|
|
3027
3137
|
return await callExtension('pcb_PrimitiveVia.delete', [String(args.primitiveId)], timeoutMs)
|
|
3028
3138
|
|
|
3139
|
+
|
|
3029
3140
|
case 'eda_pcb_create_pour': {
|
|
3030
3141
|
const layer = resolveRegionLayer(args.layer, 'eda_pcb_create_pour')
|
|
3031
3142
|
const points = requireFlatPoints(args.points, 'eda_pcb_create_pour')
|
|
@@ -3065,7 +3176,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3065
3176
|
(firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + '; retry: ' + detail + '). ' +
|
|
3066
3177
|
'polygonStage=' + (polygonOk ? 'ok' : 'failed') + ': ' +
|
|
3067
3178
|
(polygonOk
|
|
3068
|
-
? 'the app accepted the outline but refused the pour itself, so
|
|
3179
|
+
? 'the app accepted the outline but refused the pour itself, and so does the complex polygon the docs require (createComplexPolygon); single and complex polygons, every net/layer/fill combination, all refused, so pour creation is an application limit on this build, not a coordinate problem. ' +
|
|
3069
3180
|
'Pour by hand in the PCB editor (覆铜 → select GND → top + bottom); eda_design_audit reports it as a warn until then.'
|
|
3070
3181
|
: 'the outline polygon was rejected too: check that every point is inside the board outline and flat [x1,y1,...].') +
|
|
3071
3182
|
' retried=' + retried)
|
package/src/skills-content.ts
CHANGED
|
@@ -153,7 +153,7 @@ export const SKILL_PLAYBOOK_LINES: readonly string[] = [
|
|
|
153
153
|
"| eda_launch 带 project 没打开工程 | 命令行路径被应用忽略 | 用 eda_open_project({ name }) 打开 |",
|
|
154
154
|
"| 关应用后改动丢失 | eda_close 走了强制路径 | 关前先保存;看返回的 forced |",
|
|
155
155
|
"| run_code 读大 JSON 被截断 | 宿主 read 有单行上限 | 用 node:fs 读盘或分片读 |",
|
|
156
|
-
"|
|
|
156
|
+
"| 覆铜/填充一律被拒(无法创建覆铜边框图元 / 无法创建填充图元) | 应用侧限制:静态+实例入口、单/复杂多边形、原始源码数组、6 种填充枚举、全参数组合全部被拒;同一多边形管线在本构建上连丝印折线都写不出来 | 不要在 API 层反复试:覆铜请在界面手工铺(GND、顶层+底层);design_audit 持续记 copper-pour warn |",
|
|
157
157
|
"| pcb_draw_mounting_hole 报“无法创建焊盘图元” | pad 形状必填,且槽孔只能用 ELLIPSE | 已内建:圆孔 ELLIPSE(d,d),槽孔 ELLIPSE(长,宽)+hole[SLOT,宽,长];OBLONG/RECTANGLE 焊盘会被拒 |",
|
|
158
158
|
"| pcb_PrimitiveString.getAll / pcb_PrimitivePad.getAll 返回 0 | 这两个 API 按层查询 | 传 layer:丝印 3/4,多层焊盘 12;不传参恒为 0 |",
|
|
159
159
|
"| 按坐标算的越界没被发现 | 图框/原点假设与真实页面不一致 | 用 eda_get_page_info 的真实值 + eda_capture 导 PNG 看实际版面 |",
|
|
@@ -246,6 +246,13 @@ export const SKILL_PLAYBOOK_LINES: readonly string[] = [
|
|
|
246
246
|
"| `eda_delivery_checklist` | 一次串完 ERC + PCB 审计/DRC + Gerber + BOM,逐步 PASS/FAIL;`outPath` 落 Markdown 报告 | 交付前,代替\"我说做完了\" |",
|
|
247
247
|
"",
|
|
248
248
|
"**交付口径**:`eda_delivery_checklist` 返回 `ok:true` 才算可以下单;任何一步 FAIL 都要先修,不许口头绕过。报告里带时间戳与计数,可直接给人复核。",
|
|
249
|
+
"### 交付清单怎么读(1.0.1 起)",
|
|
250
|
+
"",
|
|
251
|
+
"- 一次调用会**自己切到原理图再切到 PCB**(`switchDocuments:false` 改按当前文档跑一半,另一半标注跳过原因);",
|
|
252
|
+
"- 返回 `verdict`:`pass`(四步全过)/ `incomplete`(有步骤因文档不对被跳过)/ `fail`(有步骤真失败);`ok` 等价于 `verdict==='pass'`;",
|
|
253
|
+
"- `skippedSteps` 与 `failedSteps` 分开列 —— **别把\"没跑\"当成\"通过\"**;四步全过实测约 **7.5 s**;",
|
|
254
|
+
"- 投板前最后一条命令就用它,`outPath` 落 Markdown 存档。",
|
|
255
|
+
"",
|
|
249
256
|
"",
|
|
250
257
|
"**参数持久化**:`eda_design_intake({answers})` 会把答复写进**工程描述**(`[dsh-eda-spec] {...}`),下次会话调用 `eda_design_intake` 会自动读回,不用重新猜;`reset:true` 清空。",
|
|
251
258
|
"",
|
|
Binary file
|