Update EasyEDA Pro source research
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**定位**:立创 EDA **专业版**(EDA Pro,域名 `pro.lceda.cn`)的工程源抓取链。与 EasyEDA **标准版**(`u.lceda.cn` / `oshwhub.com`,见 `oshwhub.md`)并列。
|
**定位**:立创 EDA **专业版**(EDA Pro,域名 `pro.lceda.cn`)的工程源抓取链。与 EasyEDA **标准版**(`u.lceda.cn` / `oshwhub.com`,见 `oshwhub.md`)并列。
|
||||||
**首版调研**:2026-04-24
|
**首版调研**:2026-04-24
|
||||||
**状态**:API 链路 + 加密 + 解压 + 格式 **全部打通**;Schematic 文档、对他人公开 Pro 工程、批量化仍待验证。
|
**状态**:抓取链路 + 加密 + 解压 + 格式 + 多 document 重放 **全部打通**;对他人公开 Pro 工程已验证。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -11,13 +11,17 @@
|
|||||||
| 事项 | 结论 |
|
| 事项 | 结论 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 登录态 | **必须**(`pro.lceda.cn` 有独立 session,与 `u.lceda.cn` 不共享) |
|
| 登录态 | **必须**(`pro.lceda.cn` 有独立 session,与 `u.lceda.cn` 不共享) |
|
||||||
| 核心 API | `/api/v4/projects/<uuid>/branches/<branch>/histories/<history>` (GET) |
|
| 对他人 public Pro 项目可用性 | **✅** 验证(立创开发板官方账号的"泰山派3M (RK3576)"项目用我方主号成功拉全量源) |
|
||||||
| 源文件分发 | `https://modules.lceda.cn/projects/histories/<hash>`(私有 CDN,**不需 cookie**,但只在 API 返回上下文中可下) |
|
| 核心枚举 API | `/api/v4/projects/<P>/branches/<B>/structures` — 返回整个项目树(boards / schematics / sheets / pcbs / panels / blockSymbols) |
|
||||||
| 加密 | **AES-128-GCM**(`{key, iv}` 由 API 响应给出),16-byte auth tag 附在 ciphertext 末尾 |
|
| 核心历史 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 后得到源流 |
|
| 压缩 | 解密后是 **gzip**,gunzip 后得到源流 |
|
||||||
| 数据格式 | **EPRO2** —— 自定义消息流(`\n` 分隔消息,`||` 分字段),40+ 种 message types |
|
| 数据格式 | **EPRO2** — 自定义消息流(`\n` 分隔消息,`||` 分字段),40+ 种 message types |
|
||||||
| 一次工程完整源 | 2.7 MB (示例 `无界PLUS`) / 8 357 条消息 / 含 PART / PAD / NET / WIRE / LAYER / RULE / 等全要素 |
|
| 小项目样例 | `无界PLUS`: 1 document / 1 history / 417 KB blob / 2.7 MB EPRO2 / 8 357 消息 |
|
||||||
| Editor version snapshot | `3.2.127` (2026-04,会随 pro 编辑器升级变化,见 `Editor-Version` header) |
|
| 大项目样例 | `泰山派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) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -27,80 +31,195 @@
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 编辑器入口 | `lceda.cn/editor` | `pro.lceda.cn/editor?entry=mgr-project-worker.js` |
|
| 编辑器入口 | `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`)**独立** |
|
| Cookie | `lceda_session`(hostOnly `u.lceda.cn`) | `lceda_pro_session`(hostOnly `pro.lceda.cn`)**独立** |
|
||||||
| 项目 API | `/api/projects/<uuid>`(一次返回全部) | `/api/v4/projects/<uuid>`(只返 metadata,需 4 步拿源) |
|
| 项目 API | `/api/projects/<uuid>`(一次返回全部) | `/api/v4/projects/<uuid>`(返 metadata)+ 多个辅助端点(见 §2.4) |
|
||||||
| 版本控制 | 单版本,无 branch 概念 | **Git 风格**:branches + histories(含 `parent`/`snapshot`) |
|
| 版本控制 | 单版本,无 branch 概念 | **Git 风格**:branches + linear history chain(含 `parent`,无 snapshot 压缩) |
|
||||||
| 源数据加密 | 未完整探(见 `oshwhub.md` §3.5,`modules.lceda.cn/histories/<hash>.json` 也是 403 AccessDenied,推测同样有签名机制) | **AES-128-GCM**(确认) |
|
| 源数据加密 | 未完整探(`modules.lceda.cn/histories/<hash>.json` 返 403 AccessDenied,推测同样有签名机制) | **AES-128-GCM**(确认) |
|
||||||
| 源格式 | 早期 EasyEDA JSON(扁平结构) | **EPRO2** 消息流(事件溯源式) |
|
| 源格式 | 早期 EasyEDA JSON(扁平结构) | **EPRO2** 消息流(事件溯源式) |
|
||||||
|
| 多 document 支持 | 单文档 | **多 document**(一工程 = 1 board + 1 schematic + N 个 sheet + ...) |
|
||||||
| oshwhub 标记 | 项目 `origin: "std"` | 项目 `origin: "pro"` |
|
| oshwhub 标记 | 项目 `origin: "std"` | 项目 `origin: "pro"` |
|
||||||
| 转 KiCad 成熟度 | `easyeda2kicad.py` 等第三方工具 | **无现成工具**,需自写转换器或用 Pro 编辑器内置"导出 KiCad"功能(需研究其 API) |
|
| 转 KiCad 成熟度 | `easyeda2kicad.py` 等第三方工具 | **无现成工具**,需自写转换器或用 Pro 编辑器内置"导出 KiCad"功能(其端点待挖) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 抓取 4 步链
|
## 2. 抓取流程
|
||||||
|
|
||||||
### 2.1 URL 链路
|
整体逻辑:**枚举结构 → 拿 history chain → 逐条解密 → 合并消息流 → 按 DOCHEAD 切 per-document**。
|
||||||
|
|
||||||
|
### 2.1 枚举项目结构 `/structures`
|
||||||
|
|
||||||
```
|
```
|
||||||
1. GET /api/v4/projects/<PROJ_UUID>
|
GET /api/v4/projects/<PROJ_UUID>/branches/<BRANCH_UUID>/structures
|
||||||
→ 返回 metadata + `branch_uuid`(默认分支)
|
|
||||||
|
|
||||||
2. GET /api/v4/projects/<PROJ_UUID>/branches/<BRANCH_UUID>
|
|
||||||
→ 返回该分支详情 + `history_uuid`(当前 HEAD history)
|
|
||||||
── 或走列表:
|
|
||||||
GET /api/v4/projects/<PROJ_UUID>/branches?getStartNode=true&isNotPage=yes
|
|
||||||
|
|
||||||
3. GET /api/v4/projects/<PROJ_UUID>/branches/<BRANCH_UUID>/histories/<HISTORY_UUID>
|
|
||||||
→ 返回 `[{ uuid, parent, snapshot, key, iv, dataStrUrl, num, snapshot_num }]`
|
|
||||||
── **`key` 和 `iv` 是源数据的 AES-GCM 解密密钥/IV**(都是 32 hex = 16 byte)
|
|
||||||
── `dataStrUrl = https://modules.lceda.cn/projects/histories/<HISTORY_UUID>`
|
|
||||||
|
|
||||||
4. GET <dataStrUrl>
|
|
||||||
→ 返回加密 blob(Content-Type 未声明,二进制流)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2.2 必需 HTTP Headers
|
返回嵌套结构(`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
|
||||||
|
|
||||||
|
```
|
||||||
|
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` 参数**无作用**(各种值返回相同结果)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
```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 元信息(含 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>
|
Cookie: <含 lceda_pro_session 的 pro.lceda.cn cookie>
|
||||||
Editor-Version: 3.2.127
|
Editor-Version: 3.2.127
|
||||||
Referer: https://pro.lceda.cn/editor
|
Referer: https://pro.lceda.cn/editor
|
||||||
path: <PROJ_UUID> # 自定义头,用做 CSRF 类校验
|
path: <PROJ_UUID> # 自定义头,CSRF 类校验
|
||||||
User-Agent: Mozilla/5.0 (...) Chrome/147.0.0.0 ...
|
User-Agent: Mozilla/5.0 (...) Chrome/147.0.0.0 ...
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
```
|
```
|
||||||
|
|
||||||
缺 `Editor-Version` 或 `path` 头会返回 `{"success":false,"code":1111111,"message":"没有权限操作"}`。
|
缺 `Editor-Version` 或 `path` 会返回 `{"success":false,"code":1111111,"message":"没有权限操作"}`。
|
||||||
|
|
||||||
### 2.3 解密流程(Python)
|
### 2.6 实测数据
|
||||||
|
|
||||||
```python
|
#### A. 小项目 `无界PLUS`(1 document)
|
||||||
from Crypto.Cipher import AES
|
|
||||||
import gzip, httpx
|
|
||||||
|
|
||||||
# 1-3: 走完 API 链拿到 key / iv / dataStrUrl
|
| 步骤 | 数值 |
|
||||||
key = bytes.fromhex(api_resp["key"]) # 16B
|
|---|---|
|
||||||
iv = bytes.fromhex(api_resp["iv"]) # 16B (非标准 GCM nonce 长度,12B 会失败)
|
| history chain 长度 | **1** 条 |
|
||||||
blob_url = api_resp["dataStrUrl"]
|
| blob 总下载 | 416 839 B |
|
||||||
|
| 解密 + gunzip | 2 712 282 B |
|
||||||
|
| EPRO2 消息数 | **8 358** |
|
||||||
|
| docType | `BOARD` |
|
||||||
|
|
||||||
# 4: 下载
|
chain 只有 1 条就包含整个 project,因为只有 1 个 document。
|
||||||
blob = httpx.get(blob_url).content
|
|
||||||
ciphertext, tag = blob[:-16], blob[-16:] # WebCrypto convention: tag 附末
|
|
||||||
|
|
||||||
# 5: AES-GCM decrypt_and_verify
|
#### B. 大项目 `泰山派3M (RK3576)`(36 documents)
|
||||||
plain_gz = AES.new(key, AES.MODE_GCM, nonce=iv).decrypt_and_verify(ciphertext, tag)
|
|
||||||
|
|
||||||
# 6: gunzip
|
| 步骤 | 数值 |
|
||||||
source_stream = gzip.decompress(plain_gz) # EPRO2 消息流
|
|---|---|
|
||||||
```
|
| 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 |
|
||||||
|
|
||||||
### 2.4 实测数据(示例项目 `无界PLUS`, `37879d790e1e450dad3375232bd5110f`)
|
**说明**:单 history 的 blob 小到 395 B、大到 4 KB 量级(主要是 LAYER + ATTR 为主的增量事件);**大体量在 gzip 压缩率上**(14 MB → 66 MB ≈ 4.7× 压缩比)。
|
||||||
|
|
||||||
| 步骤 | 输入 | 输出 |
|
|
||||||
|---|---|---|
|
|
||||||
| blob 下载 | `modules.lceda.cn/.../<hash>` | 416 839 bytes |
|
|
||||||
| AES-GCM 解密 | blob | 416 823 bytes (16B tag 验证通过) |
|
|
||||||
| gzip 解压 | 加密载荷 | **2 712 282 bytes** (源消息流) |
|
|
||||||
| EPRO2 切分 | 按 `\n` 分行 | **8 358 条消息** |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -108,95 +227,97 @@ source_stream = gzip.decompress(plain_gz) # EPRO2 消息流
|
|||||||
|
|
||||||
### 3.1 总体结构
|
### 3.1 总体结构
|
||||||
|
|
||||||
解压后是 UTF-8 文本流,按 `\n` 分成 N 条消息,每条形如:
|
gunzip 后是 UTF-8 文本流,按 `\n` 分成 N 条消息,每条形如:
|
||||||
|
|
||||||
```
|
```
|
||||||
{"type":"X","ticket":N,"id":"..."}||{payload JSON}||{optional extra fields}
|
{"type":"X","ticket":N,"id":"..."}||{payload JSON}||{optional extra fields}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `type`:消息类型(见 §3.2)
|
- `type`:消息类型(见 §3.2)
|
||||||
- `ticket`:单调递增的事件序号(可视为版本 diff 的编号)
|
- `ticket`:单调递增的事件序号(可视为版本 diff 编号)
|
||||||
- `id`:对象的 stable ID(PART id、网络 id、等)
|
- `id`:对象的 stable ID(PART id、NET name、document uuid、等)
|
||||||
- `payload`:消息具体内容(JSON 对象)
|
- `payload`:JSON 对象,内容随 type 变化
|
||||||
- `||`:字段分隔符;消息内最多见 2-3 段
|
- `||`:字段分隔符;消息内最多见 2-3 段
|
||||||
|
|
||||||
本格式是**事件溯源 / 增量操作日志**风格:一个工程的完整状态 = 这一串消息按序重放的结果。
|
本格式是**事件溯源 / 增量操作日志**:一个工程的完整状态 = 所有消息按 level 重放的结果。
|
||||||
|
|
||||||
### 3.2 消息类型分布(示例 `无界PLUS` — docType=`BOARD`)
|
**重放原则**:严格按 `level` 升序处理 history chain;同一 history 内按 `\n` 出现顺序处理。DOCHEAD 消息标记接下来的消息属于哪个 document(用 payload.uuid 的 16-hex)。
|
||||||
|
|
||||||
| 类别 | types(按数量) |
|
### 3.2 消息类型(≥ 40 种)
|
||||||
|
|
||||||
|
> 数字为"泰山派"项目的计数(35 条 history 合并后),给出相对权重参考。
|
||||||
|
|
||||||
|
| 类别 | types |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 文档 & 元 | `EDIT_HEAD` (1) · `DOCHEAD` (369) · `META` (138) · `CANVAS` (93) · `PREFERENCE` (1) · `PANELIZE` (1) |
|
| 文档 & 元 | `EDIT_HEAD` · `DOCHEAD` · `META` · `CANVAS` · `PREFERENCE` · `PANELIZE` |
|
||||||
| 零件 / 符号 | **`ATTR` (2 059)** · **`PART` (62)** · **`COMPONENT` (181)** · `PIN` (240) |
|
| 零件 / 符号 | **`ATTR` (53 208)** · `PART` · **`COMPONENT` (5 790)** · `PIN` |
|
||||||
| 几何图元 | `LINE` (737) · `POLY` (385) · `RECT` (39) · `CIRCLE` (8) · `ARC` (7) · `ELLIPSE` (2) · `TEXT` (17) · `STRING` (19) |
|
| 几何图元 | **`LINE` (60 392)** · `POLY` · `RECT` · `CIRCLE` · `ARC` · `ELLIPSE` · `TEXT` · `STRING` |
|
||||||
| PCB 专属 | `PAD` (204) · `VIA` (59) · **`WIRE` (128)** · **`NET` (52)** · `PAD_NET` (259) · `TEARDROP` (289) |
|
| PCB 专属 | **`PAD` (4 276)** · `VIA` (4 140) · **`WIRE` (3 932)** · `NET` · `PAD_NET` (7 892) · `TEARDROP` |
|
||||||
| 铺铜 | `FILL` (314) · `POUR` (2) · `POURED` (2) |
|
| 铺铜 | `FILL` (7 646) · `POUR` · `POURED` |
|
||||||
| 层堆叠 | **`LAYER` (1 572)** · `LAYER_PHYS` (54) · `ACTIVE_LAYER` (50) · `SILK_OPTS` (2) |
|
| 层堆叠 | **`LAYER` (16 971)** · `LAYER_PHYS` · `ACTIVE_LAYER` · `SILK_OPTS` |
|
||||||
| 设计规则 | `RULE` (16) · `RULE_SELECTOR` (53) · `RULE_TEMPLATE` (1) · `PRIMITIVE` (37) |
|
| 设计规则 | `RULE` · `RULE_SELECTOR` · `RULE_TEMPLATE` · `PRIMITIVE` (4 577) |
|
||||||
| 其他 | `FONT` (6) · `IMAGE` (2) · `BLOB` (1) · `TABLE` (1) · `OBJ` (1) · `ELE_PLACEHOLDER` (894) |
|
| 其他 | `FONT` · `IMAGE` · `BLOB` · `TABLE` · `OBJ` · **`ELE_PLACEHOLDER` (35 014)** |
|
||||||
|
|
||||||
### 3.3 各主要类型样例(简化)
|
### 3.3 各主要类型样例(简化)
|
||||||
|
|
||||||
```jsonc
|
```jsonc
|
||||||
// EDIT_HEAD — 编辑者上下文(一条)
|
// EDIT_HEAD — 编辑者上下文
|
||||||
{"type":"EDIT_HEAD"}||{"uuid":"...","username":"...","updateTime":1776959670248}
|
{"type":"EDIT_HEAD"}||{"uuid":"...","username":"...","updateTime":1776959670248}
|
||||||
|
|
||||||
// DOCHEAD — 文档头(每个 document 一条,ticket 等于当前版本号)
|
// DOCHEAD — 文档头(每个 document 一次,标记后续消息归属)
|
||||||
{"type":"DOCHEAD","ticket":2} || {"docType":"BOARD","uuid":"...","version":"...","editVersion":"3.2.127","user":{...}}
|
{"type":"DOCHEAD","ticket":2}||{"docType":"BOARD","uuid":"35086b7d90787675","version":"...","editVersion":"3.2.127","user":{...}}
|
||||||
|
|
||||||
// META — 文档元
|
// META — 文档元
|
||||||
{"type":"META","ticket":1,"id":"META"}||{"title":"无界PLUS","zIndex":1}
|
{"type":"META","ticket":1,"id":"META"}||{"title":"无界PLUS","zIndex":1}
|
||||||
|
|
||||||
// CANVAS — 画布原点
|
// CANVAS
|
||||||
{"type":"CANVAS","ticket":1,"id":"CANVAS"}||{"originX":0,"originY":0}
|
{"type":"CANVAS","ticket":1,"id":"CANVAS"}||{"originX":0,"originY":0}
|
||||||
|
|
||||||
// PART — 元件封装定义
|
// PART — 元件封装定义
|
||||||
{"type":"PART","ticket":2,"id":"0603WAF1002T5E.1"}||{"BBOX":[-10,-4,10,4],"title":"0603WAF1002T5E.1"}
|
{"type":"PART","ticket":2,"id":"0603WAF1002T5E.1"}||{"BBOX":[-10,-4,10,4],"title":"0603WAF1002T5E.1"}
|
||||||
|
|
||||||
// COMPONENT — 元件放置实例
|
// COMPONENT — 元件放置实例
|
||||||
{"type":"COMPONENT","ticket":2,"id":"e1"} || {"partId":"pid8a...", "x":0,"y":0,"rotation":0,"isMirror":false,"attrs":{}}
|
{"type":"COMPONENT","ticket":2,"id":"e1"}||{"partId":"pid8a...","x":0,"y":0,"rotation":0,"attrs":{}}
|
||||||
|
|
||||||
// ATTR — 元件/对象属性(value, designator, rotation, color...)
|
// ATTR — 属性(value / designator / rotation / color / ...)
|
||||||
{"type":"ATTR","ticket":3,"id":"e1"} || {"partId":"0603WAF1002T5E.1","key":"Symbol","value":"0603WAF1002T5E","x":null,...}
|
{"type":"ATTR","ticket":3,"id":"e1"}||{"partId":"...","key":"Symbol","value":"0603WAF1002T5E",...}
|
||||||
|
|
||||||
// PIN — 元件引脚
|
// PIN
|
||||||
{"type":"PIN","ticket":6,"id":"e3"} || {"partId":"...","x":20,"y":0,"length":10,"rotation":180,"pinShape":"NONE",...}
|
{"type":"PIN","ticket":6,"id":"e3"}||{"partId":"...","x":20,"y":0,"length":10,"rotation":180,...}
|
||||||
|
|
||||||
// NET — 电气网络
|
// NET — 电气网络
|
||||||
{"type":"NET","ticket":123,"id":"[\"NET\",\"+12V\"]"}||{"netType":null,"retLine":true,...}
|
{"type":"NET","ticket":123,"id":"[\"NET\",\"+12V\"]"}||{"netType":null,"retLine":true,...}
|
||||||
|
|
||||||
// PAD_NET — 焊盘与网络的归属
|
// PAD_NET — 焊盘 ↔ 网络
|
||||||
{"type":"PAD_NET","ticket":1230,"id":"[\"PAD_NET\",\"e793\",\"1\",\"e18\"]"} || {"padNet":"+12V","padLen":0}
|
{"type":"PAD_NET","ticket":1230,"id":"[\"PAD_NET\",\"e793\",\"1\",\"e18\"]"}||{"padNet":"+12V",...}
|
||||||
|
|
||||||
// PAD — 焊盘(PCB)
|
// PAD — PCB 焊盘
|
||||||
{"type":"PAD","ticket":134,"id":"e7"} || {"netName":"","layerId":1,"num":"2","centerX":39.37,"centerY":0,"defaultPad":{"padType":"RECT","width":55.512,"height":53.15},...}
|
{"type":"PAD","ticket":134,"id":"e7"}||{"netName":"","layerId":1,"num":"2","centerX":39.37,...}
|
||||||
|
|
||||||
// LAYER — 叠层定义(每层一条)
|
// LAYER
|
||||||
{"type":"LAYER","ticket":2,"id":"[\"LAYER\",1]"} || {"layerType":"TOP","layerName":"Top Layer","activeColor":"#ff0000",...}
|
{"type":"LAYER","ticket":2,"id":"[\"LAYER\",1]"}||{"layerType":"TOP","layerName":"Top Layer",...}
|
||||||
|
|
||||||
// WIRE + LINE — 走线(WIRE 是总线,LINE 是组成它的线段,通过 lineGroup 关联)
|
// WIRE + LINE —— WIRE 是总线,LINE 是组成它的线段,通过 lineGroup 关联
|
||||||
{"type":"WIRE","ticket":1009,"id":"e3514"}||{"groupId":"","locked":false,"zIndex":48}
|
{"type":"WIRE","ticket":1009,"id":"e3514"}||{"groupId":"","locked":false,"zIndex":48}
|
||||||
{"type":"LINE","ticket":1010,"id":"bbd65048c882128c"} || {"lineGroup":"e3514","startX":165,"startY":-708,"endX":165,"endY":-698,...}
|
{"type":"LINE","ticket":1010,"id":"..."}||{"lineGroup":"e3514","startX":165,"startY":-708,...}
|
||||||
|
|
||||||
// POUR / POURED — 铺铜定义 / 铺铜渲染结果
|
// POUR / POURED
|
||||||
{"type":"POUR",...} || {"netName":"GND","layerId":1,"pourType":{"pourType":"SOLID","fineness":8},...}
|
{"type":"POUR",...}||{"netName":"GND","layerId":1,"pourType":{"pourType":"SOLID",...},...}
|
||||||
|
|
||||||
// RULE / RULE_SELECTOR — 设计规则
|
// RULE
|
||||||
{"type":"RULE","ticket":225,"id":"[\"RULE\",\"SAFE\",\"copperThickness1oz\"]"} || {"ruleContext":{"unit":"mm","safeSpacing":[...]}}
|
{"type":"RULE","ticket":225,"id":"[\"RULE\",\"SAFE\",...]"}||{"ruleContext":{"unit":"mm",...}}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3.4 docType 取值
|
### 3.4 docType 取值
|
||||||
|
|
||||||
| docType | 含义 |
|
| docType | 含义 | 样例来源 |
|
||||||
|---|---|
|
|---|---|---|
|
||||||
| `BOARD` | PCB 布线文档(本次解出来的示例) |
|
| `BOARD` | PCB 板 | 无界PLUS HEAD history |
|
||||||
| `SCHEMATIC` | 原理图文档(**未验证**,需要含 schematic 的项目样本) |
|
| `PCB` | PCB 板(另一种叫法) | 泰山派 HEAD history |
|
||||||
| 其它可能 | 待探:`SYMBOL` / `FOOTPRINT` / `3D_MODEL` 等 |
|
| `SCHEMATIC` | 原理图父文档 | structures 中 `schematics` 类别,尚未在 DOCHEAD 中见到样例 |
|
||||||
|
| 其它待观察 | `SHEET` / `SYMBOL` / `FOOTPRINT` / `3D_MODEL` 等 | 需覆盖不同类型的 history 事件 |
|
||||||
|
|
||||||
**关键待验证**:一个 Project 下多个 document 的关系 —— 每个 document 是否有独立的 branch/history 链?还是所有 document 共享一条分支?从目前 1 个项目的样本看,API 里一个 project → 一个 branch → 一条 history → 一坨 blob,blob 内含 **1 个 `DOCHEAD`**(本例 `BOARD`)。如果原理图是另一个 document,说明需要:
|
> `BOARD` 与 `PCB` 可能是同义(两个工程的 editVersion 不同:3.2.127 给 BOARD / 3.2.91 给 PCB,疑似 Pro 3.2.x 中期改过命名)。需在更多样本里验证。
|
||||||
|
|
||||||
- 先在 API 里枚举项目的所有 documents(需找对应端点,目前 HAR 里未见)
|
|
||||||
- 然后每个 document 走独立的 4 步链
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -204,91 +325,125 @@ source_stream = gzip.decompress(plain_gz) # EPRO2 消息流
|
|||||||
|
|
||||||
### 4.1 登录态与账号
|
### 4.1 登录态与账号
|
||||||
|
|
||||||
- **必须合法账号登录**;我方主号测试已通过(见 `docs/infra.md`)。风控风险:主号被封影响大
|
- **必须合法账号登录**;我方主号测试已通过(见 `docs/infra.md`)
|
||||||
- 登录后建议**立即登出 + 重登**一次,让测试期间暴露过的 session 失效(见 CLAUDE.md §登录态)
|
- 风控风险:主号被封影响大;**放量 QPS ≤ 0.1**(10 秒一次请求)以降低风控概率;多账号轮询作为后备
|
||||||
- 放量时 **QPS ≤ 0.1**(10 秒一次请求)以降低风控概率;多账号轮询作为后备
|
- 登录后建议**立即登出 + 重登**一次,让测试期间暴露过的 session 失效(见 `CLAUDE.md` §登录态)
|
||||||
|
|
||||||
### 4.2 抓取范围的合法性
|
### 4.2 抓取范围的合法性
|
||||||
|
|
||||||
- 仅抓**公开**(`public: true`)且作者有明确 license 声明的项目
|
- 仅抓 **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
|
- 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(禁商用)变种
|
- **拒抓**:CC-BY-SA(立创默认选项,占比可能 50%+)、GPL 系列、"未声明"、NC(禁商用)变种
|
||||||
- 项目 `permissions` 字段可反查本号是否有导出权限(见 `project.file.export`)
|
- 项目 `permissions` 字段可反查本号是否有导出权限(如 `project.file.export`)
|
||||||
|
|
||||||
### 4.3 加密密钥泄漏
|
### 4.3 加密密钥泄漏
|
||||||
|
|
||||||
`key` / `iv` 随每次 API 响应明文返回 —— 这不是保密凭据,而是服务端控制**谁能拿到这对值**(通过登录态 + 签名签发)。我们把解密后的明文落盘即可,不需要保护 key/iv。
|
`key` / `iv` 随每次 API 响应明文返回 — 这不是保密凭据,而是服务端通过**登录态 + 签发**控制谁能拿到这对值。解密后的明文落盘即可,不需保护 key/iv。
|
||||||
|
|
||||||
但 **解密后的工程源是作者的知识产权**:
|
但 **解密后的工程源是作者的知识产权**:
|
||||||
- 不要将他人工程源上传到 Gitea 公开仓库,先过 license 审核
|
- 不要将他人工程源上传到 Gitea 公开仓库,先过 license 审核
|
||||||
- `~/.secrets/` 之外的本地存储也要做权限控制
|
- 本地存储权限 600(在 `~/` 下也要注意)
|
||||||
- 后续建索引时,每条记录的 `license_verified` 字段必须为 true 才可进入 Forge 交付批次
|
- 建索引时 `license_verified = true` 才进入 Forge 交付批次
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 接入 Forge 的 gap
|
## 5. 接入 Forge 的 gap
|
||||||
|
|
||||||
按 `OSHWHUB_INGEST_SPEC.md`(消费侧)要求:每项目三件套 `schematic.kicad_sch` + `manifest.json` + `source/`。本章列出从 EPRO2 到 Forge 约束的**缺口**。
|
按 `OSHWHUB_INGEST_SPEC.md`(消费侧):每项目三件套 `schematic.kicad_sch` + `manifest.json` + `source/`。
|
||||||
|
|
||||||
| Forge 要求 | 当前状态 | 缺口 |
|
| Forge 要求 | 当前状态 | 缺口 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `schematic.kicad_sch`(KiCad 7+ S-expression) | 只有 EPRO2 BOARD 源流 | **需 EPRO2 → KiCad 转换器**。现成工具(`easyeda2kicad.py`)不支持 Pro 格式。候选路线:(a) 调用 Pro 编辑器的"导出 KiCad"功能 API(需抓 HAR 找端点);(b) 自写转换器(解析 §3 全部 types → 生成 kicad_sch) |
|
| `schematic.kicad_sch`(KiCad 7+ S-expression) | 只有 EPRO2 消息流 | **EPRO2 → KiCad 转换器**。候选路线:(a) 调用 Pro 编辑器"导出 KiCad"API(端点待挖);(b) 自写转换器(解析 §3 全部 types 生成 .kicad_sch) |
|
||||||
| Schematic 单独交付(非 BOARD) | 本次样本是 BOARD | 需要找原理图 document 的 API 入口(待验证) |
|
| 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` |
|
| `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>/` | 需额外生成 `batch_<date>/oshwhub_<id>_<slug>/` 结构(建议放 `data/processed/batches/`,保留 raw 不动) |
|
| 目录命名 `oshwhub_<id>_<slug>` | 当前 `data/raw/oshwhub/<uuid>/` | 写 `scripts/export_for_forge.py` 投影到 `data/processed/batches/batch_<date>/`,保留 raw 不动 |
|
||||||
| License whitelist | 当前 whitelist 较宽(含 GPL / CC-BY-SA) | 按 §4.2 收紧;下游过滤器写进 `scripts/filter_for_forge.py` |
|
| License whitelist | 当前较宽 | 按 §4.2 收紧;写 `scripts/filter_for_forge.py` |
|
||||||
| 自测 4 项检查 | 无 | 写 `scripts/forge_batch_check.py`(见 SPEC §6) |
|
| 自测 4 项检查 | 无 | `scripts/forge_batch_check.py`(见 SPEC §6) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 已知未解决 / 待补
|
## 6. 已验证 / 未解决
|
||||||
|
|
||||||
| # | 项 | 优先级 | 行动 |
|
| # | 项 | 状态 | 备注 |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | 对**他人**公开 Pro 工程能否跑通同样 4 步链? | 高 | 下一批 HAR:挑一个 oshwhub 里 `origin: pro` 的公开项目,复现链 |
|
| 1 | 对他人 public Pro 工程能否通 | **✅** | 泰山派 (`2507dcb6...`, 立创开发板团队) 完整链打通 |
|
||||||
| 2 | `SCHEMATIC` docType 是否在独立 document?API 入口? | 高 | 打开含原理图的 Pro 工程录 HAR |
|
| 2 | 多 document 项目的枚举 | **✅** | `/structures` 返回全树;`/thumbs` 返回有缩略图的子集 |
|
||||||
| 3 | 多 document 枚举端点(project → documents 列表) | 高 | 同上 |
|
| 3 | 完整 history chain 拉取 | **✅** | `/api/v4/projects/branches/histories?...&limit=N` 批量返回 |
|
||||||
| 4 | Pro 编辑器的"导出 KiCad"功能走什么端点?产物格式? | 高 | 在编辑器里手动点一次导出,录 HAR |
|
| 4 | 按 DOCHEAD 切分 per-document | **✅** | payload.uuid 是 16-hex document uuid |
|
||||||
| 5 | 批量化对风控的敏感度 | 中 | 先从 10 → 100 → 1000 阶梯放量,监控 403/429/1111111 |
|
| 5 | `SCHEMATIC` docType 样例 | ⏳ 部分 | structures 中确认存在;尚未从实际 history 消息中解出 `docType: "SCHEMATIC"`;可能就用 `SHEET`(每个 sheet 是 document)、`SCHEMATIC` 只作为父容器 |
|
||||||
| 6 | Std 版 `u.lceda.cn` 的同构源 API(如果有) | 中 | 用 Std 编辑器打开 oshwhub 上 `origin: std` 项目录 HAR |
|
| 6 | Pro 编辑器"导出 KiCad"端点 | ❌ 未查 | 录 HAR 观察编辑器内"文件 → 导出 → KiCad"操作 |
|
||||||
| 7 | EPRO2 → KiCad 转换完整性 | 中 | 选完路线(§5 gap)后逐 type 实现,自测对比 Pro 编辑器渲染 |
|
| 7 | 风控敏感度 | ❌ 未测 | 阶梯放量(10 → 100 → 1000)监控 403/429/1111111 |
|
||||||
|
| 8 | Std 版 `u.lceda.cn` 同构源 API | ❌ 未查 | 需录 Std 编辑器打开 oshwhub 上 `origin: std` 项目的 HAR |
|
||||||
|
| 9 | EPRO2 → KiCad 转换实现 | ❌ 未做 | 是 Forge 交付的硬门槛(见 §5) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 附录 A — 重跑一次的完整命令
|
## 附录 A — 端到端重跑脚本
|
||||||
|
|
||||||
|
给定项目 UUID,完整拿到 per-document EPRO2 流:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 登录态(cookie 已备好在 ~/.secrets/pro-lceda-cookie-header.txt)
|
|
||||||
# PROJ_UUID / BRANCH / HIST 从 HAR 或 API 响应中提取
|
|
||||||
uv run python - <<'PY'
|
uv run python - <<'PY'
|
||||||
import json, gzip, httpx
|
import json, gzip, httpx
|
||||||
|
from collections import defaultdict
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
|
|
||||||
|
PROJ = "2507dcb6e20646e29377e4abd4922bfa" # or any Pro project
|
||||||
cookie = open('/home/ubuntu/.secrets/pro-lceda-cookie-header.txt').read().strip()
|
cookie = open('/home/ubuntu/.secrets/pro-lceda-cookie-header.txt').read().strip()
|
||||||
headers = {
|
headers = {
|
||||||
'User-Agent': 'Mozilla/5.0 Chrome/147.0.0.0',
|
"User-Agent": "Mozilla/5.0 Chrome/147.0.0.0",
|
||||||
'Referer': 'https://pro.lceda.cn/editor',
|
"Referer": "https://pro.lceda.cn/editor",
|
||||||
'Editor-Version': '3.2.127',
|
"Editor-Version": "3.2.127",
|
||||||
'Accept': 'application/json',
|
"Accept": "application/json",
|
||||||
'Cookie': cookie,
|
"Cookie": cookie,
|
||||||
|
"path": PROJ,
|
||||||
}
|
}
|
||||||
PROJ = 'YOUR_PROJECT_UUID'
|
|
||||||
|
|
||||||
with httpx.Client(headers=headers, timeout=30) as c:
|
with httpx.Client(headers=headers, timeout=60, follow_redirects=False) as c:
|
||||||
p = c.get(f'https://pro.lceda.cn/api/v4/projects/{PROJ}', headers={'path': PROJ}).json()['result']
|
# 1. project → branch_uuid
|
||||||
br = p['branch_uuid']
|
proj = c.get(f"https://pro.lceda.cn/api/v4/projects/{PROJ}").json()["result"]
|
||||||
h = c.get(f'https://pro.lceda.cn/api/v4/projects/{PROJ}/branches/{br}', headers={'path': PROJ}).json()['result'][0]
|
branch = proj["branch_uuid"]
|
||||||
blob = c.get(h['dataStrUrl']).content
|
|
||||||
|
# 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:]
|
ct, tag = blob[:-16], blob[-16:]
|
||||||
pt = AES.new(bytes.fromhex(h['key']), AES.MODE_GCM, nonce=bytes.fromhex(h['iv'])).decrypt_and_verify(ct, tag)
|
gz = AES.new(bytes.fromhex(h["key"]), AES.MODE_GCM, nonce=bytes.fromhex(h["iv"])).decrypt_and_verify(ct, tag)
|
||||||
stream = gzip.decompress(pt)
|
for ln in gzip.decompress(gz).split(b"\n"):
|
||||||
|
if not ln.strip(): continue
|
||||||
print(f'messages: {stream.count(chr(10).encode())}')
|
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
|
PY
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 附录 B — 变更历史
|
## 附录 B — 变更历史
|
||||||
|
|
||||||
| 日期 | 变更 |
|
| 日期 | 变更 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| 2026-04-24 | 首版:4 步 API 链路 + AES-128-GCM + gzip + EPRO2 消息流格式完整解析(40 种 types 覆盖 BOARD 全要素);与 Std 版、Forge SPEC 的 gap 列出 |
|
| 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/` 端点清单 |
|
||||||
|
|||||||
Reference in New Issue
Block a user