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

15 KiB
Raw Blame History

立创 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 有独立 sessionu.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 末尾
压缩 解密后是 gzipgunzip 后得到源流
数据格式 EPRO2 —— 自定义消息流(\n 分隔消息,`
一次工程完整源 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_sessionhostOnly u.lceda.cn lceda_pro_sessionhostOnly pro.lceda.cn 独立
项目 API /api/projects/<uuid>(一次返回全部) /api/v4/projects/<uuid>(只返 metadata需 4 步拿源)
版本控制 单版本,无 branch 概念 Git 风格branches + historiesparent/snapshot
源数据加密 未完整探(见 oshwhub.md §3.5modules.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-Versionpath 头会返回 {"success":false,"code":1111111,"message":"没有权限操作"}

2.3 解密流程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 各主要类型样例(简化)

// 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.110 秒一次请求)以降低风控概率;多账号轮询作为后备

4.2 抓取范围的合法性

  • 仅抓公开public: true)且作者有明确 license 声明的项目
  • License whitelistOSHWHUB_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_schKiCad 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_sha256converted_fromconversion_toollicense_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 — 重跑一次的完整命令

# 登录态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 列出