dsh-eda-mcp 1.0.0 → 1.0.1
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 +33 -0
- package/docs/schematic-workflow.md +7 -0
- package/docs/tools.md +1 -1
- package/dsh-eda-bridge-extension-0.4.6.zip +0 -0
- package/lib/bridge.mjs +150 -49
- package/lib/index.js +7 -0
- package/package.json +1 -1
- package/src/bridge/index.ts +166 -56
- package/src/skills-content.ts +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.1 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
1.0.0 的真机回归修掉 4 个 bug(全部由"跑一遍"发现,不是靠读代码):
|
|
6
|
+
|
|
7
|
+
1. **原理图几何审计误报 30 对重叠**(`eda_schematic_audit`)
|
|
8
|
+
`sch_PrimitiveComponent.getAll` 的列表里混着图框/标题栏图元,其包围盒是整张页面(实测 `0..1170 × 0..825`),于是与每个符号"重叠"。现在**丢弃面积超过页面 40% 的图元**并在 `droppedSheetPrimitives` 里报数量;复测:30 对误报 → **0 fail**(31 器件 / 30 个有效包围盒 / 丢弃 1 个)。
|
|
9
|
+
|
|
10
|
+
2. **参数持久化"报成功但没写"**(`eda_design_intake`)
|
|
11
|
+
① `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`。
|
|
12
|
+
|
|
13
|
+
3. **PCB 机械检查是死代码**(`eda_design_audit`)
|
|
14
|
+
`pcb_PrimitiveComponent.getAll([undefined, undefined])` 在该构建上返回**空列表**(同样的 API 在 `pcb_get_components` 里能取到 29 个器件),导致器件重叠 / 出板框两项检查从未执行。改用与 `pcb_get_components` 相同的调用形态,并把 `counts.componentsRead` 暴露出来;复测:`componentsRead: 29`。
|
|
15
|
+
|
|
16
|
+
4. **交付清单挂死超时**(`eda_delivery_checklist`)
|
|
17
|
+
自动切文档后紧接着调网表导出会被页面加载阻塞 → MCP 超时。现在切文档后等待 1.5 s;同时把**跳过**与**失败**分开:新增 `verdict`(`pass` / `incomplete` / `fail`)、`skippedSteps`、`failedSteps`、`next`,默认仍自动切文档。真机复测:**一次调用 7.5 s,四步全过 `verdict=pass`**;`switchDocuments:false` 时如实报 `incomplete` 并指出缺哪个文档。
|
|
18
|
+
|
|
19
|
+
### 回归结果(扩展 0.4.6 / 桥接 1.0.1)
|
|
20
|
+
|
|
21
|
+
| 项 | 结果 |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `eda_schematic_audit` | ✅ 0 fail / 1 warn(12 个符号偏离 10 单位栅格)/ 1 skip(无标签锚点) |
|
|
24
|
+
| `eda_netlist_erc` | ✅ 15 项全执行,14 pass / 0 fail / 1 warn(USB-C SBU1/SBU2 未接) |
|
|
25
|
+
| `eda_design_intake` 持久化 | ✅ `persisted=true via=file` |
|
|
26
|
+
| `eda_design_audit`(PCB) | ✅ `componentsRead:29`、0 违规 / 4 警告(5V 线宽偏细、无覆铜、无丝印、无定位孔) |
|
|
27
|
+
| `eda_delivery_checklist` | ✅ `verdict=pass`、7.5 s、四步(ERC / BOM / PCB 审计 / Gerber 86.8 KB) |
|
|
28
|
+
| `eda_pcb_canvas_origin` | ✅ 偏移 `0,0`,画布↔数据坐标互转一致 |
|
|
29
|
+
| `eda_pcb_create_pour` | ❌ **仍失败**:单多边形与复杂多边形都被接受,但 `pcb_PrimitivePour.create` 对任何网络/层/填充方式一律拒绝 → 判定为**应用侧限制**(1.0.0 里我写"是我用错 API",据此修正) |
|
|
30
|
+
|
|
31
|
+
### 已知遗留(需你决定)
|
|
32
|
+
|
|
33
|
+
- 侧车目录 `D:\文档\LCEDA-Pro\projects\.dsh-eda-spec\` 是回归测试写入的(1 个 json),清理需要你确认(它在你的工程目录内)。
|
|
34
|
+
- 覆铜只能手工:工具已把失败原因写准,`eda_design_audit` 持续报 `copper-pour` warn。
|
|
35
|
+
|
|
3
36
|
## 1.0.0 — 2026-10-20
|
|
4
37
|
|
|
5
38
|
第一个稳定版。0.7.4 之后这一轮把"能画"推进到"画得对、画得像参考设计、交付有证据":工具 106 → **109**,单测 6 → **8 套**,线协议 0.4.2 → **0.4.6**。
|
|
@@ -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
|
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";
|
|
@@ -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
|
@@ -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.1",
|
|
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'
|
|
@@ -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.'
|
|
@@ -3065,7 +3175,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
3065
3175
|
(firstFailure instanceof Error ? firstFailure.message : String(firstFailure)) + '; retry: ' + detail + '). ' +
|
|
3066
3176
|
'polygonStage=' + (polygonOk ? 'ok' : 'failed') + ': ' +
|
|
3067
3177
|
(polygonOk
|
|
3068
|
-
? 'the app accepted the outline but refused the pour itself, so
|
|
3178
|
+
? '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
3179
|
'Pour by hand in the PCB editor (覆铜 → select GND → top + bottom); eda_design_audit reports it as a warn until then.'
|
|
3070
3180
|
: 'the outline polygon was rejected too: check that every point is inside the board outline and flat [x1,y1,...].') +
|
|
3071
3181
|
' retried=' + retried)
|
package/src/skills-content.ts
CHANGED
|
@@ -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
|
"",
|