补齐前一批失败的 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>
28 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"功能(其端点待挖) |
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 docs)、0c467598...(梁山派 / 2.1.30 / 3 docs) |
2507dcb6...(RK3576)、b7784066...(X86 / 7374 docs)、ba64bd6f...(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 hex(64 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 实际返回整条 chain(HEAD 在数组首位,按 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 就够(泰山派实际 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 为 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-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。
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/iv,AES 加密的 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— 每块 PCBsource/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 长度 | 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 段- 行末单个
|:所有消息行都以单个|结尾,是行终止符(不是字段分隔符)。解析前应先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-VoCat(chain=12,editor=3.2.91,278 docs)+ X86 主板(chain=85,editor=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 |
几个重要观察:
BOARD与PCB是两类不同的 doc(不是同义;之前误以为是命名变化)。一个完整的"PCB 板"由一对BOARD+PCBdocument 组成 ——BOARD持有板物理 / 元属性,PCB持有具体几何元素。SCHEMATIC在 EPRO2 流里实际叫SCH;structures.schematics↔ DOCHEADdocType: SCH。SHEET在 EPRO2 流里实际叫SCH_PAGE;structures.sheets↔ DOCHEADdocType: SCH_PAGE。SYMBOL/FOOTPRINT/DEVICE是项目使用到的组件库快照,每个独特组件一份。这意味着抓 EPRO2 源 = 抓项目 + 完整的局部组件库;下游做转换时必须先把这些 lib doc 加载进 symbol cache。- ESP-VoCat 共 278 docs,结构树 (
structures) 只有 15 个用户级 doc。比例 ~18× 是因为组件 / 符号 / 封装定义都是独立 doc。
docType doc_uuid 长度并非恒定 16-hex:
SYMBOL/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.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/ 端点清单 |
| 2026-04-28 (rev 3) | HAR 实测:/branches/{branch}/histories/{head} 即返回整条 chain(无需走 ?limit=N 批量端点);落地 crawlers/oshwhub/crawler.py:fetch_pro_source 端到端打通(5 项目批量抓 EPRO2);schema 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 自动 dispatch;schema source_format enum 增 easyeda-pro-legacy;5/5 Pro 项目(3 modern + 2 legacy)全部打通 |