Files
FacereDataset/docs/sources/easyeda_pro_source.md
Zhang Jiahao a16cb11c7d Add easyeda_pro_source.md: Pro 工程源完整链 + EPRO2 格式解析
Why:
- pro.lceda.cn (立创 EDA 专业版) 的工程源抓取链已经打通:4 步 API +
  AES-128-GCM 解密 + gzip 解压 + EPRO2 消息流解析,所有信息需要落成
  文档独立保留,避免丢失;也为后续实现 EPRO2 → KiCad 转换器/选型铺路。
- 与 oshwhub.md(Std 版)并列成为独立调研文档 —— Pro 和 Std 是两套
  独立编辑器,cookie/API/格式都不同,混在一起反而乱。

What:
- docs/sources/easyeda_pro_source.md:
  * TL;DR 表 + §1 Std vs Pro 对照
  * §2 4 步 API 链 + 必需 headers (Editor-Version/path/Referer/Cookie)
    + Python 解密代码 + 实测数据(2.7 MB 源流 / 8357 条消息)
  * §3 EPRO2 格式完整分类:40 种 message type 按功能分组
    (零件/几何/PCB/层/规则/...) + 每类样例
  * §4 安全合规(风控 / license / 密钥泄漏语义)
  * §5 接入 Forge (OSHWHUB_INGEST_SPEC.md) 的 gap 表
  * §6 已知未验证 7 条
  * 附录 A 一键重跑命令

- pyproject.toml: + pycryptodome>=3.23.0(AES-GCM 解密依赖)
- log.md: 本次会话记录

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 00:11:32 +08:00

295 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 立创 EDA Pro 工程源 (pro.lceda.cn) — 数据源调研
**定位**:立创 EDA **专业版**EDA Pro域名 `pro.lceda.cn`)的工程源抓取链。与 EasyEDA **标准版**`u.lceda.cn` / `oshwhub.com`,见 `oshwhub.md`)并列。
**首版调研**2026-04-24
**状态**API 链路 + 加密 + 解压 + 格式 **全部打通**Schematic 文档、对他人公开 Pro 工程、批量化仍待验证。
---
## TL;DR
| 事项 | 结论 |
|---|---|
| 登录态 | **必须**`pro.lceda.cn` 有独立 session`u.lceda.cn` 不共享) |
| 核心 API | `/api/v4/projects/<uuid>/branches/<branch>/histories/<history>` (GET) |
| 源文件分发 | `https://modules.lceda.cn/projects/histories/<hash>`(私有 CDN**不需 cookie**,但只在 API 返回上下文中可下) |
| 加密 | **AES-128-GCM**`{key, iv}` 由 API 响应给出16-byte auth tag 附在 ciphertext 末尾 |
| 压缩 | 解密后是 **gzip**gunzip 后得到源流 |
| 数据格式 | **EPRO2** —— 自定义消息流(`\n` 分隔消息,`||` 分字段40+ 种 message types |
| 一次工程完整源 | 2.7 MB (示例 `无界PLUS`) / 8 357 条消息 / 含 PART / PAD / NET / WIRE / LAYER / RULE / 等全要素 |
| Editor version snapshot | `3.2.127` 2026-04会随 pro 编辑器升级变化,见 `Editor-Version` header |
---
## 1. 与标准版 (EasyEDA Std) 的区别
| 维度 | EasyEDA Std (`u.lceda.cn`) | EasyEDA Pro (`pro.lceda.cn`) |
|---|---|---|
| 编辑器入口 | `lceda.cn/editor` | `pro.lceda.cn/editor?entry=mgr-project-worker.js` |
| Cookie | `lceda_session`hostOnly `u.lceda.cn` | `lceda_pro_session`hostOnly `pro.lceda.cn` **独立** |
| 项目 API | `/api/projects/<uuid>`(一次返回全部) | `/api/v4/projects/<uuid>`(只返 metadata需 4 步拿源) |
| 版本控制 | 单版本,无 branch 概念 | **Git 风格**branches + histories`parent`/`snapshot` |
| 源数据加密 | 未完整探(见 `oshwhub.md` §3.5`modules.lceda.cn/histories/<hash>.json` 也是 403 AccessDenied推测同样有签名机制 | **AES-128-GCM**(确认) |
| 源格式 | 早期 EasyEDA JSON扁平结构 | **EPRO2** 消息流(事件溯源式) |
| oshwhub 标记 | 项目 `origin: "std"` | 项目 `origin: "pro"` |
| 转 KiCad 成熟度 | `easyeda2kicad.py` 等第三方工具 | **无现成工具**,需自写转换器或用 Pro 编辑器内置"导出 KiCad"功能(需研究其 API |
---
## 2. 抓取 4 步链
### 2.1 URL 链路
```
1. GET /api/v4/projects/<PROJ_UUID>
→ 返回 metadata + `branch_uuid`(默认分支)
2. GET /api/v4/projects/<PROJ_UUID>/branches/<BRANCH_UUID>
→ 返回该分支详情 + `history_uuid`(当前 HEAD history
── 或走列表:
GET /api/v4/projects/<PROJ_UUID>/branches?getStartNode=true&isNotPage=yes
3. GET /api/v4/projects/<PROJ_UUID>/branches/<BRANCH_UUID>/histories/<HISTORY_UUID>
→ 返回 `[{ uuid, parent, snapshot, key, iv, dataStrUrl, num, snapshot_num }]`
── **`key` 和 `iv` 是源数据的 AES-GCM 解密密钥/IV**(都是 32 hex = 16 byte
── `dataStrUrl = https://modules.lceda.cn/projects/histories/<HISTORY_UUID>`
4. GET <dataStrUrl>
→ 返回加密 blobContent-Type 未声明,二进制流)
```
### 2.2 必需 HTTP Headers
```
Cookie: <含 lceda_pro_session 的 pro.lceda.cn cookie>
Editor-Version: 3.2.127
Referer: https://pro.lceda.cn/editor
path: <PROJ_UUID> # 自定义头,用做 CSRF 类校验
User-Agent: Mozilla/5.0 (...) Chrome/147.0.0.0 ...
Accept: application/json
```
`Editor-Version``path` 头会返回 `{"success":false,"code":1111111,"message":"没有权限操作"}`
### 2.3 解密流程Python
```python
from Crypto.Cipher import AES
import gzip, httpx
# 1-3: 走完 API 链拿到 key / iv / dataStrUrl
key = bytes.fromhex(api_resp["key"]) # 16B
iv = bytes.fromhex(api_resp["iv"]) # 16B (非标准 GCM nonce 长度12B 会失败)
blob_url = api_resp["dataStrUrl"]
# 4: 下载
blob = httpx.get(blob_url).content
ciphertext, tag = blob[:-16], blob[-16:] # WebCrypto convention: tag 附末
# 5: AES-GCM decrypt_and_verify
plain_gz = AES.new(key, AES.MODE_GCM, nonce=iv).decrypt_and_verify(ciphertext, tag)
# 6: gunzip
source_stream = gzip.decompress(plain_gz) # EPRO2 消息流
```
### 2.4 实测数据(示例项目 `无界PLUS`, `37879d790e1e450dad3375232bd5110f`
| 步骤 | 输入 | 输出 |
|---|---|---|
| blob 下载 | `modules.lceda.cn/.../<hash>` | 416 839 bytes |
| AES-GCM 解密 | blob | 416 823 bytes (16B tag 验证通过) |
| gzip 解压 | 加密载荷 | **2 712 282 bytes** (源消息流) |
| EPRO2 切分 | 按 `\n` 分行 | **8 358 条消息** |
---
## 3. EPRO2 源格式(消息流)
### 3.1 总体结构
解压后是 UTF-8 文本流,按 `\n` 分成 N 条消息,每条形如:
```
{"type":"X","ticket":N,"id":"..."}||{payload JSON}||{optional extra fields}
```
- `type`:消息类型(见 §3.2
- `ticket`:单调递增的事件序号(可视为版本 diff 的编号)
- `id`:对象的 stable IDPART id、网络 id、等
- `payload`消息具体内容JSON 对象)
- `||`:字段分隔符;消息内最多见 2-3 段
本格式是**事件溯源 / 增量操作日志**风格:一个工程的完整状态 = 这一串消息按序重放的结果。
### 3.2 消息类型分布(示例 `无界PLUS` — docType=`BOARD`
| 类别 | types按数量 |
|---|---|
| 文档 & 元 | `EDIT_HEAD` (1) · `DOCHEAD` (369) · `META` (138) · `CANVAS` (93) · `PREFERENCE` (1) · `PANELIZE` (1) |
| 零件 / 符号 | **`ATTR` (2 059)** · **`PART` (62)** · **`COMPONENT` (181)** · `PIN` (240) |
| 几何图元 | `LINE` (737) · `POLY` (385) · `RECT` (39) · `CIRCLE` (8) · `ARC` (7) · `ELLIPSE` (2) · `TEXT` (17) · `STRING` (19) |
| PCB 专属 | `PAD` (204) · `VIA` (59) · **`WIRE` (128)** · **`NET` (52)** · `PAD_NET` (259) · `TEARDROP` (289) |
| 铺铜 | `FILL` (314) · `POUR` (2) · `POURED` (2) |
| 层堆叠 | **`LAYER` (1 572)** · `LAYER_PHYS` (54) · `ACTIVE_LAYER` (50) · `SILK_OPTS` (2) |
| 设计规则 | `RULE` (16) · `RULE_SELECTOR` (53) · `RULE_TEMPLATE` (1) · `PRIMITIVE` (37) |
| 其他 | `FONT` (6) · `IMAGE` (2) · `BLOB` (1) · `TABLE` (1) · `OBJ` (1) · `ELE_PLACEHOLDER` (894) |
### 3.3 各主要类型样例(简化)
```jsonc
// EDIT_HEAD — 编辑者上下文(一条)
{"type":"EDIT_HEAD"} || {"uuid":"...", "username":"...", "updateTime":1776959670248}
// DOCHEAD — 文档头(每个 document 一条ticket 等于当前版本号)
{"type":"DOCHEAD","ticket":2} || {"docType":"BOARD","uuid":"...","version":"...","editVersion":"3.2.127","user":{...}}
// META — 文档元
{"type":"META","ticket":1,"id":"META"} || {"title":"无界PLUS","zIndex":1}
// CANVAS — 画布原点
{"type":"CANVAS","ticket":1,"id":"CANVAS"} || {"originX":0,"originY":0}
// PART — 元件封装定义
{"type":"PART","ticket":2,"id":"0603WAF1002T5E.1"} || {"BBOX":[-10,-4,10,4],"title":"0603WAF1002T5E.1"}
// COMPONENT — 元件放置实例
{"type":"COMPONENT","ticket":2,"id":"e1"} || {"partId":"pid8a...", "x":0,"y":0,"rotation":0,"isMirror":false,"attrs":{}}
// ATTR — 元件/对象属性value, designator, rotation, color...
{"type":"ATTR","ticket":3,"id":"e1"} || {"partId":"0603WAF1002T5E.1","key":"Symbol","value":"0603WAF1002T5E","x":null,...}
// PIN — 元件引脚
{"type":"PIN","ticket":6,"id":"e3"} || {"partId":"...","x":20,"y":0,"length":10,"rotation":180,"pinShape":"NONE",...}
// NET — 电气网络
{"type":"NET","ticket":123,"id":"[\"NET\",\"+12V\"]"} || {"netType":null,"retLine":true,...}
// PAD_NET — 焊盘与网络的归属
{"type":"PAD_NET","ticket":1230,"id":"[\"PAD_NET\",\"e793\",\"1\",\"e18\"]"} || {"padNet":"+12V","padLen":0}
// PAD — 焊盘PCB
{"type":"PAD","ticket":134,"id":"e7"} || {"netName":"","layerId":1,"num":"2","centerX":39.37,"centerY":0,"defaultPad":{"padType":"RECT","width":55.512,"height":53.15},...}
// LAYER — 叠层定义(每层一条)
{"type":"LAYER","ticket":2,"id":"[\"LAYER\",1]"} || {"layerType":"TOP","layerName":"Top Layer","activeColor":"#ff0000",...}
// WIRE + LINE — 走线WIRE 是总线LINE 是组成它的线段,通过 lineGroup 关联)
{"type":"WIRE","ticket":1009,"id":"e3514"} || {"groupId":"","locked":false,"zIndex":48}
{"type":"LINE","ticket":1010,"id":"bbd65048c882128c"} || {"lineGroup":"e3514","startX":165,"startY":-708,"endX":165,"endY":-698,...}
// POUR / POURED — 铺铜定义 / 铺铜渲染结果
{"type":"POUR",...} || {"netName":"GND","layerId":1,"pourType":{"pourType":"SOLID","fineness":8},...}
// RULE / RULE_SELECTOR — 设计规则
{"type":"RULE","ticket":225,"id":"[\"RULE\",\"SAFE\",\"copperThickness1oz\"]"} || {"ruleContext":{"unit":"mm","safeSpacing":[...]}}
```
### 3.4 docType 取值
| docType | 含义 |
|---|---|
| `BOARD` | PCB 布线文档(本次解出来的示例) |
| `SCHEMATIC` | 原理图文档(**未验证**,需要含 schematic 的项目样本) |
| 其它可能 | 待探:`SYMBOL` / `FOOTPRINT` / `3D_MODEL` 等 |
**关键待验证**:一个 Project 下多个 document 的关系 —— 每个 document 是否有独立的 branch/history 链?还是所有 document 共享一条分支?从目前 1 个项目的样本看API 里一个 project → 一个 branch → 一条 history → 一坨 blobblob 内含 **1 个 `DOCHEAD`**(本例 `BOARD`)。如果原理图是另一个 document说明需要
- 先在 API 里枚举项目的所有 documents需找对应端点目前 HAR 里未见)
- 然后每个 document 走独立的 4 步链
---
## 4. 安全与合规考量
### 4.1 登录态与账号
- **必须合法账号登录**;我方主号测试已通过(见 `docs/infra.md`)。风控风险:主号被封影响大
- 登录后建议**立即登出 + 重登**一次,让测试期间暴露过的 session 失效(见 CLAUDE.md §登录态)
- 放量时 **QPS ≤ 0.1**10 秒一次请求)以降低风控概率;多账号轮询作为后备
### 4.2 抓取范围的合法性
- 仅抓**公开**`public: true`)且作者有明确 license 声明的项目
- License whitelist`OSHWHUB_INGEST_SPEC.md` §2.1MIT / BSD / Apache-2.0 / CC0 / CC-BY-4.0 / CERN-OHL-P / Unlicense
- **拒抓**CC-BY-SA立创默认选项占比可能 50%+、GPL 系列、"未声明"、NC禁商用变种
- 项目 `permissions` 字段可反查本号是否有导出权限(见 `project.file.export`
### 4.3 加密密钥泄漏
`key` / `iv` 随每次 API 响应明文返回 —— 这不是保密凭据,而是服务端控制**谁能拿到这对值**(通过登录态 + 签名签发)。我们把解密后的明文落盘即可,不需要保护 key/iv。
但**解密后的工程源是作者的知识产权**
- 不要将他人工程源上传到 Gitea 公开仓库,先过 license 审核
- `~/.secrets/` 之外的本地存储也要做权限控制
- 后续建索引时,每条记录的 `license_verified` 字段必须为 true 才可进入 Forge 交付批次
---
## 5. 接入 Forge 的 gap
`OSHWHUB_INGEST_SPEC.md`(消费侧)要求:每项目三件套 `schematic.kicad_sch` + `manifest.json` + `source/`。本章列出从 EPRO2 到 Forge 约束的**缺口**。
| Forge 要求 | 当前状态 | 缺口 |
|---|---|---|
| `schematic.kicad_sch`KiCad 7+ S-expression | 只有 EPRO2 BOARD 源流 | **需 EPRO2 → KiCad 转换器**。现成工具(`easyeda2kicad.py`)不支持 Pro 格式。候选路线:(a) 调用 Pro 编辑器的"导出 KiCad"功能 API需抓 HAR 找端点);(b) 自写转换器(解析 §3 全部 types → 生成 kicad_sch |
| Schematic 单独交付(非 BOARD | 本次样本是 BOARD | 需要找原理图 document 的 API 入口(待验证) |
| `manifest.json` 完整字段 | `metadata.json` 已覆盖多数字段 | 补:`components_used`(可从 ATTR / COMPONENT 聚合)、`kicad_sch_version`(转换产物)、`file_checksum_sha256``converted_from``conversion_tool``license_source` |
| 目录命名 `oshwhub_<id>_<slug>` | 当前 `data/raw/oshwhub/<uuid>/` | 需额外生成 `batch_<date>/oshwhub_<id>_<slug>/` 结构(建议放 `data/processed/batches/`,保留 raw 不动) |
| License whitelist | 当前 whitelist 较宽(含 GPL / CC-BY-SA | 按 §4.2 收紧;下游过滤器写进 `scripts/filter_for_forge.py` |
| 自测 4 项检查 | 无 | 写 `scripts/forge_batch_check.py`(见 SPEC §6 |
---
## 6. 已知未解决 / 待补
| # | 项 | 优先级 | 行动 |
|---|---|---|---|
| 1 | 对**他人**公开 Pro 工程能否跑通同样 4 步链? | 高 | 下一批 HAR挑一个 oshwhub 里 `origin: pro` 的公开项目,复现链 |
| 2 | `SCHEMATIC` docType 是否在独立 documentAPI 入口? | 高 | 打开含原理图的 Pro 工程录 HAR |
| 3 | 多 document 枚举端点project → documents 列表) | 高 | 同上 |
| 4 | Pro 编辑器的"导出 KiCad"功能走什么端点?产物格式? | 高 | 在编辑器里手动点一次导出,录 HAR |
| 5 | 批量化对风控的敏感度 | 中 | 先从 10 → 100 → 1000 阶梯放量,监控 403/429/1111111 |
| 6 | Std 版 `u.lceda.cn` 的同构源 API如果有 | 中 | 用 Std 编辑器打开 oshwhub 上 `origin: std` 项目录 HAR |
| 7 | EPRO2 → KiCad 转换完整性 | 中 | 选完路线§5 gap后逐 type 实现,自测对比 Pro 编辑器渲染 |
---
## 附录 A — 重跑一次的完整命令
```bash
# 登录态cookie 已备好在 ~/.secrets/pro-lceda-cookie-header.txt
# PROJ_UUID / BRANCH / HIST 从 HAR 或 API 响应中提取
uv run python - <<'PY'
import json, gzip, httpx
from Crypto.Cipher import AES
cookie = open('/home/ubuntu/.secrets/pro-lceda-cookie-header.txt').read().strip()
headers = {
'User-Agent': 'Mozilla/5.0 Chrome/147.0.0.0',
'Referer': 'https://pro.lceda.cn/editor',
'Editor-Version': '3.2.127',
'Accept': 'application/json',
'Cookie': cookie,
}
PROJ = 'YOUR_PROJECT_UUID'
with httpx.Client(headers=headers, timeout=30) as c:
p = c.get(f'https://pro.lceda.cn/api/v4/projects/{PROJ}', headers={'path': PROJ}).json()['result']
br = p['branch_uuid']
h = c.get(f'https://pro.lceda.cn/api/v4/projects/{PROJ}/branches/{br}', headers={'path': PROJ}).json()['result'][0]
blob = c.get(h['dataStrUrl']).content
ct, tag = blob[:-16], blob[-16:]
pt = AES.new(bytes.fromhex(h['key']), AES.MODE_GCM, nonce=bytes.fromhex(h['iv'])).decrypt_and_verify(ct, tag)
stream = gzip.decompress(pt)
print(f'messages: {stream.count(chr(10).encode())}')
PY
```
## 附录 B — 变更历史
| 日期 | 变更 |
|---|---|
| 2026-04-24 | 首版4 步 API 链路 + AES-128-GCM + gzip + EPRO2 消息流格式完整解析40 种 types 覆盖 BOARD 全要素);与 Std 版、Forge SPEC 的 gap 列出 |