Files
FacereDataset/log.md
Knowit fc2a45f658 docs: explain per-doc .epro2 crawl vs web-export .epro2 ZIP
Colleague-facing explainer at docs/sources/pro_crawl_vs_export.md.
Addresses the "I see 278 .epro2 files but my browser only downloaded
one" confusion: web download is a ZIP container (extension is a UX
choice, not a format), our crawl produces per-doc message streams.
Both carry equivalent EPRO2 data; only real gap is IMAGE/ binary
previews which we don't fetch yet.

Why per-doc and not ZIP: the ZIP path has no public endpoint —
three HARs confirm the export button fires zero HTTP requests, it's
pure client-side JSZip on data already loaded by the editor. Our
crawler hits the same chain endpoints the editor uses internally,
which delivers per-doc streams.

Log entry references the 278 vs 266 doc-count delta for ESP-VoCat
(we walk full history chain, web export is a current snapshot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:13:52 +08:00

738 lines
40 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# FacereDataset 执行日志
时间倒序,最新在顶部。
---
## 2026-04-29 01:00 科普文档:爬取 per-doc .epro2 vs 网页端 .epro2 ZIP 整包
**Claude 会话**
接 chain replay sleep 优化commit `1e06ba6`)后续。同事看到 `data/raw/oshwhub/<uuid>/source/` 下面躺着 278 个 `.epro2` 而不是 1 个,会直觉以为抓错了——他们认知里的 `.epro2` 是网页端"下载工程包"那个 1.4 MB 单文件。
实际上:
- **网页端 `.epro2`** = ZIP 容器(扩展名误导),里面 `.epru`(拼成一坨的 EPRO2 流)+ `project2.json` + `IMAGE/` 6 张组件预览图
- **爬取 `.epro2`** = 工程内每个文档SYMBOL / FOOTPRINT / DEVICE / SCH_PAGE / PCB ……)自己的 EPRO2 消息流per-doc 一文件
- 两者**信息量基本等价**ESP-VoCat 我们 278 vs 网页 266多的 12 个是 history chain 上演化掉的容器层旧版本);唯一真实差异是 IMAGE/ 二进制图(我们 blob 引用爬到了但没拉本体——已知 gap
- 我们走 per-doc 不走 ZIP 的硬约束:**ZIP 那条路服务端没有公开端点**,是纯前端 JS 现拼现压(三份 HAR 验证:导出按钮零 HTTP 流量)
写到 `docs/sources/pro_crawl_vs_export.md`给同事看。结构TL;DR → ESP-VoCat 实例 → docType 分布对比表 → 数量差异解释 → 体积对比 → 选型决策表。
---
## 2026-04-29 00:30 KiCad 导出 Phase 3 hierarchicalroot + global_label + 5-Voltage 电源端口
**Claude 会话**
`54f0173`。Handoff #3 多 sheet hierarchical`kicad-cli sch erc` 在 project 视角而不是单 sheet 视角下校验,理论上能把 single-sheet ERC 看不见跨 sheet 的 248+111 残留压下去。
### 三件事一起做
1. **`(label)``(global_label)`**`tools/epro2/kicad/sch_writer.py`
EPRO2 的 NET 是项目全局——一个 GND 名字横跨整个 schematic 而且通过 PCB 走线到隔壁板子。KiCad 的 `(label)` 是 sheet-scoped单 sheet ERC 看到一个名字只出现一次就报 dangling`(global_label)` 才是项目级hierarchical ERC 在 root 上能跨 sheet 解。
2. **`tools/epro2/kicad/root_sch_writer.py` 新模块**
按 EPRO2 的 `SCH_PAGE.META.schematic` 把页分组,每组 emit 一个 root `.kicad_sch`,里面 N 个 `(sheet ...)` 引子页。子页的 `(sheet_instances (path "/<assigned_uuid>" ...))` 必须回引 root 给它分配的 uuid——少了这一步 ERC 把子页当孤岛走。
3. **5-Voltage 电源端口识别**`sch_writer.py` COMPONENT 循环里加判断)
实测 ESP-VoCat 有 365 个 partId=`pid8a0e77bacb214e` 的 COMPONENT——挖了下发现这是 EPRO2 内部的 "Voltage" 占位符号,对应 KiCad 的 `(power_symbol)`。**网络名不在 PART 里,而是 placement 自己的 `Global Net Name` ATTR**101 个有名字、264 个还是空的草稿态)。每次有名字就在 placement 位置 emit 一个 `(global_label)`
### CLI 改造
`uv run python -m tools.epro2.kicad <project> --all-sch` 现在默认 hierarchical 输出:每个 SCH 一个目录,里面 root + 子页。`--flat` 兜回老行为(一张图一个文件)。`DELETE_DOC.isDelete=True` 的 SCH 直接跳——LCD-BD 那个被删了的没生成。
### ESP-VoCat 实测
`kicad-cli sch erc <each-root>` 跨 5 个 root 累加:
| Type | flat baseline | hier 后 | Δ |
|---|---:|---:|---:|
| wire_dangling | 52 | 52 | 0 |
| pin_not_connected | 196 | 190 | -6 |
| label_dangling → global_label_dangling | 111 | 105 | -6 |
| pin_not_driven | 23 | 21 | -2 |
| endpoint_off_grid | 1372 | 1340 | -32 (LCD-BD 移除带走的) |
| lib_symbol_issues | 571 | 557 | -14 (同上) |
| **TOTAL** | **2325** | **2265** | **-60** |
**修得不够多——为什么**EPRO2 的 6 个 SCH 里 5 个只有 1 pagehierarchical 对它们没用;只有 CoreBoard 是真 4-page 多 sheet。CoreBoard 自己里面也只有 GND / MCU_3V3 / VCC_3V3 是真跨 sheet 共享的网其它GPIO4, CHIP_PU, AUDIO_I2C_*, I2S_*)都是 sheet-local——名字虽然 unique 但只在一个 sheet 上有 wire 引用hierarchical ERC 也救不了,依然 dangling。
**剩下 190 PNC 的真原因**:抽样发现 C44/C45/R19/R20 这类 0402 元件wire 从一个 pin 出来直直穿过另一个 pin但 pin 落在 wire **中段**而不是端点。KiCad ERC 要求 pin 落在 wire 端点或 explicit junction 处才认连接wire 中段穿过的 pin 不自动连。EPRO2 源里这种连接合法但导出时丢了——要修需要做 wire-pin 几何相交,在中段 pin 位置 split wire 或 emit `(junction)`。下一轮再做。
### 决策Why
- **不走单 root 包全部 9 page**6 块 PCB 是物理独立板子merge 进一个 root 会把 BaseBoard 的 GND 和 CoreBoard 的 GND 误判成同一个网。EPRO2 已经按 SCH 分好组,按 SCH 拆 root 是结构对齐做法。
- **`(global_label (shape passive))`**不知道方向input/output/bidirectional 都会触发更多 ERC 检查(如 pin_not_drivenpassive 最保守。power 网用 power_in 才理想,但需要 driver 元件,超出本轮范围。
- **保留占位符号 placement 同时再 emit global_label**5-Voltage 占位符在 KiCad 里画出来虽然冗余但不影响 ERC删了反而丢视觉信息。
- **不实现自动 junction**geometry 计算成本明显高于本轮收益(<10 PNC 的预期降幅),做成下一轮独立改动更清晰。
### 测试
47 → 52 单测全过root_sch_writer 3 个 + power-port label 2 个 + sheet_path/page_num propagate 1 个 - test_named_wire 改 global_label 重写 1 个。
### 下一步建议
- **wire-pin junction emission**(中等工作量):算每个 COMPONENT 的 abs pin 位置,对每条 wire 查"非端点 pin 命中",命中就 split wire 或 emit junction。预期把 PNC 从 190 再砍一半左右。
- 或者直接进 **Phase 3 真正的 .kicad_pcb 导出**——schematic 这边已经够用PCB 才是 Forge 投影最后一块。
---
## 2026-04-28 23:55 KiCad 导出修真实连接错wire_dangling -88%, pin_not_connected -52%
**Claude 会话**
`fb577cc` 后续。Handoff 选项 #1:用 ERC violation 数从 850 降到接近 0 作为目标。Bisect 出两个根因,都是结构性的 KiCad 语义错(不是浮点 / 精度问题,所以 grid round 不是答案):
### Bug A — sym_writer 漏 Y-flip
KiCad lib symbol 是 Y-upschematic 是 Y-downplacement 时 KiCad 会再翻一次 Y。我们把 EPRO2 (Y-down) 的 PIN/RECT/POLY/CIRCLE/TEXT 坐标直接当 lib coord 写KiCad 的翻转就把整个 symbol body 上下镜像了。U8 4-pin 磁吸座实测pin 1↔4 对调、2↔3 对调,结果 wire 端点跟错位的 pin tip 撞不上 → ERC 报 pin_not_connected + wire_dangling。修法`lib_y = -epro2_y`pin/text rotation 也镜像 `lib_rot = (360 - rot) % 360`
### Bug B — 没发 net labelKiCad 看不出 EPRO2 的命名网络
EPRO2 的 WIRE op 带 NET attr`TXD` / `GND` / `VBUS` ……),多段 LINE 通过同名 NET 连成一个网,**不需要几何相邻**。KiCad 不知道这套,只看几何。修法:在 sch_writer 里查每条 LINE 的 `lineGroup → WIRE → NET attr`,命中就在 LINE 起点 emit 一个 `(label "<NET>")`。同名 label 在多条物理 wire 上 → KiCad ERC 才认这是同一个网。**per-LINE 不是 per-WIRE**:单个 WIRE id 下面的 LINE 段不一定共端点,每段都得有 label 才不被判 dangling。
### ESP-VoCat 9 sheets ERC 对比
| Type | baseline | after | Δ |
|---|---:|---:|---:|
| wire_dangling | 444 | 52 | **88%** |
| pin_not_connected | 406 | 196 | **52%** |
| **real connectivity 合计** | **850** | **248** | **71%** |
| label_dangling | 0 | 111 | new (warn) |
| pin_not_driven | 0 | 23 | new (connector pin 类型问题) |
| endpoint_off_grid | 1372 | 1372 | unchanged (cosmetic, EPRO2 用 30/20/10 mil pitch不在 KiCad 50 mil 网格上;不能 round——会把 < 50 mil 的 pin 间距压到一起) |
| lib_symbol_issues | 571 | 571 | unchanged (没注册 facere libcosmetic) |
剩余 248 real-connectivity 错主要是 single-sheet ERC 的天然限制:很多网只在一个 sheet 上有这一个 pin对端在别的 sheet。kicad-cli 一次只看一个 .kicad_sch看不见跨 sheet。彻底修要 Phase 3hierarchical 顶层 + sheet links
### 关键决策(记 Why
- **不 round to grid**50 mil grid 会塌缩 < 50 mil pin pitch实测 4-pin 磁吸座是 10 mil pitch破坏几何。EPRO2 源已经在整数 mil浮点不是 root cause。
- **per-LINE label 不是 per-WIRE**:同 WIRE id 下两条 LINE 段不共端点是常态(不同地方各连一段),都得 label 才不孤立。
- **用 `(label)` 不是 `(global_label)`**实验过两种语法single-sheet ERC 都判 dangling因为这个 sheet 上只出现一次);语义上 EPRO2 net 全局,但 single-sheet 校验视角看不见跨 sheet换 global_label 帮不上。Phase 3 hierarchical 重构时再切。
- **不做 ERC config tweak**KiCad 8 的 connection grid 是硬编码 50 milschematic 文件里没法配;想消 endpoint_off_grid 必须破坏 EPRO2 几何或者升级到 KiCad 9 + custom severity。
### 测试
41 → 46 单测全过:新增 `test_pin_y_negated_for_kicad_lib_y_up_convention` / `test_pin_rotation_mirrored_to_compensate_y_flip` / `test_rect_y_negated` / `test_named_wire_emits_label_at_line_start` / `test_unnamed_wire_emits_no_label`
### 下一步建议
- Phase 3 hierarchical写 root .kicad_sch 引用所有子 sheet + 把跨 sheet 的 NET 升级成 `(global_label)`single-sheet ERC 残留的 248 + 111 大概率随之降到 < 100。
- (并行) 消 lib_symbol_issues 571emit `sym-lib-table` + 独立 .kicad_sym。
---
## 2026-04-28 23:30 oshwhub 全量 listing 索引落本地33,695 项 / 28.4 MB
**Claude 会话**
为了在"扩量到 top-30 / top-50 / 全量"前先量化候选池规模 + 质量分布,把 oshwhub listing API 全量扫一遍落地。
### 关键收获(之前以为是黑箱)
- **listing API 直接返回 `total` 字段**Pro 21,202 / Std 12,493**合计 33,695**。
- **`pageSize` 无上限**,实测 1000 工作良好;全量索引 = 35 次请求 / 71 秒 / 35 MB 流量。
- **`sort` 参数被服务端静默忽略**——传啥都返回相同顺序grade desc → 隐式质量分 desc。"按时间排序"必须先拉全集再本地排。
- **`origin` 默认 std**——不带参数永远看不到 Pro 池。
- **`license` 不在 listing 响应**必须挨个抓详情页QPS=0.5 → ~19 小时全量)。
### 数据画像(写到 `docs/sources/oshwhub_listing_full.md`
- **Pro 长尾极重**grade=1 占 82%,真正 A 档grade≥3 & like≥10只有 1,356 (6.4%)
- **Std 高质量比例反而高**A 档 1,450 (11.6%),因为平台老 7 年2016 起 vs Pro 2021 起),项目有时间累积点赞
- **Std 已停滞**2021-2022 见顶3.4k/年之后断崖1.5k → 0.9k → 0.4k → 0.05k 2026Q1
- **Pro 还在快速膨胀**2023 起线性增长2025 全年 7.4k2026Q1 已 1.1k
- **作者长尾健康**Pro 10,536 个 / Std 5,531 个唯一作者top-1 占比 0.4% / 1.5%
- **立创官方账号占据头部**course-examples / li-chuang-kai-fa-ban / li-chuang-zhi-neng-ying-jian-bu
### 实操含义
放量决策有了量化锚点S 档 583 项 / A 档 2,806 项 / B 档 6,243 项 / 全量 33,695。Pro 工程源体积外推(基于 5 项实测均值),全 Pro 约 1 TB——超出 Gitea LFS 舒适区,必须配 size cap + license 白名单。
### 下一步
1. 在本地 jsonl 上按 A 档过滤,做 license 详情页扫描(一次性 ~7 小时)
2. license 白名单 ∩ A 档 → 真候选清单
3. 然后再决定批量下载源
### 文件
- `scripts/dump_listing_index.py` —— 一次性全量扫描脚本,可重抓
- `data/state/oshwhub_listing_full.jsonl` —— 28.4 MBgitignore可重建不入库
- `docs/sources/oshwhub_listing_full.md` —— 给人看的简报
---
## 2026-04-28 22:00 Pro 2.x 旧版工程源抓取链路打通5/5 Pro 项目全部 ✅
**Claude 会话**
承接刚做的 3/5 modern Pro 批量。用户录了第三份 HAR`tmp/prodownload3.har` 103 MB / 178 请求,目标是登录态打开**梁山派** legacy Pro 2.x 项目),让我把 RK3566 / 梁山派两个旧版项目也补上。
### HAR 反推Pro 2.x 用一套完全不同的端点
旧版(`editorVersion: 2.1.x`)没有 git-style branch + history 模型。HAR 里看到的关键端点:
| 端点 | 作用 |
|---|---|
| `GET /api/projects/<P>/ticket?uuid=<P>&g_ticket=-1` | 完整项目 manifestschematics / schs / pcbs / coppers / textpath / blobs / symbols / footprints / devices / block_symbol |
| `POST /api/schematic/lists {uuids:[<sch>]}` | 父 schematic 容器,含 `sort: [{uuid: <sheet>, ticket}]` 即子图 UUIDs |
| `POST /api/v2/documents/lists {uuids:[...], docType:1}` | 抓 schematic 子图(**plaintext** dataStr跟 Std 一样的 `["DOCTYPE","SCH","1.1"]\n[HEAD,...]` 格式) |
| `POST /api/v2/documents/lists {uuids:[...], docType:3}` | 抓 PCB同样 plaintext dataStr |
| `POST /api/coppers/search {paths:[...]}` | 铺铜层数据PCB 上独立分发的增量层) |
| `POST /api/textpath/search {paths, project_uuid, path}` | 字体 / 文字路径(同上) |
| `POST /api/v2/resources/search {hash, project_uuid}` | BLOB嵌入图片 |
| `POST /api/v2/components/searchByIds` / `/api/devices/searchByIds` | 元件库 metadata`dataStrId` 仍走 modules.lceda.cn AES 加密 blob |
**关键差异**:旧版的工程**主体 plaintext**(无加密 / 无 history 重放),只有元件库走 Pro 3.x 的 AES 方案。这反而比 3.x 简单很多。
**判别规则**`/api/v4/projects/<P>` 返回的 `branch_uuid` 是不是 null。null 即旧版。
### 实现
`crawlers/oshwhub/crawler.py` refactor
- `fetch_pro_source()` 拆成 dispatcher先 GET project meta → 检查 `branch_uuid`
- `_fetch_pro_modern()` —— 原 EPRO2 chain 流程,去掉重复的 project meta 调用
- `_fetch_pro_legacy()` —— 新增,按上面 9 步流程拉所有 doc + 辅助层
- `_pro_post_json()` —— POST helper与 GET helper 对称)
落盘约定(旧版):
- `source/ticket.json` —— 完整 manifest保留备 lib 重建)
- `source/<sheet_uuid>.json` —— 每张原理图
- `source/pcb_<pcb_uuid>.json` —— 每块 PCB
- `source/coppers.json` / `textpath.json` / `blobs.json` —— 辅助 PCB 层资源
- `source/manifest.json` —— 索引 + structure_summary
schema`source_format` enum 加 `easyeda-pro-legacy`
### 实测2/2 legacy 项目打通
| 项目 | editor | sheets | pcbs | sym | fp | dev | coppers | textpath | blobs | size |
|---|---|---|---|---|---|---|---|---|---|---|
| 立创·梁山派 | 2.1.30 | 2 | 1 | 78 | 191 | 128 | 29 | 3 | 1 | 1.0 MB |
| 立创·泰山派 RK3566 | 2.1.40 | 29 | 1 | 299 | 524 | 295 | 0 | 0 | 32 | 0.8 MB |
**旧版项目比新版小两个数量级**(梁山派 1 MB vs RK3576 66 MB—— 没有增量 history、组件库走单独端点、本身就是当前快照。
### 5/5 Pro 项目终极汇总
| # | 项目 | source_format | editor | docs | size |
|---|---|---|---|---|---|
| 1 | X86 主板 | easyeda-pro | 3.2.15 | 7374 | 481 MB |
| 2 | 立创·泰山派 RK3566 | easyeda-pro-legacy | 2.1.40 | 30 | 0.8 MB |
| 3 | 立创·梁山派 | easyeda-pro-legacy | 2.1.30 | 3 | 1.0 MB |
| 4 | 220V 桌面电源 | easyeda-pro | 3.2.69 | 771 | 26 MB |
| 5 | ESP-VoCat 喵伴 | easyeda-pro | 3.2.91 | 278 | 7.5 MB |
合计 8456 docs / ~516 MB plain 源数据5/5 端到端打通。
### 下一步
1. EPRO2 → KiCad 转换器(仍是 Forge 投影硬门槛ESP-VoCat 7.5 MB 是不错的小样本起手)
2. 旧版 dataStr → KiCad 复用 Std 转换链(同格式,已有 `easyeda2kicad.py` 支持)
3. 阶梯放量到 50 / 500 项目时做风控压测
---
## 2026-04-28 21:35 Pro 工程源EPRO2批量抓取打通3/5 modern Pro 项目 ✅2/5 legacy 2.x ❌
**Claude 会话**
承接刚做完 Std 链路;用户给了浏览器 HAR (`tmp/prodownload.har`174 请求,目标 = 立创·泰山派 RK3576 现代 Pro 项目) 验证 endpoint 形态,要求"找 5 个专业版项目,看能不能批量抓 EPRO2"。
### 5 个候选
`oshwhub /api/project?origin=pro&sort=hot` 取前 5详情 API 拿 license
| # | UUID | License | 项目 | 结果 |
|---|---|---|---|---|
| 1 | `b77840665e2e...` | GPL 3.0 | 【全网首发】X86电脑主板 | ✅ |
| 2 | `7360e73de5dd...` | GPL 3.0 | 立创·泰山派 RK3566 开发板 | ❌ legacy 2.x |
| 3 | `0c4675983733...` | GPL 3.0 | 立创·梁山派开发板 | ❌ legacy 2.x |
| 4 | `dc91a91e6693...` | CC BY-NC-SA 4.0 | 高颜值220V 300W 桌面电源 | ✅ |
| 5 | `ba64bd6f1c9c...` | GPL 3.0 | ESP-VoCat 喵伴 AI 萌宠 | ✅ |
License 全是限制性CC-NC-SA / GPL— Pro 用户群是立创团队/教育机构,默认上 NC-SA。和用户对齐本仓库为研究用、不再分发license 字段忠实落库;下游 Forge 投影时再用白名单过滤。
### 关键发现Pro 2.x ≠ Pro 3.x重要
立创开发板的旗舰板RK3566 / 梁山派)抓 `/api/v4/projects/<uuid>` 返回 **`branch_uuid: null`** + `editorVersion: "2.1.40"`
这是 **Pro 编辑器旧版2.x**:没有 git-style branch/history 模型,文档直接通过 `boards: [{sch, name, pcb}]` 字段定位。我们之前调研的全是 3.x泰山派 RK3576 / 无界 PLUS3.x 才有 `/branches/<B>/structures` + `/histories/<H>` 全套。
旧版的访问端点暂未挖通:`/api/v4/documents/<doc>` 404`/api/documents/<doc>` 401`/api/v4/projects/<P>/snapshots` 200 但响应体是 project meta 不是 doc。需要录一份"在 pro.lceda.cn 编辑器里打开 RK3566"的 HAR 才能继续。已记入 `docs/sources/easyeda_pro_source.md §1.1`
### EPRO2 解析坑:行末单 `|`(找了 2 轮才看到)
第一轮跑 5 个项目结果X86 board 7374 docs 抽出来只剩 **2 个**220V 电源和 ESP-VoCat 都是 **0 docs**
定位过程:
1. 直接 dump 一条 history 的 lines看到 DOCHEAD payload **行末有单个 `|`**,例如 `{"docType":"BOARD",...,"version":"..."}|`
2. 我的解析 `json.loads(ln.split(b"||")[1])` 拿到带尾随 `|` 的字符串 → `Extra data: line 1 column 127`
3. silently swallow exception → `cur_doc` 没设 → 全 chain 的 message 被丢弃。
4. 修复:解析前先 `ln.rstrip(b"|").split(b"||")`。已在 `docs/sources/easyeda_pro_source.md §3.1` 记录"行末单 `|` 是行终止符不是字段分隔符"。
### 修复后批量结果
3 个 modern Pro 项目完整解出来:
| 项目 | chain | docs | plain | blob | editor |
|---|---|---|---|---|---|
| ESP-VoCat | 12 | 278 | 7.5 MB | 1.1 MB | 3.2.91 |
| 220V 电源 | 28 | 771 | 26.3 MB | 7.4 MB | 3.2.69 |
| X86 主板 | 85 | 7374 | **481 MB** | 61 MB | 3.2.15 |
X86 主板5123 FOOTPRINT + 1243 DEVICE + 837 SYMBOL = 7203 个组件库 doc数据量惊人是个超复杂工程。
### docType 取值表(实测扩展)
之前 doc 只列了 `BOARD/PCB/SCHEMATIC/SHEET`。实测 EPRO2 流里 docType 实际取值更细:
- 用户级文档:`BOARD`(板物理边框)+ `PCB`(板内容)+ `SCH`(原理图)+ `SCH_PAGE`(子图)。一个完整 PCB 板 = 一对 `BOARD` + `PCB`,不是命名变化。
- 组件库 / 资源:`SYMBOL` / `FOOTPRINT` / `DEVICE` / `BLOB` / `FONT` / `CONFIG`。每个独特组件 / 字体 / 项目配置都是独立 doc。
- 抓 EPRO2 = 抓项目 + **完整的局部组件库快照**。下游做 EPRO2 → KiCad 转换时必须先把 lib doc 加载进 symbol cache。
已更新到 `docs/sources/easyeda_pro_source.md §3.4`
### 代码改动
- `crawlers/oshwhub/crawler.py`
- 新增 `make_pro_source_client()` —— 加载 `~/.secrets/pro-lceda-cookie-header.txt`,自动配 `Editor-Version` / `Referer` / per-request `path` header
- 新增 `fetch_pro_source(client, project_uuid, proj_dir, sleep)` —— 4 步流程project meta → branch HEAD → structures → history chain然后逐 history 解密AES-128-GCM, 16 字节 IV+ gunzip + 按 DOCHEAD 切 per-doc
- 新增 `_order_history_chain()` —— 沿 parent 链从 root 到 HEAD 排序
- 新增 `_pro_get_json()` —— 包装 `/api/v4` GET 调用,自动加 `path` header + 校验 success
- 扩展 `crawl_one()``pro_source_client` 参数,按 `list_item.origin` 自动 dispatch
- 新增 CLI flag `--with-pro-source` / `--backfill-pro-source` / `--pro-cookie` / `--origin`
- 新增 `_run_backfill_pro_source()`filter on `raw_fields.origin == "pro"`
- `schemas/project.schema.json``docType` 类型从 `integer` 放宽到 `["integer","string","null"]`(兼容 Std 的 1/3 + Pro 的 BOARD/PCB/SCH 等字符串),新增 `message_count` 字段
- `docs/sources/easyeda_pro_source.md` rev 3加 §1.1 Pro 2.x vs 3.x、§3.1 行末 `|` 警告、§3.4 docType 实测表、§2.2 单 history endpoint 即返完整 chain
### 数据落地
```
data/raw/oshwhub/
b77840665e2e48148c1b04ce84b5f7e7/ # X86 主板modern Pro 3.2.15
source/
manifest.json # 7374 docs index
structure.json # 项目树boards/schematics/sheets/pcbs
<doc_uuid>.epro2 # 7374 个 EPRO2 文件
dc91a91e669349898d709a5ba02f5b5f/ # 220V 电源modern Pro 3.2.69
ba64bd6f1c9c467ba3b674a54943557d/ # ESP-VoCatmodern Pro 3.2.91
7360e73de5dd428e9f29e10573f2d8ac/ # legacy Pro 2.x无 source/
0c46759837334318aa4882d6d37f96fa/ # legacy Pro 2.x无 source/
```
### 下一步
1. 重要legacy Pro 2.x 抓取链:录 HAR 看 RK3566 / 梁山派 在 pro.lceda.cn 编辑器打开时走什么 endpoint
2. 想跑量到 50 / 500 项目时,先做风控测试:阶梯放量,监控 403 / 429 / 1111111
3. EPRO2 → KiCad 转换器是 Forge 投影前置硬门槛
4. 可考虑 cookie 轮换 / 多账号 poolPro 风控相对 Std 严)
---
## 2026-04-28 19:50 Std 工程源链路打通 + 10 板子 schematic/PCB 全部回填
**Claude 会话**
承接计划:把已抓 10 个板子的"需登录才能下载的原理图 + PCB"补齐。
### 关键发现:根本不需要登录
10 个板子全是 `origin: "std"`EasyEDA 标准版)。原 `plan.md §1.6` 假设源数据要登录 — 实际**公开项目的 `dataStr` 匿名可访**。
调研路径4 轮探测,留痕在 `data/state/std_probe[1-5]/`
1. `/api/user``/api/projects` 401cookie 已过期,但和源抓取无关)
2. **`oshwhub.com/api/project/<uuid>` 浏览器 UA 匿名 200**,返回 `version_documents[]`(含 doc uuid + master + history chain
3. `modules.lceda.cn/histories/<hash>.json` 仍 403与 Pro 同结构,但 Std 不走这条路)
4. 翻 Std 编辑器 `/editor/6.5.51/js/main.min.js`5 MBgrep `/api/`,找到 76 个端点。关键的 `ajaxDetail = '/api/documents/{uuid}'`
5. 命中:**`https://lceda.cn/api/documents/<doc>?uuid=<doc>&path=<doc>`** 匿名 200body 是完整 EasyEDA JSON`dataStr.{head,canvas,shape,BBox,colors[layers,objects,DRCRULE,...]}`
**两种响应 shape**(按 docType 区分):
- docType=1 (Schematic):返"项目视图"`result.schematics[0].dataStr`
- docType=3 (PCB):返"文档视图"`result.dataStr` 直接在顶层
### 与 Pro 的差异
| 维度 | Std (本轮) | Pro (`docs/sources/easyeda_pro_source.md`) |
|---|---|---|
| 鉴权 | **无需** | `lceda_pro_session` 必须 |
| 加密 | **无** | AES-128-GCM + gzip |
| 源格式 | 扁平 EasyEDA JSON`shape[]` | EPRO2 消息流(事件溯源) |
| 多 doc | `version_documents[]` 逐个 GET | `/structures` + history chain 重放 |
### 实施
- `docs/sources/easyeda_std_source.md`:完整调研(含 dataStr 字段、抓取伪代码、附录重跑脚本)
- `crawlers/oshwhub/crawler.py`
- 新增 `make_source_client()` —— 浏览器 UA + Referer规避 oshwhub `/api/project/<uuid>` 端点对 `FacereDataset/0.1` UA 的 reject在 commit message 注明 UA 例外原因)
- 新增 `fetch_std_source()`:项目 → version_documents → 逐文档 dataStr → 落 `source/<doc>.json` + `source/manifest.json`
- 新增 `--with-source` 标志(爬新项目时一并抓源)和 `--backfill-source`(仅扫已有项目补源)
- QPS ≤ 0.2`SLEEP_SOURCE = 5.0s`
- `schemas/project.schema.json`:加 `source_format`/`source_path`/`source_documents`/`editor_version` 字段(前 3 个进 enum 锁定,后续新源好对齐)
### 跑批结果dev1QPS 0.2
10/10 全成功schema 验证 10/10 pass
| 项目 | docs | docTypes | 大小 | editor |
|---|---|---|---|---|
| ST-LINK V2-1 | 2 | [1,3] | 682 KB | 6.5.39 |
| USB 电压电流表 | **4** | **[3]** | 1.2 MB | 6.5.15 |
| 红外热成像 | 2 | [1,3] | 1.6 MB | 6.5.22 |
| t12-858d 焊台 | **11** | [1,3] | 6.1 MB | 6.5.15 |
| 加热台量产计划 | 6 | [1,3] | **12.0 MB** | 6.5.43 |
| ESP32-S3 智能手表 | 4 | [1,3] | 1.4 MB | 6.5.41 |
| RT300-MKV 可调电源 | 3 | [1,3] | 3.3 MB | 6.5.23 |
| YuzuMaix V831 | 4 | [1,3] | 5.4 MB | 6.5.37 |
| 盖革计数器 | 6 | [1,3] | 1.2 MB | 6.5.47 |
| ZVS 感应加热 | 3 | [1,3] | 990 KB | 6.5.40 |
**合计**45 个文档 / 33.2 MB中位 ~1.5 MB / 项目,附件主体约为附件主流量的 6%
观察:
- USB 电压电流表只有 PCB 文档4 个:主板 + 盖板 + 底板 + 面板,作者未上传原理图源)
- t12 焊台 11 个文档(拆得碎,估计含多个独立模块)
- editor 版本散布在 6.5.15 - 6.5.47(取决于作者上一次保存项目时的客户端版本)
### 落盘结构per project
```
data/raw/oshwhub/<uuid>/
├── metadata.json # ★ 新增字段source_format/source_path/source_documents/editor_version
├── description.md
├── cover.*
├── _urls.json
├── files/ # 用户附件已存在LFS
└── source/ # ★ 新EasyEDA Std 工程源
├── manifest.json # 文档清单 + 抓取时间 + upstream version_documents 留档
└── <doc_uuid>.json # 完整 dataStr 响应(普通 git文件 1-5 MB 量级)
```
`source/*.json` **走普通 git** 而不是 LFS10 项目共 33 MB 完全够用;放量时再考虑加 LFS 规则)。
### 安全 / 合规
- 无登录态、无凭据使用 → 无账号封禁风险,无 cookie 泄漏顾虑
- UA 例外:源抓取使用浏览器 UA 而非 `FacereDataset/0.1`,原因写入 `docs/sources/easyeda_std_source.md §3` 与本 commit message
- License与原项目附件相同的 license 字段已在 metadata下游 whitelist 过滤逻辑不变
### 未决 / 下一步
1. dev1 上的 `~/.secrets/lceda.json` cookie 已过期XSRF 4-22 失效,今天 4-28但**本任务已不依赖**它。是否保留待定 —— Pro 流程可能仍要
2. `easyeda2kicad.py` 转换:现有 45 个 dataStr 是关键测试样本,可立刻跑(`plan.md §1.7`
3. 放量决策:从 10 → 50 → 全量 12,493 时,按 33 MB / 10 ≈ 3.3 MB/proj 估算,全量源 ~40 GB不含附件本体
4. 多账号轮询、Pro 链路打通仍是 `plan.md §1.6` 的开放项(仅当遇到 Pro 项目时才用得上)
### 改动清单
- 新增:`docs/sources/easyeda_std_source.md``scripts/probe_std_api[1-5].py``data/raw/oshwhub/<uuid>/source/`10 项目)
- 修改:`crawlers/oshwhub/crawler.py``schemas/project.schema.json`、10 项目的 `metadata.json`
- 待人工:`projects.md` 重生成(脚本未跑);`plan.md §1.6` 状态从 ⏳ → ✅§1.7 unblocked
---
## 2026-04-24 00:25 打通 pro.lceda.cn 工程源完整链 + EPRO2 格式解析
**Claude 会话**
核心成果:**立创 EDA Pro 工程源的 API + 加密 + 格式三层全打通**。
### 完整链路
```
1. GET /api/v4/projects/<PROJ> → branch_uuid
2. GET /api/v4/projects/<PROJ>/branches/<BRANCH> → history_uuid
3. GET /api/v4/projects/<PROJ>/branches/<BRANCH>/histories/<HIST> → {key, iv, dataStrUrl}
4. GET <dataStrUrl> (modules.lceda.cn) → 417 KB 加密 blob
5. AES-128-GCM decrypt (tag=blob[-16:]) → 417 KB gzip
6. gunzip → 2.7 MB EPRO2 源流
```
关键 headers`Editor-Version: 3.2.127` / `path: <PROJ_UUID>` / `Referer: https://pro.lceda.cn/editor` /
`Cookie: lceda_pro_session=...`(与 u.lceda.cn 的 session 不共享)
### 加密细节
- 算法: AES-128-GCM`modules.lceda.cn/pro-mgr/.../project-worker.js``this.tool.decrypt({name:"AES-GCM",iv:this.iv,tagLength:128},...)` 反查确认)
- key / iv 都是 32 hex = 16 byte
- WebCrypto 约定ciphertext || 16-byte-authTag末尾附
- 解密后 gzip magic `1f 8b 08`gunzip 得最终源流
### EPRO2 格式
立创 EDA Pro 2 的**事件溯源**格式:消息流按 `\n` 分行,每行 `{"type":...,"ticket":N,"id":...}||{payload}||[extra]`
示例样本(`无界PLUS` BOARD 文档)**8 357 条消息****40 种 type**
- PART / COMPONENT / ATTR / PIN零件与属性
- PAD / VIA / WIRE / NET / PAD_NETPCB 电气)
- LINE / POLY / RECT / ARC / CIRCLE / ELLIPSE / TEXT几何
- LAYER (1572) / LAYER_PHYS / ACTIVE_LAYER层堆叠
- FILL / POUR / POURED铺铜
- RULE / RULE_SELECTOR / RULE_TEMPLATE设计规则
### 与 Std 版对比
| | u.lceda.cn (Std) | pro.lceda.cn (Pro) |
|---|---|---|
| Cookie | `lceda_session` | `lceda_pro_session` |
| 源 API | 单一 `/api/projects/<uuid>` | 4 步 `/api/v4/...` 链 |
| 版本控制 | 无 | branches + histories |
| 加密 | 待验证 | AES-128-GCM |
| 源格式 | EasyEDA JSON扁平 | EPRO2 消息流 |
| 工具 | `easyeda2kicad.py` 第三方 | **无**现成 KiCad 转换器 |
### 落地
- 新建 `docs/sources/easyeda_pro_source.md`(完整调研,见该文档附录 A 一键重跑)
- `pyproject.toml``pycryptodome>=3.23.0`
- 清理dev1 上 `/tmp/source.blob``/tmp/source.json`(后者含 Charles 私人工程源 2.7 MB
### 待验证 / 下一步
1. **他人公开 Pro 工程**能否同样 4 步通 —— 需 HAR
2. **SCHEMATIC docType** 的 API 入口(本次只解出 BOARD
3. **多 document 枚举**project → documents 列表端点)
4. Pro 编辑器"**导出 KiCad**"功能的 API 端点(若存在,能省自写转换器的工作)
5. 对齐 `OSHWHUB_INGEST_SPEC.md`Forge 消费侧要求 `.kicad_sch` + 更严 license whitelist
### ⚠️ 安全
Charles 在聊天里粘过两次 cookieu.lceda.cn 一次 + pro.lceda.cn 一次),已写入 dev1 `~/.secrets/`
当前会话 transcript 含明文 —— 本轮验证完 Charles 应登出再重登一次,让测试期间暴露过的 session invalidate。
---
## 2026-04-23 20:10 策略大调:登录内容入场 + 云服务器 + EDA→KiCad 转换
**Claude 会话**
四项变更落实到文档(暂不写代码,等云服务器到位):
### 1. 登录态内容纳入范围
原则(`CLAUDE.md`
- 合法账号登录后抓,**禁止**盗号 / 共享号
- 凭据集中云服务器 `~/.secrets/` (mode 700)**不入** git / 日志 / metadata
- 仍不绕付费墙、不破 DRM、不抓站点明确禁抓的内容
- 换号 / 重登事件记 `docs/secrets.md`(只事件、不含值)
### 2. 云服务器(广州,待交付)
新增 plan.md §0.5 基础设施段:
- 0.5.1 机器初始化git / git-lfs / uv / python 3.11+,非 root SSH`~/.secrets/`
- 0.5.2 调度tmux/nohup 长跑 + systemd timer 增量
- 0.5.3 登录态获取cookie 导出流程
### 3. 存储分级演进
plan.md §1.4 改写:
- 前期 < 50 GB云服务器磁盘 + Gitea LFS
- 中期 50200 GB评估 Gitea 容量压力;扩容 or 分仓
- 后期 > 200 GB迁对象存储OSS / COS / MinIOGitea 只存元数据 + 指针
- 50 GB 是决策评估点,**不**过早迁移
### 4. 立创 EDA → KiCad 转换
新增 plan.md §1.6(登录态工程源抓取)+ §1.7EDA→KiCad 转换):
- §1.6 用登录账号抓 `u.lceda.cn/api/project/<uuid>` 工程源 JSON`source.json`
- §1.7 写 `scripts/convert_to_kicad.py`,候选工具 `easyeda2kicad.py`pypi活跃维护
- 批处理扫 `data/raw/oshwhub/` → 输出 `data/processed/oshwhub/<uuid>/kicad/`
-`kicad-cli sch erc / pcb drc` 做语法校验,失败样本记 `data/state/convert_failed.jsonl`
- 目的:打通 oshwhub (EasyEDA) 与 bshada/open-schematics (KiCad) 两个生态的训练语料
### 同步改动
- `docs/sources/oshwhub.md` §3.5 从"未开放"改为"需登录,纳入范围"R4 风险更新
- `README.md` 数据源表加「登录态」列,加运行环境说明
### 等待
- 广州云服务器到位 → 启动 Phase 0.5
- 账号登录凭据由 Charles 提供
---
## 2026-04-23 19:55 oshwhub.md 重写成完整调研文档
**Claude 会话**
Charles 要求把 12 493 总数验证、90 项目采样结果合进主调研文档。
`docs/sources/oshwhub.md` 重写为 9 节 + 2 附录的完整调研:
1. 一页纸 TL;DR 表
2. 站点架构
3. robots.txt 与合规
4. API 与抓取入口(列表 / SSR 详情 / 附件 CDN / 排除项 / 未开放端点)
5. **项目总数验证(新)**:三路 sort 一致 + 分页二分搜索250 × 50 = 12 500 吻合)+ grade 覆盖抽样
6. **抽样语料特征(从 oshwhub_corpus_estimate.md 并入)**:体积 / 文件类型 / license 分布
7. Schema 映射
8. 速率与礼貌
9. 目录输出约定
10. 风险与未解决7 条)
11. 附录:重跑命令、变更历史
删除重复文件 `oshwhub_corpus_estimate.md`(内容已并入 §5
---
## 2026-04-23 19:50 加入 HF bshada/open-schematics 计划
**Claude 会话**
Charles 点名把 https://huggingface.co/datasets/bshada/open-schematics 纳入第一批。
调研结论:
- 这是**已预处理**的 HF 数据集,非待爬网站
- 78 parquet shards **6.4 GB**CC-BY-4.0(商用友好)
- 10K+ 条记录,每条含 `.kicad_sch` 源 / PNG / 组件列表 / JSON / YAML / name / description
- 与 oshwhub (EasyEDA) 互补,补 KiCad 生态
决定:
- **整包镜像**到 `data/external/huggingface/bshada--open-schematics/`**不**拆 per-project 目录
-`huggingface-cli download ... --repo-type dataset`parquet 走 LFS
- 维护单独的 `datasets.md`,不与 per-project 的 `projects.md`
改动:
- 新增 `docs/sources/hf_bshada_open_schematics.md` 完整调研
- `plan.md` 加 Phase 1.5
- `README.md` 数据源表加一行
**未下载**,等拍板 6.4 GB LFS 预算。
---
## 2026-04-23 19:30 Phase 1 MVP10 个高质量 oshwhub 项目入库
**Claude 会话**:承接仓库初始化
### API 调研结论
- 列表 API`GET https://oshwhub.com/api/project?page=N&pageSize=M&sort=hot`,无鉴权,返回 12493 个项目元数据(含 grade / likes / stars / views / forks
- 详情:`GET https://oshwhub.com/<path>` 是 SSR HTML嵌入 escaped JSON`license` + `attachments[]`(每个带 name / src / size / md5 / ext / mime / download_count
- 附件 CDN`https://image.lceda.cn{src}` — 已验证无鉴权直接下载
- EasyEDA 工程源 JSON`u.lceda.cn`需登录v0.1 不抓
- 详细调研见 `docs/sources/oshwhub.md`
### 选 10 个高质量项目
判据:`grade == 4`(平台精品徽章) + `likes ≥ 100` + 应用领域多样(避免同类堆叠)+ 排除 `_copy` 派生仓。
10 个项目覆盖调试器、加热台、盖革计数器、可调电源、焊台、智能手表、USB 测电流、ZVS 感应加热、AI 开发板、红外热成像。
### MVP 爬虫
位置:`crawlers/oshwhub/crawler.py`
- `list_projects()` — 列表 API 分页
- `pick_top()` — 按 like×3 + star + fork×2 + views/100 + comments×2 + grade×50 排序
- `parse_detail_html()` — 从 SSR HTML 提取 title / license / description / attachments
- `crawl_one()` — 每项目产出:`metadata.json` / `description.md` / `cover.*` / `files/*` / `_urls.json`
- QPS ≤ 0.5`SLEEP_BETWEEN = 2.0`UA 显式声明 `FacereDataset/0.1`
### 抓取与入库
- 10/10 成功52 个附件,**524 MB**
- Gitea LFSv25.4.3 原生支持)+ 本地 `git-lfs 3.5.1`(用户态二进制装在 `~/.local/bin/`
- `.gitattributes` 规则:`data/raw/**/files/**` 一律走 LFS元数据metadata.json / description.md / \_urls.json / cover.\*)走普通 git
- 每项目目录结构:
```
data/raw/oshwhub/<uuid>/
├── metadata.json # 按 schemas/project.schema.json
├── description.md
├── cover.{jpg,png,jpeg}
├── _urls.json # 所有原始 URL 清单
└── files/* # 原始附件LFS
```
### 改动汇总
- 新增:`crawlers/oshwhub/{__init__,__main__,crawler}.py`、`schemas/project.schema.json`、`docs/sources/oshwhub.md`、`pyproject.toml`
- 修改:`.gitattributes`(缩窄到 `data/raw/**/files/**`)、`.gitignore`(移除 `data/raw/*` 排除)
### 下一步建议
1. 验收 10 个项目元数据质量(随机抽 2-3 条对照原站)
2. 决定 Phase 1.4 放量目标50500全量 12493
3. Phase 2 准备GitHub KiCad repo 调研
---
## 2026-04-23 19:40 fs-web-stream 排查 + schema 自动校验
**Claude 会话**(自主推进)
### fs-web-stream.jlc.com 定性
重新抓 `/CYIIOT/ST_LINK-V2_1` 并看 13 个 `fs-web-stream.jlc.com` 链接的上下文:全部是嘉立创服务侧栏/推广图标3D 打印、发热片、Ican、EDA 扩展广场、开源硬件平台 badge 等),**与项目本身无关**。`image.lceda.cn/attachments/` 就是项目附件的唯一入口,已确认闭环。`docs/sources/oshwhub.md` 对应章节已更新。
### scripts/validate.py
jsonschema 做两层校验:
- 默认:所有 `data/raw/**/metadata.json` 对 `schemas/project.schema.json` 的结构校验
- `--check-files`:另外验证每条 file 的本地 path 存在且 sha256 匹配
**结果**10/10 项目两项全通过。
### 新增
- `scripts/validate.py`
- `pyproject.toml` 加 `jsonschema>=4.26`
### 待决策
- 放量规模 —— 已提供实测数据:**median ≈ 110 GBp90 上界 ≈ 660 GB建议预算 150180 GB**(见 `docs/sources/oshwhub_corpus_estimate.md`
- 是否需要抓 `u.lceda.cn` 的 EasyEDA 源 JSON需登录v0.1 跳过)
---
## 2026-04-23 19:45 全量规模实测 + License 分布
**Claude 会话**(自主推进)
写 `scripts/estimate_size.py`,只抓详情 HTML 解析 `attachments[].size`,不下载;采样 90 个 hot 项目3 页 × 30
**关键发现**
- 单项目 median 9 MB / mean 22 MB / p90 54 MB / max 204 MB12493 全量 median 估算 **110 GB**p90 上界 660 GB
- **视频 (.mp4 + .qt) 占 54% 存储**!如果训练只要 PCB/原理图/BOM加 `--skip-ext mp4,qt` 存储直接砍半
- License 分布健康GPL 3.0 占 49%Public Domain 21%CC 系列 ~20%CERN/TAPR OHL 6%;样本内无闭源
- **NC (Non-Commercial) 占 ~11%**,商用场景必须过滤
结果固化到 `docs/sources/oshwhub_corpus_estimate.md`,可随时重跑验证。
### 建议
1. 存储预算定 **180 GB**median + 15% buffer
2. Phase 1.4 前给 crawler 加 `--skip-ext` 开关滤视频
3. 下游建立 license whitelist 过滤 NC / 未知
---
## 2026-04-23 18:50 仓库初始化 & 数据源调研
**Claude 会话**:初始化
完成:
- 从 `git.deepknow.site/Facere/FacereDataset` 克隆空仓到 `~/repo/FacereDataset`
- 调研立创开源平台oshwhub.com初步数据
- `robots.txt` 仅 Disallow `/posts`,其他路径允许
- 存在 `sitemap.xml`(首页 + explore + activities + market 等入口已列出)
- 项目详情页路径为 `/detail/<uuid>`(示例 `f0652fd2ae3e40b8a0ecc8dc773e3512`
- 图片 CDN`image.lceda.cn/oshwhub/pullImage/...`
- 文件下载:`fs-web-stream.jlc.com/fs-web-stream/file-operation/download/<snowflake-id>`
- 页面是 Next.js SPA首屏 HTML 800KB但数据加载具体 API 入口需要浏览器 trace留给 Phase 1.1
- 创建项目骨架:
- `README.md` — 项目简介与数据源表
- `CLAUDE.md` — 项目级 Claude 指令爬虫规约、合规红线、schema 要求)
- `plan.md` — 6 阶段建设计划Phase 0 骨架 → Phase 5 数据清洗 → Phase 6 持续运营)
- `log.md` — 本文件
- `.gitignore` — 排除 `data/raw` `data/processed` `data/state` Python 缓存等
- 目录骨架 `crawlers/ schemas/ scripts/ data/{raw,processed,state} docs/{sources,}`
- 每个空目录放 `.gitkeep`
- 首次提交 & 推送到 `origin main`
**下一步建议**
1. 拍板存储方案(本地盘 / Gitea LFS / 外部 OSS—— 影响 Phase 1.4 放量时机
2. 目标规模1 万 / 10 万 / 全量)
3. 决定是否保留二进制附件或只存 URL
4. 完成上述 3 项后启动 Phase 1.1(用 `chrome-devtools` MCP 录 oshwhub 的 network trace 定位真实 API