Files
FacereDataset/docs/sources/easyeda_pro_source.md
2026-04-24 00:40:18 +08:00

21 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 状态:抓取链路 + 加密 + 解压 + 格式 + 多 document 重放 全部打通;对他人公开 Pro 工程已验证。


TL;DR

事项 结论
登录态 必须pro.lceda.cn 有独立 sessionu.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 chainlinear, 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 末尾)
压缩 解密后是 gzipgunzip 后得到源流
数据格式 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_sessionhostOnly u.lceda.cn lceda_pro_sessionhostOnly pro.lceda.cn独立
项目 API /api/projects/<uuid>(一次返回全部) /api/v4/projects/<uuid>(返 metadata+ 多个辅助端点(见 §2.4
版本控制 单版本,无 branch 概念 Git 风格branches + linear history chainparent,无 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 hex64 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 就够(泰山派实际 35limit 值 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单调递增到 HEAD
  • snapshot 字段全为 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 字节)。pycryptodomeMODE_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-Versionpath 会返回 {"success":false,"code":1111111,"message":"没有权限操作"}

2.6 实测数据

A. 小项目 无界PLUS1 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 长度 35linearlevel 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 IDPART id、NET name、document uuid、等
  • payloadJSON 对象,内容随 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 事件

BOARDPCB 可能是同义(两个工程的 editVersion 不同3.2.127 给 BOARD / 3.2.91 给 PCB疑似 Pro 3.2.x 中期改过命名)。需在更多样本里验证。


4. 安全与合规考量

4.1 登录态与账号

  • 必须合法账号登录;我方主号测试已通过(见 docs/infra.md
  • 风控风险:主号被封影响大;放量 QPS ≤ 0.110 秒一次请求)以降低风控概率;多账号轮询作为后备
  • 登录后建议立即登出 + 重登一次,让测试期间暴露过的 session 失效(见 CLAUDE.md §登录态)

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 审核
  • 本地存储权限 600~/ 下也要注意)
  • 建索引时 license_verified = true 才进入 Forge 交付批次

5. 接入 Forge 的 gap

OSHWHUB_INGEST_SPEC.md(消费侧):每项目三件套 schematic.kicad_sch + manifest.json + source/

Forge 要求 当前状态 缺口
schematic.kicad_schKiCad 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_versionfile_checksum_sha256converted_fromconversion_toollicense_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 是 documentSCHEMATIC 只作为父容器
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/ 端点清单