21 KiB
立创 EDA Pro 工程源 (pro.lceda.cn) — 数据源调研
定位:立创 EDA 专业版(EDA Pro,域名 pro.lceda.cn)的工程源抓取链。与 EasyEDA 标准版(u.lceda.cn / oshwhub.com,见 oshwhub.md)并列。
首版调研:2026-04-24
状态:抓取链路 + 加密 + 解压 + 格式 + 多 document 重放 全部打通;对他人公开 Pro 工程已验证。
TL;DR
| 事项 | 结论 |
|---|---|
| 登录态 | 必须(pro.lceda.cn 有独立 session,与 u.lceda.cn 不共享) |
| 对他人 public Pro 项目可用性 | ✅ 验证(立创开发板官方账号的"泰山派3M (RK3576)"项目用我方主号成功拉全量源) |
| 核心枚举 API | /api/v4/projects/<P>/branches/<B>/structures — 返回整个项目树(boards / schematics / sheets / pcbs / panels / blockSymbols) |
| 核心历史 API | /api/v4/projects/branches/histories?project=P&branch=B&history=H&limit=N — 批量返回 history chain(linear, parent 链表) |
| 源文件分发 | https://modules.lceda.cn/projects/histories/<hash>(无 cookie 鉴权,但 URL 由 API 响应提供) |
| 缩略图 CDN | https://image-pro.lceda.cn/projects/thumbs/<hash>.webp(文档缩略图) |
| 加密 | AES-128-GCM({key, iv} 由 history API 响应给出,16-byte auth tag 附在 ciphertext 末尾) |
| 压缩 | 解密后是 gzip,gunzip 后得到源流 |
| 数据格式 | EPRO2 — 自定义消息流(\n 分隔消息,` |
| 小项目样例 | 无界PLUS: 1 document / 1 history / 417 KB blob / 2.7 MB EPRO2 / 8 357 消息 |
| 大项目样例 | 泰山派3M (RK3576): 36 documents / 35 histories / 14 MB 总 blob / 66 MB 解压 / 226 K 消息 / 端到端 2.6 秒(未限速) |
| 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)+ 多个辅助端点(见 §2.4) |
| 版本控制 | 单版本,无 branch 概念 | Git 风格:branches + linear history chain(含 parent,无 snapshot 压缩) |
| 源数据加密 | 未完整探(modules.lceda.cn/histories/<hash>.json 返 403 AccessDenied,推测同样有签名机制) |
AES-128-GCM(确认) |
| 源格式 | 早期 EasyEDA JSON(扁平结构) | EPRO2 消息流(事件溯源式) |
| 多 document 支持 | 单文档 | 多 document(一工程 = 1 board + 1 schematic + N 个 sheet + ...) |
| oshwhub 标记 | 项目 origin: "std" |
项目 origin: "pro" |
| 转 KiCad 成熟度 | easyeda2kicad.py 等第三方工具 |
无现成工具,需自写转换器或用 Pro 编辑器内置"导出 KiCad"功能(其端点待挖) |
2. 抓取流程
整体逻辑:枚举结构 → 拿 history chain → 逐条解密 → 合并消息流 → 按 DOCHEAD 切 per-document。
2.1 枚举项目结构 /structures
GET /api/v4/projects/<PROJ_UUID>/branches/<BRANCH_UUID>/structures
返回嵌套结构(result.structure 是字符串形式的 JSON,需二次 parse):
{
"result": {
"ticket": "82443",
"structure": "{\"boards\":{...},\"schematics\":{...},\"sheets\":{...},\"pcbs\":{...},...}"
}
}
顶层分组(至少观察到):
| Key | 含义 | 示例(泰山派) |
|---|---|---|
boards |
板子定义(版本/标题) | 1 个 |
schematics |
父原理图 | 1 个 原理图 |
sheets |
原理图子表(multi-sheet hierarchy) | 33 个(MIPI_DSI / KEY / POWER / Audio / LPDDR5 / ...) |
pcbs |
PCB 文档 | 1 个 |
panels |
拼板 | 0 个 |
blockSymbols |
模块符号 | 0 个 |
owner |
权限相关(非 document,结构不同) | 5 条 |
每个 sheet 通过 schematic_uuid 关联到父 schematics。所有 document uuid 都是 16 hex(64 bit),区别于 project/branch 的 32 hex uuid。
2.2 枚举完整 history chain
GET /api/v4/projects/branches/histories?project=<P>&branch=<B>&history=<H>&limit=<N>&path=
project/branch/history必填;history传 branch 当前 HEAD 即可limit上限实测 ≫ chain 实际长度,设 50 000 就够(泰山派实际 35,limit 值 500/5000/50000 返回一致)path参数无作用(各种值返回相同结果)
返回:
{
"result": [
{ "uuid": "H35", "parent": "H34", "snapshot": null,
"key": "...", "iv": "...", "num": 0, "snapshot_num": 0, "level": "35",
"dataStrUrl": "https://modules.lceda.cn/projects/histories/H35" },
{ "uuid": "H34", "parent": "H33", ... },
...
{ "uuid": "H1", "parent": null, "snapshot": null, "level": "1", ... }
]
}
观察(泰山派 & 无界PLUS):
- Chain 严格线性(每条有唯一 parent,只一个 root)
level = "1"是 root,单调递增到 HEADsnapshot字段全为 null(没见过有值的;存在但未使用?)- 没有"merge commit" 结构
2.3 解密 + 解压 + 重放(Python)
from Crypto.Cipher import AES
import gzip, httpx, json
from collections import defaultdict
# 1. 拿到 chain(按 level 升序)
chain = httpx.get(HIST_API, params={...}, cookies=COOKIE).json()["result"]
chain.sort(key=lambda h: int(h["level"]))
# 2. 逐条下载 + 解密 + gunzip,拼接事件流
all_lines = []
for h in chain:
blob = httpx.get(h["dataStrUrl"]).content
ct, tag = blob[:-16], blob[-16:] # WebCrypto 约定:auth tag 附末
gz = AES.new(
bytes.fromhex(h["key"]),
AES.MODE_GCM,
nonce=bytes.fromhex(h["iv"]),
).decrypt_and_verify(ct, tag)
data = gzip.decompress(gz)
all_lines.extend(data.split(b"\n"))
# 3. 按 DOCHEAD 切到 per-document 流
docs = defaultdict(list) # doc_uuid -> [msg_line, ...]
current_doc = None
for ln in all_lines:
if not ln.strip(): continue
head = json.loads(ln.split(b"||", 1)[0])
if head["type"] == "DOCHEAD":
payload = json.loads(ln.split(b"||")[1])
current_doc = payload["uuid"] # 16-hex document uuid
if current_doc:
docs[current_doc].append(ln)
关于 GCM nonce 长度:Pro 实现用 16 字节 IV(标准 GCM 用 12 字节)。
pycryptodome的MODE_GCM支持非标准 nonce 长度,直接用即可。
2.4 Pro 编辑器全套 /api/v4/ 端点清单
从 https://modules.lceda.cn/pro-mgr/3.2.127.fa33aea3/js/project-worker.js 反查得到 29 条:
| 端点 | 方法 | 作用 |
|---|---|---|
/api/v4/projects/{uuid} |
GET | 项目详情 |
/api/v4/projects/{uuid}/delete |
POST? | 删项目 |
/api/v4/projects/add |
POST | 建项目 |
/api/v4/projects/{uuid}/branches |
GET | 分支列表 |
/api/v4/projects/{uuid}/branches/{branch} |
GET | 分支详情 |
/api/v4/projects/{uuid}/branches/{branch}/histories |
GET | 单分支 history 列表(有时 404;用下面的 batch 版) |
/api/v4/projects/{uuid}/branches/{branch}/histories/{history} |
GET | 单 history 元信息(含 dataStrUrl/key/iv) |
/api/v4/projects/{uuid}/branches/{branch}/structures |
GET | 项目结构树 |
/api/v4/projects/{uuid}/branches/{branch}/thumbs |
GET | 文档列表(含缩略图 URL) |
/api/v4/projects/{uuid}/branches/{branch}/thumbs/{thumbUuid} |
GET/POST | 单文档缩略图 |
/api/v4/projects/branches/histories?...&limit=N |
GET | 批量 history chain(有效) |
/api/v4/projects/{uuid}/draft-history |
GET | 草稿历史列表 |
/api/v4/projects/{uuid}/draft-history/{draft} |
GET | 单个草稿 |
/api/v4/documents/checkin/{project}/{branch} |
POST | 提交 document 变更 |
/api/v4/documents/checkout/{project}/{branch} |
POST | 取出 document |
/api/v4/components/{uuid}/delete |
POST | 删组件 |
/api/v4/components/{uuid}/histories |
GET | 组件 history |
/api/v4/components/{uuid}/histories/{historyUuid} |
GET | 组件单 history |
/api/v4/componentLogs |
GET | 组件日志 |
/api/v4/devices/{uuid}/delete |
POST | 删设备 |
/api/v4/snapshots/apply |
POST | 应用 snapshot |
/api/v4/snapshots/create |
POST | 建 snapshot |
/api/v4/snapshots/upload-confirm |
POST | snapshot 上传确认 |
/api/v4/snapshots/upload-signatures |
POST | snapshot 上传签名 |
爬取只用到 GET:{uuid} / {uuid}/branches / {uuid}/branches/{branch} / {uuid}/branches/{branch}/structures / {uuid}/branches/{branch}/thumbs / projects/branches/histories?... / {uuid}/branches/{branch}/histories/{history}。
2.5 必需 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.6 实测数据
A. 小项目 无界PLUS(1 document)
| 步骤 | 数值 |
|---|---|
| history chain 长度 | 1 条 |
| blob 总下载 | 416 839 B |
| 解密 + gunzip | 2 712 282 B |
| EPRO2 消息数 | 8 358 |
| docType | BOARD |
chain 只有 1 条就包含整个 project,因为只有 1 个 document。
B. 大项目 泰山派3M (RK3576)(36 documents)
| 步骤 | 数值 |
|---|---|
| structures 返回 documents | 1 board + 1 schematic + 33 sheets + 1 pcb ≈ 36 |
| thumbs 返回(有缩略图的) | 26(部分 sheet 未渲染) |
| history chain 长度 | 35 条(linear,level 1..35) |
| blob 总下载 | 14.0 MB |
| 解密 + gunzip 总量 | 65.9 MB |
| EPRO2 消息数 | 225 880 |
| 端到端耗时(未限速) | 2.6 秒 |
| 消息类型 top | LINE 60K · ATTR 53K · ELE_PLACEHOLDER 35K · LAYER 17K · PAD_NET 7.9K · FILL 7.6K · COMPONENT 5.8K · PAD 4.3K · VIA 4.1K · WIRE 3.9K |
说明:单 history 的 blob 小到 395 B、大到 4 KB 量级(主要是 LAYER + ATTR 为主的增量事件);大体量在 gzip 压缩率上(14 MB → 66 MB ≈ 4.7× 压缩比)。
3. EPRO2 源格式(消息流)
3.1 总体结构
gunzip 后是 UTF-8 文本流,按 \n 分成 N 条消息,每条形如:
{"type":"X","ticket":N,"id":"..."}||{payload JSON}||{optional extra fields}
type:消息类型(见 §3.2)ticket:单调递增的事件序号(可视为版本 diff 编号)id:对象的 stable ID(PART id、NET name、document uuid、等)payload:JSON 对象,内容随 type 变化||:字段分隔符;消息内最多见 2-3 段
本格式是事件溯源 / 增量操作日志:一个工程的完整状态 = 所有消息按 level 重放的结果。
重放原则:严格按 level 升序处理 history chain;同一 history 内按 \n 出现顺序处理。DOCHEAD 消息标记接下来的消息属于哪个 document(用 payload.uuid 的 16-hex)。
3.2 消息类型(≥ 40 种)
数字为"泰山派"项目的计数(35 条 history 合并后),给出相对权重参考。
| 类别 | types |
|---|---|
| 文档 & 元 | EDIT_HEAD · DOCHEAD · META · CANVAS · PREFERENCE · PANELIZE |
| 零件 / 符号 | ATTR (53 208) · PART · COMPONENT (5 790) · PIN |
| 几何图元 | LINE (60 392) · POLY · RECT · CIRCLE · ARC · ELLIPSE · TEXT · STRING |
| PCB 专属 | PAD (4 276) · VIA (4 140) · WIRE (3 932) · NET · PAD_NET (7 892) · TEARDROP |
| 铺铜 | FILL (7 646) · POUR · POURED |
| 层堆叠 | LAYER (16 971) · LAYER_PHYS · ACTIVE_LAYER · SILK_OPTS |
| 设计规则 | RULE · RULE_SELECTOR · RULE_TEMPLATE · PRIMITIVE (4 577) |
| 其他 | FONT · IMAGE · BLOB · TABLE · OBJ · ELE_PLACEHOLDER (35 014) |
3.3 各主要类型样例(简化)
// EDIT_HEAD — 编辑者上下文
{"type":"EDIT_HEAD"}||{"uuid":"...","username":"...","updateTime":1776959670248}
// DOCHEAD — 文档头(每个 document 一次,标记后续消息归属)
{"type":"DOCHEAD","ticket":2}||{"docType":"BOARD","uuid":"35086b7d90787675","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,"attrs":{}}
// ATTR — 属性(value / designator / rotation / color / ...)
{"type":"ATTR","ticket":3,"id":"e1"}||{"partId":"...","key":"Symbol","value":"0603WAF1002T5E",...}
// PIN
{"type":"PIN","ticket":6,"id":"e3"}||{"partId":"...","x":20,"y":0,"length":10,"rotation":180,...}
// 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",...}
// PAD — PCB 焊盘
{"type":"PAD","ticket":134,"id":"e7"}||{"netName":"","layerId":1,"num":"2","centerX":39.37,...}
// LAYER
{"type":"LAYER","ticket":2,"id":"[\"LAYER\",1]"}||{"layerType":"TOP","layerName":"Top Layer",...}
// WIRE + LINE —— WIRE 是总线,LINE 是组成它的线段,通过 lineGroup 关联
{"type":"WIRE","ticket":1009,"id":"e3514"}||{"groupId":"","locked":false,"zIndex":48}
{"type":"LINE","ticket":1010,"id":"..."}||{"lineGroup":"e3514","startX":165,"startY":-708,...}
// POUR / POURED
{"type":"POUR",...}||{"netName":"GND","layerId":1,"pourType":{"pourType":"SOLID",...},...}
// RULE
{"type":"RULE","ticket":225,"id":"[\"RULE\",\"SAFE\",...]"}||{"ruleContext":{"unit":"mm",...}}
3.4 docType 取值
| docType | 含义 | 样例来源 |
|---|---|---|
BOARD |
PCB 板 | 无界PLUS HEAD history |
PCB |
PCB 板(另一种叫法) | 泰山派 HEAD history |
SCHEMATIC |
原理图父文档 | structures 中 schematics 类别,尚未在 DOCHEAD 中见到样例 |
| 其它待观察 | SHEET / SYMBOL / FOOTPRINT / 3D_MODEL 等 |
需覆盖不同类型的 history 事件 |
BOARD与PCB可能是同义(两个工程的 editVersion 不同:3.2.127 给 BOARD / 3.2.91 给 PCB,疑似 Pro 3.2.x 中期改过命名)。需在更多样本里验证。
4. 安全与合规考量
4.1 登录态与账号
- 必须合法账号登录;我方主号测试已通过(见
docs/infra.md) - 风控风险:主号被封影响大;放量 QPS ≤ 0.1(10 秒一次请求)以降低风控概率;多账号轮询作为后备
- 登录后建议立即登出 + 重登一次,让测试期间暴露过的 session 失效(见
CLAUDE.md§登录态)
4.2 抓取范围的合法性
- 仅抓 public: true 且作者有明确 license 声明的项目
- License whitelist(见
OSHWHUB_INGEST_SPEC.md§2.1):MIT / 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 审核
- 本地存储权限 600(在
~/下也要注意) - 建索引时
license_verified = true才进入 Forge 交付批次
5. 接入 Forge 的 gap
按 OSHWHUB_INGEST_SPEC.md(消费侧):每项目三件套 schematic.kicad_sch + manifest.json + source/。
| Forge 要求 | 当前状态 | 缺口 |
|---|---|---|
schematic.kicad_sch(KiCad 7+ S-expression) |
只有 EPRO2 消息流 | EPRO2 → KiCad 转换器。候选路线:(a) 调用 Pro 编辑器"导出 KiCad"API(端点待挖);(b) 自写转换器(解析 §3 全部 types 生成 .kicad_sch) |
| Schematic 单独交付 | 已能按 DOCHEAD 切分 per-document 流 | schematic & sheet 层级的消息流重组(主 sheet 为 root,子 sheet 关联 schematic_uuid) |
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>/ |
写 scripts/export_for_forge.py 投影到 data/processed/batches/batch_<date>/,保留 raw 不动 |
| License whitelist | 当前较宽 | 按 §4.2 收紧;写 scripts/filter_for_forge.py |
| 自测 4 项检查 | 无 | scripts/forge_batch_check.py(见 SPEC §6) |
6. 已验证 / 未解决
| # | 项 | 状态 | 备注 |
|---|---|---|---|
| 1 | 对他人 public Pro 工程能否通 | ✅ | 泰山派 (2507dcb6..., 立创开发板团队) 完整链打通 |
| 2 | 多 document 项目的枚举 | ✅ | /structures 返回全树;/thumbs 返回有缩略图的子集 |
| 3 | 完整 history chain 拉取 | ✅ | /api/v4/projects/branches/histories?...&limit=N 批量返回 |
| 4 | 按 DOCHEAD 切分 per-document | ✅ | payload.uuid 是 16-hex document uuid |
| 5 | SCHEMATIC docType 样例 |
⏳ 部分 | structures 中确认存在;尚未从实际 history 消息中解出 docType: "SCHEMATIC";可能就用 SHEET(每个 sheet 是 document)、SCHEMATIC 只作为父容器 |
| 6 | Pro 编辑器"导出 KiCad"端点 | ❌ 未查 | 录 HAR 观察编辑器内"文件 → 导出 → KiCad"操作 |
| 7 | 风控敏感度 | ❌ 未测 | 阶梯放量(10 → 100 → 1000)监控 403/429/1111111 |
| 8 | Std 版 u.lceda.cn 同构源 API |
❌ 未查 | 需录 Std 编辑器打开 oshwhub 上 origin: std 项目的 HAR |
| 9 | EPRO2 → KiCad 转换实现 | ❌ 未做 | 是 Forge 交付的硬门槛(见 §5) |
附录 A — 端到端重跑脚本
给定项目 UUID,完整拿到 per-document EPRO2 流:
uv run python - <<'PY'
import json, gzip, httpx
from collections import defaultdict
from Crypto.Cipher import AES
PROJ = "2507dcb6e20646e29377e4abd4922bfa" # or any Pro project
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,
"path": PROJ,
}
with httpx.Client(headers=headers, timeout=60, follow_redirects=False) as c:
# 1. project → branch_uuid
proj = c.get(f"https://pro.lceda.cn/api/v4/projects/{PROJ}").json()["result"]
branch = proj["branch_uuid"]
# 2. branch → history_uuid (HEAD)
br = c.get(f"https://pro.lceda.cn/api/v4/projects/{PROJ}/branches/{branch}").json()["result"]
head = br["history_uuid"]
# 3. structures (optional: 打出 document 树)
st = c.get(f"https://pro.lceda.cn/api/v4/projects/{PROJ}/branches/{branch}/structures").json()["result"]
structure = json.loads(st["structure"])
for cat, items in structure.items():
if isinstance(items, dict):
print(f"{cat}: {len(items)}")
# 4. 完整 history chain
r = c.get(f"https://pro.lceda.cn/api/v4/projects/branches/histories",
params={"project": PROJ, "branch": branch, "history": head, "limit": 50000, "path": ""})
chain = sorted(r.json()["result"], key=lambda h: int(h["level"]))
print(f"chain length: {len(chain)}")
# 5. 下载 + 解密 + gunzip + DOCHEAD 分组
docs = defaultdict(list)
cur = None
for h in chain:
blob = c.get(h["dataStrUrl"]).content
ct, tag = blob[:-16], blob[-16:]
gz = AES.new(bytes.fromhex(h["key"]), AES.MODE_GCM, nonce=bytes.fromhex(h["iv"])).decrypt_and_verify(ct, tag)
for ln in gzip.decompress(gz).split(b"\n"):
if not ln.strip(): continue
head_msg = json.loads(ln.split(b"||",1)[0])
if head_msg.get("type") == "DOCHEAD":
cur = json.loads(ln.split(b"||")[1])["uuid"]
if cur:
docs[cur].append(ln)
print(f"documents reconstructed: {len(docs)}")
PY
附录 B — 变更历史
| 日期 | 变更 |
|---|---|
| 2026-04-24 (rev 1) | 首版:单 history 4 步链 + AES-128-GCM + gzip + EPRO2 消息流(40 种 types 覆盖 BOARD 全要素) |
| 2026-04-24 (rev 2) | 本次:加入 /structures 枚举、/projects/branches/histories?... 批量 chain 端点、完整重放流水线;大项目(泰山派 36 docs / 35 histories / 66 MB)实测;对他人 public Pro 项目已验证;29 条 /api/v4/ 端点清单 |