Files
FacereDataset/docs/sources/easyeda_pro_source.md
Knowit c6279bff08 Add EasyEDA Pro 2.x legacy source ingestion (5/5 batch closure)
补齐前一批失败的 2 个 legacy Pro 项目(立创·泰山派 RK3566、立创·梁山派),
打通 Pro 2.x 旧版工程的源抓取链路。结合上一 commit 的 modern Pro 3.x
路径,本仓库 5/5 Pro 项目 EPRO2/dataStr 全部端到端打通。

Pro 2.x 与 Pro 3.x 是两个完全不同的存储模型:
- Pro 3.x:git-style branch + linear history chain,AES-128-GCM 加密的
  EPRO2 增量消息流,按 history 重放(已在前一 commit 打通)
- Pro 2.x:无 branch / 无 history。文档以 EasyEDA Std plaintext dataStr
  存储(同 ["DOCTYPE","SCH","1.1"] 格式),按 doc UUID 通过
  /api/v2/documents/lists 批量 GET,主体无加密,只组件库走 AES

Pro 2.x 抓取链由 HAR (tmp/prodownload3.har, 178 请求) 反推:

  GET  /api/v4/projects/<P>                     → boards: [{sch, pcb, name}]
  GET  /api/projects/<P>/ticket?uuid=&g_ticket=-1
                                                → 完整项目 manifest
  POST /api/schematic/lists {uuids:[<sch>]}     → sort: [{uuid:<sheet>}]
  POST /api/v2/documents/lists {uuids,docType:1} → schematic plaintext
  POST /api/v2/documents/lists {uuids,docType:3} → PCB plaintext
  POST /api/coppers/search {paths}              → 铺铜层
  POST /api/textpath/search {paths,project_uuid}→ 字体/文字
  POST /api/v2/resources/search {hash,project_uuid} → BLOB 图片

实现:
- crawlers/oshwhub/crawler.py:
  - fetch_pro_source() refactor 成 dispatcher,先 GET project meta
    检查 branch_uuid,null 即旧版走 _fetch_pro_legacy(),非空走
    _fetch_pro_modern()
  - _fetch_pro_legacy() 新增(按上面 9 步流程拉所有 doc + 辅助层)
  - _pro_post_json() POST helper(与 _pro_get_json 对称)
- schemas/project.schema.json: source_format enum 加 easyeda-pro-legacy
- docs/sources/easyeda_pro_source.md rev 4: §1.1 旧版 vs 新版判别表更新、
  §2.7 新增旧版抓取流程 + 实测数据

落盘约定(旧版):
  source/ticket.json                     完整 manifest
  source/<sheet_uuid>.json               每张原理图(含 dataStr)
  source/pcb_<pcb_uuid>.json             每块 PCB
  source/coppers.json/textpath.json/blobs.json  辅助 PCB 层资源
  source/manifest.json                   索引

实测:
  立创·梁山派      editor=2.1.30, 2 sheets+1 pcb,    1.0 MB,  78 sym/191 fp/128 dev
  立创·泰山派 RK3566 editor=2.1.40, 29 sheets+1 pcb, 0.8 MB, 299 sym/524 fp/295 dev

旧版项目体量比新版小两个数量级(梁山派 1 MB vs RK3576 66 MB)—— 没有
增量 history,组件库走单独端点,本身就是当前快照。

5/5 Pro 项目终极汇总:
  X86 主板          easyeda-pro        3.2.15  7374 docs / 481 MB
  泰山派 RK3566     easyeda-pro-legacy 2.1.40    30 docs / 0.8 MB
  梁山派            easyeda-pro-legacy 2.1.30     3 docs / 1.0 MB
  220V 桌面电源     easyeda-pro        3.2.69   771 docs /  26 MB
  ESP-VoCat         easyeda-pro        3.2.91   278 docs / 7.5 MB

共 8456 docs / ~516 MB plain。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 21:59:25 +08:00

28 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"功能(其端点待挖)

1.1 Pro 2.x 旧版与 3.x 新版(重要)

Pro 不是单一存储模型。2026-04 实测发现:

维度 Pro 2.x 旧版 Pro 3.x 新版
editorVersion "2.1.30" / "2.1.40" 等 "3.2.15" / "3.2.69" / "3.2.91" / "3.2.127" 等
branch_uuid null(无分支模型) UUID有 main 分支)
文档定位 项目 meta 的 boards: [{sch, name, pcb}] + ticket 端点 manifest /structures 返回多 document 树
历史链 不存在 完整 git-style linear chain
文档存储 plaintext dataStr同 Std ["DOCTYPE","SCH","1.1"]\n[HEAD,...]),按 doc UUID 批量 GET AES-128-GCM 加密的 EPRO2 增量消息流,按 history 重放
加密 仅组件库symbols/devices走 AES文档本身明文 全部 history blob 都加密
实例 7360e73d...RK3566 / 2.1.40 / 30 docs0c467598...(梁山派 / 2.1.30 / 3 docs 2507dcb6...RK3576b7784066...X86 / 7374 docsba64bd6f...ESP-VoCat / 278 docs
状态 已打通HAR tmp/prodownload3.har 反推) 已打通
schema source_format easyeda-pro-legacy easyeda-pro

判别:先 GET /api/v4/projects/<P>,看 branch_uuid 是否非空。null 即旧版,走 §2.7 的旧版抓取路径;非空走 §2.1-§2.6 的新版路径。crawlers/oshwhub/crawler.py:fetch_pro_source() 是 dispatcher根据 branch_uuid 自动分流到 _fetch_pro_modern()_fetch_pro_legacy()


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

两条等价路径(任选其一,均返回相同 chain 数组):

(a) 编辑器实测路径(首选 — 2026-04-28 HAR 验证):

GET /api/v4/projects/<P>/branches/<B>/histories/<HEAD>

URL 看起来像"取单 history 元信息",但 server 实际返回整条 chainHEAD 在数组首位,按 parent 链回溯。无需分页crawler 默认走这条。

(b) 批量端点(可选 — 早期挖出的接口,仍可用):

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 为 HEAD 的完整 chain 数组编辑器加载实测HAR 2026-04-28非"单 history"
/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。

2.7 Pro 2.x 旧版抓取(无 branch / 无 history chain

适用于 branch_uuid: null 的工程(editorVersion 形如 2.1.40)。文档以 EasyEDA Std plaintext dataStr 形式存储,按 UUID 批量 GET不需要解密 / 解压 / 重放。

流程

GET  /api/v4/projects/<P>                                  → boards: [{sch, pcb, name}]
GET  /api/projects/<P>/ticket?uuid=<P>&g_ticket=-1         → manifest: {schematics, schs, pcbs, coppers, textpath, blobs, symbols, footprints, devices, ...}
POST /api/schematic/lists           {uuids:[<sch>]}        → 父 schematic 容器,含 sort: [{uuid: <sheet>, ticket}, ...]
POST /api/v2/documents/lists        {uuids:[<sheets>], docType:1} → 每个 sheet 的 dataStr 明文
POST /api/v2/documents/lists        {uuids:[<pcbs>],   docType:3} → 每个 PCB 的 dataStr 明文
POST /api/coppers/search            {paths:[<copper_paths>]}      → 铺铜数据PCB 增量层)
POST /api/textpath/search           {paths:[<textpath_paths>], project_uuid, path}  → 字体 / 文字路径
POST /api/v2/resources/search       {hash:[<sha256>], project_uuid}                 → BLOB嵌入图片等
POST /api/v2/components/searchByIds {uuids:[<symbol_uuids>]}      → 元件符号定义(含 dataStrId/key/ivAES 加密的 lib 数据;可选)
POST /api/devices/searchByIds       {uuids:[<device_uuids>]}      → 元件库 metadata

必需 headers

与 §2.5 相同(Editor-Version / path: <PROJ_UUID> / Cookie)。

实测数据

工程 editorVersion sheets pcbs symbols footprints devices blobs coppers size
立创·梁山派 2.1.30 2 1 78 191 128 1 29 1.0 MB
立创·泰山派 RK3566 2.1.40 29 1 299 524 295 32 0 0.8 MB

关键观察

  • 旧版项目体量比新版小两个数量级(梁山派 1 MB vs RK3576 66 MB plain—— 因为没有 history 增量、组件库走单独端点、本身就是当前快照。
  • manifest_ticket['schematics'] ↔ schematic CONTAINER板级"原理图"实体);manifest_ticket['schs'] ↔ 单个 SHEET一页图纸boards[].sch 指向前者,需要 schematic/lists 一步把 sort 拆出 sheet UUIDs。
  • 抓 plaintext docs 后,无需 AES 解密(这点跟 Std 一样)。但 symbols/devices 端点返回的 lib 数据仍然是 AES 加密的 dataStrId blob,跟 Pro 3.x 同方案;如需还原 lib 内容需照 §2.3 解密流程。

落盘约定

  • source/ticket.json — 完整 manifest_ticket保留以备后续 lib 重建)
  • source/<sheet_uuid>.json — 每张原理图(含 dataStr 字段)
  • source/pcb_<pcb_uuid>.json — 每块 PCB
  • source/coppers.json / source/textpath.json / source/blobs.json — 辅助层资源
  • source/manifest.json — 索引 + structure_summary

实现:crawlers/oshwhub/crawler.py:_fetch_pro_legacy()

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 段
  • 行末单个 |:所有消息行都以单个 | 结尾,是行终止符(不是字段分隔符)。解析前应先 rstrip("|") 再按 || split否则末尾片段含尾随 | 会让 JSON 解析失败 —— 2026-04-28 实测的坑

本格式是事件溯源 / 增量操作日志:一个工程的完整状态 = 所有消息按 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 取值

2026-04-28 在 ESP-VoCatchain=12editor=3.2.91278 docs+ X86 主板chain=85editor=3.2.127,待补全)上实测覆盖情况:

docType 含义 计数ESP-VoCat 12-history 全量)
BOARD PCB 板(板物理边框 + 板级元 = "外壳" 容器) 6
PCB PCB primitives焊盘 / 走线 / 铺铜 / 网络等"内容" 6
SCH 原理图容器 6
SCH_PAGE 子图multi-sheet hierarchy 的具体一页,对应 structures 的 sheets 9
SYMBOL 元件符号定义(每个独特符号一份) 105
FOOTPRINT 封装定义 55
DEVICE 元件库元数据LCSC 等) 88
CONFIG 项目级配置layer 表、规则模板、网络颜色等) 1
BLOB 二进制资源(图片 / 3D 模型等) 1
FONT 字体 1

几个重要观察

  • BOARDPCB两类不同的 doc(不是同义;之前误以为是命名变化)。一个完整的"PCB 板"由一对 BOARD + PCB document 组成 —— BOARD 持有板物理 / 元属性,PCB 持有具体几何元素。
  • SCHEMATIC 在 EPRO2 流里实际叫 SCHstructures.schematics ↔ DOCHEAD docType: SCH
  • SHEET 在 EPRO2 流里实际叫 SCH_PAGEstructures.sheets ↔ DOCHEAD docType: SCH_PAGE
  • SYMBOL / FOOTPRINT / DEVICE 是项目使用到的组件库快照,每个独特组件一份。这意味着抓 EPRO2 源 = 抓项目 + 完整的局部组件库;下游做转换时必须先把这些 lib doc 加载进 symbol cache。
  • ESP-VoCat 共 278 docs结构树 (structures) 只有 15 个用户级 doc。比例 ~18× 是因为组件 / 符号 / 封装定义都是独立 doc。

docType doc_uuid 长度并非恒定 16-hexSYMBOL / FOOTPRINT / DEVICE 等组件类的 uuid 是 32 hex与 project / branch 同长);BOARD / PCB / SCH / SCH_PAGE 等用户文档是 16 hex。CONFIG / FONT / BLOB 用 ASCII 字面量名作为 uuid。


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/ 端点清单
2026-04-28 (rev 3) HAR 实测:/branches/{branch}/histories/{head} 即返回整条 chain无需走 ?limit=N 批量端点);落地 crawlers/oshwhub/crawler.py:fetch_pro_source 端到端打通5 项目批量抓 EPRO2schema docType 兼容 string 取值 + 增 message_count 字段
2026-04-28 (rev 4) HAR prodownload3.har 反推 Pro 2.x 旧版抓取链/api/projects/<P>/ticket + /api/v2/documents/lists 批量端点plaintext dataStr+ supplementary coppers/textpath/blobs/components/devices新增 _fetch_pro_legacy() + 在 fetch_pro_source()branch_uuid 自动 dispatchschema source_format enum 增 easyeda-pro-legacy5/5 Pro 项目3 modern + 2 legacy全部打通