dsh-eda-mcp 0.7.2 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +90 -0
- package/README.md +3 -1
- package/README.zh.md +11 -1
- package/docs/release.md +2 -0
- package/docs/schematic-workflow.md +17 -0
- package/docs/tools.md +2 -2
- package/dsh-eda-bridge-extension-0.4.2.zip +0 -0
- package/jlc-extension/api-bridge.js +19 -3
- package/jlc-extension/extension.json +1 -1
- package/lib/bridge.mjs +64 -16
- package/lib/index.js +46 -8
- package/package.json +3 -2
- package/src/bridge/index.ts +86 -17
- package/src/guidance.ts +1 -0
- package/src/index.ts +34 -9
- package/src/skills-content.ts +17 -0
- package/test/seams.test.ts +16 -0
- package/test/skills.test.ts +6 -1
- package/dsh-eda-bridge-extension-0.4.0.zip +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,95 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.3 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
本版把原理图与 PCB 两侧都跑了一遍真机,修掉 5 个真问题,并把「三层改动的生效范围」写进随包文档,避免新会话无从下手。
|
|
6
|
+
|
|
7
|
+
### 真机验证 0.7.2 时发现并修掉的 3 个问题(线协议 0.4.0 → **0.4.1**)
|
|
8
|
+
|
|
9
|
+
> 线协议升到 0.4.1(扩展版本只递增最小位,不跳 minor):**需要重新导入 `dsh-eda-bridge-extension-0.4.1.zip`**(扩展代码有改动)。
|
|
10
|
+
|
|
11
|
+
**1. BOM CSV 仍乱码(真因在扩展)**
|
|
12
|
+
|
|
13
|
+
- 现象:`eda_export_bom` 返回 `��N\u0000o\u0000.`。
|
|
14
|
+
- 排查:桥接侧的 `decodeTextPayload` 没救回,因为字节在**扩展**里就按 UTF-8 解成替换字符了(`blobToFilePayload` 对所有 `text/*` 一律 `TextDecoder("utf-8")`)。
|
|
15
|
+
- 修复:扩展按**BOM 判定编码**(`FF FE`→utf-16le、`FE FF`→utf-16be、`EF BB BF`→utf-8),并在返回值里带 `encoding`;桥接侧解码保留为旧扩展兜底,返回值新增 `encoding` 字段。
|
|
16
|
+
|
|
17
|
+
**2. `eda_open_document` 静默不建标签页**
|
|
18
|
+
|
|
19
|
+
- 现象:应用刚启动停在 HOME 时,`dmt_EditorControl.openDocument(uuid)` 返回空且**不创建标签页**,随后所有 tab 相关调用读空——调用方以为页面已激活。
|
|
20
|
+
- 修复:调用后 600 ms 复查 `getSplitScreenTree` 与活动文档,返回 `opened` / `activeDocumentUuid` / `tabIds`;未建标签页时给 `hint`("该构建不会从 HOME 程序化打开文档,请先在应用里点开页面,别假设已激活")。真机复测:工程加载完成后 `opened:true`,标签列表含原理图页与 PCB 页。
|
|
21
|
+
|
|
22
|
+
**3. 构建会把旧版本 zip 一起打包**
|
|
23
|
+
|
|
24
|
+
- 现象:升版本后目录里同时存在新旧两个扩展 zip,而 `package.json files` 用的是 `dsh-eda-bridge-extension-*.zip` 通配 → 两个都会进 npm 包。
|
|
25
|
+
- 修复:构建前**清掉所有** `dsh-eda-bridge-extension*.zip`;`npm pack --dry-run` 现在只有 1 个 zip(40 files)。
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### 真机验证 PCB 侧:又抓到一个拦截 bug(线协议 0.4.1 → **0.4.2**)
|
|
30
|
+
|
|
31
|
+
**1. 所有导出(BOM / 网表 / Gerber)本该被第二张白名单挡住**
|
|
32
|
+
|
|
33
|
+
- 现象:`eda_pcb_export_gerber` 报 `API method not allowed: pcb_ManufactureData.getGerberFile`。
|
|
34
|
+
- 真因:扩展的分发门只查 `ALLOWED_API`,而 `FILE_METHODS`(返回 File 的方法:BOM、网表、Gerber、截图)是**第二张表**,被门挡在外面 —— 上线以来所有 `FILE_METHODS` 导出其实都走不通(BOM/网表"能用"是因为它们同时也在 `ALLOWED_API` 里)。
|
|
35
|
+
- 修复:门改为 `!ALLOWED_API.has(m) && !FILE_METHODS.has(m)`,报错信息里给出两表合计数量;`contract-check` 增加一条断言:**分发门必须同时接受 FILE_METHODS**(之前只校验两表并集,所以漏了)。
|
|
36
|
+
|
|
37
|
+
**2. PCB 写入没有校验网络是否存在**
|
|
38
|
+
|
|
39
|
+
- 现象:`pcb_draw_track({ net: "NO_SUCH_NET" })` **被应用接受**,板上多出一条幽灵网络的线(DRC 会报 netlist error)。验证时实际留下了一条,已用 `eda_pcb_delete_track` 清掉(98 条线、0 条幽灵)。
|
|
40
|
+
- 修复:`pcb_draw_track` / `pcb_draw_via` 写入前用 `pcb_Net.getAllNetsName` 校验网络是否存在,不存在则拒绝并列出前 12 个已知网络;`force:true` 仍可强制。真机复测:两条都被拒绝,提示里带 17 个真实网络名。
|
|
41
|
+
|
|
42
|
+
### PCB 侧真机验证结论(EDA 3.2.186 / 扩展 0.4.1)
|
|
43
|
+
|
|
44
|
+
| 项 | 结果 |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `eda_open_document(pcb)` | ✅ 活动文档 `documentType:3`,tabId 带 `@projectUuid` |
|
|
47
|
+
| `pcb_get_layers` | ✅ 289 个层(id/name/type/color) |
|
|
48
|
+
| `pcb_get_nets` | ✅ 17 个网络 |
|
|
49
|
+
| `pcb_get_components` | ✅ 33 个器件,含封装与 LCSC 料号 |
|
|
50
|
+
| `eda_pcb_get_drc_rules` | ✅ 返回完整规则配置(间距矩阵等) |
|
|
51
|
+
| `eda_pcb_run_drc({includeVerboseError:true})` | ✅ **2.8 s** 返回结构化违规(`name/errorType/ruleName/explanation`),本次 1 条:Netlist Error(PCB 与原理图不一致) |
|
|
52
|
+
| `eda_design_audit({drc:false})` | ✅ 返回 `counts/minWidthByNet/violations/warnings/checklist/rules`(1 violation、1 warning) |
|
|
53
|
+
| `pcb_draw_via`(孔径 0.1mm) | ✅ 拒绝:孔径低于 0.2mm 且焊环 0.1mm 低于 0.2mm |
|
|
54
|
+
| `pcb_draw_track`(未知网络) | ❌→✅ 修复后拒绝(见上) |
|
|
55
|
+
| `eda_pcb_export_gerber` | ❌ `API method not allowed` → 已修,需重导 **0.4.2** 复测 |
|
|
56
|
+
### 批量标签改用受控并发(实测 2.2 倍)
|
|
57
|
+
|
|
58
|
+
- `eda_place_net_labels` 原来是串行 for 循环:真机实测**每个标签约 2.1 s**(应用按图元落库),133 个仍需 ~4.6 分钟。
|
|
59
|
+
- 改为 `mapLimit` 受控并发(默认 4,可用 `concurrency` 调 1–8),结果仍按调用方顺序返回。
|
|
60
|
+
- 真机复测:**8 个标签 7.5 s = 938 ms/个**(串行 2.1 s/个),清理后计数 19 → 11。
|
|
61
|
+
### 真机验证结论(EDA 3.2.186 + 扩展 0.4.0)
|
|
62
|
+
|
|
63
|
+
| 项 | 结果 |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `eda_status` / `eda_launch` | ✅ 走 explorer 启动,8.7 s 内 1 个客户端连上,版本一致 |
|
|
66
|
+
| `eda_list_projects` | ✅ 7 个工程(跨 2 个本地目录聚合) |
|
|
67
|
+
| `eda_open_project({name})` | ✅ 中文名解析并打开,`opened:true, openReturned:true` |
|
|
68
|
+
| `eda_export_project` | ✅ 1 056 444 字节 |
|
|
69
|
+
| `eda_export_netlist` | ✅ `parsed:true`,80 647 字节,返回真实 netlist 对象(含 `DC1` 等器件) |
|
|
70
|
+
| `eda_get_components` / `eda_get_pins` | ✅ 34 个器件;引脚返回 `pinNumber` / `pinName`(3/3 有值) |
|
|
71
|
+
| `eda_get_net_labels` | ✅ `count:11, attributeCount:0, textCount:11`(以前恒为 0) |
|
|
72
|
+
| `eda_place_net_labels`(批量 3 个) | ✅ 单次调用 7.0 s、`done:3 failed:0`、`method:"text"`;计数 11 → 14,`sessionTextLabels:3` |
|
|
73
|
+
| 批量标签清理 | ✅ `eda_delete_primitive(kind:"text")` ×3 后回到 11,未保存文档 |
|
|
74
|
+
| `eda_zoom_to_fit` | ✅ 应用返回的越界 bounds(-121…1776 / -2.5…1172)被裁剪为 `0..1170 / 0..825` 并重新应用 |
|
|
75
|
+
| `eda_run_drc` | ⚠️ 只能拿到聚合项 `{type:"warn",count:1}`:即使 `includeVerboseError:true`,该构建也不返回 primitiveId/坐标。工具说明与 hint 已按实情改写 |
|
|
76
|
+
| `eda_export_bom`(真机) | ❌ → 已修(见上),需重导 0.4.1 后复测 |
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
### 发布流水线修掉两个 CI bug(v0.7.3 因此没上 npm)
|
|
80
|
+
|
|
81
|
+
**1. `publish.yml` 的"无构建机路径"守卫误杀**
|
|
82
|
+
|
|
83
|
+
- 守卫用 `Select-String -Pattern 'node_modules/'` 扫 `lib/*.js`,命中了我自己写的**文档字符串**`扩展 zip 位置:node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<版本>.zip`(这是要告诉用户的正确信息,不是泄露的构建机路径),守卫直接 `exit 1`,**publish 步骤从没执行**。
|
|
84
|
+
- 修复:守卫抽成 `scripts/guard-bundle-paths.mjs`(`pnpm guard:bundle`),只认**绝对路径**形态的泄露(`D:/…/node_modules`、`/home/`、`/Users/`、`/root/`、`node_modules/.pnpm`),两个 workflow 共用同一实现。
|
|
85
|
+
- 真机验证:注入 `D:/Program/…/node_modules/.pnpm/x.js` → 守卫 `exit 1` 并报出文件与行号;恢复后 PASS。
|
|
86
|
+
|
|
87
|
+
**2. `check.yml` 上传的是已被移除的别名 zip**
|
|
88
|
+
|
|
89
|
+
- `path: dsh-eda-bridge-extension.zip` + `if-no-files-found: error`,但构建自 0.7.0 起只产出**带版本号**的 zip(别名是故意删掉的)→ 上传步骤失败,整个 check workflow 红。
|
|
90
|
+
- 修复:改成 `dsh-eda-bridge-extension-*.zip`,并把 `pnpm guard:bundle` 也加进 check。
|
|
91
|
+
|
|
92
|
+
> 影响:`v0.7.3` 的 tag 已推,但 npm 上仍只有 `0.7.2`。补法:在 Actions 里手动运行 `publish` 工作流并勾选 `publish`(`workflow_dispatch` 从 main 取代码,自动带上这两个修复),无需重打 tag。
|
|
3
93
|
## 0.7.2 — 2026-10-20
|
|
4
94
|
|
|
5
95
|
本版把原理图批量落地的 15 个问题全部处理完(13 修、2 记为限制),并把工作规范正式注册成 DSH 技能、扩成完整项目手册。
|
package/README.md
CHANGED
|
@@ -128,6 +128,8 @@ after routing.
|
|
|
128
128
|
|
|
129
129
|
Schematic/symbol coordinates are in **0.01 inch** units; PCB coordinates are in **mil**. Rotation values are `0/90/180/270`. Wire points are a flat array `[x1, y1, x2, y2, ...]`.
|
|
130
130
|
|
|
131
|
+
**What takes effect when** (read this before retrying anything): plugin source in `src/` needs `node build.mjs` plus a **DSH restart** — the running host keeps the old bundle in memory; the EDA extension in `jlc-extension/` needs the zip **re-imported** and JLCEDA Pro restarted (`eda_status.extensionVersions` must match `bridgeVersion`); only the bridge child can be restarted by the agent (`POST /api/dsh-eda-mcp/reconnect`). The generated `docs/schematic-workflow.md` chapter 12 has the full table, and the skill `jlceda-schematic-workflow` carries the same text.
|
|
132
|
+
|
|
131
133
|
Every PCB tool has an equivalent `eda_pcb_*` alias (`eda_pcb_get_components` = `pcb_get_components`). See `README.zh.md` for the full PCB table and the region-layer rules (`pcb_PrimitiveRegion.create` accepts copper layers and MULTI only, so the board outline stays a closed line loop on layer 11).
|
|
132
134
|
|
|
133
135
|
## Tool reference
|
|
@@ -222,7 +224,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the repository layout, the release/ta
|
|
|
222
224
|
- Importing a `.epro2` copies its internal uuids (schematic/page/pcb), which can collide with the source project: `eda_import_project` reports them in `collisions`, but cannot regenerate them.
|
|
223
225
|
- `dmt_Project.copyProject` is as inert as `createProject` / `deleteProject` for local projects; copy with `eda_export_project` + `eda_import_project` instead.
|
|
224
226
|
- Automatic layout/routing is off by default (`allowAutoLayoutRouting`); `eda_auto_layout` / `eda_auto_route` only run with `force:true`.
|
|
225
|
-
- The extension and bridge wire versions must move **together** (currently 0.4.
|
|
227
|
+
- The extension and bridge wire versions must move **together** (currently 0.4.1, patch-only bumps): a changed allow-list requires re-importing the zip, otherwise new tools answer `API method not allowed`. `eda_status` reports every connected page's version in `extensionVersions`.
|
|
226
228
|
|
|
227
229
|
## Repository
|
|
228
230
|
|
package/README.zh.md
CHANGED
|
@@ -117,6 +117,16 @@ curl.exe -s -X POST http://127.0.0.1:19387/api/dsh-eda-mcp/self-test
|
|
|
117
117
|
|
|
118
118
|
> 桌面端加载的是打包后的 `lib/index.js`,**改完 `src/` 必须重新 `node build.mjs` 并重启桌面应用**(插件重载不会重新 import 已缓存的 ESM 模块)。
|
|
119
119
|
|
|
120
|
+
**三层改动的生效范围(新会话/接手的人先看这张表,别凭直觉重试)**:
|
|
121
|
+
|
|
122
|
+
| 改了什么 | 怎么生效 | 怎么确认 |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| 插件源码 `src/`(配置、路由、技能注册、工具表) | `node build.mjs` 后**重启 DSH** | 会话出现新工具;`/api/dsh-eda-mcp/diagnostics` 的 `skills` 字段 |
|
|
125
|
+
| 扩展 `jlc-extension/`(白名单、内部助手、编码) | **重新导入** `dsh-eda-bridge-extension-<版本>.zip` 并**重启嘉立创EDA** | `eda_status` 的 `extensionVersions` 与 `bridgeVersion` 一致 |
|
|
126
|
+
| 只需重启桥接子进程 | `POST /api/dsh-eda-mcp/reconnect`(agent 可自行) | 返回 200 且 `connectedClients ≥ 1` |
|
|
127
|
+
|
|
128
|
+
完整版(含排查顺序与版本纪律)见生成文档 `docs/schematic-workflow.md` 第 12 章,或会话内 `skill` 加载 `jlceda-schematic-workflow`。
|
|
129
|
+
|
|
120
130
|
### 4. 在嘉立创EDA里手动连接
|
|
121
131
|
|
|
122
132
|
DSH 桥接服务开着但 EDA 没有连上时,有三种入口:
|
|
@@ -384,7 +394,7 @@ pnpm check # 以上三步
|
|
|
384
394
|
- 半离线模式下 EDA 把**每个本地工程目录当成一个 "team"**:`eda_list_teams` 返回的 `uuid` 就是目录路径,而工程记录里的 `teamUuid` 是另一种真 uuid。`eda_list_projects` 无过滤时会遍历所有 team 聚合,因此第一次调用会比带 `teamUuid` 时慢一些。
|
|
385
395
|
- `lceda-pro.exe` 直接 `spawn`(含 ShellExecute)会**立刻以退出码 0 退出**:只有交给桌面外壳(`explorer.exe`)才能起来,所以 `eda_launch` 在 Windows 上固定走 explorer + 临时 `.cmd`。
|
|
386
396
|
- 命令行带工程路径启动**被应用忽略**(活动文档仍是 HOME),所以 `eda_launch({ project })` 只保证"把参数传下去",返回值里 `projectOpenedByArgument: false` 与 `projectNote` 会明确说明;要打开工程请用 `eda_open_project`。
|
|
387
|
-
- 扩展与桥接的**线协议版本必须成对升级**(当前 0.4.
|
|
397
|
+
- 扩展与桥接的**线协议版本必须成对升级**(当前 0.4.1,只递增最小位):白名单变了就要重新导入 zip,否则新工具会报 `API method not allowed`;`eda_status` 的 `extensionVersions` 会指出每个页面连上来的版本。
|
|
388
398
|
|
|
389
399
|
## 仓库结构
|
|
390
400
|
|
package/docs/release.md
CHANGED
|
@@ -162,6 +162,8 @@ CHANGELOG 里要写明「是否需要重新导入扩展」。
|
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
- [ ] `CHANGELOG.md` 把该版本标为「已发布到 npm」
|
|
165
|
+
- [ ] 发布失败要补发时**不要重打 tag**:在 Actions 里手动运行 `publish`,勾选 `publish` 输入(`workflow_dispatch` 从 main 取代码,自动带上失败之后的所有修复;tag 校验只在 tag 触发时生效)。
|
|
166
|
+
- [ ] 流水线自身失败时先本地复跑同一条命令:`pnpm check`(= typecheck + 6 套单测 + contract-check + build + guard:bundle)
|
|
165
167
|
- [ ] npmmirror 已同步(`dist-tags.latest` 一致)
|
|
166
168
|
|
|
167
169
|
## 7. 已知失败模式速查
|
|
@@ -150,3 +150,20 @@ C. 只做 PCB:
|
|
|
150
150
|
- [ ] 视觉:导出图片自查,标签不压符号、器件不重叠、功能框清晰。
|
|
151
151
|
- [ ] 保存:文档已保存,工程已 eda_export_project 备份。
|
|
152
152
|
- [ ] Gerber 已导出且层数与板层一致。
|
|
153
|
+
|
|
154
|
+
## 12. 环境与生效规则(全新会话必读)
|
|
155
|
+
|
|
156
|
+
这套工具分三层,改动生效范围完全不同——不确定时先看这张表,不要凭直觉重试。
|
|
157
|
+
|
|
158
|
+
| 改了什么 | 谁来做 | 怎么生效 | 怎么确认 |
|
|
159
|
+
|---|---|---|---|
|
|
160
|
+
| 插件源码 `src/`(宿主侧:配置、路由、技能注册、MCP 工具表) | 开发者 | `node build.mjs` 后**重启 DSH**(桌面端关掉重开) | 会话里能看到新工具;`/api/dsh-eda-mcp/diagnostics` 的 `skills` 字段 |
|
|
161
|
+
| 扩展 `jlc-extension/`(白名单、内部助手、编码处理) | 用户 | 嘉立创EDA 里**重新导入** `dsh-eda-bridge-extension-<版本>.zip` 并**重启应用** | `eda_status` 的 `extensionVersions` 与 `bridgeVersion` 一致 |
|
|
162
|
+
| 只需重启桥接子进程(改了 `lib/bridge.mjs`、换端口、卡住) | agent 可自行 | `POST /api/dsh-eda-mcp/reconnect` | 返回 200 且 `eda_status.connectedClients ≥ 1` |
|
|
163
|
+
| 没改任何东西但工具莫名消失 | agent | 先 reconnect;仍不行看 `/diagnostics` 的 `clientStartupError` | 工具重新出现在会话里 |
|
|
164
|
+
|
|
165
|
+
- **插件代码改动对当前会话完全不生效**:宿主在启动时把 `lib/index.js` 载入内存,之后改文件、重新 build 都不会热更新。
|
|
166
|
+
- 版本纪律:扩展线协议**只递增最小位**(`0.4.1 → 0.4.2`),每次改动必须同时改 `jlc-extension/extension.json` 的 `version` 与桥接的 `VERSION`;不一致时 `eda_status.versionMismatch` 为 true。
|
|
167
|
+
- 新会话开工顺序:`eda_status`(连没连、版本对不对)→ 本技能(怎么干)→ 再动手;不要先假设环境就绪。
|
|
168
|
+
- 扩展 zip 位置:`node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<版本>.zip`,或设置卡片下载(文件名自带版本号)。
|
|
169
|
+
- 排查顺序:`eda_status` → `/api/dsh-eda-mcp/diagnostics`(`clientStartupError` / `skills` / `bridgeHealth`)→ `eda_list_api_methods`(白名单)→ 应用侧 DRC 面板。
|
package/docs/tools.md
CHANGED
|
@@ -71,7 +71,7 @@ Design limits enforced by these tools (track widths, via sizes, net naming) are
|
|
|
71
71
|
| `eda_draw_circle` | Draw a circle. Coordinates are MCP units. | `centerX: number, centerY: number, radius: number, color?: string, fillColor?: string, lineWidth?: integer, lineType?: integer, fillStyle?: string` |
|
|
72
72
|
| `eda_draw_text` | Draw text. Coordinates are MCP units. | `x: number, y: number, content: string, rotation?: integer, textColor?: string, fontName?: string, fontSize?: number, bold?: boolean, italic?: boolean, underLine?: boolean, alignMode?: integer` |
|
|
73
73
|
| `eda_place_net_label` | Place a net label at x/y. The real API (sch_PrimitiveAttribute.createNetLabel) is probed once per session; when the build never answers, a plain TEXT label is placed instead, which carries no electrical meaning and must be paired with a stub wire. Pass probe:true to re-check. | `x: number, y: number, net: string, fontSize?: number, rotation?: integer` |
|
|
74
|
-
| `eda_place_net_labels` | Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }]. | `items: array, probe?: boolean` |
|
|
74
|
+
| `eda_place_net_labels` | Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }]. | `items: array, probe?: boolean, concurrency?: integer` |
|
|
75
75
|
| `eda_move_component` | Move/rotate/mirror a schematic component (sch_PrimitiveComponent.modify) without deleting and re-placing it, so the designator and value survive. | `primitiveId: string, x?: number, y?: number, rotation?: number, mirror?: boolean` |
|
|
76
76
|
| `eda_delete_primitive` | Delete a schematic primitive by kind and primitive id. Net ports, net flags and net labels are attribute primitives and need kind "netport"/"netflag"/"netlabel"; the text fallback labels are kind "text". | `kind: string, primitiveId: string` |
|
|
77
77
|
|
|
@@ -82,7 +82,7 @@ Design limits enforced by these tools (track widths, via sizes, net naming) are
|
|
|
82
82
|
| `eda_save_document` | Save the active schematic document. | `` |
|
|
83
83
|
| `eda_zoom_to_fit` | Zoom to fit all primitives in the active document. | `tabId?: string` |
|
|
84
84
|
| `eda_zoom_to_region` | Zoom to a region. Schematic coordinates are MCP units. | `left: number, right: number, top: number, bottom: number, tabId?: string` |
|
|
85
|
-
| `eda_run_drc` | Run schematic DRC with includeVerboseError
|
|
85
|
+
| `eda_run_drc` | Run schematic DRC with includeVerboseError. On EDA 3.2.186 the app still returns AGGREGATED entries ({type:"warn",count:N}) with no primitiveId/coordinates, so entries are reported as-is and raw keeps the untouched payload; expect counts plus a hint until the app exposes details. | `strict?: boolean` |
|
|
86
86
|
| `eda_auto_layout` | Run schematic auto-layout (sch_Document.autoLayout). DISABLED by default: automated layout is not used unless the user asks for it. Pass force:true only then. | `uuids?: array, force?: boolean` |
|
|
87
87
|
| `eda_auto_route` | Run schematic auto-routing (sch_Document.autoRouting). DISABLED by default: route by hand unless the user explicitly asks. Pass force:true only then. | `uuids?: array, force?: boolean` |
|
|
88
88
|
| `eda_export_netlist` | Export the schematic netlist as text (default JLCEDA format). Returns null when no schematic is active - check eda_get_active_document / open the schematic page first. | `type?: string` |
|
|
Binary file
|
|
@@ -326,7 +326,19 @@ function dshEdaMcpStatus() {
|
|
|
326
326
|
/(csv|json|net|plain|xml)/i.test(mimeType) ||
|
|
327
327
|
/\.(csv|json|txt|net|xml)$/i.test(name);
|
|
328
328
|
if (isText && typeof TextDecoder !== 'undefined') {
|
|
329
|
-
|
|
329
|
+
// A blanket UTF-8 decode destroyed text payloads that carry another encoding: the
|
|
330
|
+
// BOM CSV is UTF-16LE, so the bytes became replacement characters before the bridge
|
|
331
|
+
// ever saw them. Detect the BOM and report which decoder ran.
|
|
332
|
+
var encoding = 'utf-8';
|
|
333
|
+
var offset = 0;
|
|
334
|
+
if (bytes.length >= 2 && bytes[0] === 0xFF && bytes[1] === 0xFE) { encoding = 'utf-16le'; offset = 2; }
|
|
335
|
+
else if (bytes.length >= 2 && bytes[0] === 0xFE && bytes[1] === 0xFF) { encoding = 'utf-16be'; offset = 2; }
|
|
336
|
+
else if (bytes.length >= 3 && bytes[0] === 0xEF && bytes[1] === 0xBB && bytes[2] === 0xBF) { offset = 3; }
|
|
337
|
+
var body = bytes.subarray(offset);
|
|
338
|
+
var text;
|
|
339
|
+
try { text = new TextDecoder(encoding).decode(body); }
|
|
340
|
+
catch (_) { text = new TextDecoder('utf-8').decode(body); encoding = 'utf-8'; }
|
|
341
|
+
return { mimeType: mimeType, name: name, size: bytes.length, encoding: encoding, text: text };
|
|
330
342
|
}
|
|
331
343
|
return { mimeType: mimeType, name: name, size: bytes.length, dataUrl: 'data:' + mimeType + ';base64,' + bytesToBase64(bytes) };
|
|
332
344
|
}
|
|
@@ -474,8 +486,12 @@ function dshEdaMcpStatus() {
|
|
|
474
486
|
error: JOB.error
|
|
475
487
|
};
|
|
476
488
|
}
|
|
477
|
-
|
|
478
|
-
|
|
489
|
+
// Two sets gate the wire: ALLOWED_API for plain calls and FILE_METHODS for the ones
|
|
490
|
+
// that hand a File back. Only checking the first one made every export (BOM,
|
|
491
|
+
// netlist, Gerber) answer "API method not allowed".
|
|
492
|
+
if (!ALLOWED_API.has(message.method) && !FILE_METHODS.has(message.method)) {
|
|
493
|
+
throw new Error('API method not allowed: ' + String(message.method) +
|
|
494
|
+
' (the extension accepts ' + (ALLOWED_API.size + FILE_METHODS.size) + ' methods: ALLOWED_API + FILE_METHODS)');
|
|
479
495
|
}
|
|
480
496
|
if (message.method === 'dmt_EditorControl.getCurrentRenderedAreaImage') {
|
|
481
497
|
var blob = await callApi(message.method, message.args || []);
|
|
@@ -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.2",
|
|
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
|
@@ -332,7 +332,7 @@ var WORKFLOW_GUIDE = WORKFLOW_SUMMARY + "\n\n" + [
|
|
|
332
332
|
].join("\n");
|
|
333
333
|
|
|
334
334
|
// src/bridge/index.ts
|
|
335
|
-
var VERSION = "0.4.
|
|
335
|
+
var VERSION = "0.4.2";
|
|
336
336
|
var WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009);
|
|
337
337
|
var WS_HOST = "127.0.0.1";
|
|
338
338
|
var ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? "1") !== "0";
|
|
@@ -363,9 +363,14 @@ function safeAreas(paperName) {
|
|
|
363
363
|
function decodeTextPayload(value) {
|
|
364
364
|
const text = typeof value === "string" ? value : "";
|
|
365
365
|
if (text === "") return text;
|
|
366
|
-
|
|
367
|
-
if (
|
|
368
|
-
|
|
366
|
+
if (text.charCodeAt(0) === 65279) return text.slice(1);
|
|
367
|
+
if (text.charCodeAt(0) === 255 && text.charCodeAt(1) === 254) {
|
|
368
|
+
return Buffer.from(text.slice(2), "latin1").toString("utf16le");
|
|
369
|
+
}
|
|
370
|
+
if (text.length > 3 && text.charCodeAt(1) === 0 && text.charCodeAt(3) === 0) {
|
|
371
|
+
return Buffer.from(text, "latin1").toString("utf16le");
|
|
372
|
+
}
|
|
373
|
+
return text;
|
|
369
374
|
}
|
|
370
375
|
function checkPointInFrame(x, y, paperName, margin = 0) {
|
|
371
376
|
const paper = paperOf(paperName);
|
|
@@ -604,8 +609,8 @@ var TOOLS = [
|
|
|
604
609
|
{ name: "eda_open_document", description: "Open/activate a document tab by uuid (dmt_EditorControl.openDocument). Works for a schematic page or a PCB, so tab-dependent APIs can run.", inputSchema: objectSchema({ documentUuid: { type: "string" }, splitScreenId: { type: "string" } }, ["documentUuid"]) },
|
|
605
610
|
{ name: "eda_get_net_labels", description: "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.", inputSchema: objectSchema({ parentPrimitiveId: { type: "string" } }) },
|
|
606
611
|
{ name: "eda_place_net_label", description: "Place a net label at x/y. The real API (sch_PrimitiveAttribute.createNetLabel) is probed once per session; when the build never answers, a plain TEXT label is placed instead, which carries no electrical meaning and must be paired with a stub wire. Pass probe:true to re-check.", inputSchema: objectSchema({ x: { type: "number" }, y: { type: "number" }, net: { type: "string" }, fontSize: { type: "number" }, rotation: { type: "integer", enum: [0, 90, 180, 270] } }, ["x", "y", "net"]) },
|
|
607
|
-
{ name: "eda_place_net_labels", description: "Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].", inputSchema: objectSchema({ items: { type: "array", items: { type: "object", additionalProperties: true } }, probe: { type: "boolean" } }, ["items"]) },
|
|
608
|
-
{ name: "eda_run_drc", description:
|
|
612
|
+
{ name: "eda_place_net_labels", description: "Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].", inputSchema: objectSchema({ items: { type: "array", items: { type: "object", additionalProperties: true } }, probe: { type: "boolean" }, concurrency: { type: "integer", minimum: 1, maximum: 8 } }, ["items"]) },
|
|
613
|
+
{ name: "eda_run_drc", description: 'Run schematic DRC with includeVerboseError. On EDA 3.2.186 the app still returns AGGREGATED entries ({type:"warn",count:N}) with no primitiveId/coordinates, so entries are reported as-is and raw keeps the untouched payload; expect counts plus a hint until the app exposes details.', inputSchema: objectSchema({ strict: { type: "boolean" } }) },
|
|
609
614
|
{ name: "eda_auto_layout", description: "Run schematic auto-layout (sch_Document.autoLayout). DISABLED by default: automated layout is not used unless the user asks for it. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
|
|
610
615
|
{ name: "eda_auto_route", description: "Run schematic auto-routing (sch_Document.autoRouting). DISABLED by default: route by hand unless the user explicitly asks. Pass force:true only then.", inputSchema: objectSchema({ uuids: { type: "array", items: { type: "string" } }, force: { type: "boolean" } }) },
|
|
611
616
|
{ name: "eda_export_netlist", description: "Export the schematic netlist as text (default JLCEDA format). Returns null when no schematic is active - check eda_get_active_document / open the schematic page first.", inputSchema: objectSchema({ type: { type: "string" } }) },
|
|
@@ -1435,6 +1440,22 @@ function internalIdsOf(info) {
|
|
|
1435
1440
|
}
|
|
1436
1441
|
return ids;
|
|
1437
1442
|
}
|
|
1443
|
+
async function assertKnownPcbNet(toolName, net, timeoutMs, force) {
|
|
1444
|
+
const name = String(net ?? "").trim();
|
|
1445
|
+
if (name === "") throw new Error(toolName + " requires a net name");
|
|
1446
|
+
if (force === true) return;
|
|
1447
|
+
let names;
|
|
1448
|
+
try {
|
|
1449
|
+
const raw = await callExtension("pcb_Net.getAllNetsName", [], Math.min(timeoutMs, 8e3));
|
|
1450
|
+
names = new Set((Array.isArray(raw) ? raw : []).map((value) => String(value)));
|
|
1451
|
+
} catch {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (names.size === 0) return;
|
|
1455
|
+
if (names.has(name)) return;
|
|
1456
|
+
const sample = [...names].slice(0, 12).join(", ");
|
|
1457
|
+
throw new Error(toolName + ': net "' + name + '" is not on this PCB (' + names.size + " nets: " + sample + "\u2026). A write on an unknown net creates a phantom net that DRC reports as a netlist error; pass force:true if that is really intended.");
|
|
1458
|
+
}
|
|
1438
1459
|
async function executeTool(name, rawArgs, timeoutMs) {
|
|
1439
1460
|
const args = asRecord(rawArgs);
|
|
1440
1461
|
const tool = PCB_TOOL_ALIASES[name] ?? name;
|
|
@@ -1981,16 +2002,16 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1981
2002
|
case "eda_place_net_labels": {
|
|
1982
2003
|
const items = Array.isArray(args.items) ? args.items.map(asRecord) : [];
|
|
1983
2004
|
if (items.length === 0) throw new Error("eda_place_net_labels requires items: [{ pinPrimitiveId, net }] or [{ x, y, net }]");
|
|
1984
|
-
const
|
|
1985
|
-
|
|
2005
|
+
const concurrency = Number.isFinite(Number(args.concurrency)) ? Math.max(1, Math.min(8, Number(args.concurrency))) : 4;
|
|
2006
|
+
const results = await mapLimit(items, concurrency, async (item) => {
|
|
1986
2007
|
const net = item.net === void 0 ? "" : String(item.net);
|
|
1987
2008
|
try {
|
|
1988
2009
|
const placed = item.pinPrimitiveId !== void 0 ? await placeAtPin("eda_place_net_label_at_pin", { ...item, net }, timeoutMs) : await placeNetLabel(net, Number(item.x), Number(item.y), item.fontSize, timeoutMs, { probe: args.probe === true });
|
|
1989
|
-
|
|
2010
|
+
return { ok: true, net, ...placed };
|
|
1990
2011
|
} catch (error) {
|
|
1991
|
-
|
|
2012
|
+
return { ok: false, net, error: error instanceof Error ? error.message : String(error) };
|
|
1992
2013
|
}
|
|
1993
|
-
}
|
|
2014
|
+
});
|
|
1994
2015
|
const done = results.filter((entry) => entry.ok).length;
|
|
1995
2016
|
return {
|
|
1996
2017
|
total: items.length,
|
|
@@ -2045,11 +2066,35 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2045
2066
|
const type = Number(record.documentType);
|
|
2046
2067
|
return { document: info, documentTypeName: DOCUMENT_TYPES[type] ?? String(record.documentType ?? "unknown"), documentType: record.documentType };
|
|
2047
2068
|
}
|
|
2048
|
-
case "eda_open_document":
|
|
2049
|
-
|
|
2050
|
-
|
|
2069
|
+
case "eda_open_document": {
|
|
2070
|
+
const documentUuid = String(args.documentUuid ?? "");
|
|
2071
|
+
if (documentUuid === "") throw new Error("documentUuid is required");
|
|
2072
|
+
const raw = await callExtension("dmt_EditorControl.openDocument", [
|
|
2073
|
+
documentUuid,
|
|
2051
2074
|
args.splitScreenId === void 0 ? void 0 : String(args.splitScreenId)
|
|
2052
2075
|
], timeoutMs);
|
|
2076
|
+
await sleep(600);
|
|
2077
|
+
let tabIds = [];
|
|
2078
|
+
try {
|
|
2079
|
+
const tree = asRecord(await callExtension("dmt_EditorControl.getSplitScreenTree", [], Math.min(timeoutMs, 4e3)));
|
|
2080
|
+
tabIds = (Array.isArray(tree.tabs) ? tree.tabs : []).map((tab) => String(pickValue(asRecord(tab), "tabId") ?? ""));
|
|
2081
|
+
} catch {
|
|
2082
|
+
}
|
|
2083
|
+
let activeUuid = "";
|
|
2084
|
+
try {
|
|
2085
|
+
const info = asRecord(await callExtension("dmt_SelectControl.getCurrentDocumentInfo", [], Math.min(timeoutMs, 4e3)));
|
|
2086
|
+
activeUuid = String(pickValue(info, "uuid", "Uuid") ?? "");
|
|
2087
|
+
} catch {
|
|
2088
|
+
}
|
|
2089
|
+
const tabOpened = tabIds.length > 1 || activeUuid !== "" && activeUuid !== "0";
|
|
2090
|
+
return {
|
|
2091
|
+
...raw !== null && typeof raw === "object" ? raw : { result: raw ?? null },
|
|
2092
|
+
opened: tabOpened,
|
|
2093
|
+
activeDocumentUuid: activeUuid || null,
|
|
2094
|
+
tabIds,
|
|
2095
|
+
hint: tabOpened ? null : "The app accepted openDocument but created no editor tab (only the Start Page is open). This build does not open a document programmatically from the HOME screen: open the schematic/PCB page once in the app (double-click the project), then retry. Do not assume the page is active."
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
2053
2098
|
case "eda_set_no_connect": {
|
|
2054
2099
|
const ids = Array.isArray(args.primitiveIds) ? args.primitiveIds.map(String) : [];
|
|
2055
2100
|
if (ids.length === 0) throw new Error("primitiveIds is required");
|
|
@@ -2254,7 +2299,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2254
2299
|
raw: entry
|
|
2255
2300
|
};
|
|
2256
2301
|
}),
|
|
2257
|
-
hint: '
|
|
2302
|
+
hint: 'EDA 3.2.186 returns AGGREGATED entries only (e.g. {type:"warn",count:1}) even with includeVerboseError, so message/primitiveId stay null \u2014 "raw" carries exactly what the app sent. Per-problem coordinates are not available on this build; use eda_pcb_run_drc for the PCB side and the DRC panel for details.'
|
|
2258
2303
|
};
|
|
2259
2304
|
}
|
|
2260
2305
|
return { ok: Boolean(verbose), count: null, entries: [], raw: verbose };
|
|
@@ -2297,7 +2342,8 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2297
2342
|
const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, format], toolTimeout(args, timeoutMs));
|
|
2298
2343
|
if (format === "xlsx") return payload;
|
|
2299
2344
|
const raw = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : "";
|
|
2300
|
-
|
|
2345
|
+
const encoding = payload !== null && typeof payload === "object" ? payload.encoding : void 0;
|
|
2346
|
+
return { format, bom: decodeTextPayload(raw), encoding: encoding ?? "unknown" };
|
|
2301
2347
|
}
|
|
2302
2348
|
case "eda_delete_primitive": {
|
|
2303
2349
|
const kind = String(args.kind);
|
|
@@ -2387,6 +2433,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2387
2433
|
if (layer === void 0) throw new Error("pcb_draw_track requires a valid layer");
|
|
2388
2434
|
const net = String(args.net);
|
|
2389
2435
|
assertNetName(net, "pcb_draw_track");
|
|
2436
|
+
await assertKnownPcbNet("pcb_draw_track", net, timeoutMs, args.force === true);
|
|
2390
2437
|
const width = checkTrackWidth(net, args.lineWidth === void 0 ? void 0 : Number(args.lineWidth), args.force === true);
|
|
2391
2438
|
if (!width.ok) throw new Error("pcb_draw_track: " + width.errors.join("; "));
|
|
2392
2439
|
const created = await callExtension("pcb_PrimitiveLine.create", [
|
|
@@ -2403,6 +2450,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2403
2450
|
case "pcb_draw_via": {
|
|
2404
2451
|
const net = String(args.net);
|
|
2405
2452
|
assertNetName(net, "pcb_draw_via");
|
|
2453
|
+
await assertKnownPcbNet("pcb_draw_via", net, timeoutMs, args.force === true);
|
|
2406
2454
|
const role = classifyNet(net);
|
|
2407
2455
|
const defaultVia = role === "signal" ? VIA_SIZES_MM[0] : VIA_SIZES_MM[1];
|
|
2408
2456
|
const hole = args.holeDiameter === void 0 ? defaultVia.hole * 39.37007874015748 : Number(args.holeDiameter);
|
package/lib/index.js
CHANGED
|
@@ -19,6 +19,7 @@ var EDA_MCP_GUIDANCE = [
|
|
|
19
19
|
"\u7ED8\u5236\u7EC6\u8282\uFF1A\u539F\u7406\u56FE\u5750\u6807\u4E3A 0.01inch\uFF0C\u539F\u70B9\u5728 A4 \u56FE\u6846\u5DE6\u4E0B\u89D2\uFF0Cy \u5411\u4E0A\uFF0C\u56FE\u6846 x 0~1170\u3001y 0~825\uFF0C\u6807\u9898\u680F x 460~1160\u3001y 10~190 \u5FC5\u987B\u907F\u8BA9\uFF08\u5148 eda_get_page_info\uFF09\u3002PCB \u5750\u6807\u4E3A mil\u3002\u677F\u6846\u7528 pcb_draw_board_outline\uFF08\u677F\u6846\u5C42 11 \u7684\u4E00\u6761 pcb_PrimitivePolyline\uFF0C\u7EBF\u5BBD\u9ED8\u8BA4 10mil\uFF09\uFF1B\u533A\u57DF\u53EA\u80FD\u7528\u94DC\u5C42 top/bottom/\u5185\u5C42(15\u201344)/MULTI(12)\u3002",
|
|
20
20
|
"\u957F\u4EFB\u52A1\uFF1Aeda_pcb_auto_route \u9ED8\u8BA4\u5F02\u6B65\u542F\u52A8\uFF08EDA \u81EA\u5DF1\u663E\u793A\u8FDB\u5EA6\u6761\uFF09\uFF0C\u5FC5\u987B\u7528 eda_pcb_job_status \u8F6E\u8BE2 running \u76F4\u5230 false\uFF0C\u4E0D\u8981\u52A0\u5927\u8D85\u65F6\uFF1Beda_pcb_clear_routing \u540C\u7406\uFF08\u65E7\u6269\u5C55\u81EA\u52A8\u5206\u6279\uFF0C\u6309 done \u7EE7\u7EED\u8C03\u7528\uFF09\u3002",
|
|
21
21
|
"\u539F\u7406\u56FE API\uFF08sch_Net.*\u3001\u7F51\u8868\u5BFC\u51FA\uFF09\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u6D3B\u52A8\u6587\u6863\uFF1A\u8FD4\u56DE\u7A7A\u65F6\u5148\u7528 eda_get_active_document \u67E5\u3001eda_open_document \u5207\u5230\u539F\u7406\u56FE\u9875\u3002",
|
|
22
|
+
"\u751F\u6548\u8303\u56F4\uFF08\u6539\u52A8\u4E0D\u70ED\u66F4\u65B0\uFF09\uFF1A\u63D2\u4EF6\u6E90\u7801 src/ \u6539\u52A8\u8981 node build.mjs \u540E\u91CD\u542F DSH\uFF0C\u5F53\u524D\u4F1A\u8BDD\u4E0D\u4F1A\u751F\u6548\uFF1B\u6269\u5C55 jlc-extension/ \u6539\u52A8\u8981\u91CD\u65B0\u5BFC\u5165 dsh-eda-bridge-extension-<\u7248\u672C>.zip \u5E76\u91CD\u542F\u5609\u7ACB\u521BEDA\uFF08eda_status \u7684 extensionVersions \u5FC5\u987B\u4E0E bridgeVersion \u4E00\u81F4\uFF09\uFF1B\u53EA\u6709\u91CD\u542F\u6865\u63A5\u5B50\u8FDB\u7A0B\u80FD\u7531 agent \u81EA\u5DF1\u505A\uFF08POST /api/dsh-eda-mcp/reconnect\uFF09\u3002\u6392\u67E5\u51FA\u53E3\uFF1A/api/dsh-eda-mcp/diagnostics \u7684 clientStartupError / skills / bridgeHealth\u3002\u6269\u5C55\u7248\u672C\u53EA\u9012\u589E\u6700\u5C0F\u4F4D\u3002",
|
|
22
23
|
'\u539F\u7406\u56FE\u5DE5\u4F5C\u89C4\u8303\uFF08\u9ED8\u8BA4\u624B\u5DE5\uFF0C\u7981\u6B62\u4E3A\u7701\u4E8B\u7528\u81EA\u52A8\u5E03\u5C40/\u81EA\u52A8\u5E03\u7EBF\uFF09\uFF1A\u6309 10 \u5355\u4F4D\u6805\u683C\u5BF9\u9F50\uFF1B\u4FE1\u53F7\u5DE6\u2192\u53F3\u3001\u7535\u6E90\u5728\u4E0A\u3001\u5730\u5728\u4E0B\uFF1B\u5668\u4EF6\u95F4\u7559 \u226520 \u5355\u4F4D\u8D70\u7EBF\u4E0E\u6807\u7B7E\u7A7A\u95F4\uFF1B\u5148 eda_get_page_info \u770B\u5B89\u5168\u533A\u518D\u6446\u4F4D\uFF1B\u6446\u5B8C\u7528 eda_get_components \u68C0\u67E5\u5305\u56F4\u76D2\u4E0D\u91CD\u53E0\u3002eda_auto_layout / eda_auto_route \u9ED8\u8BA4\u88AB\u62D2\u7EDD\uFF08\u9700 force:true \u4E14\u8BBE\u7F6E\u91CC\u653E\u884C\uFF09\uFF0C\u4EC5\u5F53\u7528\u6237\u660E\u786E\u8981\u6C42\u65F6\u4F7F\u7528\uFF1B\u7248\u9762\u68C0\u67E5\u4F18\u5148"\u5BFC\u51FA\u56FE\u7247\u81EA\u884C\u8BC6\u522B"\u800C\u4E0D\u662F\u8BA9 EDA \u91CD\u6392\u3002',
|
|
23
24
|
'\u8DE8\u533A\u57DF\u540C\u540D\u7F51\u7EDC\u5FC5\u987B\u7528 eda_place_net_port / \u7F51\u7EDC\u6807\u7B7E\uFF1Aeda_draw_wire \u7684 net \u53EA\u5BF9\u60AC\u7A7A\u5BFC\u7EBF\u6709\u6548\uFF0C\u5BFC\u7EBF\u4E00\u65E6\u63A5\u89E6\u5F15\u811A\uFF0CEDA \u4F1A\u6309\u8FDE\u901A\u6027\u91CD\u7B97\u7F51\u7EDC\u540D\uFF08\u5B9E\u6D4B TESTNET \u88AB\u5E76\u5165 LED_A\uFF09\uFF0C\u6240\u4EE5"\u540C\u540D\u5BFC\u7EBF = \u540C\u540D\u7F51\u7EDC"\u4E0D\u6210\u7ACB\u3002',
|
|
24
25
|
"\u7F51\u7EDC\u6807\u7B7E\uFF1Aeda_place_net_label \u5148\u771F\u5B9E\u8C03 createNetLabel\uFF0C\u8BE5 API \u5728 3.2.186 \u4E0A\u4E0D\u54CD\u5E94 \u2192 \u63A2\u6D4B\u7ED3\u8BBA\u6309\u4F1A\u8BDD\u7F13\u5B58\u540E\u81EA\u52A8\u964D\u7EA7\u4E3A\u7EAF\u6587\u672C\u6807\u7B7E\uFF08\u65E0\u7535\u6C14\u8BED\u4E49\uFF0C\u9760 stub \u5BFC\u7EBF\u627F\u8F7D\u7F51\u7EDC\uFF09\uFF1B\u6279\u91CF\u52A1\u5FC5\u7528 eda_place_net_labels \u4E00\u6B21\u8C03\u7528\uFF0C\u4E0D\u8981\u4E32\u884C\u4E0A\u767E\u6B21\uFF08\u4F1A\u6253\u7206 run_code \u8D85\u65F6\u4E14\u6CA1\u6709\u8FDB\u5EA6\uFF09\u3002\u6807\u7B7E\u53EF\u679A\u4E3E/\u6E05\u7406\uFF1Aeda_get_net_labels + eda_delete_primitive(kind: netlabel|netport|netflag|text)\u3002",
|
|
@@ -176,7 +177,24 @@ var SKILL_PLAYBOOK_LINES = [
|
|
|
176
177
|
"- [ ] BOM\uFF1A\u4F4D\u53F7\u8FDE\u7EED\u3001\u5C01\u88C5\u975E\u7A7A\u3001\u65E0 DUMMY\u3002",
|
|
177
178
|
"- [ ] \u89C6\u89C9\uFF1A\u5BFC\u51FA\u56FE\u7247\u81EA\u67E5\uFF0C\u6807\u7B7E\u4E0D\u538B\u7B26\u53F7\u3001\u5668\u4EF6\u4E0D\u91CD\u53E0\u3001\u529F\u80FD\u6846\u6E05\u6670\u3002",
|
|
178
179
|
"- [ ] \u4FDD\u5B58\uFF1A\u6587\u6863\u5DF2\u4FDD\u5B58\uFF0C\u5DE5\u7A0B\u5DF2 eda_export_project \u5907\u4EFD\u3002",
|
|
179
|
-
"- [ ] Gerber \u5DF2\u5BFC\u51FA\u4E14\u5C42\u6570\u4E0E\u677F\u5C42\u4E00\u81F4\u3002"
|
|
180
|
+
"- [ ] Gerber \u5DF2\u5BFC\u51FA\u4E14\u5C42\u6570\u4E0E\u677F\u5C42\u4E00\u81F4\u3002",
|
|
181
|
+
"",
|
|
182
|
+
"## 12. \u73AF\u5883\u4E0E\u751F\u6548\u89C4\u5219\uFF08\u5168\u65B0\u4F1A\u8BDD\u5FC5\u8BFB\uFF09",
|
|
183
|
+
"",
|
|
184
|
+
"\u8FD9\u5957\u5DE5\u5177\u5206\u4E09\u5C42\uFF0C\u6539\u52A8\u751F\u6548\u8303\u56F4\u5B8C\u5168\u4E0D\u540C\u2014\u2014\u4E0D\u786E\u5B9A\u65F6\u5148\u770B\u8FD9\u5F20\u8868\uFF0C\u4E0D\u8981\u51ED\u76F4\u89C9\u91CD\u8BD5\u3002",
|
|
185
|
+
"",
|
|
186
|
+
"| \u6539\u4E86\u4EC0\u4E48 | \u8C01\u6765\u505A | \u600E\u4E48\u751F\u6548 | \u600E\u4E48\u786E\u8BA4 |",
|
|
187
|
+
"|---|---|---|---|",
|
|
188
|
+
"| \u63D2\u4EF6\u6E90\u7801 `src/`\uFF08\u5BBF\u4E3B\u4FA7\uFF1A\u914D\u7F6E\u3001\u8DEF\u7531\u3001\u6280\u80FD\u6CE8\u518C\u3001MCP \u5DE5\u5177\u8868\uFF09 | \u5F00\u53D1\u8005 | `node build.mjs` \u540E**\u91CD\u542F DSH**\uFF08\u684C\u9762\u7AEF\u5173\u6389\u91CD\u5F00\uFF09 | \u4F1A\u8BDD\u91CC\u80FD\u770B\u5230\u65B0\u5DE5\u5177\uFF1B`/api/dsh-eda-mcp/diagnostics` \u7684 `skills` \u5B57\u6BB5 |",
|
|
189
|
+
"| \u6269\u5C55 `jlc-extension/`\uFF08\u767D\u540D\u5355\u3001\u5185\u90E8\u52A9\u624B\u3001\u7F16\u7801\u5904\u7406\uFF09 | \u7528\u6237 | \u5609\u7ACB\u521BEDA \u91CC**\u91CD\u65B0\u5BFC\u5165** `dsh-eda-bridge-extension-<\u7248\u672C>.zip` \u5E76**\u91CD\u542F\u5E94\u7528** | `eda_status` \u7684 `extensionVersions` \u4E0E `bridgeVersion` \u4E00\u81F4 |",
|
|
190
|
+
"| \u53EA\u9700\u91CD\u542F\u6865\u63A5\u5B50\u8FDB\u7A0B\uFF08\u6539\u4E86 `lib/bridge.mjs`\u3001\u6362\u7AEF\u53E3\u3001\u5361\u4F4F\uFF09 | agent \u53EF\u81EA\u884C | `POST /api/dsh-eda-mcp/reconnect` | \u8FD4\u56DE 200 \u4E14 `eda_status.connectedClients \u2265 1` |",
|
|
191
|
+
"| \u6CA1\u6539\u4EFB\u4F55\u4E1C\u897F\u4F46\u5DE5\u5177\u83AB\u540D\u6D88\u5931 | agent | \u5148 reconnect\uFF1B\u4ECD\u4E0D\u884C\u770B `/diagnostics` \u7684 `clientStartupError` | \u5DE5\u5177\u91CD\u65B0\u51FA\u73B0\u5728\u4F1A\u8BDD\u91CC |",
|
|
192
|
+
"",
|
|
193
|
+
"- **\u63D2\u4EF6\u4EE3\u7801\u6539\u52A8\u5BF9\u5F53\u524D\u4F1A\u8BDD\u5B8C\u5168\u4E0D\u751F\u6548**\uFF1A\u5BBF\u4E3B\u5728\u542F\u52A8\u65F6\u628A `lib/index.js` \u8F7D\u5165\u5185\u5B58\uFF0C\u4E4B\u540E\u6539\u6587\u4EF6\u3001\u91CD\u65B0 build \u90FD\u4E0D\u4F1A\u70ED\u66F4\u65B0\u3002",
|
|
194
|
+
"- \u7248\u672C\u7EAA\u5F8B\uFF1A\u6269\u5C55\u7EBF\u534F\u8BAE**\u53EA\u9012\u589E\u6700\u5C0F\u4F4D**\uFF08`0.4.1 \u2192 0.4.2`\uFF09\uFF0C\u6BCF\u6B21\u6539\u52A8\u5FC5\u987B\u540C\u65F6\u6539 `jlc-extension/extension.json` \u7684 `version` \u4E0E\u6865\u63A5\u7684 `VERSION`\uFF1B\u4E0D\u4E00\u81F4\u65F6 `eda_status.versionMismatch` \u4E3A true\u3002",
|
|
195
|
+
"- \u65B0\u4F1A\u8BDD\u5F00\u5DE5\u987A\u5E8F\uFF1A`eda_status`\uFF08\u8FDE\u6CA1\u8FDE\u3001\u7248\u672C\u5BF9\u4E0D\u5BF9\uFF09\u2192 \u672C\u6280\u80FD\uFF08\u600E\u4E48\u5E72\uFF09\u2192 \u518D\u52A8\u624B\uFF1B\u4E0D\u8981\u5148\u5047\u8BBE\u73AF\u5883\u5C31\u7EEA\u3002",
|
|
196
|
+
"- \u6269\u5C55 zip \u4F4D\u7F6E\uFF1A`node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<\u7248\u672C>.zip`\uFF0C\u6216\u8BBE\u7F6E\u5361\u7247\u4E0B\u8F7D\uFF08\u6587\u4EF6\u540D\u81EA\u5E26\u7248\u672C\u53F7\uFF09\u3002",
|
|
197
|
+
"- \u6392\u67E5\u987A\u5E8F\uFF1A`eda_status` \u2192 `/api/dsh-eda-mcp/diagnostics`\uFF08`clientStartupError` / `skills` / `bridgeHealth`\uFF09\u2192 `eda_list_api_methods`\uFF08\u767D\u540D\u5355\uFF09\u2192 \u5E94\u7528\u4FA7 DRC \u9762\u677F\u3002"
|
|
180
198
|
];
|
|
181
199
|
var SKILL_SECTIONS = SKILL_PLAYBOOK_LINES.filter((line) => line.startsWith("## "));
|
|
182
200
|
var SKILL_PLAYBOOK = SKILL_PLAYBOOK_LINES.join(String.fromCharCode(10));
|
|
@@ -828,6 +846,12 @@ function describeBridgeLaunch() {
|
|
|
828
846
|
return { command: runtime.command, args: [BRIDGE_PATH], source: runtime.source };
|
|
829
847
|
}
|
|
830
848
|
function apply(ctx, config) {
|
|
849
|
+
let skillState = {
|
|
850
|
+
attempted: false,
|
|
851
|
+
registered: false,
|
|
852
|
+
error: null,
|
|
853
|
+
names: []
|
|
854
|
+
};
|
|
831
855
|
let current = () => unwrapConfig(config ?? {});
|
|
832
856
|
let disposeClient;
|
|
833
857
|
let disposeSection;
|
|
@@ -879,7 +903,9 @@ function apply(ctx, config) {
|
|
|
879
903
|
extensionZipPath: resolveExtensionZipPath(),
|
|
880
904
|
reconnect: remountClient,
|
|
881
905
|
bridgeLaunch: describeBridgeLaunch,
|
|
882
|
-
|
|
906
|
+
// Surface the skill registration so "did it register?" is answerable from
|
|
907
|
+
// /api/dsh-eda-mcp/diagnostics instead of guessing from the skill list.
|
|
908
|
+
diagnostics: () => ({ ...diagnostics.liveSnapshot(), skills: skillState }),
|
|
883
909
|
launchConfig: () => bridgeLaunchFor(current()),
|
|
884
910
|
selfTest: async () => {
|
|
885
911
|
const runtime = resolveBridgeRuntime();
|
|
@@ -899,12 +925,24 @@ function apply(ctx, config) {
|
|
|
899
925
|
}, "eda-mcp: settings bridge");
|
|
900
926
|
});
|
|
901
927
|
ctx.inject(["skills"], (sctx) => {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
928
|
+
sctx.effect(() => {
|
|
929
|
+
const skills = sctx.skills;
|
|
930
|
+
if (skills === void 0 || typeof skills.registerProvider !== "function") {
|
|
931
|
+
skillState = { ...skillState, attempted: true, registered: false, error: "the host exposes no skills service" };
|
|
932
|
+
return () => {
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
skillState = { ...skillState, attempted: true };
|
|
936
|
+
const dispose = skills.registerProvider(() => {
|
|
937
|
+
const provider = createEdaSkillProvider();
|
|
938
|
+
skillState = { ...skillState, registered: true, error: null, names: [EDA_SKILL_NAME] };
|
|
939
|
+
return provider;
|
|
940
|
+
});
|
|
941
|
+
return () => {
|
|
942
|
+
skillState = { ...skillState, registered: false };
|
|
943
|
+
dispose();
|
|
944
|
+
};
|
|
945
|
+
}, "eda-mcp: schematic skill");
|
|
908
946
|
});
|
|
909
947
|
const sync = () => {
|
|
910
948
|
if (disposeSection !== void 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-eda-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
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": {
|
|
@@ -75,7 +75,8 @@
|
|
|
75
75
|
"test": "node --experimental-strip-types test/node-runtime.test.ts && node --experimental-strip-types test/config-values.test.ts && node --experimental-strip-types test/design-rules.test.ts && node --experimental-strip-types test/seams.test.ts && node --experimental-strip-types test/eda-launch.test.ts && node --experimental-strip-types test/skills.test.ts",
|
|
76
76
|
"seam-check": "node scripts/seam-check.mjs",
|
|
77
77
|
"contract-check": "node scripts/contract-check.mjs",
|
|
78
|
-
"
|
|
78
|
+
"guard:bundle": "node scripts/guard-bundle-paths.mjs",
|
|
79
|
+
"check": "pnpm typecheck && pnpm test && pnpm contract-check && pnpm build && pnpm guard:bundle",
|
|
79
80
|
"docs": "node scripts/gen-tools-doc.mjs && node scripts/gen-skill-doc.mjs",
|
|
80
81
|
"prepublishOnly": "pnpm check",
|
|
81
82
|
"prepare": "pnpm build"
|
package/src/bridge/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from './design-rules.ts'
|
|
19
19
|
import { WORKFLOW_GUIDE, WORKFLOW_SUMMARY } from './workflow-guide.ts'
|
|
20
20
|
|
|
21
|
-
const VERSION = '0.4.
|
|
21
|
+
const VERSION = '0.4.2'
|
|
22
22
|
const WS_PORT = Number(process.env.DSH_EDA_MCP_WS_PORT ?? 39009)
|
|
23
23
|
const WS_HOST = '127.0.0.1'
|
|
24
24
|
const ALLOW_RAW_API = (process.env.DSH_EDA_MCP_ALLOW_RAW_API ?? '1') !== '0'
|
|
@@ -58,9 +58,18 @@ function safeAreas(paperName) {
|
|
|
58
58
|
function decodeTextPayload(value) {
|
|
59
59
|
const text = typeof value === 'string' ? value : ''
|
|
60
60
|
if (text === '') return text
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// Three shapes reach us: an already-decoded string with a U+FEFF prefix, the raw
|
|
62
|
+
// BOM bytes mangled into the characters 0xFF 0xFE, and a null-interleaved UTF-16
|
|
63
|
+
// body without a BOM. Measured on EDA 3.2.186 the CSV BOM arrives mangled, which
|
|
64
|
+
// the first version of this helper missed.
|
|
65
|
+
if (text.charCodeAt(0) === 0xfeff) return text.slice(1)
|
|
66
|
+
if (text.charCodeAt(0) === 0xff && text.charCodeAt(1) === 0xfe) {
|
|
67
|
+
return Buffer.from(text.slice(2), 'latin1').toString('utf16le')
|
|
68
|
+
}
|
|
69
|
+
if (text.length > 3 && text.charCodeAt(1) === 0 && text.charCodeAt(3) === 0) {
|
|
70
|
+
return Buffer.from(text, 'latin1').toString('utf16le')
|
|
71
|
+
}
|
|
72
|
+
return text
|
|
64
73
|
}
|
|
65
74
|
|
|
66
75
|
function checkPointInFrame(x, y, paperName, margin = 0) {
|
|
@@ -296,8 +305,8 @@ const TOOLS = [
|
|
|
296
305
|
{ name: 'eda_open_document', description: 'Open/activate a document tab by uuid (dmt_EditorControl.openDocument). Works for a schematic page or a PCB, so tab-dependent APIs can run.', inputSchema: objectSchema({ documentUuid: { type: 'string' }, splitScreenId: { type: 'string' } }, ['documentUuid']) },
|
|
297
306
|
{ name: 'eda_get_net_labels', description: '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.', inputSchema: objectSchema({ parentPrimitiveId: { type: 'string' } }) },
|
|
298
307
|
{ name: 'eda_place_net_label', description: 'Place a net label at x/y. The real API (sch_PrimitiveAttribute.createNetLabel) is probed once per session; when the build never answers, a plain TEXT label is placed instead, which carries no electrical meaning and must be paired with a stub wire. Pass probe:true to re-check.', inputSchema: objectSchema({ x: { type: 'number' }, y: { type: 'number' }, net: { type: 'string' }, fontSize: { type: 'number' }, rotation: { type: 'integer', enum: [0, 90, 180, 270] } }, ['x', 'y', 'net']) },
|
|
299
|
-
{ name: 'eda_place_net_labels', description: 'Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].', inputSchema: objectSchema({ items: { type: 'array', items: { type: 'object', additionalProperties: true } }, probe: { type: 'boolean' } }, ['items']) },
|
|
300
|
-
{ name: 'eda_run_drc', description: 'Run schematic DRC with includeVerboseError
|
|
308
|
+
{ name: 'eda_place_net_labels', description: 'Place many net labels in ONE call (per-pin or by coordinate) and report done/failed per item: a 133-label schematic used to exceed the caller timeout with no progress. items: [{ pinPrimitiveId, net, stubLength?, offsetX?, offsetY? }] or [{ x, y, net, fontSize? }].', inputSchema: objectSchema({ items: { type: 'array', items: { type: 'object', additionalProperties: true } }, probe: { type: 'boolean' }, concurrency: { type: 'integer', minimum: 1, maximum: 8 } }, ['items']) },
|
|
309
|
+
{ name: 'eda_run_drc', description: 'Run schematic DRC with includeVerboseError. On EDA 3.2.186 the app still returns AGGREGATED entries ({type:"warn",count:N}) with no primitiveId/coordinates, so entries are reported as-is and raw keeps the untouched payload; expect counts plus a hint until the app exposes details.', inputSchema: objectSchema({ strict: { type: 'boolean' } }) },
|
|
301
310
|
{ name: 'eda_auto_layout', description: 'Run schematic auto-layout (sch_Document.autoLayout). DISABLED by default: automated layout is not used unless the user asks for it. Pass force:true only then.', inputSchema: objectSchema({ uuids: { type: 'array', items: { type: 'string' } }, force: { type: 'boolean' } }) },
|
|
302
311
|
{ name: 'eda_auto_route', description: 'Run schematic auto-routing (sch_Document.autoRouting). DISABLED by default: route by hand unless the user explicitly asks. Pass force:true only then.', inputSchema: objectSchema({ uuids: { type: 'array', items: { type: 'string' } }, force: { type: 'boolean' } }) },
|
|
303
312
|
{ name: 'eda_export_netlist', description: 'Export the schematic netlist as text (default JLCEDA format). Returns null when no schematic is active - check eda_get_active_document / open the schematic page first.', inputSchema: objectSchema({ type: { type: 'string' } }) },
|
|
@@ -1174,6 +1183,32 @@ function internalIdsOf(info) {
|
|
|
1174
1183
|
return ids
|
|
1175
1184
|
}
|
|
1176
1185
|
|
|
1186
|
+
/**
|
|
1187
|
+
* Refuse PCB writes on a net the board does not have.
|
|
1188
|
+
*
|
|
1189
|
+
* Measured on 3.2.186: pcb_PrimitiveLine.create happily accepts an arbitrary net
|
|
1190
|
+
* name and the board then carries a phantom net (which DRC reports as a netlist
|
|
1191
|
+
* error). force:true is the documented escape hatch.
|
|
1192
|
+
*/
|
|
1193
|
+
async function assertKnownPcbNet(toolName, net, timeoutMs, force) {
|
|
1194
|
+
const name = String(net ?? '').trim()
|
|
1195
|
+
if (name === '') throw new Error(toolName + ' requires a net name')
|
|
1196
|
+
if (force === true) return
|
|
1197
|
+
let names
|
|
1198
|
+
try {
|
|
1199
|
+
const raw = await callExtension('pcb_Net.getAllNetsName', [], Math.min(timeoutMs, 8000))
|
|
1200
|
+
names = new Set((Array.isArray(raw) ? raw : []).map(value => String(value)))
|
|
1201
|
+
} catch {
|
|
1202
|
+
// The PCB tab may not be active yet; let the write itself decide.
|
|
1203
|
+
return
|
|
1204
|
+
}
|
|
1205
|
+
if (names.size === 0) return
|
|
1206
|
+
if (names.has(name)) return
|
|
1207
|
+
const sample = [...names].slice(0, 12).join(', ')
|
|
1208
|
+
throw new Error(toolName + ': net "' + name + '" is not on this PCB (' + names.size + ' nets: ' + sample +
|
|
1209
|
+
'…). A write on an unknown net creates a phantom net that DRC reports as a netlist error; pass force:true if that is really intended.')
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1177
1212
|
async function executeTool(name, rawArgs, timeoutMs) {
|
|
1178
1213
|
const args = asRecord(rawArgs)
|
|
1179
1214
|
const tool = PCB_TOOL_ALIASES[name] ?? name
|
|
@@ -1770,18 +1805,21 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1770
1805
|
// caller's timeout with no way to learn what had already been placed.
|
|
1771
1806
|
const items = Array.isArray(args.items) ? args.items.map(asRecord) : []
|
|
1772
1807
|
if (items.length === 0) throw new Error('eda_place_net_labels requires items: [{ pinPrimitiveId, net }] or [{ x, y, net }]')
|
|
1773
|
-
|
|
1774
|
-
|
|
1808
|
+
// Measured on 3.2.186: one label costs ~2 s (the app writes per primitive), so a
|
|
1809
|
+
// sequential 133-label run still took minutes. Items are independent, so run them
|
|
1810
|
+
// with bounded concurrency; results keep the caller's order.
|
|
1811
|
+
const concurrency = Number.isFinite(Number(args.concurrency)) ? Math.max(1, Math.min(8, Number(args.concurrency))) : 4
|
|
1812
|
+
const results = await mapLimit(items, concurrency, async (item) => {
|
|
1775
1813
|
const net = item.net === undefined ? '' : String(item.net)
|
|
1776
1814
|
try {
|
|
1777
1815
|
const placed = item.pinPrimitiveId !== undefined
|
|
1778
1816
|
? await placeAtPin('eda_place_net_label_at_pin', { ...item, net }, timeoutMs)
|
|
1779
1817
|
: await placeNetLabel(net, Number(item.x), Number(item.y), item.fontSize, timeoutMs, { probe: args.probe === true })
|
|
1780
|
-
|
|
1818
|
+
return { ok: true, net, ...placed }
|
|
1781
1819
|
} catch (error) {
|
|
1782
|
-
|
|
1820
|
+
return { ok: false, net, error: error instanceof Error ? error.message : String(error) }
|
|
1783
1821
|
}
|
|
1784
|
-
}
|
|
1822
|
+
})
|
|
1785
1823
|
const done = results.filter(entry => entry.ok).length
|
|
1786
1824
|
return {
|
|
1787
1825
|
total: items.length,
|
|
@@ -1845,10 +1883,37 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
1845
1883
|
return { document: info, documentTypeName: DOCUMENT_TYPES[type] ?? String(record.documentType ?? 'unknown'), documentType: record.documentType }
|
|
1846
1884
|
}
|
|
1847
1885
|
|
|
1848
|
-
case 'eda_open_document':
|
|
1849
|
-
|
|
1850
|
-
|
|
1886
|
+
case 'eda_open_document': {
|
|
1887
|
+
const documentUuid = String(args.documentUuid ?? '')
|
|
1888
|
+
if (documentUuid === '') throw new Error('documentUuid is required')
|
|
1889
|
+
const raw = await callExtension('dmt_EditorControl.openDocument', [
|
|
1890
|
+
documentUuid, args.splitScreenId === undefined ? undefined : String(args.splitScreenId),
|
|
1851
1891
|
], timeoutMs)
|
|
1892
|
+
// Measured on 3.2.186: from a freshly launched app sitting on HOME, openDocument
|
|
1893
|
+
// answers without creating a tab, so callers thought the page was active and every
|
|
1894
|
+
// tab-dependent call then returned empty. Verify instead of trusting it.
|
|
1895
|
+
await sleep(600)
|
|
1896
|
+
let tabIds = []
|
|
1897
|
+
try {
|
|
1898
|
+
const tree = asRecord(await callExtension('dmt_EditorControl.getSplitScreenTree', [], Math.min(timeoutMs, 4000)))
|
|
1899
|
+
tabIds = (Array.isArray(tree.tabs) ? tree.tabs : []).map(tab => String(pickValue(asRecord(tab), 'tabId') ?? ''))
|
|
1900
|
+
} catch { /* the tree is only a hint */ }
|
|
1901
|
+
let activeUuid = ''
|
|
1902
|
+
try {
|
|
1903
|
+
const info = asRecord(await callExtension('dmt_SelectControl.getCurrentDocumentInfo', [], Math.min(timeoutMs, 4000)))
|
|
1904
|
+
activeUuid = String(pickValue(info, 'uuid', 'Uuid') ?? '')
|
|
1905
|
+
} catch { /* ignore */ }
|
|
1906
|
+
const tabOpened = tabIds.length > 1 || (activeUuid !== '' && activeUuid !== '0')
|
|
1907
|
+
return {
|
|
1908
|
+
...(raw !== null && typeof raw === 'object' ? raw : { result: raw ?? null }),
|
|
1909
|
+
opened: tabOpened,
|
|
1910
|
+
activeDocumentUuid: activeUuid || null,
|
|
1911
|
+
tabIds,
|
|
1912
|
+
hint: tabOpened
|
|
1913
|
+
? null
|
|
1914
|
+
: 'The app accepted openDocument but created no editor tab (only the Start Page is open). This build does not open a document programmatically from the HOME screen: open the schematic/PCB page once in the app (double-click the project), then retry. Do not assume the page is active.',
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1852
1917
|
|
|
1853
1918
|
case 'eda_set_no_connect': {
|
|
1854
1919
|
const ids = Array.isArray(args.primitiveIds) ? args.primitiveIds.map(String) : []
|
|
@@ -2019,7 +2084,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2019
2084
|
raw: entry,
|
|
2020
2085
|
}
|
|
2021
2086
|
}),
|
|
2022
|
-
hint: '
|
|
2087
|
+
hint: 'EDA 3.2.186 returns AGGREGATED entries only (e.g. {type:"warn",count:1}) even with includeVerboseError, so message/primitiveId stay null — "raw" carries exactly what the app sent. Per-problem coordinates are not available on this build; use eda_pcb_run_drc for the PCB side and the DRC panel for details.',
|
|
2023
2088
|
}
|
|
2024
2089
|
}
|
|
2025
2090
|
return { ok: Boolean(verbose), count: null, entries: [], raw: verbose }
|
|
@@ -2069,8 +2134,10 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2069
2134
|
const raw = typeof payload === 'string'
|
|
2070
2135
|
? payload
|
|
2071
2136
|
: (payload !== null && typeof payload === 'object' && typeof payload.text === 'string' ? payload.text : '')
|
|
2072
|
-
//
|
|
2073
|
-
|
|
2137
|
+
// The extension (0.4.1+) decodes text payloads with the encoding named by their BOM;
|
|
2138
|
+
// decodeTextPayload stays as a fallback for older extensions.
|
|
2139
|
+
const encoding = payload !== null && typeof payload === 'object' ? payload.encoding : undefined
|
|
2140
|
+
return { format, bom: decodeTextPayload(raw), encoding: encoding ?? 'unknown' }
|
|
2074
2141
|
}
|
|
2075
2142
|
|
|
2076
2143
|
case 'eda_delete_primitive': {
|
|
@@ -2175,6 +2242,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2175
2242
|
if (layer === undefined) throw new Error('pcb_draw_track requires a valid layer')
|
|
2176
2243
|
const net = String(args.net)
|
|
2177
2244
|
assertNetName(net, 'pcb_draw_track')
|
|
2245
|
+
await assertKnownPcbNet('pcb_draw_track', net, timeoutMs, args.force === true)
|
|
2178
2246
|
// Width defaults to the role-based suggestion and is checked against the
|
|
2179
2247
|
// 0.15 mm design minimum (docs/design-rules.md §5.1); force overrides.
|
|
2180
2248
|
const width = checkTrackWidth(net, args.lineWidth === undefined ? undefined : Number(args.lineWidth), args.force === true)
|
|
@@ -2189,6 +2257,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2189
2257
|
case 'pcb_draw_via': {
|
|
2190
2258
|
const net = String(args.net)
|
|
2191
2259
|
assertNetName(net, 'pcb_draw_via')
|
|
2260
|
+
await assertKnownPcbNet('pcb_draw_via', net, timeoutMs, args.force === true)
|
|
2192
2261
|
// Defaults follow the standard drill pairs; geometry is checked in mm.
|
|
2193
2262
|
const role = classifyNet(net)
|
|
2194
2263
|
const defaultVia = role === 'signal' ? VIA_SIZES_MM[0] : VIA_SIZES_MM[1]
|
package/src/guidance.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const EDA_MCP_GUIDANCE = [
|
|
|
21
21
|
'绘制细节:原理图坐标为 0.01inch,原点在 A4 图框左下角,y 向上,图框 x 0~1170、y 0~825,标题栏 x 460~1160、y 10~190 必须避让(先 eda_get_page_info)。PCB 坐标为 mil。板框用 pcb_draw_board_outline(板框层 11 的一条 pcb_PrimitivePolyline,线宽默认 10mil);区域只能用铜层 top/bottom/内层(15–44)/MULTI(12)。',
|
|
22
22
|
'长任务:eda_pcb_auto_route 默认异步启动(EDA 自己显示进度条),必须用 eda_pcb_job_status 轮询 running 直到 false,不要加大超时;eda_pcb_clear_routing 同理(旧扩展自动分批,按 done 继续调用)。',
|
|
23
23
|
'原理图 API(sch_Net.*、网表导出)只作用于当前活动文档:返回空时先用 eda_get_active_document 查、eda_open_document 切到原理图页。',
|
|
24
|
+
'生效范围(改动不热更新):插件源码 src/ 改动要 node build.mjs 后重启 DSH,当前会话不会生效;扩展 jlc-extension/ 改动要重新导入 dsh-eda-bridge-extension-<版本>.zip 并重启嘉立创EDA(eda_status 的 extensionVersions 必须与 bridgeVersion 一致);只有重启桥接子进程能由 agent 自己做(POST /api/dsh-eda-mcp/reconnect)。排查出口:/api/dsh-eda-mcp/diagnostics 的 clientStartupError / skills / bridgeHealth。扩展版本只递增最小位。',
|
|
24
25
|
'原理图工作规范(默认手工,禁止为省事用自动布局/自动布线):按 10 单位栅格对齐;信号左→右、电源在上、地在下;器件间留 ≥20 单位走线与标签空间;先 eda_get_page_info 看安全区再摆位;摆完用 eda_get_components 检查包围盒不重叠。eda_auto_layout / eda_auto_route 默认被拒绝(需 force:true 且设置里放行),仅当用户明确要求时使用;版面检查优先"导出图片自行识别"而不是让 EDA 重排。',
|
|
25
26
|
'跨区域同名网络必须用 eda_place_net_port / 网络标签:eda_draw_wire 的 net 只对悬空导线有效,导线一旦接触引脚,EDA 会按连通性重算网络名(实测 TESTNET 被并入 LED_A),所以"同名导线 = 同名网络"不成立。',
|
|
26
27
|
'网络标签:eda_place_net_label 先真实调 createNetLabel,该 API 在 3.2.186 上不响应 → 探测结论按会话缓存后自动降级为纯文本标签(无电气语义,靠 stub 导线承载网络);批量务必用 eda_place_net_labels 一次调用,不要串行上百次(会打爆 run_code 超时且没有进度)。标签可枚举/清理:eda_get_net_labels + eda_delete_primitive(kind: netlabel|netport|netflag|text)。',
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url'
|
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs'
|
|
9
9
|
import { dirname } from 'node:path'
|
|
10
10
|
import { EDA_MCP_GUIDANCE, SECTION_ORDER } from './guidance.ts'
|
|
11
|
-
import { createEdaSkillProvider } from './skills.ts'
|
|
11
|
+
import { createEdaSkillProvider, EDA_SKILL_NAME } from './skills.ts'
|
|
12
12
|
import { makeEdaMcpRoutes, makeEdaMcpSettingsRoutes, type SettingsBridge } from './routes.ts'
|
|
13
13
|
import { resolveBridgeRuntime } from './node-runtime.ts'
|
|
14
14
|
import { BridgeDiagnostics } from './bridge-diagnostics.ts'
|
|
@@ -226,7 +226,15 @@ function describeBridgeLaunch(): { command: string; args: string[]; source: stri
|
|
|
226
226
|
export function apply(ctx: Context, config?: EdaMcpConfig): void {
|
|
227
227
|
// Config values always reach the rest of the plugin unwrapped: on DSH >= 0.2
|
|
228
228
|
// `volatile` fields arrive as live value handles.
|
|
229
|
-
|
|
229
|
+
/** Skill registration outcome, reported by /api/dsh-eda-mcp/diagnostics. */
|
|
230
|
+
let skillState: { attempted: boolean; registered: boolean; error: string | null; names: string[] } = {
|
|
231
|
+
attempted: false,
|
|
232
|
+
registered: false,
|
|
233
|
+
error: null,
|
|
234
|
+
names: [],
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let current: () => EdaMcpConfig = () => unwrapConfig(config ?? {})
|
|
230
238
|
let disposeClient: (() => Promise<void>) | undefined
|
|
231
239
|
let disposeSection: (() => void) | undefined
|
|
232
240
|
let mounting: Promise<void> = Promise.resolve()
|
|
@@ -286,7 +294,9 @@ export function apply(ctx: Context, config?: EdaMcpConfig): void {
|
|
|
286
294
|
extensionZipPath: resolveExtensionZipPath(),
|
|
287
295
|
reconnect: remountClient,
|
|
288
296
|
bridgeLaunch: describeBridgeLaunch,
|
|
289
|
-
|
|
297
|
+
// Surface the skill registration so "did it register?" is answerable from
|
|
298
|
+
// /api/dsh-eda-mcp/diagnostics instead of guessing from the skill list.
|
|
299
|
+
diagnostics: () => ({ ...diagnostics.liveSnapshot(), skills: skillState }),
|
|
290
300
|
launchConfig: () => bridgeLaunchFor(current()),
|
|
291
301
|
selfTest: async () => {
|
|
292
302
|
const runtime = resolveBridgeRuntime()
|
|
@@ -307,12 +317,27 @@ export function apply(ctx: Context, config?: EdaMcpConfig): void {
|
|
|
307
317
|
// catalog next to local/project skills and loads on demand with the `skill` tool.
|
|
308
318
|
// Structural typing + a runtime guard keeps older hosts (no skills service) working.
|
|
309
319
|
ctx.inject(['skills'], (sctx) => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
320
|
+
// Mirrors the reference implementation (dsh matlab-mcp plugin): the registration is
|
|
321
|
+
// an effect, so it is torn down with the plugin instead of leaking a provider.
|
|
322
|
+
sctx.effect(() => {
|
|
323
|
+
const skills = (sctx as unknown as {
|
|
324
|
+
skills?: { registerProvider?: (factory: (control: unknown) => unknown) => () => void }
|
|
325
|
+
}).skills
|
|
326
|
+
if (skills === undefined || typeof skills.registerProvider !== 'function') {
|
|
327
|
+
skillState = { ...skillState, attempted: true, registered: false, error: 'the host exposes no skills service' }
|
|
328
|
+
return () => { /* nothing to undo */ }
|
|
329
|
+
}
|
|
330
|
+
skillState = { ...skillState, attempted: true }
|
|
331
|
+
const dispose = skills.registerProvider(() => {
|
|
332
|
+
const provider = createEdaSkillProvider()
|
|
333
|
+
skillState = { ...skillState, registered: true, error: null, names: [EDA_SKILL_NAME] }
|
|
334
|
+
return provider
|
|
335
|
+
})
|
|
336
|
+
return () => {
|
|
337
|
+
skillState = { ...skillState, registered: false }
|
|
338
|
+
dispose()
|
|
339
|
+
}
|
|
340
|
+
}, 'eda-mcp: schematic skill')
|
|
316
341
|
})
|
|
317
342
|
|
|
318
343
|
const sync = (): void => {
|
package/src/skills-content.ts
CHANGED
|
@@ -157,6 +157,23 @@ export const SKILL_PLAYBOOK_LINES: readonly string[] = [
|
|
|
157
157
|
"- [ ] 视觉:导出图片自查,标签不压符号、器件不重叠、功能框清晰。",
|
|
158
158
|
"- [ ] 保存:文档已保存,工程已 eda_export_project 备份。",
|
|
159
159
|
"- [ ] Gerber 已导出且层数与板层一致。",
|
|
160
|
+
"",
|
|
161
|
+
"## 12. 环境与生效规则(全新会话必读)",
|
|
162
|
+
"",
|
|
163
|
+
"这套工具分三层,改动生效范围完全不同——不确定时先看这张表,不要凭直觉重试。",
|
|
164
|
+
"",
|
|
165
|
+
"| 改了什么 | 谁来做 | 怎么生效 | 怎么确认 |",
|
|
166
|
+
"|---|---|---|---|",
|
|
167
|
+
"| 插件源码 `src/`(宿主侧:配置、路由、技能注册、MCP 工具表) | 开发者 | `node build.mjs` 后**重启 DSH**(桌面端关掉重开) | 会话里能看到新工具;`/api/dsh-eda-mcp/diagnostics` 的 `skills` 字段 |",
|
|
168
|
+
"| 扩展 `jlc-extension/`(白名单、内部助手、编码处理) | 用户 | 嘉立创EDA 里**重新导入** `dsh-eda-bridge-extension-<版本>.zip` 并**重启应用** | `eda_status` 的 `extensionVersions` 与 `bridgeVersion` 一致 |",
|
|
169
|
+
"| 只需重启桥接子进程(改了 `lib/bridge.mjs`、换端口、卡住) | agent 可自行 | `POST /api/dsh-eda-mcp/reconnect` | 返回 200 且 `eda_status.connectedClients ≥ 1` |",
|
|
170
|
+
"| 没改任何东西但工具莫名消失 | agent | 先 reconnect;仍不行看 `/diagnostics` 的 `clientStartupError` | 工具重新出现在会话里 |",
|
|
171
|
+
"",
|
|
172
|
+
"- **插件代码改动对当前会话完全不生效**:宿主在启动时把 `lib/index.js` 载入内存,之后改文件、重新 build 都不会热更新。",
|
|
173
|
+
"- 版本纪律:扩展线协议**只递增最小位**(`0.4.1 → 0.4.2`),每次改动必须同时改 `jlc-extension/extension.json` 的 `version` 与桥接的 `VERSION`;不一致时 `eda_status.versionMismatch` 为 true。",
|
|
174
|
+
"- 新会话开工顺序:`eda_status`(连没连、版本对不对)→ 本技能(怎么干)→ 再动手;不要先假设环境就绪。",
|
|
175
|
+
"- 扩展 zip 位置:`node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<版本>.zip`,或设置卡片下载(文件名自带版本号)。",
|
|
176
|
+
"- 排查顺序:`eda_status` → `/api/dsh-eda-mcp/diagnostics`(`clientStartupError` / `skills` / `bridgeHealth`)→ `eda_list_api_methods`(白名单)→ 应用侧 DRC 面板。",
|
|
160
177
|
]
|
|
161
178
|
|
|
162
179
|
export const SKILL_SECTIONS: readonly string[] = SKILL_PLAYBOOK_LINES.filter(line => line.startsWith("## "))
|
package/test/seams.test.ts
CHANGED
|
@@ -142,3 +142,19 @@ for (const name of ['@deepseek-ai/dsh-client-ui-slots', '@deepseek-ai/dsh-client
|
|
|
142
142
|
|
|
143
143
|
console.log(failures === 0 ? '\nALL PASS' : `\n${failures} FAILED`)
|
|
144
144
|
process.exit(failures === 0 ? 0 : 1)
|
|
145
|
+
|
|
146
|
+
// --- CI workflows must reference artefacts the build actually produces -------
|
|
147
|
+
// Two released pipelines broke on this exact class of drift: the bundle-path guard
|
|
148
|
+
// flagged our own documentation string, and check.yml uploaded an unversioned zip the
|
|
149
|
+
// build had stopped producing. Both are asserted here instead of being rediscovered.
|
|
150
|
+
{
|
|
151
|
+
const check = readFileSync(join(root, '.github/workflows/check.yml'), 'utf8')
|
|
152
|
+
const publish = readFileSync(join(root, '.github/workflows/publish.yml'), 'utf8')
|
|
153
|
+
report('check.yml uploads the versioned zip glob', check.includes('dsh-eda-bridge-extension-*.zip'))
|
|
154
|
+
report('check.yml runs the shared bundle guard', check.includes('pnpm guard:bundle'))
|
|
155
|
+
report('publish.yml runs the shared bundle guard', publish.includes('pnpm guard:bundle'))
|
|
156
|
+
report('no workflow re-implements the path guard inline', !/Select-String[\s\S]{0,200}node_modules/.test(check + publish))
|
|
157
|
+
const pkgJson = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
|
158
|
+
report('guard:bundle is wired into the scripts', typeof pkgJson.scripts?.['guard:bundle'] === 'string')
|
|
159
|
+
report('pnpm check runs the bundle guard', String(pkgJson.scripts?.check ?? '').includes('guard:bundle'))
|
|
160
|
+
}
|
package/test/skills.test.ts
CHANGED
|
@@ -26,7 +26,7 @@ report('skill is model and user invocable', candidate.invocation.modelInvocable
|
|
|
26
26
|
report('description states a trigger', candidate.description.includes('TRIGGER when'))
|
|
27
27
|
report('description states a non-trigger', candidate.description.includes('DO NOT TRIGGER'))
|
|
28
28
|
report('rank beats the user root (400)', candidate.rank > 400, String(candidate.rank))
|
|
29
|
-
report('playbook has all
|
|
29
|
+
report('playbook has all thirteen sections', SKILL_SECTIONS.length === 13, String(SKILL_SECTIONS.length))
|
|
30
30
|
report('playbook is a real manual, not a summary', body.length > 6000 && skillLineCount() > 120, body.length + ' chars / ' + skillLineCount() + ' lines')
|
|
31
31
|
|
|
32
32
|
const definition = await provider.get(candidate)
|
|
@@ -51,6 +51,11 @@ const required = [
|
|
|
51
51
|
'eda_get_net_labels',
|
|
52
52
|
'eda_list_api_methods',
|
|
53
53
|
'| 现象 | 原因 | 动作 |',
|
|
54
|
+
'## 12. 环境与生效规则(全新会话必读)',
|
|
55
|
+
'重启 DSH',
|
|
56
|
+
'重新导入',
|
|
57
|
+
'/api/dsh-eda-mcp/diagnostics',
|
|
58
|
+
'只递增最小位',
|
|
54
59
|
]
|
|
55
60
|
for (const token of required) {
|
|
56
61
|
report('manual mentions ' + token, body.includes(token))
|
|
Binary file
|