@windypro-rourou/dsh-eda 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -0
- package/cordis.patch.yml +9 -0
- package/lib/activity.js +144 -0
- package/lib/backend.js +240 -0
- package/lib/capabilities.js +106 -0
- package/lib/client.js +656 -0
- package/lib/index.js +1119 -0
- package/lib/installer.js +162 -0
- package/lib/layout.js +46 -0
- package/lib/snapshot.js +252 -0
- package/package.json +46 -0
- package/src/json-gen.js +789 -0
- package/src/nl-to-design.js +158 -0
- package/src/validate.js +159 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# dsh-lichuang-eda(嘉立创 EDA 助手)
|
|
2
|
+
|
|
3
|
+
面向 0 基础开发者,在 DSH Web GUI 里自动生成**嘉立创 EDA(专业版/标准版)原理图 / PCB 草图**的插件。
|
|
4
|
+
|
|
5
|
+
> **定位 = 平台层**:本插件提供官方桥的 **一键安装 / 启动 / 连接 / 状态**、执行**官方 `eda.*` API** 的双手(`eda_exec` + 校验工具)、以及**离线兜底**(`eda_generate_schematic_json`)。
|
|
6
|
+
> **生成电路图 = 在对话里进行(skill 式直觉)**:用户说"嘉立创EDA,启动!帮我画一个…"→ agent 用 `eda_exec` 逐步执行官方 `eda.*` API 在用户的**云端画板**实时画。官方文档/样例已随桥 vendored 到 `~/.dsh/eda/bridge/`(`docs/`、`user-guide/`、`guide/`、`format/` 目录),agent 先 `read` 再调用。
|
|
7
|
+
> 仅使用**官方栈**:官方桥 `easyeda/easyeda-api-skill` + 官方扩展 **Run API Gateway** + 官方 API(prodocs.easyeda.com)。**无任何第三方后端**。
|
|
8
|
+
|
|
9
|
+
## 目录结构
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
plugin/
|
|
13
|
+
├── package.json # 包清单:dsh.bundle.patch / dsh.client / exports(. 和 ./client)
|
|
14
|
+
├── cordis.patch.yml # bundle patch:向 web profile 名册插入插件行(id: eda)
|
|
15
|
+
├── lib/
|
|
16
|
+
│ ├── index.js # 宿主端:apply() 注册 10 个 agent 工具 + 5 条路由 + 系统提示
|
|
17
|
+
│ ├── backend.js # 官方桥适配层:spawn bridge-server.mjs + 端口发现(49620-49629) + /execute
|
|
18
|
+
│ └── client.js # 浏览器端(React,无构建步骤):侧边栏入口 + 工作台面板
|
|
19
|
+
├── installer.js # 一键安装官方桥:下载官方源码包 → 解压 → 离线 vendoring ws(npm 兜底)→ 验证
|
|
20
|
+
├── selfcheck.mjs # 自检脚本:stub ctx 挂载插件,断言 DSH 四表面(见「测试 / 自检」)
|
|
21
|
+
├── fixtures/
|
|
22
|
+
│ └── mock-bridge.mjs # mock 官方桥(HTTP/WS),供 backend 单测
|
|
23
|
+
├── test/
|
|
24
|
+
│ ├── status.test.mjs # 状态工具/路由
|
|
25
|
+
│ ├── generate.test.mjs # 离线生成端到端 + 桥接门控
|
|
26
|
+
│ └── backend.test.mjs # EdaBackend 官方桥客户端 + installer
|
|
27
|
+
└── README.md
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> 依赖仓库根的 `src/`(生成器/校验/翻译层)与 `docs/`(调研/引导文档)。**独立打包发布时需把 `src/` 一并纳入包**(见「不确定/未验证」#6)。
|
|
31
|
+
|
|
32
|
+
## 已注册的 agent 工具(13 个)
|
|
33
|
+
|
|
34
|
+
| 工具 | 用途 | 可用性 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `eda_status` | 插件/官方桥状态(装没装 / 端口 / 是否就绪) | ✅ |
|
|
37
|
+
| `eda_template_list` | 模板卡目录(含 supported 标记,如实标注) | ✅ |
|
|
38
|
+
| `eda_translate_request` | 中文需求 → 结构化设计草稿(只翻译不生成) | ✅ |
|
|
39
|
+
| `eda_generate_schematic_json` | **离线兜底**:需求/模板 → 可导入嘉立创 EDA 标准版的原理图 JSON + 结构/连通性校验,产物写入 `~/.dsh/eda/output/` | ✅ |
|
|
40
|
+
| `eda_bridge_install` | **一键安装官方桥**(下载官方 easyeda-api-skill → 解压 → 离线 vendoring ws(npm 兜底)→ 验证 bridge-server.mjs) | ✅ 纯函数已测;网络安装真机待验 |
|
|
41
|
+
| `eda_backend_connect` | 启动/连接官方桥(bridge-server.mjs,端口 49620-49629 + health 探测) | 🧪 mock 已测,真机待验 |
|
|
42
|
+
| `eda_exec` | **云端实时生成的双手**:执行任意官方 `eda.*` 代码(需已连接);自动记录每步新建的图元(可撤回) | 🧪 桥接门控,真机待验 |
|
|
43
|
+
| `eda_pick_spot` | **框内定位**:读页面尺寸(A4≈1170×825,10mil)+已有图元 → 返回互不冲突的框内网格空位(边距80/网格100/间距150);放元件前必调 | ✅ 算法 5 例单测;桥读 mock/真机待验 |
|
|
44
|
+
| `eda_capabilities` | **能力清单**:官方 eda.* API 结构化能力目录(域/方法/用法/坑/实测片段)——agent 规划时主动查询后调用 | ✅ 内置数据,注册已测 |
|
|
45
|
+
| `eda_snapshot` | **紧急保存**:画板现场(`.epro2` 专业版完整恢复 + 预览 SVG + 网表/BOM)+ agent 动作日志 → `~/.dsh/eda/snapshots/`;每步独立降级,断连也能留档步骤 | 🧪 mock 已测(11 例)+ 真机 0 降级 |
|
|
46
|
+
| `eda_sch_drc` | 对当前画板跑原理图 DRC(官方 `eda.sch_Drc.check(true,false,true)`——**verbose 重载**) | 🧪 需已连接 |
|
|
47
|
+
| `eda_get_netlist` | 导出当前网表(**File.text() 修正**:直接返回 File 会序列化 `{}`) | 🧪 需已连接 |
|
|
48
|
+
| `eda_get_bom` | 导出 BOM(**二进制 xlsx 走 base64**,返回 名称/大小 摘要) | 🧪 需已连接 |
|
|
49
|
+
|
|
50
|
+
**生命周期**:插件加载时若检测到官方桥已安装,**自动启动并自动连接**(无需人工);面板实时显示 装没装 / 端口 / 是否就绪。
|
|
51
|
+
|
|
52
|
+
**面板 = 平台仪表盘/监看器**(生成发生在对话里,agent 通过 `eda_exec` 驱动官方 `eda.*` API):
|
|
53
|
+
- 顶部紧凑连接卡 + 可折叠连接教程(默认收起);已连接时显示「💬 去对话里说:嘉立创EDA,启动!」CTA(点击复制)。
|
|
54
|
+
- 主体 **「Agent 正在使用官方 API」记录式时间线**(参考 DSH 会话轨迹,3s 轮询 `GET /activity`):
|
|
55
|
+
- **持久化**:每一步(序号/时间/动作/耗时/结果/会话)落盘 `~/.dsh/eda/activity.jsonl`——重启不空、历史可回溯;
|
|
56
|
+
- **撤回**:每个 `eda_exec` 步骤自动记录新建/删除的图元 id(前后快照 diff),面板「撤回」按钮可一键删除该步新建图元(删除的不可恢复,如实提示);
|
|
57
|
+
- **清空记录**:卡片头部一键清空(内存+磁盘);
|
|
58
|
+
- 点击条目展开完整代码/结果/撤回数据;pending 态「执行中…」;长任务/短任务均实时滚入;
|
|
59
|
+
- 空态展示"你可以让 agent 做…"能力速览(不空白)。
|
|
60
|
+
- **按会话隔离**:每条活动记录携带调用方会话 id(工具执行上下文 `exec.agent.id`);面板默认跟随最新会话,多会话时可下拉切换;无会话的(面板按钮点击)归为「平台操作」。
|
|
61
|
+
- 底部 **「紧急保存」**:一键把画板现场抓到本地(`.epro2` 专业版完整恢复 + 预览 SVG 通用可看 + 网表/BOM 数据 + agent 动作日志 → `~/.dsh/eda/snapshots/`)——云端没同步上也能找回最后工程;画板未连接时仍保存动作日志留档。v0 离线生成(标准版 JSON,仅电阻/LED)保留为对话内工具 `eda_generate_schematic_json`,不再占用面板。
|
|
62
|
+
|
|
63
|
+
**兼容性边界(如实)**:官方 API 仅能导出 epro/epro2(专业版原生)与 PDF/PNG/SVG,**没有「标准版 v6 JSON」导出通道**;因此紧急保存 = 专业版完整恢复 + 通用预览 + 数据文件,标准版可编辑需走离线重建(简单电路)。
|
|
64
|
+
|
|
65
|
+
**实战经验(真机固化,详见 `docs/eda-conversation-skill.md` 附四~附六)**:框内定位规则(10mil/A4≈1170×825/边距80/网格100/间距150,先 `eda_pick_spot` 再放件);网表/BOM/DRC 的正确用法(`File.text()` / base64 / `check(true,false,true)`);已知官方缺陷(`importChanges`、`setNetlist`、`getNetlist('EasyEDA')`、板框无 API、`getAll` 间歇失败)。
|
|
66
|
+
|
|
67
|
+
**会话说明**:`/activity` 返回 `{ activities, currentSid, sessions }`;`?sid=` 固定到某会话,缺省跟随最新。
|
|
68
|
+
|
|
69
|
+
## 路由
|
|
70
|
+
|
|
71
|
+
- `GET /api/dsh-eda/status` — 状态(loopback-only)
|
|
72
|
+
- `GET /api/dsh-eda/activity` — 记录式时间线(`?sid=` 可固定会话;loopback-only)
|
|
73
|
+
- `POST /api/dsh-eda/activity/clear` — 清空时间线(内存+磁盘)
|
|
74
|
+
- `POST /api/dsh-eda/activity/revoke?id=N` — 撤回步骤 N(删除该步新建图元)
|
|
75
|
+
- `GET /api/dsh-eda/templates` — 模板目录
|
|
76
|
+
- `POST /api/dsh-eda/bridge` — 启动/复用官方桥
|
|
77
|
+
- `POST /api/dsh-eda/install` — 一键安装官方桥
|
|
78
|
+
- `POST /api/dsh-eda/generate` — 离线生成(`{ description?, template? }` → 结果 JSON)
|
|
79
|
+
- `POST /api/dsh-eda/snapshot` — 紧急保存(画板现场 + 动作日志 → `~/.dsh/eda/snapshots/`)
|
|
80
|
+
|
|
81
|
+
## 测试 / 自检
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# 前置:@deepseek-ai/dsh-tools 可解析(工作区已有 junction,缺失时重建:
|
|
85
|
+
# mklink /J F:\dsh-lichuang\node_modules\@deepseek-ai C:\Users\35081\.dsh\profiles\node_modules\@deepseek-ai )
|
|
86
|
+
# 本机 npm.ps1 被 PowerShell ExecutionPolicy 禁用时,用 npm.cmd:
|
|
87
|
+
# C:\Program Files\nodejs\npm.cmd run test:all
|
|
88
|
+
|
|
89
|
+
# 自检脚本(stub ctx,断言四表面)
|
|
90
|
+
node plugin/selfcheck.mjs
|
|
91
|
+
|
|
92
|
+
# 插件单元测试(沙箱受限用 --experimental-test-isolation=none;普通终端可直接跑)
|
|
93
|
+
node --experimental-test-isolation=none --test "plugin/test/*.test.mjs"
|
|
94
|
+
|
|
95
|
+
# 生成器单元测试(14 例)
|
|
96
|
+
node --test --experimental-test-isolation=none src/json-gen.test.js
|
|
97
|
+
|
|
98
|
+
# v6 样本校验(62 项结构 + 13 项工程包装/连通)
|
|
99
|
+
node src/validate.mjs
|
|
100
|
+
node src/validate2.mjs
|
|
101
|
+
|
|
102
|
+
# 一键全部门禁(根目录)
|
|
103
|
+
npm run test:all
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**当前实测通过**:插件 **27/27**、生成器 **14/14**、自检 **OK**、v6 样本 **62/62 + 13/13**,全部 0 失败。
|
|
107
|
+
|
|
108
|
+
覆盖:`eda_status` 严格 schema/render、路由 200/405、`provide`/section;官方桥客户端(端口发现/health/`/execute`/`sch.drc` 等动作);离线生成端到端(中文需求 → JSON 结构+连通性通过、不识别的需求报错、桥接门控报错);installer(官方仓库/市场地址、`~/.dsh/eda/bridge` 路径、幂等安装标志);生成器 14 例(结构 lint、sheet 级网表自检、design 级连通性、故意做坏的 3 种电路必须报错、main 产物可复现)。
|
|
109
|
+
|
|
110
|
+
## 如何在 DSH 里加载
|
|
111
|
+
|
|
112
|
+
参考 dsh-logcat(`~/.dsh/profiles/web` 即本机 web profile),三选一、**不要同时用**:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# 方式一:本地路径(bundle 层)
|
|
116
|
+
dsh plugin --profile web add link:F:\dsh-lichuang\plugin
|
|
117
|
+
# 方式二:源码本地链接(profile node_modules)+ 在 profile 的 cordis.patch.yml 追加:
|
|
118
|
+
# - insert:
|
|
119
|
+
# - id: eda
|
|
120
|
+
# name: '@dsh-lichuang/dsh-eda'
|
|
121
|
+
# 方式三:发布 npm 后
|
|
122
|
+
dsh plugin --profile web add @dsh-lichuang/dsh-eda
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
装完后**重启 GUI** 生效。
|
|
126
|
+
|
|
127
|
+
## 验证清单(当前实测状态)
|
|
128
|
+
|
|
129
|
+
- [x] `node plugin/selfcheck.mjs`(OK)
|
|
130
|
+
- [x] `node --test ... plugin/test/*.test.mjs`(27 pass,0 fail)
|
|
131
|
+
- [x] `node --test ... src/json-gen.test.js`(14 pass)
|
|
132
|
+
- [x] `node src/validate.mjs`(62/62)· `node src/validate2.mjs`(13/13)
|
|
133
|
+
- [x] `node --check plugin/lib/{index,client,backend}.js`(语法通过)
|
|
134
|
+
- [x] v6 真实导出样本逐字段校验通过(`docs/ref/SCH_ESP32-PICO-D4_smart_watch_2023-09-02.json`)
|
|
135
|
+
- [ ] `dsh plugin --profile web add link:...` 实测 + GUI 重启后侧边栏出现「嘉立创 EDA」入口
|
|
136
|
+
- [ ] 生成的标准版 JSON 在**真实嘉立创 EDA 标准版**里导入(无报错)
|
|
137
|
+
- [ ] **真机(云端实时端到端)**:登录浏览器打开专业版画板 → 装官方扩展 Run API Gateway → `eda_backend_connect` 启动/连接官方桥 → **扩展连桥后 `eda_status.connected` 为 `true`** → `eda_exec` 放元件/连线/网络标签 → `eda_sch_drc`/`eda_get_netlist`/`eda_get_bom`
|
|
138
|
+
|
|
139
|
+
## 不确定 / 未验证(诚实标注)
|
|
140
|
+
|
|
141
|
+
1. **云端实时端到端(最重要)**:官方桥(bridge-server.mjs)与官方扩展 Run API Gateway 的**真机联通**未验证。诚实做法:扩展在编辑器连上桥后,`eda_status` 返回 `connected:true` 即为联通信号(本插件据此对外宣称"就绪")。在此之前,`eda_exec`/`eda_sch_drc`/`eda_get_netlist`/`eda_get_bom` 会在未连接时返回明确门控错误,不会伪造结果。
|
|
142
|
+
2. **「能被嘉立创 EDA 标准版无报错导入」**:生成物与真实 v6 导出样本逐键同构、结构/连通性全自动断言通过,但**未在真实编辑器实测导入**(无账号/编辑器)。官方「EasyEDA 源文件对话框粘贴 Apply」是最低成本自检路径。
|
|
143
|
+
3. **官方桥的网络安装**:`eda_bridge_install` 的下载/解压/装依赖逻辑已测(`isBridgeInstalled`/路径/幂等/离线 vendoring ws),但**真实下载官方源码包**需联网,本机待真机/网络可用时验证。
|
|
144
|
+
4. **`dsh plugin --profile web add link:` 语法 / client bundle 装载 / `inject=["slots"]`**:照抄 logcat,未在本机实测。
|
|
145
|
+
5. **v0 离线生成器符号**:仅支持 **电阻 / LED**;模板卡里标 ⏳ 的是暂不支持,不会编造。
|
|
146
|
+
6. **打包边界**:`lib/index.js` 以相对路径 `../../src/...` 引入生成器/校验/翻译层(monorepo 布局)。独立发布需将 `src/` 纳入包或改为打包构建。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# dsh-lichuang-eda bundle patch: inserts the eda plugin row into the web
|
|
2
|
+
# profile roster. Applied as a profile bundle layer over dsh-base; the row is
|
|
3
|
+
# a bare plugin by package name — the node half (exports ".") runs in the
|
|
4
|
+
# host process (/api/dsh-eda routes, agent tools, system-prompt section), and
|
|
5
|
+
# the `dsh.client` declaration in package.json makes the browser half
|
|
6
|
+
# (exports "./client", served at /plugins/eda/client.js) load in the web GUI.
|
|
7
|
+
- insert:
|
|
8
|
+
- id: eda
|
|
9
|
+
name: '@dsh-lichuang/dsh-eda'
|
package/lib/activity.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-lichuang-eda — 活动日志(记录式时间线)。
|
|
3
|
+
*
|
|
4
|
+
* 参考 DSH 会话轨迹:每一步 = 一条记录(序号/时间/动作/耗时/结果/会话),
|
|
5
|
+
* 持久化到 ~/.dsh/eda/activity.jsonl(重启面板不空、历史可回溯);
|
|
6
|
+
* 每步可携带 revoke 数据(该步创建的图元 id),实现"撤回上一步"。
|
|
7
|
+
*
|
|
8
|
+
* 性能:内存 ring buffer(cap)+ 串行落盘(写全量 JSONL,200 条为上限)。
|
|
9
|
+
*/
|
|
10
|
+
import { promises as fsp, readFileSync } from 'node:fs'
|
|
11
|
+
import { homedir } from 'node:os'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
|
|
14
|
+
export const ACTIVITY_CAP = 200
|
|
15
|
+
export function defaultActivityFile() {
|
|
16
|
+
return join(homedir(), '.dsh', 'eda', 'activity.jsonl')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ActivityLog {
|
|
20
|
+
constructor({ file = defaultActivityFile(), cap = ACTIVITY_CAP } = {}) {
|
|
21
|
+
this.file = file
|
|
22
|
+
this.cap = cap
|
|
23
|
+
this.entries = []
|
|
24
|
+
this.seq = 0
|
|
25
|
+
this._writeChain = Promise.resolve()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Synchronous load (apply-time — the log is readable instantly). */
|
|
29
|
+
loadSync() {
|
|
30
|
+
try {
|
|
31
|
+
const text = readFileSync(this.file, 'utf8')
|
|
32
|
+
const lines = text.split('\n').filter((l) => l.trim() !== '')
|
|
33
|
+
const parsed = []
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
try {
|
|
36
|
+
const e = JSON.parse(line)
|
|
37
|
+
if (e && typeof e.id === 'number') parsed.push(e)
|
|
38
|
+
} catch { /* skip corrupt line */ }
|
|
39
|
+
}
|
|
40
|
+
this.entries = parsed.slice(-this.cap)
|
|
41
|
+
this.seq = this.entries.reduce((m, e) => Math.max(m, e.id), 0)
|
|
42
|
+
} catch { /* first run / missing file */ }
|
|
43
|
+
return this
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Asynchronous load (still available for tests/tools). */
|
|
47
|
+
async load() {
|
|
48
|
+
try {
|
|
49
|
+
const text = await fsp.readFile(this.file, 'utf8')
|
|
50
|
+
const lines = text.split('\n').filter((l) => l.trim() !== '')
|
|
51
|
+
const parsed = []
|
|
52
|
+
for (const line of lines) {
|
|
53
|
+
try {
|
|
54
|
+
const e = JSON.parse(line)
|
|
55
|
+
if (e && typeof e.id === 'number') parsed.push(e)
|
|
56
|
+
} catch { /* skip corrupt line */ }
|
|
57
|
+
}
|
|
58
|
+
this.entries = parsed.slice(-this.cap)
|
|
59
|
+
this.seq = this.entries.reduce((m, e) => Math.max(m, e.id), 0)
|
|
60
|
+
} catch { /* first run / missing file */ }
|
|
61
|
+
return this
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Append one entry; fire-and-forget persist (serialized). */
|
|
65
|
+
push(entry) {
|
|
66
|
+
const id = ++this.seq
|
|
67
|
+
const e = {
|
|
68
|
+
id,
|
|
69
|
+
ts: new Date().toISOString(),
|
|
70
|
+
status: entry.status ?? 'done',
|
|
71
|
+
sid: entry.sid ?? '',
|
|
72
|
+
...entry,
|
|
73
|
+
error: String(entry.error ?? '').slice(0, 4000),
|
|
74
|
+
code: String(entry.code ?? '').slice(0, 4000),
|
|
75
|
+
result: String(entry.result ?? '').slice(0, 4000),
|
|
76
|
+
durationMs: entry.durationMs ?? 0,
|
|
77
|
+
}
|
|
78
|
+
this.entries.push(e)
|
|
79
|
+
this._trim()
|
|
80
|
+
void this._persist()
|
|
81
|
+
return id
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Patch an entry in place (pending → done/error). */
|
|
85
|
+
update(id, patch) {
|
|
86
|
+
const e = this.entries.find((x) => x.id === id)
|
|
87
|
+
if (!e) return
|
|
88
|
+
if (patch.status !== undefined) e.status = patch.status
|
|
89
|
+
if (patch.ok !== undefined) e.ok = patch.ok
|
|
90
|
+
if (patch.action !== undefined) e.action = patch.action
|
|
91
|
+
if (patch.result !== undefined) e.result = String(patch.result ?? '').slice(0, 4000)
|
|
92
|
+
if (patch.error !== undefined) e.error = String(patch.error ?? '').slice(0, 4000)
|
|
93
|
+
if (patch.durationMs !== undefined) e.durationMs = patch.durationMs
|
|
94
|
+
void this._persist()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get(id) {
|
|
98
|
+
return this.entries.find((x) => x.id === id)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Clear everything (memory + disk). */
|
|
102
|
+
async clear() {
|
|
103
|
+
this.entries = []
|
|
104
|
+
await this._writeChain.catch(() => {}) // let queued writes settle first
|
|
105
|
+
try { await fsp.writeFile(this.file, '', 'utf8') } catch { /* best-effort */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Feed for the panel: newest-first entries (optionally pinned to one
|
|
110
|
+
* session via `sid`), plus a session summary.
|
|
111
|
+
*/
|
|
112
|
+
feed({ sid = '', limit = 50 } = {}) {
|
|
113
|
+
const list = this.entries.slice().reverse().slice(0, limit)
|
|
114
|
+
const sessions = []
|
|
115
|
+
const seen = new Set()
|
|
116
|
+
for (const a of list) {
|
|
117
|
+
const key = a.sid || '(platform)'
|
|
118
|
+
const existing = sessions.find((s) => (s.sid || '(platform)') === key)
|
|
119
|
+
if (existing !== undefined) existing.count += 1
|
|
120
|
+
else sessions.push({ sid: a.sid || '', label: a.sid ? a.sid.slice(0, 8) + '…' : '平台操作', count: 1, lastTs: a.ts })
|
|
121
|
+
}
|
|
122
|
+
const wantSid = sid !== '' ? sid : (list[0]?.sid ?? '')
|
|
123
|
+
return {
|
|
124
|
+
currentSid: wantSid,
|
|
125
|
+
sessions,
|
|
126
|
+
activities: wantSid === '' ? list : list.filter((a) => a.sid === wantSid),
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_trim() {
|
|
131
|
+
if (this.entries.length > this.cap) this.entries.splice(0, this.entries.length - this.cap)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_persist() {
|
|
135
|
+
const snapshot = this.entries.map((e) => JSON.stringify(e)).join('\n') + '\n'
|
|
136
|
+
this._writeChain = this._writeChain.then(async () => {
|
|
137
|
+
try {
|
|
138
|
+
await fsp.mkdir(dirname(this.file), { recursive: true })
|
|
139
|
+
await fsp.writeFile(this.file, snapshot, 'utf8')
|
|
140
|
+
} catch { /* disk write is best-effort */ }
|
|
141
|
+
}).catch(() => {})
|
|
142
|
+
return this._writeChain
|
|
143
|
+
}
|
|
144
|
+
}
|
package/lib/backend.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-lichuang-eda — EDA backend adapter (OFFICIAL STACK ONLY).
|
|
3
|
+
*
|
|
4
|
+
* Based entirely on the official 嘉立创EDA(专业版) extension ecosystem:
|
|
5
|
+
*
|
|
6
|
+
* [EasyEDA Pro 窗口(官方扩展 Run API Gateway,已开「允许外部交互」)]
|
|
7
|
+
* │ WS 客户端连出(自动扫描端口 49620-49629 + 握手 easyeda-bridge)
|
|
8
|
+
* ▼
|
|
9
|
+
* [官方 Bridge Server(easyeda/easyeda-api-skill 的 scripts/bridge-server.mjs)]
|
|
10
|
+
* │ HTTP: GET /health · POST /execute {"code": "return await eda.xxx()"}
|
|
11
|
+
* ▼
|
|
12
|
+
* [DSH 插件 dsh-lichuang-eda](本文件:spawn/health/execute)
|
|
13
|
+
*
|
|
14
|
+
* Sources (all official, verified in-repo 2026-08):
|
|
15
|
+
* - easyeda/easyeda-api-skill (Skill `easyeda-api` v1.1.28, MIT, author JLCEDA)
|
|
16
|
+
* - easyeda/eext-run-api-gateway (扩展 Run API Gateway, jlc-ext.com/item/oshwhub/run-api-gateway)
|
|
17
|
+
* - prodocs.easyeda.com/cn/api/ (官方 API 参考: SCH_Drc/SCH_Netlist/SCH_ManufactureData/…)
|
|
18
|
+
*
|
|
19
|
+
* No third-party backend (easyeda-agent / jlcmcp / easyeda-mcp-pro) is used here.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { spawn } from 'node:child_process'
|
|
23
|
+
|
|
24
|
+
export const BRIDGE_PORTS = Array.from({ length: 10 }, (_, i) => 49620 + i)
|
|
25
|
+
export const BRIDGE_SERVICE = 'easyeda-bridge'
|
|
26
|
+
|
|
27
|
+
export const STATE = {
|
|
28
|
+
idle: 'idle',
|
|
29
|
+
starting: 'starting',
|
|
30
|
+
ready: 'ready',
|
|
31
|
+
connected: 'connected',
|
|
32
|
+
error: 'error',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Official API snippets per action id (prodocs.easyeda.com/cn/api/).
|
|
36
|
+
* Notes: DRC must use the verbose overload to GET the error list (the bare
|
|
37
|
+
* `check()` returns only boolean); netlist is a File whose bridge JSON
|
|
38
|
+
* serialization is `{}`, so read `f.text()`; BOM is a BINARY xlsx File, so
|
|
39
|
+
* it is returned as base64 (`{name,size,b64}`). */
|
|
40
|
+
export const OFFICIAL_CODES = {
|
|
41
|
+
'sch.drc': 'return await eda.sch_Drc.check(true, false, true);',
|
|
42
|
+
'sch.netlist': `return await (async () => { const f = await eda.sch_ManufactureData.getNetlistFile(); return f ? await f.text() : null; })();`,
|
|
43
|
+
'sch.bom': `return await (async () => { const f = await eda.sch_ManufactureData.getBomFile(); if (!f) return null; const bytes = new Uint8Array(await f.arrayBuffer()); let bin = ''; for (let i = 0; i < bytes.length; i += 0x8000) bin += String.fromCharCode.apply(null, bytes.subarray(i, i + 0x8000)); return { name: f.name, size: f.size, b64: btoa(bin) }; })();`,
|
|
44
|
+
'pcb.drc': 'return await eda.pcb_Drc.check(true, false, true);',
|
|
45
|
+
'project.info': 'return await eda.dmt_Project.getCurrentProjectInfo();',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function httpJson(method, url, body, timeoutMs = 15000) {
|
|
49
|
+
const res = await fetch(url, {
|
|
50
|
+
method,
|
|
51
|
+
headers: { 'content-type': 'application/json' },
|
|
52
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
53
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
54
|
+
})
|
|
55
|
+
const text = await res.text()
|
|
56
|
+
let parsed = null
|
|
57
|
+
try { parsed = JSON.parse(text) } catch { parsed = null }
|
|
58
|
+
return { ok: res.ok, status: res.status, text, json: parsed }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Scan the official port range (or an injected set) for a live bridge. */
|
|
62
|
+
export async function findBridgePort(timeoutMs = 6000, ports = BRIDGE_PORTS) {
|
|
63
|
+
const deadline = Date.now() + timeoutMs
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
for (const port of ports) {
|
|
66
|
+
try {
|
|
67
|
+
const r = await httpJson('GET', `http://127.0.0.1:${port}/health`, undefined, 1000)
|
|
68
|
+
if (r.ok && /easyeda-bridge/.test(r.text)) return port
|
|
69
|
+
} catch { /* port closed — keep scanning */ }
|
|
70
|
+
}
|
|
71
|
+
await new Promise((res) => setTimeout(res, 500))
|
|
72
|
+
}
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The official bridge: spawns the official Bridge Server (if not already
|
|
78
|
+
* running), discovers its port, and executes `eda.*` code through it.
|
|
79
|
+
* @param {object} cfg {
|
|
80
|
+
* bridgeDir='~/.dsh/eda/bridge' — vendored easyeda-api-skill (npm install 后含 scripts/bridge-server.mjs)
|
|
81
|
+
* nodeBin='node', discoveryPorts=BRIDGE_PORTS, spawnBridge=true, timeouts
|
|
82
|
+
* }
|
|
83
|
+
*/
|
|
84
|
+
export class EdaBackend {
|
|
85
|
+
constructor(cfg = {}) {
|
|
86
|
+
this.cfg = cfg
|
|
87
|
+
this.bridgeDir = cfg.bridgeDir ?? ''
|
|
88
|
+
this.nodeBin = cfg.nodeBin ?? process.execPath
|
|
89
|
+
this.discoveryPorts = cfg.discoveryPorts ?? BRIDGE_PORTS
|
|
90
|
+
this.spawnBridge = cfg.spawnBridge ?? true
|
|
91
|
+
this.timeouts = cfg.timeouts ?? { startMs: 25000, callMs: 60000 }
|
|
92
|
+
this.state = STATE.idle
|
|
93
|
+
this.error = null
|
|
94
|
+
this.child = null
|
|
95
|
+
this.port = null
|
|
96
|
+
this.lastHealth = null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
isRunning() {
|
|
100
|
+
return this.port !== null ||
|
|
101
|
+
(this.spawnBridge && this.child !== null && this.child.exitCode === null)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Probe the discovery port range (real or injected). */
|
|
105
|
+
async discover() {
|
|
106
|
+
for (const port of this.discoveryPorts) {
|
|
107
|
+
try {
|
|
108
|
+
const r = await httpJson('GET', `http://127.0.0.1:${port}/health`, undefined, 1500)
|
|
109
|
+
if (r.ok && /easyeda-bridge/.test(r.text)) {
|
|
110
|
+
this.port = port
|
|
111
|
+
this.lastHealth = { ok: true, port, raw: r.text.slice(0, 300) }
|
|
112
|
+
return port
|
|
113
|
+
}
|
|
114
|
+
} catch { /* keep scanning */ }
|
|
115
|
+
}
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** One live probe of the current (or first reachable) bridge port. */
|
|
120
|
+
async probeCurrent() {
|
|
121
|
+
for (const port of this.port !== null ? [this.port] : this.discoveryPorts) {
|
|
122
|
+
try {
|
|
123
|
+
const r = await httpJson('GET', `http://127.0.0.1:${port}/health`, undefined, 1500)
|
|
124
|
+
if (r.ok && /easyeda-bridge/.test(r.text)) {
|
|
125
|
+
this.port = port
|
|
126
|
+
this.lastHealth = { ok: true, port, raw: r.text.slice(0, 300) }
|
|
127
|
+
return this.lastHealth
|
|
128
|
+
}
|
|
129
|
+
} catch { /* port closed */ }
|
|
130
|
+
}
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Refresh the health snapshot live (the cached one goes stale: an editor may
|
|
136
|
+
* connect/disconnect after `start()`). Called by /status, eda_status and the
|
|
137
|
+
* bridge-gated tools so they always see the CURRENT connection facts.
|
|
138
|
+
*/
|
|
139
|
+
async refresh() {
|
|
140
|
+
const h = await this.probeCurrent()
|
|
141
|
+
if (h !== null && this.state !== STATE.error) this.state = STATE.connected
|
|
142
|
+
return this.status()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async start() {
|
|
146
|
+
if (this.port !== null) return this.status()
|
|
147
|
+
this.state = STATE.starting
|
|
148
|
+
this.error = null
|
|
149
|
+
// 1) spawn the official bridge server (background) if nothing is on the range.
|
|
150
|
+
if (!(await this.discover()) && this.spawnBridge) {
|
|
151
|
+
try {
|
|
152
|
+
const sep = process.platform === 'win32' ? '\\' : '/'
|
|
153
|
+
const script = this.bridgeDir.replace(/[\\/]+$/, '') + sep + 'scripts' + sep + 'bridge-server.mjs'
|
|
154
|
+
const child = spawn(this.nodeBin, [script], {
|
|
155
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
156
|
+
windowsHide: true,
|
|
157
|
+
cwd: this.bridgeDir || undefined,
|
|
158
|
+
})
|
|
159
|
+
this.child = child
|
|
160
|
+
child.stderr?.on('data', (d) => { this.error = String(d).slice(-400) })
|
|
161
|
+
child.on('exit', () => { if (this.state !== STATE.idle) this.state = STATE.error })
|
|
162
|
+
} catch (error) {
|
|
163
|
+
this.state = STATE.error
|
|
164
|
+
this.error = error instanceof Error ? error.message : String(error)
|
|
165
|
+
return this.status()
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// 2) wait for health on the official port range.
|
|
169
|
+
const deadline = Date.now() + this.timeouts.startMs
|
|
170
|
+
let port = null
|
|
171
|
+
while (Date.now() < deadline && port === null) {
|
|
172
|
+
port = await this.discover()
|
|
173
|
+
if (port !== null) break
|
|
174
|
+
await new Promise((res) => setTimeout(res, 700))
|
|
175
|
+
}
|
|
176
|
+
if (port !== null) {
|
|
177
|
+
this.state = STATE.connected
|
|
178
|
+
this.error = ''
|
|
179
|
+
} else {
|
|
180
|
+
this.state = STATE.error
|
|
181
|
+
if (!this.error) {
|
|
182
|
+
this.error = '官方桥未就绪(49620-49629 无 easyeda-bridge 服务)。请确认:① 已一键安装/启动官方桥;② EasyEDA Pro 已安装 Run API Gateway 扩展并勾选「允许外部交互」。'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return this.status()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Execute official `eda.*` code through the bridge. */
|
|
189
|
+
async execute(code, timeoutMs = this.timeouts.callMs) {
|
|
190
|
+
if (this.port === null) {
|
|
191
|
+
return { ok: false, error: '官方桥未连接(请先启动桥并确认扩展已勾选「允许外部交互」)' }
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const r = await httpJson('POST', `http://127.0.0.1:${this.port}/execute`, { code }, timeoutMs)
|
|
195
|
+
if (r.ok) {
|
|
196
|
+
// Normalize: prefer the JSON `result` field; fall back to raw text.
|
|
197
|
+
const val = r.json?.result !== undefined ? r.json.result : r.text
|
|
198
|
+
return { ok: true, result: typeof val === 'string' ? val : JSON.stringify(val) }
|
|
199
|
+
}
|
|
200
|
+
return { ok: false, error: `execute 失败(HTTP ${r.status}):${r.text.slice(0, 300)}` }
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) }
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Call one action: uses the official snippet for known ids, or runs a raw
|
|
208
|
+
* code snippet passed in params.code (official execute accepts any code).
|
|
209
|
+
*/
|
|
210
|
+
async callTool(name, params = {}) {
|
|
211
|
+
const code = (typeof params?.code === 'string' && params.code.trim() !== '')
|
|
212
|
+
? params.code
|
|
213
|
+
: OFFICIAL_CODES[name]
|
|
214
|
+
if (code === undefined) {
|
|
215
|
+
return { ok: false, error: `未知动作 ${name}(可用官方 eda.* 代码直接传 code 执行;已知映射:${Object.keys(OFFICIAL_CODES).join(', ')})` }
|
|
216
|
+
}
|
|
217
|
+
return this.execute(code)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
status() {
|
|
221
|
+
return {
|
|
222
|
+
backend: 'official-easyeda-bridge',
|
|
223
|
+
state: this.state,
|
|
224
|
+
running: this.port !== null || (this.child !== null && this.child.exitCode === null),
|
|
225
|
+
connected: this.port !== null && this.state === STATE.connected,
|
|
226
|
+
port: this.port,
|
|
227
|
+
health: this.lastHealth?.raw ?? '',
|
|
228
|
+
error: this.error ?? '',
|
|
229
|
+
ports: this.discoveryPorts,
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
dispose() {
|
|
234
|
+
try { this.child?.kill() } catch { /* gone */ }
|
|
235
|
+
this.child = null
|
|
236
|
+
this.port = null
|
|
237
|
+
this.lastHealth = null
|
|
238
|
+
if (this.state !== STATE.error) this.state = STATE.idle
|
|
239
|
+
}
|
|
240
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-lichuang-eda — 官方 API 能力清单(eda_capabilities 工具数据源)。
|
|
3
|
+
*
|
|
4
|
+
* 从官方文档(~/.dsh/eda/bridge/references/classes/*)与真机实测(esp32_multitool
|
|
5
|
+
* 全流程/14 大类/完整 demo)整理:告诉 agent「官方 eda.* API 能做什么、怎么调用、
|
|
6
|
+
* 有什么坑」。agent 不确定能力时调用 eda_capabilities 即可获得结构化清单。
|
|
7
|
+
*
|
|
8
|
+
* 仅收录实测/官方文档确认可用的核心能力;标注 ⚠ 的为已知不可靠项。
|
|
9
|
+
*/
|
|
10
|
+
export const CAPABILITIES = [
|
|
11
|
+
{
|
|
12
|
+
domain: 'dmt_Project',
|
|
13
|
+
note: '工程级(当前焦点工程)',
|
|
14
|
+
methods: [
|
|
15
|
+
{ name: 'getCurrentProjectInfo()', desc: '读当前工程(名称/图页/PCB 树 + titleBlockData.Width/Height 页面尺寸)' },
|
|
16
|
+
{ name: 'openProject(uuid)', desc: '打开指定工程(会丢未保存改动)' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
domain: 'dmt_SelectControl / dmt_EditorControl',
|
|
21
|
+
note: '文档状态与视口',
|
|
22
|
+
methods: [
|
|
23
|
+
{ name: 'getCurrentDocumentInfo()', desc: '当前文档 documentType(1=原理图/3=PCB)/uuid/tabId——**任何操作前先确认**' },
|
|
24
|
+
{ name: 'openDocument(uuid)', desc: '打开图页/PCB,返回 tabId(切换后等 1~1.5s)' },
|
|
25
|
+
{ name: 'activateDocument(tabId)', desc: '激活已开标签' },
|
|
26
|
+
{ name: 'zoomToAllPrimitives()', desc: '缩放到全部图元(截图前必调)' },
|
|
27
|
+
{ name: 'getCurrentRenderedAreaImage(tabId)', desc: '截图返回 Blob(→arrayBuffer→base64→PNG)' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
domain: 'lib_Device',
|
|
32
|
+
note: '系统库搜索(结果可直接喂 create)',
|
|
33
|
+
methods: [
|
|
34
|
+
{ name: 'search(keyword)', desc: '英文关键词(R0402/LED/10K/S8050/ESP32/BUZZER/PTC…)→ 器件数组' },
|
|
35
|
+
{ name: 'searchByProperties({supplierId})', desc: '按立创 C 编号精确搜索' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
domain: 'sch_PrimitiveComponent',
|
|
40
|
+
note: '原理图元件(单位 10mil,A4≈1170×825)',
|
|
41
|
+
methods: [
|
|
42
|
+
{ name: 'create(dev, x, y, …)', desc: '放元件(dev=search 结果或 {libraryUuid,uuid});**先 eda_pick_spot 定位再放**' },
|
|
43
|
+
{ name: 'createNetFlag(type, net, x, y)', desc: "网络标志:'Power'/'Ground'/'AnalogGround'/'ProtectGround'" },
|
|
44
|
+
{ name: 'createNetPort(type, net, x, y)', desc: "网络端口:'IN'/'OUT'/'BI'" },
|
|
45
|
+
{ name: 'getAllPinsByPrimitiveId(id)', desc: '取真实引脚坐标(连线前必调,只连引脚)' },
|
|
46
|
+
{ name: 'delete(ids)', desc: '删除(与导线分开调用,勿混传)' },
|
|
47
|
+
{ name: 'modify(id, prop)', desc: '改属性(改后重新 get 读最新)' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
domain: 'sch_PrimitiveWire',
|
|
52
|
+
note: '原理图导线',
|
|
53
|
+
methods: [
|
|
54
|
+
{ name: 'create(line, net?, color?, width?, type?)', desc: '画线:line 为水平/垂直段数组;net 指定网络名;端点落引脚才连通' },
|
|
55
|
+
{ name: 'delete(ids)', desc: '删除导线' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
domain: 'sch_Document / sch_Drc / sch_ManufactureData',
|
|
60
|
+
note: '保存/校验/导出',
|
|
61
|
+
methods: [
|
|
62
|
+
{ name: 'sch_Document.save()', desc: '保存当前原理图(每步后必调,失败返回 false)' },
|
|
63
|
+
{ name: 'sch_Drc.check(true, false, true)', desc: 'DRC **必须 verbose 重载**;返回聚合 [{type,count}](明细在 UI 面板)' },
|
|
64
|
+
{ name: 'getNetlistFile()', desc: '导出网表:返回 **File 对象,须 await f.text()**(直接返回会变 {})' },
|
|
65
|
+
{ name: 'getBomFile()', desc: '导出 BOM:**二进制 xlsx**,arrayBuffer→base64 处理' },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
domain: 'pcb_PrimitiveComponent / pcb_PrimitiveVia / pcb_PrimitiveLine',
|
|
70
|
+
note: 'PCB 图元(单位 1mil,居中布件)',
|
|
71
|
+
methods: [
|
|
72
|
+
{ name: 'pcb_PrimitiveComponent.create(dev, 1, x, y)', desc: '放元件(layer 1=TOP);get(id)→getAllPins() 取焊盘坐标' },
|
|
73
|
+
{ name: 'pcb_PrimitiveVia.create(net, x, y, hole, dia)', desc: '过孔(网络名直给)' },
|
|
74
|
+
{ name: 'pcb_PrimitiveLine.create(net, 1, x1,y1,x2,y2, width)', desc: '走线(TOP 层);焊盘间连线的正解' },
|
|
75
|
+
{ name: 'pcb_Document.save()', desc: '保存 PCB(每步后必调)' },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
domain: 'pcb_Drc / pcb_Net',
|
|
80
|
+
note: 'PCB 校验与网表(⚠ 部分受限)',
|
|
81
|
+
methods: [
|
|
82
|
+
{ name: 'pcb_Drc.check(true, false, true)', desc: 'DRC 详细数组(规则/对象/层/间距数值)——真实可用' },
|
|
83
|
+
{ name: "pcb_Net.getNetlist('JLCEDA')", desc: '读 PCB 网表(组件数/网络)——JLCEDA 格式可用;"EasyEDA" 格式会挂起,禁用' },
|
|
84
|
+
{ name: 'pcb_Document.importChanges()', desc: '⚠ 实测返回 true 但元件不进画布/网表不变并留 Netlist Error——勿用于画 PCB' },
|
|
85
|
+
{ name: 'pcb_Net.setNetlist(type, text)', desc: '⚠ 实测返回 true 但不生效——勿依赖' },
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
domain: 'sys_FileManager / 快照链路',
|
|
90
|
+
note: '文档导出与本地留档',
|
|
91
|
+
methods: [
|
|
92
|
+
{ name: 'sys_FileManager.getDocumentFile(name)', desc: '导出当前文档 .epro2(专业版原生,zip, File→text)——紧急保存核心' },
|
|
93
|
+
{ name: 'sch_ManufactureData.getExportDocumentFile(name,\'SVG\',…)', desc: '导出当前图页 SVG 通用预览' },
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
domain: '已知边界',
|
|
98
|
+
note: '官方 API 缺陷清单(实测)',
|
|
99
|
+
methods: [
|
|
100
|
+
{ name: 'PCB 板框', desc: 'pcb_PrimitiveLine layer=11 四组参数全失败;无绘制 API——板框人工/导入' },
|
|
101
|
+
{ name: 'sch_Netlist.getNetlist()', desc: '挂起 30s——用 sch_ManufactureData.getNetlistFile()' },
|
|
102
|
+
{ name: 'getAll()', desc: '间歇性报错——重试 3 次(间隔 700ms)' },
|
|
103
|
+
{ name: 'PCB 电气级连网', desc: '直放元件焊盘无网络;网表同步链路不可靠——画布级可用,DRC 可能报 SMD Pad to Track' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
]
|