# 立创 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/

/branches//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/`(无 cookie 鉴权,但 URL 由 API 响应提供) | | 缩略图 CDN | `https://image-pro.lceda.cn/projects/thumbs/.webp`(文档缩略图) | | 加密 | **AES-128-GCM**(`{key, iv}` 由 history API 响应给出,16-byte auth tag 附在 ciphertext 末尾) | | 压缩 | 解密后是 **gzip**,gunzip 后得到源流 | | 数据格式 | **EPRO2** — 自定义消息流(`\n` 分隔消息,`||` 分字段),40+ 种 message types | | 小项目样例 | `无界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/`(一次返回全部) | `/api/v4/projects/`(返 metadata)+ 多个辅助端点(见 §2.4) | | 版本控制 | 单版本,无 branch 概念 | **Git 风格**:branches + linear history chain(含 `parent`,无 snapshot 压缩) | | 源数据加密 | 未完整探(`modules.lceda.cn/histories/.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.40" 等 | "3.2.91" / "3.2.127" | | `branch_uuid` | **null**(无分支模型) | UUID(有 main 分支) | | 文档定位 | 项目 meta 的 **`boards: [{sch, name, pcb}]`** 直接指向 doc UUID | `/structures` 返回多 document 树 | | 历史链 | **不存在**(无 `/branches//histories/` 端点) | 完整 git-style chain,本节后续描述 | | 状态 | ⏳ **本爬虫暂不支持**;HAR 待录 | ✅ 已打通 | | 实例 | `7360e73d...`(立创·泰山派RK3566)、`0c467598...`(立创·梁山派) | `2507dcb6...`(立创·泰山派RK3576)、`b7784066...`(X86电脑主板)、`ba64bd6f...`(ESP-VoCat) | **判别**:先 GET `/api/v4/projects/

`,看 `branch_uuid` 是否非空。`null` 即旧版;记录跳过原因到 `data/state/oshwhub_excluded.jsonl`。**旧版工程的 sch/pcb document UUID 在 `boards[]` 字段里**(每个 board = 一对 sch+pcb),但访问端点未知 —— 候选探测:`/api/documents/`(实测 401,需要不同 cookie scope)、`/api/v4/projects/

/snapshots`(200 但返回的是 project meta 而非 doc)。 --- ## 2. 抓取流程 整体逻辑:**枚举结构 → 拿 history chain → 逐条解密 → 合并消息流 → 按 DOCHEAD 切 per-document**。 ### 2.1 枚举项目结构 `/structures` ``` GET /api/v4/projects//branches//structures ``` 返回嵌套结构(`result.structure` 是**字符串形式的 JSON**,需二次 parse): ```jsonc { "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/

/branches//histories/ ``` URL 看起来像"取单 history 元信息",但 server 实际返回**整条 chain**(HEAD 在数组首位,按 parent 链回溯)。无需分页,crawler 默认走这条。 **(b) 批量端点**(可选 — 早期挖出的接口,仍可用): ``` GET /api/v4/projects/branches/histories?project=

&branch=&history=&limit=&path= ``` - `project` / `branch` / `history` 必填;`history` 传 branch 当前 HEAD 即可 - `limit` 上限实测 ≫ chain 实际长度,设 50 000 就够(泰山派实际 35,limit 值 500/5000/50000 返回一致) - `path` 参数**无作用**(各种值返回相同结果) 返回: ```jsonc { "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) ```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: # 自定义头,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 段 - **行末单个 `|`**:所有消息行都以**单个** `|` 结尾,是行终止符(不是字段分隔符)。解析前应先 `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 各主要类型样例(简化) ```jsonc // 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` + `PCB` document 组成 —— `BOARD` 持有板物理 / 元属性,`PCB` 持有具体几何元素。 - `SCHEMATIC` 在 EPRO2 流里实际叫 **`SCH`**;`structures.schematics` ↔ DOCHEAD `docType: SCH`。 - `SHEET` 在 EPRO2 流里实际叫 **`SCH_PAGE`**;`structures.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-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__` | 当前 `data/raw/oshwhub//` | 写 `scripts/export_for_forge.py` 投影到 `data/processed/batches/batch_/`,保留 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 流: ```bash 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 字段 |