dsh-eda-mcp 0.7.2 → 0.7.4
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 +113 -0
- package/README.md +3 -1
- package/README.zh.md +11 -1
- package/docs/release.md +2 -0
- package/docs/schematic-workflow.md +18 -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 +69 -16
- package/lib/index.js +47 -8
- package/package.json +3 -2
- package/src/bridge/index.ts +101 -17
- package/src/guidance.ts +1 -0
- package/src/index.ts +34 -9
- package/src/skills-content.ts +18 -0
- package/test/seams.test.ts +16 -0
- package/test/skills.test.ts +7 -1
- package/dsh-eda-bridge-extension-0.4.0.zip +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.4 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
发布后补:v0.7.3 已上 npm,但它的两个流水线问题和两处诚实性问题是在 tag 之后才修的,这一版把它们发出去。
|
|
6
|
+
|
|
7
|
+
### 诚实性修复(宿主侧,线协议不变 = 0.4.2,**无需重导扩展**)
|
|
8
|
+
|
|
9
|
+
- **`opened` 必须表示"目标文档真的被激活了"**:导入新扩展会重启 EDA,应用回到 Start Page(`tab_page1`),旧实现靠"标签数 > 1"猜,于是又谎报 `opened: true`。现在要求 `activeDocumentUuid === documentUuid`,新增 `activated` 字段,提示里直接写"先 `eda_open_project` 再开文档"。
|
|
10
|
+
- **空 BOM 不再装成功**:PCB 活动时 `getBomFile` 返回 null,原来给 `{bom:"", encoding:"unknown"}`,看着像"BOM 是空的"。现在直接报错并给出修复动作(切到原理图页)。
|
|
11
|
+
|
|
12
|
+
### CI 两处真 bug(这就是 v0.7.3 一度没上 npm 的原因)
|
|
13
|
+
|
|
14
|
+
- **`publish.yml` 的守卫误杀自己的文档字符串**:守卫用 `Select-String -Path lib/*.js -Pattern 'node_modules/'`,而它命中我写进技能第 12 章的正确文案(`node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<版本>.zip`),于是 `exit 1`,`Publish` 步骤从未执行。已抽成 `scripts/guard-bundle-paths.mjs`(`pnpm guard:bundle`),只认**绝对路径形态**的构建机泄露(`D:/…/node_modules`、`/home/`、`/Users/`、`/root/`、`node_modules/.pnpm`),两个 workflow 共用;并做了负向验证(注入泄露 → exit 1 并报出文件与行号)。
|
|
15
|
+
- **`check.yml` 上传已被删除的别名 zip**:0.7.0 起构建只产出 `dsh-eda-bridge-extension-<版本>.zip`,而 workflow 仍指向 `dsh-eda-bridge-extension.zip` 且 `if-no-files-found: error`,导致 check 一直红。改为版本号通配,并把 `guard:bundle` 加进 check。
|
|
16
|
+
- **防复发**:`test/seams.test.ts` 新增 6 条断言 —— check.yml 必须用版本号通配、两个 workflow 都必须调用共享守卫、不得内联重写守卫、`guard:bundle` 必须在 `pnpm check` 链里。
|
|
17
|
+
|
|
18
|
+
### 仓库卫生
|
|
19
|
+
|
|
20
|
+
- `.tmp-*` 加入 `.gitignore`,并删掉之前误提交的临时文件(提交信息临时文件曾随 `git add -A` 进库)。
|
|
21
|
+
|
|
22
|
+
### 门禁
|
|
23
|
+
|
|
24
|
+
`pnpm check` = typecheck → 6 套单测(含 seams 新增断言)→ `contract-check` → `build` → **`guard:bundle`**;本次发布前全部复跑通过。
|
|
25
|
+
|
|
26
|
+
## 0.7.3 — 2026-10-20
|
|
27
|
+
|
|
28
|
+
本版把原理图与 PCB 两侧都跑了一遍真机,修掉 5 个真问题,并把「三层改动的生效范围」写进随包文档,避免新会话无从下手。
|
|
29
|
+
|
|
30
|
+
### 真机验证 0.7.2 时发现并修掉的 3 个问题(线协议 0.4.0 → **0.4.1**)
|
|
31
|
+
|
|
32
|
+
> 线协议升到 0.4.1(扩展版本只递增最小位,不跳 minor):**需要重新导入 `dsh-eda-bridge-extension-0.4.1.zip`**(扩展代码有改动)。
|
|
33
|
+
|
|
34
|
+
**1. BOM CSV 仍乱码(真因在扩展)**
|
|
35
|
+
|
|
36
|
+
- 现象:`eda_export_bom` 返回 `��N\u0000o\u0000.`。
|
|
37
|
+
- 排查:桥接侧的 `decodeTextPayload` 没救回,因为字节在**扩展**里就按 UTF-8 解成替换字符了(`blobToFilePayload` 对所有 `text/*` 一律 `TextDecoder("utf-8")`)。
|
|
38
|
+
- 修复:扩展按**BOM 判定编码**(`FF FE`→utf-16le、`FE FF`→utf-16be、`EF BB BF`→utf-8),并在返回值里带 `encoding`;桥接侧解码保留为旧扩展兜底,返回值新增 `encoding` 字段。
|
|
39
|
+
|
|
40
|
+
**2. `eda_open_document` 静默不建标签页**
|
|
41
|
+
|
|
42
|
+
- 现象:应用刚启动停在 HOME 时,`dmt_EditorControl.openDocument(uuid)` 返回空且**不创建标签页**,随后所有 tab 相关调用读空——调用方以为页面已激活。
|
|
43
|
+
- 修复:调用后 600 ms 复查 `getSplitScreenTree` 与活动文档,返回 `opened` / `activeDocumentUuid` / `tabIds`;未建标签页时给 `hint`("该构建不会从 HOME 程序化打开文档,请先在应用里点开页面,别假设已激活")。真机复测:工程加载完成后 `opened:true`,标签列表含原理图页与 PCB 页。
|
|
44
|
+
|
|
45
|
+
**3. 构建会把旧版本 zip 一起打包**
|
|
46
|
+
|
|
47
|
+
- 现象:升版本后目录里同时存在新旧两个扩展 zip,而 `package.json files` 用的是 `dsh-eda-bridge-extension-*.zip` 通配 → 两个都会进 npm 包。
|
|
48
|
+
- 修复:构建前**清掉所有** `dsh-eda-bridge-extension*.zip`;`npm pack --dry-run` 现在只有 1 个 zip(40 files)。
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### 真机验证 PCB 侧:又抓到一个拦截 bug(线协议 0.4.1 → **0.4.2**)
|
|
53
|
+
|
|
54
|
+
**1. 所有导出(BOM / 网表 / Gerber)本该被第二张白名单挡住**
|
|
55
|
+
|
|
56
|
+
- 现象:`eda_pcb_export_gerber` 报 `API method not allowed: pcb_ManufactureData.getGerberFile`。
|
|
57
|
+
- 真因:扩展的分发门只查 `ALLOWED_API`,而 `FILE_METHODS`(返回 File 的方法:BOM、网表、Gerber、截图)是**第二张表**,被门挡在外面 —— 上线以来所有 `FILE_METHODS` 导出其实都走不通(BOM/网表"能用"是因为它们同时也在 `ALLOWED_API` 里)。
|
|
58
|
+
- 修复:门改为 `!ALLOWED_API.has(m) && !FILE_METHODS.has(m)`,报错信息里给出两表合计数量;`contract-check` 增加一条断言:**分发门必须同时接受 FILE_METHODS**(之前只校验两表并集,所以漏了)。
|
|
59
|
+
|
|
60
|
+
**2. PCB 写入没有校验网络是否存在**
|
|
61
|
+
|
|
62
|
+
- 现象:`pcb_draw_track({ net: "NO_SUCH_NET" })` **被应用接受**,板上多出一条幽灵网络的线(DRC 会报 netlist error)。验证时实际留下了一条,已用 `eda_pcb_delete_track` 清掉(98 条线、0 条幽灵)。
|
|
63
|
+
- 修复:`pcb_draw_track` / `pcb_draw_via` 写入前用 `pcb_Net.getAllNetsName` 校验网络是否存在,不存在则拒绝并列出前 12 个已知网络;`force:true` 仍可强制。真机复测:两条都被拒绝,提示里带 17 个真实网络名。
|
|
64
|
+
|
|
65
|
+
### PCB 侧真机验证结论(EDA 3.2.186 / 扩展 0.4.1)
|
|
66
|
+
|
|
67
|
+
| 项 | 结果 |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `eda_open_document(pcb)` | ✅ 活动文档 `documentType:3`,tabId 带 `@projectUuid` |
|
|
70
|
+
| `pcb_get_layers` | ✅ 289 个层(id/name/type/color) |
|
|
71
|
+
| `pcb_get_nets` | ✅ 17 个网络 |
|
|
72
|
+
| `pcb_get_components` | ✅ 33 个器件,含封装与 LCSC 料号 |
|
|
73
|
+
| `eda_pcb_get_drc_rules` | ✅ 返回完整规则配置(间距矩阵等) |
|
|
74
|
+
| `eda_pcb_run_drc({includeVerboseError:true})` | ✅ **2.8 s** 返回结构化违规(`name/errorType/ruleName/explanation`),本次 1 条:Netlist Error(PCB 与原理图不一致) |
|
|
75
|
+
| `eda_design_audit({drc:false})` | ✅ 返回 `counts/minWidthByNet/violations/warnings/checklist/rules`(1 violation、1 warning) |
|
|
76
|
+
| `pcb_draw_via`(孔径 0.1mm) | ✅ 拒绝:孔径低于 0.2mm 且焊环 0.1mm 低于 0.2mm |
|
|
77
|
+
| `pcb_draw_track`(未知网络) | ❌→✅ 修复后拒绝(见上) |
|
|
78
|
+
| `eda_pcb_export_gerber` | ❌ `API method not allowed` → 已修,需重导 **0.4.2** 复测 |
|
|
79
|
+
### 批量标签改用受控并发(实测 2.2 倍)
|
|
80
|
+
|
|
81
|
+
- `eda_place_net_labels` 原来是串行 for 循环:真机实测**每个标签约 2.1 s**(应用按图元落库),133 个仍需 ~4.6 分钟。
|
|
82
|
+
- 改为 `mapLimit` 受控并发(默认 4,可用 `concurrency` 调 1–8),结果仍按调用方顺序返回。
|
|
83
|
+
- 真机复测:**8 个标签 7.5 s = 938 ms/个**(串行 2.1 s/个),清理后计数 19 → 11。
|
|
84
|
+
### 真机验证结论(EDA 3.2.186 + 扩展 0.4.0)
|
|
85
|
+
|
|
86
|
+
| 项 | 结果 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `eda_status` / `eda_launch` | ✅ 走 explorer 启动,8.7 s 内 1 个客户端连上,版本一致 |
|
|
89
|
+
| `eda_list_projects` | ✅ 7 个工程(跨 2 个本地目录聚合) |
|
|
90
|
+
| `eda_open_project({name})` | ✅ 中文名解析并打开,`opened:true, openReturned:true` |
|
|
91
|
+
| `eda_export_project` | ✅ 1 056 444 字节 |
|
|
92
|
+
| `eda_export_netlist` | ✅ `parsed:true`,80 647 字节,返回真实 netlist 对象(含 `DC1` 等器件) |
|
|
93
|
+
| `eda_get_components` / `eda_get_pins` | ✅ 34 个器件;引脚返回 `pinNumber` / `pinName`(3/3 有值) |
|
|
94
|
+
| `eda_get_net_labels` | ✅ `count:11, attributeCount:0, textCount:11`(以前恒为 0) |
|
|
95
|
+
| `eda_place_net_labels`(批量 3 个) | ✅ 单次调用 7.0 s、`done:3 failed:0`、`method:"text"`;计数 11 → 14,`sessionTextLabels:3` |
|
|
96
|
+
| 批量标签清理 | ✅ `eda_delete_primitive(kind:"text")` ×3 后回到 11,未保存文档 |
|
|
97
|
+
| `eda_zoom_to_fit` | ✅ 应用返回的越界 bounds(-121…1776 / -2.5…1172)被裁剪为 `0..1170 / 0..825` 并重新应用 |
|
|
98
|
+
| `eda_run_drc` | ⚠️ 只能拿到聚合项 `{type:"warn",count:1}`:即使 `includeVerboseError:true`,该构建也不返回 primitiveId/坐标。工具说明与 hint 已按实情改写 |
|
|
99
|
+
| `eda_export_bom`(真机) | ❌ → 已修(见上),需重导 0.4.1 后复测 |
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
### 发布流水线修掉两个 CI bug(v0.7.3 因此没上 npm)
|
|
103
|
+
|
|
104
|
+
**1. `publish.yml` 的"无构建机路径"守卫误杀**
|
|
105
|
+
|
|
106
|
+
- 守卫用 `Select-String -Pattern 'node_modules/'` 扫 `lib/*.js`,命中了我自己写的**文档字符串**`扩展 zip 位置:node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<版本>.zip`(这是要告诉用户的正确信息,不是泄露的构建机路径),守卫直接 `exit 1`,**publish 步骤从没执行**。
|
|
107
|
+
- 修复:守卫抽成 `scripts/guard-bundle-paths.mjs`(`pnpm guard:bundle`),只认**绝对路径**形态的泄露(`D:/…/node_modules`、`/home/`、`/Users/`、`/root/`、`node_modules/.pnpm`),两个 workflow 共用同一实现。
|
|
108
|
+
- 真机验证:注入 `D:/Program/…/node_modules/.pnpm/x.js` → 守卫 `exit 1` 并报出文件与行号;恢复后 PASS。
|
|
109
|
+
|
|
110
|
+
**2. `check.yml` 上传的是已被移除的别名 zip**
|
|
111
|
+
|
|
112
|
+
- `path: dsh-eda-bridge-extension.zip` + `if-no-files-found: error`,但构建自 0.7.0 起只产出**带版本号**的 zip(别名是故意删掉的)→ 上传步骤失败,整个 check workflow 红。
|
|
113
|
+
- 修复:改成 `dsh-eda-bridge-extension-*.zip`,并把 `pnpm guard:bundle` 也加进 check。
|
|
114
|
+
|
|
115
|
+
> 影响:`v0.7.3` 的 tag 已推,但 npm 上仍只有 `0.7.2`。补法:在 Actions 里手动运行 `publish` 工作流并勾选 `publish`(`workflow_dispatch` 从 main 取代码,自动带上这两个修复),无需重打 tag。
|
|
3
116
|
## 0.7.2 — 2026-10-20
|
|
4
117
|
|
|
5
118
|
本版把原理图批量落地的 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. 已知失败模式速查
|
|
@@ -110,6 +110,7 @@
|
|
|
110
110
|
| connectedClients=0 / 报未连接 | EDA 没开、扩展没导入或没点「连接」、端口被占 | 先 eda_status 看 hint;必要时 eda_launch 起应用并等扩展连上 |
|
|
111
111
|
| extensionVersions 与 bridgeVersion 不一致 | 装的是旧扩展 | 重新导入对应版本 zip(文件名带版本号)并重启应用 |
|
|
112
112
|
| API method not allowed | 该 API 不在扩展白名单 | 调 eda_list_api_methods 看清单;优先用包装好的工具 |
|
|
113
|
+
| open_document 返回 opened:false / activated:false | 应用停在 Start Page(tab_page1),该构建无法从 HOME 程序化打开文档 | 先 eda_open_project 打开工程,再 eda_open_document;用 activeDocumentUuid 确认,别假设已激活 |
|
|
113
114
|
| 调用 60s 超时 | 页面切走留下僵尸 socket,或单次改太多 | 只读调用会自动换页面重试一次;把批量拆到 ≤20/批 |
|
|
114
115
|
| 读图返回空 / null | 活动文档不是原理图页 | eda_get_active_document → eda_open_document 切页 |
|
|
115
116
|
| createNetLabel 无响应 | 3.2.186 该 API 不 resolve | 自动降级文本标签 + stub 导线;批量用 eda_place_net_labels |
|
|
@@ -150,3 +151,20 @@ C. 只做 PCB:
|
|
|
150
151
|
- [ ] 视觉:导出图片自查,标签不压符号、器件不重叠、功能框清晰。
|
|
151
152
|
- [ ] 保存:文档已保存,工程已 eda_export_project 备份。
|
|
152
153
|
- [ ] Gerber 已导出且层数与板层一致。
|
|
154
|
+
|
|
155
|
+
## 12. 环境与生效规则(全新会话必读)
|
|
156
|
+
|
|
157
|
+
这套工具分三层,改动生效范围完全不同——不确定时先看这张表,不要凭直觉重试。
|
|
158
|
+
|
|
159
|
+
| 改了什么 | 谁来做 | 怎么生效 | 怎么确认 |
|
|
160
|
+
|---|---|---|---|
|
|
161
|
+
| 插件源码 `src/`(宿主侧:配置、路由、技能注册、MCP 工具表) | 开发者 | `node build.mjs` 后**重启 DSH**(桌面端关掉重开) | 会话里能看到新工具;`/api/dsh-eda-mcp/diagnostics` 的 `skills` 字段 |
|
|
162
|
+
| 扩展 `jlc-extension/`(白名单、内部助手、编码处理) | 用户 | 嘉立创EDA 里**重新导入** `dsh-eda-bridge-extension-<版本>.zip` 并**重启应用** | `eda_status` 的 `extensionVersions` 与 `bridgeVersion` 一致 |
|
|
163
|
+
| 只需重启桥接子进程(改了 `lib/bridge.mjs`、换端口、卡住) | agent 可自行 | `POST /api/dsh-eda-mcp/reconnect` | 返回 200 且 `eda_status.connectedClients ≥ 1` |
|
|
164
|
+
| 没改任何东西但工具莫名消失 | agent | 先 reconnect;仍不行看 `/diagnostics` 的 `clientStartupError` | 工具重新出现在会话里 |
|
|
165
|
+
|
|
166
|
+
- **插件代码改动对当前会话完全不生效**:宿主在启动时把 `lib/index.js` 载入内存,之后改文件、重新 build 都不会热更新。
|
|
167
|
+
- 版本纪律:扩展线协议**只递增最小位**(`0.4.1 → 0.4.2`),每次改动必须同时改 `jlc-extension/extension.json` 的 `version` 与桥接的 `VERSION`;不一致时 `eda_status.versionMismatch` 为 true。
|
|
168
|
+
- 新会话开工顺序:`eda_status`(连没连、版本对不对)→ 本技能(怎么干)→ 再动手;不要先假设环境就绪。
|
|
169
|
+
- 扩展 zip 位置:`node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<版本>.zip`,或设置卡片下载(文件名自带版本号)。
|
|
170
|
+
- 排查顺序:`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,37 @@ 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 activeIsTarget = activeUuid === documentUuid;
|
|
2090
|
+
const tabOpened = activeIsTarget || tabIds.some((tab) => tab.startsWith(documentUuid + "@"));
|
|
2091
|
+
return {
|
|
2092
|
+
...raw !== null && typeof raw === "object" ? raw : { result: raw ?? null },
|
|
2093
|
+
opened: tabOpened,
|
|
2094
|
+
activated: activeIsTarget,
|
|
2095
|
+
activeDocumentUuid: activeUuid || null,
|
|
2096
|
+
tabIds,
|
|
2097
|
+
hint: tabOpened ? null : "The app accepted openDocument but the document is not active (activeDocumentUuid=" + (activeUuid || "unknown") + ", tabs: " + (tabIds.join(", ") || "none") + "). After an app restart the editor sits on the Start Page: open the project first (eda_open_project), then open the document. This build cannot open a document from the HOME screen programmatically, so never assume a page is active without checking activeDocumentUuid."
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2053
2100
|
case "eda_set_no_connect": {
|
|
2054
2101
|
const ids = Array.isArray(args.primitiveIds) ? args.primitiveIds.map(String) : [];
|
|
2055
2102
|
if (ids.length === 0) throw new Error("primitiveIds is required");
|
|
@@ -2254,7 +2301,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2254
2301
|
raw: entry
|
|
2255
2302
|
};
|
|
2256
2303
|
}),
|
|
2257
|
-
hint: '
|
|
2304
|
+
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
2305
|
};
|
|
2259
2306
|
}
|
|
2260
2307
|
return { ok: Boolean(verbose), count: null, entries: [], raw: verbose };
|
|
@@ -2297,7 +2344,11 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2297
2344
|
const payload = await callExtension("sch_ManufactureData.getBomFile", [void 0, format], toolTimeout(args, timeoutMs));
|
|
2298
2345
|
if (format === "xlsx") return payload;
|
|
2299
2346
|
const raw = typeof payload === "string" ? payload : payload !== null && typeof payload === "object" && typeof payload.text === "string" ? payload.text : "";
|
|
2300
|
-
|
|
2347
|
+
if (raw === "") {
|
|
2348
|
+
throw new Error("eda_export_bom: no BOM data (the schematic page must be the active document; call eda_get_active_document, then eda_open_document on the schematic page and retry)");
|
|
2349
|
+
}
|
|
2350
|
+
const encoding = payload !== null && typeof payload === "object" ? payload.encoding : void 0;
|
|
2351
|
+
return { format, bom: decodeTextPayload(raw), encoding: encoding ?? "unknown" };
|
|
2301
2352
|
}
|
|
2302
2353
|
case "eda_delete_primitive": {
|
|
2303
2354
|
const kind = String(args.kind);
|
|
@@ -2387,6 +2438,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2387
2438
|
if (layer === void 0) throw new Error("pcb_draw_track requires a valid layer");
|
|
2388
2439
|
const net = String(args.net);
|
|
2389
2440
|
assertNetName(net, "pcb_draw_track");
|
|
2441
|
+
await assertKnownPcbNet("pcb_draw_track", net, timeoutMs, args.force === true);
|
|
2390
2442
|
const width = checkTrackWidth(net, args.lineWidth === void 0 ? void 0 : Number(args.lineWidth), args.force === true);
|
|
2391
2443
|
if (!width.ok) throw new Error("pcb_draw_track: " + width.errors.join("; "));
|
|
2392
2444
|
const created = await callExtension("pcb_PrimitiveLine.create", [
|
|
@@ -2403,6 +2455,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2403
2455
|
case "pcb_draw_via": {
|
|
2404
2456
|
const net = String(args.net);
|
|
2405
2457
|
assertNetName(net, "pcb_draw_via");
|
|
2458
|
+
await assertKnownPcbNet("pcb_draw_via", net, timeoutMs, args.force === true);
|
|
2406
2459
|
const role = classifyNet(net);
|
|
2407
2460
|
const defaultVia = role === "signal" ? VIA_SIZES_MM[0] : VIA_SIZES_MM[1];
|
|
2408
2461
|
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",
|
|
@@ -137,6 +138,7 @@ var SKILL_PLAYBOOK_LINES = [
|
|
|
137
138
|
"| connectedClients=0 / \u62A5\u672A\u8FDE\u63A5 | EDA \u6CA1\u5F00\u3001\u6269\u5C55\u6CA1\u5BFC\u5165\u6216\u6CA1\u70B9\u300C\u8FDE\u63A5\u300D\u3001\u7AEF\u53E3\u88AB\u5360 | \u5148 eda_status \u770B hint\uFF1B\u5FC5\u8981\u65F6 eda_launch \u8D77\u5E94\u7528\u5E76\u7B49\u6269\u5C55\u8FDE\u4E0A |",
|
|
138
139
|
"| extensionVersions \u4E0E bridgeVersion \u4E0D\u4E00\u81F4 | \u88C5\u7684\u662F\u65E7\u6269\u5C55 | \u91CD\u65B0\u5BFC\u5165\u5BF9\u5E94\u7248\u672C zip\uFF08\u6587\u4EF6\u540D\u5E26\u7248\u672C\u53F7\uFF09\u5E76\u91CD\u542F\u5E94\u7528 |",
|
|
139
140
|
"| API method not allowed | \u8BE5 API \u4E0D\u5728\u6269\u5C55\u767D\u540D\u5355 | \u8C03 eda_list_api_methods \u770B\u6E05\u5355\uFF1B\u4F18\u5148\u7528\u5305\u88C5\u597D\u7684\u5DE5\u5177 |",
|
|
141
|
+
"| open_document \u8FD4\u56DE opened:false / activated:false | \u5E94\u7528\u505C\u5728 Start Page\uFF08tab_page1\uFF09\uFF0C\u8BE5\u6784\u5EFA\u65E0\u6CD5\u4ECE HOME \u7A0B\u5E8F\u5316\u6253\u5F00\u6587\u6863 | \u5148 eda_open_project \u6253\u5F00\u5DE5\u7A0B\uFF0C\u518D eda_open_document\uFF1B\u7528 activeDocumentUuid \u786E\u8BA4\uFF0C\u522B\u5047\u8BBE\u5DF2\u6FC0\u6D3B |",
|
|
140
142
|
"| \u8C03\u7528 60s \u8D85\u65F6 | \u9875\u9762\u5207\u8D70\u7559\u4E0B\u50F5\u5C38 socket\uFF0C\u6216\u5355\u6B21\u6539\u592A\u591A | \u53EA\u8BFB\u8C03\u7528\u4F1A\u81EA\u52A8\u6362\u9875\u9762\u91CD\u8BD5\u4E00\u6B21\uFF1B\u628A\u6279\u91CF\u62C6\u5230 \u226420/\u6279 |",
|
|
141
143
|
"| \u8BFB\u56FE\u8FD4\u56DE\u7A7A / null | \u6D3B\u52A8\u6587\u6863\u4E0D\u662F\u539F\u7406\u56FE\u9875 | eda_get_active_document \u2192 eda_open_document \u5207\u9875 |",
|
|
142
144
|
"| createNetLabel \u65E0\u54CD\u5E94 | 3.2.186 \u8BE5 API \u4E0D resolve | \u81EA\u52A8\u964D\u7EA7\u6587\u672C\u6807\u7B7E + stub \u5BFC\u7EBF\uFF1B\u6279\u91CF\u7528 eda_place_net_labels |",
|
|
@@ -176,7 +178,24 @@ var SKILL_PLAYBOOK_LINES = [
|
|
|
176
178
|
"- [ ] BOM\uFF1A\u4F4D\u53F7\u8FDE\u7EED\u3001\u5C01\u88C5\u975E\u7A7A\u3001\u65E0 DUMMY\u3002",
|
|
177
179
|
"- [ ] \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
180
|
"- [ ] \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"
|
|
181
|
+
"- [ ] Gerber \u5DF2\u5BFC\u51FA\u4E14\u5C42\u6570\u4E0E\u677F\u5C42\u4E00\u81F4\u3002",
|
|
182
|
+
"",
|
|
183
|
+
"## 12. \u73AF\u5883\u4E0E\u751F\u6548\u89C4\u5219\uFF08\u5168\u65B0\u4F1A\u8BDD\u5FC5\u8BFB\uFF09",
|
|
184
|
+
"",
|
|
185
|
+
"\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",
|
|
186
|
+
"",
|
|
187
|
+
"| \u6539\u4E86\u4EC0\u4E48 | \u8C01\u6765\u505A | \u600E\u4E48\u751F\u6548 | \u600E\u4E48\u786E\u8BA4 |",
|
|
188
|
+
"|---|---|---|---|",
|
|
189
|
+
"| \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 |",
|
|
190
|
+
"| \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 |",
|
|
191
|
+
"| \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` |",
|
|
192
|
+
"| \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 |",
|
|
193
|
+
"",
|
|
194
|
+
"- **\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",
|
|
195
|
+
"- \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",
|
|
196
|
+
"- \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",
|
|
197
|
+
"- \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",
|
|
198
|
+
"- \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
199
|
];
|
|
181
200
|
var SKILL_SECTIONS = SKILL_PLAYBOOK_LINES.filter((line) => line.startsWith("## "));
|
|
182
201
|
var SKILL_PLAYBOOK = SKILL_PLAYBOOK_LINES.join(String.fromCharCode(10));
|
|
@@ -828,6 +847,12 @@ function describeBridgeLaunch() {
|
|
|
828
847
|
return { command: runtime.command, args: [BRIDGE_PATH], source: runtime.source };
|
|
829
848
|
}
|
|
830
849
|
function apply(ctx, config) {
|
|
850
|
+
let skillState = {
|
|
851
|
+
attempted: false,
|
|
852
|
+
registered: false,
|
|
853
|
+
error: null,
|
|
854
|
+
names: []
|
|
855
|
+
};
|
|
831
856
|
let current = () => unwrapConfig(config ?? {});
|
|
832
857
|
let disposeClient;
|
|
833
858
|
let disposeSection;
|
|
@@ -879,7 +904,9 @@ function apply(ctx, config) {
|
|
|
879
904
|
extensionZipPath: resolveExtensionZipPath(),
|
|
880
905
|
reconnect: remountClient,
|
|
881
906
|
bridgeLaunch: describeBridgeLaunch,
|
|
882
|
-
|
|
907
|
+
// Surface the skill registration so "did it register?" is answerable from
|
|
908
|
+
// /api/dsh-eda-mcp/diagnostics instead of guessing from the skill list.
|
|
909
|
+
diagnostics: () => ({ ...diagnostics.liveSnapshot(), skills: skillState }),
|
|
883
910
|
launchConfig: () => bridgeLaunchFor(current()),
|
|
884
911
|
selfTest: async () => {
|
|
885
912
|
const runtime = resolveBridgeRuntime();
|
|
@@ -899,12 +926,24 @@ function apply(ctx, config) {
|
|
|
899
926
|
}, "eda-mcp: settings bridge");
|
|
900
927
|
});
|
|
901
928
|
ctx.inject(["skills"], (sctx) => {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
929
|
+
sctx.effect(() => {
|
|
930
|
+
const skills = sctx.skills;
|
|
931
|
+
if (skills === void 0 || typeof skills.registerProvider !== "function") {
|
|
932
|
+
skillState = { ...skillState, attempted: true, registered: false, error: "the host exposes no skills service" };
|
|
933
|
+
return () => {
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
skillState = { ...skillState, attempted: true };
|
|
937
|
+
const dispose = skills.registerProvider(() => {
|
|
938
|
+
const provider = createEdaSkillProvider();
|
|
939
|
+
skillState = { ...skillState, registered: true, error: null, names: [EDA_SKILL_NAME] };
|
|
940
|
+
return provider;
|
|
941
|
+
});
|
|
942
|
+
return () => {
|
|
943
|
+
skillState = { ...skillState, registered: false };
|
|
944
|
+
dispose();
|
|
945
|
+
};
|
|
946
|
+
}, "eda-mcp: schematic skill");
|
|
908
947
|
});
|
|
909
948
|
const sync = () => {
|
|
910
949
|
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.4",
|
|
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,45 @@ 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
|
+
// "opened" must mean "this document is now the active one": right after an app
|
|
1907
|
+
// restart the call answers success while the Start Page (tab_page1) stays active,
|
|
1908
|
+
// and a length-based guess reported opened:true for it.
|
|
1909
|
+
const activeIsTarget = activeUuid === documentUuid
|
|
1910
|
+
const tabOpened = activeIsTarget || tabIds.some(tab => tab.startsWith(documentUuid + '@'))
|
|
1911
|
+
return {
|
|
1912
|
+
...(raw !== null && typeof raw === 'object' ? raw : { result: raw ?? null }),
|
|
1913
|
+
opened: tabOpened,
|
|
1914
|
+
activated: activeIsTarget,
|
|
1915
|
+
activeDocumentUuid: activeUuid || null,
|
|
1916
|
+
tabIds,
|
|
1917
|
+
hint: tabOpened
|
|
1918
|
+
? null
|
|
1919
|
+
: 'The app accepted openDocument but the document is not active (activeDocumentUuid=' +
|
|
1920
|
+
(activeUuid || 'unknown') + ', tabs: ' + (tabIds.join(', ') || 'none') +
|
|
1921
|
+
'). After an app restart the editor sits on the Start Page: open the project first'
|
|
1922
|
+
+ ' (eda_open_project), then open the document. This build cannot open a document from the HOME screen programmatically, so never assume a page is active without checking activeDocumentUuid.',
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1852
1925
|
|
|
1853
1926
|
case 'eda_set_no_connect': {
|
|
1854
1927
|
const ids = Array.isArray(args.primitiveIds) ? args.primitiveIds.map(String) : []
|
|
@@ -2019,7 +2092,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2019
2092
|
raw: entry,
|
|
2020
2093
|
}
|
|
2021
2094
|
}),
|
|
2022
|
-
hint: '
|
|
2095
|
+
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
2096
|
}
|
|
2024
2097
|
}
|
|
2025
2098
|
return { ok: Boolean(verbose), count: null, entries: [], raw: verbose }
|
|
@@ -2069,8 +2142,17 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2069
2142
|
const raw = typeof payload === 'string'
|
|
2070
2143
|
? payload
|
|
2071
2144
|
: (payload !== null && typeof payload === 'object' && typeof payload.text === 'string' ? payload.text : '')
|
|
2072
|
-
//
|
|
2073
|
-
|
|
2145
|
+
// Measured: with the PCB active the export answers null and this used to return
|
|
2146
|
+
// { bom: '', encoding: 'unknown' }, which reads like an empty BOM instead of the
|
|
2147
|
+
// real cause. Say it plainly, like eda_export_netlist does.
|
|
2148
|
+
if (raw === '') {
|
|
2149
|
+
throw new Error('eda_export_bom: no BOM data (the schematic page must be the active document; ' +
|
|
2150
|
+
'call eda_get_active_document, then eda_open_document on the schematic page and retry)')
|
|
2151
|
+
}
|
|
2152
|
+
// The extension (0.4.1+) decodes text payloads with the encoding named by their BOM;
|
|
2153
|
+
// decodeTextPayload stays as a fallback for older extensions.
|
|
2154
|
+
const encoding = payload !== null && typeof payload === 'object' ? payload.encoding : undefined
|
|
2155
|
+
return { format, bom: decodeTextPayload(raw), encoding: encoding ?? 'unknown' }
|
|
2074
2156
|
}
|
|
2075
2157
|
|
|
2076
2158
|
case 'eda_delete_primitive': {
|
|
@@ -2175,6 +2257,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2175
2257
|
if (layer === undefined) throw new Error('pcb_draw_track requires a valid layer')
|
|
2176
2258
|
const net = String(args.net)
|
|
2177
2259
|
assertNetName(net, 'pcb_draw_track')
|
|
2260
|
+
await assertKnownPcbNet('pcb_draw_track', net, timeoutMs, args.force === true)
|
|
2178
2261
|
// Width defaults to the role-based suggestion and is checked against the
|
|
2179
2262
|
// 0.15 mm design minimum (docs/design-rules.md §5.1); force overrides.
|
|
2180
2263
|
const width = checkTrackWidth(net, args.lineWidth === undefined ? undefined : Number(args.lineWidth), args.force === true)
|
|
@@ -2189,6 +2272,7 @@ async function executeTool(name, rawArgs, timeoutMs) {
|
|
|
2189
2272
|
case 'pcb_draw_via': {
|
|
2190
2273
|
const net = String(args.net)
|
|
2191
2274
|
assertNetName(net, 'pcb_draw_via')
|
|
2275
|
+
await assertKnownPcbNet('pcb_draw_via', net, timeoutMs, args.force === true)
|
|
2192
2276
|
// Defaults follow the standard drill pairs; geometry is checked in mm.
|
|
2193
2277
|
const role = classifyNet(net)
|
|
2194
2278
|
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
|
@@ -117,6 +117,7 @@ export const SKILL_PLAYBOOK_LINES: readonly string[] = [
|
|
|
117
117
|
"| connectedClients=0 / 报未连接 | EDA 没开、扩展没导入或没点「连接」、端口被占 | 先 eda_status 看 hint;必要时 eda_launch 起应用并等扩展连上 |",
|
|
118
118
|
"| extensionVersions 与 bridgeVersion 不一致 | 装的是旧扩展 | 重新导入对应版本 zip(文件名带版本号)并重启应用 |",
|
|
119
119
|
"| API method not allowed | 该 API 不在扩展白名单 | 调 eda_list_api_methods 看清单;优先用包装好的工具 |",
|
|
120
|
+
"| open_document 返回 opened:false / activated:false | 应用停在 Start Page(tab_page1),该构建无法从 HOME 程序化打开文档 | 先 eda_open_project 打开工程,再 eda_open_document;用 activeDocumentUuid 确认,别假设已激活 |",
|
|
120
121
|
"| 调用 60s 超时 | 页面切走留下僵尸 socket,或单次改太多 | 只读调用会自动换页面重试一次;把批量拆到 ≤20/批 |",
|
|
121
122
|
"| 读图返回空 / null | 活动文档不是原理图页 | eda_get_active_document → eda_open_document 切页 |",
|
|
122
123
|
"| createNetLabel 无响应 | 3.2.186 该 API 不 resolve | 自动降级文本标签 + stub 导线;批量用 eda_place_net_labels |",
|
|
@@ -157,6 +158,23 @@ export const SKILL_PLAYBOOK_LINES: readonly string[] = [
|
|
|
157
158
|
"- [ ] 视觉:导出图片自查,标签不压符号、器件不重叠、功能框清晰。",
|
|
158
159
|
"- [ ] 保存:文档已保存,工程已 eda_export_project 备份。",
|
|
159
160
|
"- [ ] Gerber 已导出且层数与板层一致。",
|
|
161
|
+
"",
|
|
162
|
+
"## 12. 环境与生效规则(全新会话必读)",
|
|
163
|
+
"",
|
|
164
|
+
"这套工具分三层,改动生效范围完全不同——不确定时先看这张表,不要凭直觉重试。",
|
|
165
|
+
"",
|
|
166
|
+
"| 改了什么 | 谁来做 | 怎么生效 | 怎么确认 |",
|
|
167
|
+
"|---|---|---|---|",
|
|
168
|
+
"| 插件源码 `src/`(宿主侧:配置、路由、技能注册、MCP 工具表) | 开发者 | `node build.mjs` 后**重启 DSH**(桌面端关掉重开) | 会话里能看到新工具;`/api/dsh-eda-mcp/diagnostics` 的 `skills` 字段 |",
|
|
169
|
+
"| 扩展 `jlc-extension/`(白名单、内部助手、编码处理) | 用户 | 嘉立创EDA 里**重新导入** `dsh-eda-bridge-extension-<版本>.zip` 并**重启应用** | `eda_status` 的 `extensionVersions` 与 `bridgeVersion` 一致 |",
|
|
170
|
+
"| 只需重启桥接子进程(改了 `lib/bridge.mjs`、换端口、卡住) | agent 可自行 | `POST /api/dsh-eda-mcp/reconnect` | 返回 200 且 `eda_status.connectedClients ≥ 1` |",
|
|
171
|
+
"| 没改任何东西但工具莫名消失 | agent | 先 reconnect;仍不行看 `/diagnostics` 的 `clientStartupError` | 工具重新出现在会话里 |",
|
|
172
|
+
"",
|
|
173
|
+
"- **插件代码改动对当前会话完全不生效**:宿主在启动时把 `lib/index.js` 载入内存,之后改文件、重新 build 都不会热更新。",
|
|
174
|
+
"- 版本纪律:扩展线协议**只递增最小位**(`0.4.1 → 0.4.2`),每次改动必须同时改 `jlc-extension/extension.json` 的 `version` 与桥接的 `VERSION`;不一致时 `eda_status.versionMismatch` 为 true。",
|
|
175
|
+
"- 新会话开工顺序:`eda_status`(连没连、版本对不对)→ 本技能(怎么干)→ 再动手;不要先假设环境就绪。",
|
|
176
|
+
"- 扩展 zip 位置:`node_modules/dsh-eda-mcp/dsh-eda-bridge-extension-<版本>.zip`,或设置卡片下载(文件名自带版本号)。",
|
|
177
|
+
"- 排查顺序:`eda_status` → `/api/dsh-eda-mcp/diagnostics`(`clientStartupError` / `skills` / `bridgeHealth`)→ `eda_list_api_methods`(白名单)→ 应用侧 DRC 面板。",
|
|
160
178
|
]
|
|
161
179
|
|
|
162
180
|
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,12 @@ 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
|
+
'activated:false',
|
|
59
|
+
'只递增最小位',
|
|
54
60
|
]
|
|
55
61
|
for (const token of required) {
|
|
56
62
|
report('manual mentions ' + token, body.includes(token))
|
|
Binary file
|