Files
FacereDataset/log.md
Knowit 6aa72faf84 docs: std corpus 2026-05 snapshot + batch-1000/4000/remaining log
Snapshot of full oshwhub std corpus delivery:
- 12,493 projects total, 12,166 (97.4%) with editor source
- 4 sweep batches + 1 early-mixed = 5 zip artifacts in COS GZ + SG buckets
- 30-day SG-region presigned URLs for downstream pickup

log.md tracks the multi-batch sweep including driver bug postmortem
(bash heredoc python3 missed httpx → 26-min run wasted on empty zips,
recovered by switching to uv run).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 10:56:09 +08:00

1397 lines
77 KiB
Markdown
Raw Permalink 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-05-03 07:17 batch-remaining-std扫完所有未抓 std 项目7,381双桶副本
**Claude 会话**
收尾批:把 unfetched std 池子7,381 项)一次扫完。拆 A/B 两批 ~3690 each按 rank 50/50 切A 头部、B 长尾),**取消单作者上限 2**(这是 sweep 全量,不需要再做多样性约束)。两批走完整 1-4 步 + zip + 双桶推送。
### 候选筛选
- Atop 3691 by rankgrade 0-4 都有likes p50=8 / p90=29 / max=275
- Bbottom 3690几乎全 grade 0/1likes p50=1 / p90=4 / max=21
- 候选 jsonl`data/state/oshwhub_remaining_{a,b}_std_candidates.jsonl`
### 第一轮 driver bug 全军覆没(必读)
**症状**driver 跑了 26 min "顺利完成",但所有 7,381 项 source 都 0——`source_documents=[]` 全空。两个 zip 才 7 MB / 6.8 MB只 metadata + description没 source
**根因**driver 里 Step 4 用 `python3 - <<PYEOF` 而不是 `uv run python -u -`
- Step 1`uv run -m crawlers.oshwhub`)走的是项目 venvhttpx 在
- Step 4 用 system python3**`crawlers.oshwhub.crawler` 第一行 `import httpx` 直接 ImportError**
- `set -uo pipefail` 没设 `-e`,每个 Step 的 stderr 写到日志后继续走,外面看 zip 也成功 / COS 也成功
- driver 不会 fail但源工程 0 拿到
**教训**bash heredoc + 多 venv 项目里,**所有依赖项目代码的 Python 调用必须 `uv run`**,仅 stdlib + 系统 pip 包(`qcloud_cos``zipfile`)的可以 system python3。下次写 driver 检查时把每个 PYEOF 块对应的 import 列出来对照。
### Recovery driver
重写 `/tmp/recover_driver.sh`:所有 Python 调用改成 `cat > /tmp/step_X.py <<PYEOF ... && uv run python -u /tmp/step_X.py`(避免 heredoc-stdin 路径文件式更稳。Step 1/Step 2 已经成功就跳过,只做 Step 4 + retry + zip + COS。
| 阶段 | 时间 |
|---|---:|
| RECOVERY START | 06:18:44 |
| batch_remaining_a 收工 | 06:48:5530 min |
| batch_remaining_b 收工 | 07:17:1128 min |
| **整 recovery 走时** | **58 min** |
### 完成度
| batch | meta | with_source | attach_only | docs | src bytes |
|---|---:|---:|---:|---:|---:|
| remaining_a | 3,691 | 3,641 (98.6%) | 50 | 8,877 | 3,317 MB |
| remaining_b | 3,690 | 3,570 (96.7%) | 120 | 8,231 | 2,783 MB |
attach_only 比例A 1.4% / B 3.3%)比 batch-1000 (3.7%)、batch-4000 (0.05%) 没明显异常D/E tier 项目被废弃 / upstream 删的比例本来就偏高,正常。
### 双桶最终状态
两个桶现在内容完全一致:
| 对象 | 大小 |
|---|---:|
| `batch1000_std.zip` | 471 MB |
| `batch4000_std.zip` | 1.38 GB |
| `batch_remaining_a.zip` | 1.06 GB |
| `batch_remaining_b.zip` | 891 MB |
| **每桶合计** | **~3.78 GB** |
走 GZ→SG 服务端 cross-region copy 链路,完全不碰 dev1↔SG 公网丢包链路。100 GB 套餐占 ~7.6 GB双桶
### 整体 corpus 落幕
- listing 里 origin=std 总数:**12,493**
- corpus 里 std 项目12,4935,112 旧 + 7,381 本批)—— **100% 覆盖**
- corpus 总目录数(含 Pro / 早期混抓12,523
- dev1 占盘13 GB40 GB 总,余 13 GB / 33% free
- license 主流:约 60% GPL 3.0 + 18% Public Domain + 5% MIT + 5% NC variants混合 batch-1-4 趋势一致)
### 决策Why
- **不设作者上限**sweep 全量批次,多样性已在 batch-1000 / batch-4000 阶段保证;这里要的是 "全部",不能掉作者。
- **batch-A vs batch-B 按 rank 切而不是随机**:万一空间不够,优先保 A更高质量实际两批都顺利完成但万一中断 A 是先做的更稳。
- **不删 GZ 桶副本**:用户有 100 GB 套餐,~3.78 GB 双桶舒适;多副本对未来跨区拉取友好。
### 下一步建议
- crawler 加 `--backfill-uuids-file <path>` 选项替代 `--uuids` 字符串,下次大批量不用绕路写 driver 内嵌 Python参考 `/tmp/backfill_4000.py` 模式可以直接搬进 crawler
- driver 模板加 sanity check跑完每个 batch 用 `du -sh /tmp/${SHORT}.zip` 与 raw bytes 比,如果压缩比 < 5% 报警(这次的 7 MB zip 用这个能立即抓到)
- 全 std corpus 已落地,下次扩量目标应该是 Pro 项目93 项飞控 Pro 候选还没动)或扩到 oshwhub 之外站点
---
## 2026-05-03 03:58 batch-4000-stdStep 1-4 + zip + COS 链路落地 SG box
**Claude 会话**
接 batch-1000-std。再扩 4000 项 std → corpus 142 → 1142 → 5142。本批走完整 1-4 步zip 后通过 COS 跨区链路拉到 SG box 本地(绕开 dev1↔SG 公网丢包链路)。
### 候选筛选
- 数据源同上
- A 档grade≥3 & like≥10剩 396 项不够 4000下沉到 unfetched 全池 11,381 按 rank score 倒排,单作者 ≤ 2取前 4000
- 候选 jsonl 落 dev1 `data/state/oshwhub_batch4000_std_candidates.jsonl`
- 质量分布grade 4: 16 / 3: 448 / 2: 1798 / 1: 1319 / 0: 419likes p50=10 / p90=42A 档第三梯队 + B + C 头部)
### 抓取dev1concurrency=5
- **Step 1** 详情扫 license~24 min3989/4000 OK + 11 fail全 "Server disconnected"),重抓 concurrency=2 全过 → 4000/4000 metadata
- **Step 2** license 盘点56% GPL 3.0、20% PD、4.8% MIT、4.8% NC-SA、4.3% unknownunknown 比例比 batch-1000 高B/C tier 项目 license 标注更随便)
- **Step 4** std-source backfill~31 min3983 OK + 17 fail
- 15 项 "Server disconnected" 瞬态,重抓全过
- 2 项 upstream 真实问题1× `404 文档未找到`doc 被删1× `code 104001`(项目封)。这 2 项保留 metadata-only
- 最终4000/4000 metadata · **3998 含完整 sch/pcb 源工程** · 2 metadata-only
### 关键修:`--uuids` 撞 ARG_MAX
- backfill 路径用 `--uuids "$(jq ... | paste -sd,)"`4000 UUID × 33B ≈ 132 KB > ARG_MAX (128 KiB)
- 现象:`bash: /usr/bin/nohup: Argument list too long`,进程没启动,但 pgrep 误匹配 stale shell
- 修:临时脚本 `/tmp/backfill_4000.py`,直接 import `crawlers.oshwhub.crawler` 内部函数(`_run_backfill_concurrent` / `fetch_std_source`UUID 集从 candidates jsonl 读,绕开命令行
- 长期crawler 应加 `--backfill-uuids-file <path>` 选项,下次扩量再改
### ZIP 打包dev1
- 4000 dirs26,098 文件4,489 MB raw → **1,445 MB zip**,压缩比 32.2%99 秒完成
- 用 Python `zipfile` + `compresslevel=1`dev1 没装 `zip` 二进制)
### 传输链路COS 三段0 字节走公网丢包链路)
| 段 | 时长 | 速度 | 路径 |
|---|---:|---:|---|
| dev1 → `facere-gz-1321068335` (ap-guangzhou) | 17s | 166 MB/s | 同区内网 |
| GZ 桶 → `facere-1321068335` (ap-singapore) | 23s | — | COS 服务端 copyVM 不参与) |
| SG 桶 → SG box | 8.5s | 201 MB/s | 同区内网 |
| **整 1.4 GB 跨地域 ~50 秒** | | | |
哈希校验穿三段:`a79a87e4a3f5dfbad80d9ba94f557b09010e104f6e0c968ea87eba2267b262b3`
### 决策Why
- **rank-score top-4000 不设硬阈值**硬过滤太挑会漏rank score 已综合 like\*3 + grade\*50 + views/100 + comments\*2 + fork\*2 + star自动平衡。最低 like=0 也进了几百项(多数是 grade≥1 但社区互动少的),可接受。
- **不传统 scp走 COS 三段**:之前飞控-77 33 MB scp 走 dev1↔SG 6.5%-loss 链路要 3 min这次 1.4 GB 走 COS 50 s 完事,提速 ~150×。COS 跨区复制流量计费 ~¥0.5/GB × 1.4 GB ≈ ¥0.7,零头。
- **zip vs tar.gz**zip 选 level=1速度 vs 体积平衡tar.gz 单线程 deflate 跟 zip-l1 体积相近但慢 2-3×
### 完成度
- corpus 由 1142 项扩到 **5142 项**+4000
- License: 56% GPL 3.0 主流不变unknown 比例从 0.4% (batch-1000) 涨到 4.3% (batch-4000),与 B/C tier 项目低标注度一致
- editor 版本从 6.3 全谱系到 6.5.42 都覆盖到了
- 源文件体积dev1 上 `data/raw/oshwhub/` 占 ~10 GB含全部历史 + 本批)
### 下一步建议
- corpus 已经达 5142 项,足够下游 EPRO2/Std → KiCad 训练数据规模
- 真要继续扩A+B+C 档已基本吃完头部,下沉 D 档grade=0 或 like=0质量回报递减可暂缓
- crawler 加 `--backfill-uuids-file` 选项,避免下次 ARG_MAX 撞墙
- COS GZ 桶里的 `batch1000_std.zip` + `batch4000_std.zip` 用完可以删SG 桶副本足够),节省 ~¥0.2/月 存储费
---
## 2026-05-03 batch-1000-stdStep 1-41000 块标准板源工程入库dev1
**Claude 会话**
走 batch-200 脚手架抓 1000 项 std A 档剩量。**用户指令"只走 1-4 步"**:不抓附件、不传 SG、不 push gitea数据留 dev1。
### 候选筛选
- 数据源:`data/state/oshwhub_listing_full.jsonl`33,695 项)
- 过滤:`origin=std AND grade≥3 AND like≥10`A 档),减去已抓 142 项 → 池子 1,396 项
- 排序rank score 倒序;单作者 ≤ 2 → 取前 1000791 唯一作者)
- 候选 jsonl 落 dev1 `data/state/oshwhub_batch1000_std_candidates.jsonl`(不入 git可重算
- like p50=43 / p90=170 / max=420A 档第三梯队,吃掉 1,396 池子的 ~72%
### 抓取dev1 Guangzhouconcurrency=5
- **Step 1** 详情扫 license~80s996/1000 OK + 4 "Server disconnected" 瞬态 fail
- 4 项重抓 (concurrency=2) 全 OK → 1000/1000 metadata
- **Step 2** license 盘点57% GPL 3.0、19% PD、8.6% CC-BY-NC-SA、5.4% MIT、其它 < 2%。形态与 batch-50 / batch-200 一致。
- **Step 3** SKIP本批 std-only没有 Pro 候选
- **Step 4** std-source backfill~6 分钟1000/1000 OK0 fail
### 完成度
- 1000/1000 metadata
- 963 项有完整 std 源工程2,853 个 sch+pcb doc平均 2.96 docs/proj
- 37 项 upstream attachments-only`source_documents=[]` 真实状态,跟飞控-77 4/77 同形态)
- 源文件体积1.47 GB on dev1按 batch-50 估算 12 MB/项 偏高,实测 1.5 MB/项——A 档第三梯队项目体量比头部小)
- editor 版本194 项 6.4.25 / 91 项 6.5.5 / 77 项 6.5.16.4-6.5 全谱系覆盖
- corpus 由 142 项扩到 1,142 项(+1000
### 决策Why
- **不传 SG / 不 push gitea**:用户指明只走 1-4 步。数据 1.47 GB 走 dev1↔SG 6.5%-loss link 估 ~3 hr 单 scp没必要现在花。要传时再走 COS 跨区或 split-parallel-scp。
- **concurrency=5 全程**:飞控-77 验证过 std doc endpoint 这个并发安全;实测 Step 1 ~12s/100项、Step 4 ~36s/100项零限流告警。
- **Step 3 跳过**:候选池纯 std-originPro backfill 没数据可处理。
### 下一步建议
- 真要消费这批数据:(a) 在 dev1 直接 push giteaSSH transport~10 min @ 1.5 GB或 (b) 走 COS 跨区同步到 SG。
- 该批的 metadata-only 部分37 项 attachments-only若想补 sch/pcb需要回头单独扫 attachment ZIP 看里面是否 bundled 了 EasyEDA 工程,那得改 crawler。
- A 档剩余只有 396 项了;下次再扩可以下沉到 B 档grade≥2 & like≥53,884 项 unfetched
---
## 2026-04-30 19:10 飞控-77主题定向抓 77 块标准飞控板
**Claude 会话**
走完整 pipeline本地索引筛 → dev1 抓 → tar+scp 回 SG → push gitea。
### 候选筛选
- 数据源:`data/state/oshwhub_listing_full.jsonl`33,695 项)
- 过滤:`origin=std AND ('飞控' in name OR '飞控' in introduction)` → 79 hits
- 减去已抓的 2 项 → 77 个新候选
- 工具:临时脚本,候选 jsonl 落 dev1 `data/state/oshwhub_feikong_candidates.jsonl`(不入 git可重算
### 抓取dev1 Guangzhouconcurrency=5
- Step 1 详情扫 license: ~12s, 74/77 OK + 3 fail
- 3 fail 都是同一个 buglisting entry 的 `count` dict 缺 `like` 字段crawler 直接 `count["like"]` 抛 KeyError
- 修:`rank_score` / `pick_top` / metadata builder 全改 `count.get("like", 0)` 形式commit `29530e0`
- 重抓 3 项 → 全 OK
- Step 4 std-source backfill: ~80s, 73/77 拉到源工程文档4 项 upstream 就是 attachments-only没编辑器 session`source_documents=[]` 是真实状态)
### 传输tar+scp 而非 dev1 push gitea
- dev1 → SG 同样吃 6.5% 丢包 link单 TCP cwnd 压扁
- 33 MB tarball 走 scp ~3 min与之前 dev1 push gitea 同量级)
- 落 SG 后从 SG 直推 gitea同区低延迟秒级完成
- rebasedev1 端有人手动推了 74-项 commit (`c199840`),本地 77-项 superset rebase 上去conflicts 仅 projects.mdregen 一遍即解)
### 完成度
- 79/79 飞控 std 项目都有 metadata
- 73 项有完整 std 源工程
- 4 项是真实 attachments-onlyupstream API 返空)
- License 分布65% GPL 3.011% PD11% MIT~6% CC variants与 batch-50 同形态)
- corpus 由 65 项扩到 142 项(+77
### 下一步建议
- 跨区传输优化tencent-cloud COS 同 cloud 跨区复制走骨干网,比 scp 快几倍;下次大批量再装。或者 split + 并行 scp 也能拉 3-5x。
- 清理 stash 里那两份 .decrypted.txtpre-existing 调试残留)
- 可以再试一波 Pro 飞控93 hitsorigin=pro
---
## 2026-04-29 04:30 std/ writer 翻 Option 2raw objects dump + mapping doc
**Claude 会话**
`fe6971f`。下游同学回了具体规格:选 Option 2objects dict 直接 dump不要我们做 tilde 串映射,他自己写 ~100 LoC adapter 翻。把之前 Option 3full Std `shape: ["TRACK~...", ...]`)那套删了,重写。
### 下游确认的规格
1. **`shape[]` 不要保留**——adapter 从 `objects` 重建,空占位反而误导
2. **全 mil**——不转 mmBBox 也 milStd `canvas` 那一行 `~mil~` 已经写了)
3. **`head` 必带**
- `editorVersion``facere-epro2/0.1 (epro2 <doc.head.editVersion>)`Wokwi 据此选解析分支
- `docType``"3"` (PCB) / `"1"` (SCH)
- `units``"mil"`
4. **`layers[]`** 沿用 Std 53-layer 字符串数组格式(`"1~TopLayer~#FF0000~..."`inner SIGNAL 有用到才追加
5. **保留空 stub** `preference / netColors / DRCRULE`——失败时 grep 路径稳定
6. **per-doc 一个 .json**,平铺,不合并
### 写完三件
#### `tools/epro2/std/pcb_writer.py` 重写
40 行核心逻辑(之前 Option 3 是 500 行):
- `_gather_bbox_points`:扫所有 op 的 `x/y/startX/...` 等坐标字段min/max
- `_used_inner_signal_layers`:找真正有 primitive 的 SIGNAL 内层 id
- envelope 直接 dump `dict(doc.objects)`
#### `tools/epro2/std/sch_writer.py` 重写
更短——schematic 没 copper layerlayers=[]。其它结构跟 PCB 一样。
#### `docs/sources/epro2_to_std_mapping.md` 新增
**这是给下游 adapter 的关键文档**——他写 100 LoC adapter 时按这个表查。内容:
- EPRO2 layer id → Std layer id 重映射表最坑5↔7 mask/paste 反着、11→10 outline、12→11 multi、SIGNAL 15+→Std 21+
- PCB OPTYPE → Std verb 全表TRACK / VIA / COPPERAREA / SOLIDREGION / CIRCLE / LIB+#@$PAD+TEXT
- SCH OPTYPE → Std verb 全表W / N / T / LIB+#@$P—— 标了 best-effort没 Std SCH 实样
- COMPONENT placement 旋转 + 平移公式footprint-local → PCB-absolute
- 5-Voltage 占位符 `pid8a0e77bacb214e` 的 Global Net Name → 额外 N flag 的 trick
mapping doc 直接从 `fe6971f` 那个 Option 3 writer 提炼出来——他不用读我们代码,照表填就行。
### 实测
ESP-VoCat 6 PCB + 9 SCH = 15 JSON
- 一个典型 PCB: `objects=1719, layers=17, BBox=(-2293, -900, 2997×1899)`
- `head.units = "mil"``head.editorVersion = "facere-epro2/0.1 (epro2 3.2.91)"``head.docType = "3"`
- `shape` 字段已确认**不存在**
- `objects` 原 payload 1:1 保留(含 LAYER ops 的 activeColor 等所有字段)
### 决策Why
- **不留空 `shape: []`**——下游说"误导 adapter"。明确不存在,比空数组更诚实
- **`head.editorVersion` 加前缀 `facere-epro2/0.1`**——区分我们的输出 vs lceda 真实 Stdadapter 看到这个能猜出是 EPRO2 转过来的
- **保留 `preference/netColors/DRCRULE` 空 stub**——下游说失败 grep 排查方便
- **mapping doc 单独成文不混在 README**——adapter 作者一个文件就够,不用读源码
### 测试
82 → 84 单测全过:原 Option 3 的 11 个测试改成验 Option 2envelope 必带 key / 无 shape / head units&version / objects 1:1 / BBox min-max / 内层 SIGNAL append / docType reject
### Push
`fe6971f` 的 Option 3 已被这次 commit 覆盖 / 简化掉。下游回来如果说 mapping table 有错位再修——但他自己拿表填 adapter跑通后给反馈我们再迭代。
---
## 2026-04-29 04:00 Std-format JSON 转换器EPRO2 → 下游同学 Wokwi pipeline 的输入格式
**Claude 会话**
KiCad 那条路下游同学不需要——他们的 Wokwi pipeline 吃 oshwhub Std (lceda) 的 JSON dict-format。EPRO2 解密我们已经搞定per-doc 流就在 `data/raw/<uuid>/source/`),现在缺的是把 EPRO2 op-stream 翻成 Std 的 `dataStr.shape` 字符串数组。
### 新增 `tools/epro2/std/`(跟 `kicad/` 平级,旧的不动)
参照 `data/raw/oshwhub/3e2f893d.../25931ddab8.json` 一个 Std PCB 实样反推协议:
- 信封:`{success, code, result: {uuid, puuid, title, docType, components, dataStr: {head, canvas, shape, layers, ...}}}`
- shape 字符串:`VERB~field1~field2~...``~` 分隔
- LIBfootprint placement下面挂 PAD/TEXT 用 `#@$` 分隔器嵌套
#### 已实现 verb 映射
**PCBdocType=3高保真对照实样**
| EPRO2 op | Std verb | 备注 |
|---|---|---|
| LINE | TRACK | layer 单独映射 |
| VIA | VIA | 字段顺序 `x~y~outerD~net~innerD~uuid~lock` |
| POUR | COPPERAREA | path 转成 SVG `M..L..Z` |
| FILL | SOLIDREGION | 同 SVG path |
| POLY (CIRCLE) | CIRCLE | |
| COMPONENT + FOOTPRINT.PADs | LIB...#@$PAD...#@$PAD... | 内层 PAD 坐标做了 placement rotate + translate |
**SCHdocType=1best-effort无实样**
- LINE → Wwire 段)
- LINE.lineGroup → WIRE.NET → 在端点放一个 Nnet flag
- COMPONENT → LIB...#@$P...(嵌 PIN/TEXT包括我们之前发现的 5-Voltage 电源占位符的 Global Net Name
- TEXT → T
**重要 caveat**:我们 corpus 里所有 Std 项目都只有 PCBdocType=3没有 SCHdocType=1实样。SCH 的 verb 字段顺序是按 EasyEDA Std 公开 spec 写的,**可能跟下游 parser 实际期望的字段顺序有出入**。下游同学 review 后给反馈,错的位移修一下就行。
### Layer 映射(重要,跟 KiCad 不一样)
EPRO2 跟 Std 的 layer id 不完全对齐:
- EPRO2 layer 5 (TOP_SOLDER_MASK) → Std 7
- EPRO2 layer 7 (TOP_PASTE_MASK) → Std 5 ← 跟 5 互换!
- EPRO2 layer 11 (OUTLINE) → Std 10 (BoardOutLine)
- EPRO2 layer 12 (MULTI) → Std 11 (Multi-Layer)
inner SIGNAL 层EPRO2 15+ → Std 21+ (Inner1 起步)。
### CLI 平铺输出
```
uv run python -m tools.epro2.std <project_dir> --all --out <dst>
```
输出按 Std 习惯**平铺**`<dst>/<doc_uuid>.json`,不分 board 子目录。三个互斥模式:`--all-pcb` / `--all-sch` / `--all`
### ESP-VoCat 实测
15 个 doc → 15 个 JSON
| 类型 | 数量 | 实测产物 |
|---|---:|---|
| PCBdocType=3| 6 | tracks 2K+, vias 700+, copperareas 19, libs 206, pads 807 |
| SCHdocType=1| 9 | wires 814, libs 477, netflags 838 (含 power-port), texts 71 |
`libs_unresolved=0` 全过——FOOTPRINT/SYMBOL doc 跨文档解析全部命中。
JSON 信封跟 Std 实样对比top-level keys 一致(`success/code/result``result``master/owner/created_at/...` 这些**爬取层 metadata**(不是数据本体,下游应该不需要);`dataStr.shape/layers/canvas/head` 全有。
### 决策Why
- **不替换 KiCad 那套**:用户说"原先那套页不要换"——保留 `tools/epro2/kicad/`,新写 `tools/epro2/std/` 平级,命令行也独立 `python -m tools.epro2.std` vs `python -m tools.epro2.kicad`
- **`json.dumps``separators=(",",":")` 不缩进**:实样 Std 文件就是单行紧凑 JSON没换行也没缩进节省空间也方便 diff。
- **数字格式 `_num()`**:实样 Std 输出整数不带 `.0``4303` 不是 `4303.0`),用 `math.isclose(f, int(f))` 判断后选择 int repr跟 Std 风格对齐。
### 测试
71 → 82 单测全过std_writers 11 个新(信封 / TRACK 字段顺序 / VIA 字段顺序 / COPPERAREA SVG path / LIB 嵌 PAD via `#@$` / docType=1 / W+N 配对 / power-port netflag / json.dumps round-trip
### 下游交付
15 个 ESP-VoCat JSON 已经在 `/tmp/std_json/`。要给下游同学的最小 deliverable
```
data/processed/std_json/<project_uuid>/<doc_uuid>.json
```
下一步:跑剩 4 块 Pro 项目X86主板 / 220V电源 / 泰山派 / 梁山派)—— Pro 2.x 那两块仍然不行,需要 Pro 2.x JSON 解析器。
---
## 2026-04-29 03:30 rate-limit benchmark 整理成正式报告
**Claude 会话**
把零散跑出来的 rate-limit ladder 探针结果整理成 `docs/sources/probe_rate_limit_results.md`,从临时增量笔记升级成正式 benchmark 文档。
**5 个 host 全部探完**
| Host | 旧 sleep | 新 sleep | 加速 |
|---|---:|---:|---:|
| `pro.lceda.cn/api/v4/...` | 5.0s | 0.5s | 10× |
| `lceda.cn/api/documents/...` | 5.0s | 0.5s | 10× |
| `oshwhub.com/<owner>/<path>` | 2.0s | 1.0s | 2× |
| `oshwhub.com/api/project` | 2.0s | 1.0s | 2× |
| `modules.lceda.cn/...` | 0.2s | 0.2s | — (已优化) |
**关键发现**:原 5s/req 完全是出于"Pro 要登录、被封号最痛"的心理顾虑而设没有实测依据。Pro API 实测 25 distinct UUID 连发 0/25 badmedian 410ms latencyQPS=2 完全经得住。Std doc endpoint 同样的故事。
**对 batch-50 的净效**sleep 总时间 32 min → 3 min约 10x整批 walltime 估算 ~2h → ~10-15 min。
报告结构TL;DR 总表 → 方法论(包括安全约束 + 限制)→ 5 个 host 各自详细数据 → 最终设置 → 复现指南 → 后续考虑。
下一步:直接跑 batch-50 计划的 Step 1详情页扫 license就行。
---
## 2026-04-29 03:00 跑完 5 块 Pro 项目 export发现并修两个 --all 崩溃路径
**Claude 会话**
`3c00edf`。给下游同学打包5 块 Wokwi pipeline 不吃的 Pro 项目X86主板 / 220V电源 / ESP-VoCat / 泰山派 / 梁山派)跑 `--all` 一键 export。
### 实战结果
| 项目 | EPRO2 类型 | --all 结果 | 备注 |
|---|---|---|---|
| ESP-VoCat | Pro 3.x AES | ✅ 6 boards | 之前已验证 |
| 220V 电源 | Pro 3.x AES | ✅ **7 boards** | 修了两个 bug 之后 |
| X86 主板 | Pro 3.x AES | ⚠️ 4/5 boards | 7374 docsCPU 板 PCB 写到 OOM swap death-spiral14 min 还没完,**杀掉**保剩 4 板 |
| 泰山派 (RK3566) | **Pro 2.x JSON** | ❌ no BOARDs | 我们的 EPRO2 pipeline 不识别 Pro 2.x docType |
| 梁山派 | **Pro 2.x JSON** | ❌ no BOARDs | 同上 |
### 220V 跑出来发现的两个崩溃路径
#### Bug A: KiCad 拒收奇数铜层数
```
12:45:56 AM: Error: 3 is not a valid layer count in '...kicad_pcb', line 31
```
220V 电源里的板子有 **1 个内层 SIGNAL** 用到GND 一层),加 F.Cu+B.Cu 总共 3 层。KiCad 8 要求铜层数偶数2/4/6/...)。修法:奇数时 pad 一个空的 In{N+1}.Cu总层数凑偶。
#### Bug B: 同标题 BOARD 互覆盖
220V 项目里有**两块都叫 "显示板"** 的不同 BOARD不同 uuid。我之前的 `--all` 用 title 推 basename两块争同一个目录第二块写完就把第一块覆盖。修法title 撞名时给所有撞名实例都加 BOARD uuid 前 8 位后缀(`显示板_52e8cc76` / `显示板_55d32906`);只一份的还是干净 basename。
### Pro 2.x 没拿下
泰山派 / 梁山派的 source 文件是**纯 JSON**`<uuid>.json`),不是 `.epro2` 二进制流。我们的 `replay_project` 读得了文件但 `doc_type=None`——head 里没 docType 字段,整个识别链路断掉,`_group_by_board` 拿不到 SCH/PCB 分组。
要补这 2 块得写**单独的 Pro 2.x → KiCad writer**plaintext dataStr 解析跟 EPRO2 是两套不一样的对象模型;也许能跟 Std json 共用)。本轮范围外。
### 决策Why
- **基板撞名一律加 uuid**:第一份保 clean basename 的方案有歧义性,"哪个是真的"不可靠。所有撞名一律带 uuid 后缀虽然多两个字符但绝对安全。
- **不在 ESP-VoCat 上重跑验证奇数层 fix**ESP-VoCat 的板要么 2 层(无 inner要么 4 层2 inner不会触发奇数层路径重跑没意义。
### 测试
71 → 73`test_odd_inner_signal_count_padded_to_even_total` + `test_duplicate_board_titles_get_distinct_basenames`
### X86 OOM 真因
跑到 14 分钟还没出 CPU 板 .kicad_pcb看进程状态
- VmRSS 1.96 GB + VmSwap 1.41 GB = 实占 3.4 GB
- 系统 3.3 GB RAM + 4 GB swapfree 120 Mi、swap free 434 Mi——**死循环 swap 抖**
- read_bytes 24 GB远超数据本身—— 全是 swap-in/swap-out
- State: Duninterruptible disk sleep
CPU 板 PCB doc 是 X86 项目里最大的(>8K objects + 35+ 子 SCH 页),我们的 `pcb_writer` 把整个输出 list 在内存里建好再 to_sexpr 一次性序列化,加上 35+ 次 `write_sch_page`(每次 `Relations.build` 加 lib_symbols 嵌入)累积爆 RAM。
**杀掉,保已经写完的 4 块**。要修得做 streaming 输出(边算边写文件,不在内存里建大 list。下一轮独立改动。
### 下游交付
| 来源 | 板数 | 状态 |
|---|---:|---|
| ESP-VoCat | 6 | ✅ 全 |
| 220V 电源 | 7 | ✅ 全 |
| X86 主板 | **4/5** | ✅ 部分CPU 板 SCHEMATIC1_1 缺 .kicad_pcb子页 SCH 都在)|
| 泰山派 | 0 | ❌ Pro 2.x本轮不支持 |
| 梁山派 | 0 | ❌ Pro 2.x本轮不支持 |
| **合计** | **17** | 32/32 文件 kicad-cli 解析通过 |
---
## 2026-04-29 02:30 KiCad 工程文件 + `--all` 一键导:双击 .kicad_pro 打开 GUI
**Claude 会话**
`adc5dc5`。两件小事一起做:发 `.kicad_pro` + CLI 加 `--all`。目标是让消费者(咱们的 ML 训练代码 / 下游同学)双击就能在 KiCad GUI 里同时打开 schematic + PCB 配对。
### 1. `tools/epro2/kicad/pro_writer.py`
KiCad 8 的 .kicad_pro 是 JSON。最小可用集
```json
{
"meta": {"filename": "<basename>.kicad_pro", "version": 1},
"sheets": [["<root_sheet_uuid>", ""]], // 绑 schematic 根 sheet
"board": {...}, "schematic": {...}, "net_settings": {...}, ...
}
```
只要 .kicad_pro / .kicad_sch / .kicad_pcb **三个文件同名同目录**KiCad 自动配对——双击 .kicad_pro 同时弹两个编辑器。`sheets` 数组里的 uuid 必须和 .kicad_sch 的 `(uuid ...)` 对得上,所以 `write_root_sheet` 加了 `root_uuid` 参数让调用方注入确定值。
### 2. CLI `--all`
按 BOARD uuid 分组 SCH 和 PCB两边的 META 都带 `board: <uuid>`,自动 1:1 对应),每个 BOARD 一个目录:
```
out/
├── EchoEar-BaseBoard-V1_0/
│ ├── EchoEar-BaseBoard-V1_0.kicad_pro
│ ├── EchoEar-BaseBoard-V1_0.kicad_sch (root)
│ ├── EchoEar-BaseBoard-V1_0.kicad_pcb
│ └── P1_45092758.kicad_sch (子页)
├── EchoEar-CoreBoard-V1_0/
│ ├── EchoEar-CoreBoard-V1_0.kicad_pro
│ ├── EchoEar-CoreBoard-V1_0.kicad_sch
│ ├── EchoEar-CoreBoard-V1_0.kicad_pcb
│ ├── Overview_dc13d6d2.kicad_sch (4 个子页)
│ ├── MCU_510cff33.kicad_sch
│ ├── Interface_b336a7c7.kicad_sch
│ └── codec_0b0163fa.kicad_sch
... (5 boards total)
```
basename 从 SCH/PCB title 剥掉 `SCH-`/`PCB-` 前缀(`SCH-EchoEar-CoreBoard-V1_0``EchoEar-CoreBoard-V1_0`schematic 和 PCB 自然 collapse 到一个项目。
### ESP-VoCat 实测
6 个 BOARD含 LCD-BD 那个 SCH 被删的——只有 PCB照样生成 `.kicad_pro``.kicad_pcb`sheets:[]
| 项目 | sch 解析 | pcb 渲染 |
|---|:-:|:-:|
| EchoEar-BaseBoard-V1_0 | ✓ ERC 485 | ✓ |
| EchoEar-CoreBoard-V1_0 | ✓ ERC 1205 | ✓ |
| EchoEar-Rotating-Base-LCD-BD-V1_0 | (no sch) | ✓ |
| EchoEar-Rotating-Base-Mainboard-V1_0 | ✓ ERC 412 | ✓ |
| ESP-VoCat-MicBoard-V1_0 | ✓ ERC 133 | ✓ |
| ESP-VoCat-Rotating-Base-Sub-board-V1_0 | ✓ ERC 30 | ✓ |
`pro.sheets[0][0]``.kicad_sch` 第一行 `(uuid ...)` 字符串相等(验证过 CoreBoard`366d3e53-5167-4e17-9325-c2fccbe4330b`)。
### 决策Why
- **basename 对齐 = KiCad pairing 的全部**KiCad 不读 .kicad_pro 里的 `boards`/`schematic` 字段去找文件,纯靠**同目录同 basename**自动配对。其它字段都是项目 settings缺了 KiCad 用 default 兜底。
- **`sheets: []` 兜底 LCD-BD**SCH 被 DELETE_DOC 的板子没 schematic rootpro 里空数组兜底KiCad 打开时只显示 pcbnew不会卡。
- **`--all` 不替代 `--all-sch` / `--all-pcb`**:保留两个细粒度命令——只想看 schematic 或只想 batch-export PCB 时不必生成 .kicad_pro 噪音。
### 测试
68 → 71 单测全过pro_writer 3 个filename + root uuid 绑定 / sheets 空数组 fallback / KiCad 8 顶层 key 完整性)。
### 下游交付
下游同学的 Wokwi pipeline 不吃的 5 个 Pro 项目3 块 EPRO2 AES + 2 块 Pro 2.x现在一条命令就能转
```
uv run python -m tools.epro2.kicad data/raw/oshwhub/<uuid> --all --out <dst>
```
每个项目目录拷贝到他们 corpus 即可。
---
## 2026-04-29 02:00 PCB Phase-2POUR → KiCad zoneCoreBoard unconnected -43%
**Claude 会话**
`e614044`。Phase-1 PCB 6 板都解析了但 DRC 报很多 unconnected——大头是 GND/AGND 走 POUR 覆铜没导出来。补 zone 导出。
### 做的
`pcb_writer.py``_decode_zone_path` 处理 EPRO2 POUR.path 三种形态:
- `[['R', x, y, w, h, ...]]` — 矩形实测最常见CoreBoard/MicBoard 全是这个)
- `[['CIRCLE', cx, cy, r]]` — 圆形(按 36 段近似成 polygon
- `[[x1, y1, 'L', x2, y2, ...]]` — polylineARC token 跳过 4 个 token按弦近似
每个 POUR 输出 `(zone (net N) (net_name "..." ) (layer "F.Cu") (polygon (pts ...)) (filled_polygon (pts ...)))`
### 关键坑:必须 emit `(filled_polygon)` 块
第一次只发 `(zone)``(polygon)` 边界的版本DRC 完全不变——`kicad-cli pcb drc` **不**自动跑 zone fill只有 GUI 的"填充覆铜"会跑。所以 file 必须自己声明已填充。简单做法:用 boundary polygon 当 filled_polygon= "整个 pour 区域都是铜",忽略 pad clearance/thermal
### ESP-VoCat 6 板 DRC 对比unconnected_items count
| 板 | before zones | after zones | Δ |
|---|---:|---:|---:|
| BaseBoard | 227 | 227 | 0 |
| CoreBoard | 358 | **205** | **-43%** |
| MicBoard | 75 | 75 | 0 |
| LCD-BD | 43 | 43 | 0 |
| Mainboard | 179 | 179 | 0 |
| Sub-board | 1 | 1 | 0 |
| **TOTAL** | **883** | **730** | **-17%** |
### 为什么只 CoreBoard 改善明显
抽样 MicBoard 残留 75 unconnected
- AGND 94 个 item 里很多 pad 在 zone boundary 之外——POUR 矩形是 (72.3, 112.3)→(126.8, 126.0),但 AGND pad 在 y=107 上方
- 大量 `$1N1865` 这种内部网——根因是 via 没绑对网(不是 POUR 能解决的)
EPRO2 用户画 POUR 时通常只覆元件密集区,不覆全板;外围 trace 自己接。zone 解决"靠 pour 接到 GND"的 pad但解不了"trace 路由不通"或"via 网漂移"。
CoreBoard zones=74-layerGND+POWER+AGND 各一对),覆盖面广,效果明显。其它板 zones 多是 2 个,覆盖小。
### 决策Why
- **filled_polygon = boundary 不做 clearance/thermal 计算**:自己实现 zone fill 算法工作量爆炸KiCad 实现是 C++ 几千行。boundary fill 是"连通性正确clearance 不精确"——KiCad GUI 一键 refill 即可矫正。这条路保留 EPRO2 user-drawn boundary 作为 single source of truth。
- **不读 POURED.pourFill**POURED 是 EPRO2 自己 fill 算法的输出path 含 ARC 难解析、坐标系跟 POUR 不一定对齐。boundary 直接用更可靠。
- **ARC 在 polyline 里按弦近似**:跟 Phase-1 ARC 处理一致KiCad 解析得了,几何稍偏(不会比 POUR 不导更糟)。
- **不强行优化 MicBoard 那种 zone 之外的 pad**:那是 EPRO2 source 本身的连通方式trace + via不是 zone 能修的。
### 测试
65 → 68 单测全过rectangle path → 4 corners + filled_polygon mirror / circle → 36-seg polygon / 非 copper layer skip。
### 下一步建议
- **ARC 圆心反推**(中等工作量):消 invalid_outline 警告 + zone polyline 里 ARC 段更准。需要三点定圆。
- **schematic + PCB 同时跑**小工作量CLI 加 `--all` 同时输出两套,目录配对。
- **`.kicad_pro` 项目文件**(小工作量):双击就能打开 KiCad GUIschematic 和 PCB 自动配对。
---
## 2026-04-29 01:30 KiCad 导出 Phase 3 PCB6/6 .kicad_pcb 全部 kicad-cli 通过
**Claude 会话**
`ff5553f` 后续。Forge 投影最后一块——schematic 已经够用,做 `.kicad_pcb` 导出。一上来就铺了 Phase-1 最小可解析 scope要先有"能在 KiCad 8 里打开 + kicad-cli 处理"的底线),不追全保真。
### 做的
1. **`tools/epro2/kicad/pcb_writer.py`** — 单 PCB doc → `.kicad_pcb` 主入口
- 数据驱动的层映射 `_build_layer_map`:扫所有 primitive 的 `layerId`,把真正用到的 SIGNAL 内层id 15+)按 EPRO2-id 顺序铺成 `In1.Cu`/`In2.Cu`/...F.Cu 永远 ordinal 0、B.Cu 永远 ordinal 31KiCad 硬约定)。
- net 表:从 NET op 取名字,从 1 开始分配 id0 留给 KiCad 的 "no net")。
- 走 LINEcopper 层 → `(segment ...)`layer 11 OUTLINE → `(gr_line layer Edge.Cuts)`;走 VIA → `(via)`;走 layer-11 POLY → 把 path 拆 polyline 段 emit 成 Edge.Cuts 上的 gr_line 链(实测板子轮廓全在 POLY 不在 LINE少这步 edge=0
- ARC 现在按弦近似Phase 1 不解 EPRO2 ARC.path 的圆心/半径表示KiCad 收得下DRC 报 invalid_outline——下一轮改
2. **`tools/epro2/kicad/footprint_writer.py`** — inline `(footprint ...)`
- 走 FOOTPRINT.PAD`RECT/ELLIPSE/OVAL/POLYGON` → KiCad `rect/circle/oval/custom`hole 存在则 SMD→thru_hole + `(drill ...)`SLOT 走 `(drill oval w h)` 二参形式。
- layer 映射layerId 1 → `F.Cu+F.Mask+F.Paste`、2 → `B.Cu+B.Mask+B.Paste`、12 (MULTI/THT) → `*.Cu+*.Mask`
- net 解析借 `pcb_rel.pad_nets_by_pad[pad_id]`(之前 `ProjectRelations` 已经为 PCB→FOOTPRINT cross-doc 攒过的索引现在派上用场)。
- bottom-side COMPONENT (`layerId=2`) 整个 footprint 走 `(layer B.Cu)`Reference 标签同步落到 `B.SilkS`
3. **CLI 加 `--all-pcb`**`tools/epro2/kicad/__main__.py`):每个 PCB doc 一文件,文件名取 META.title。
### ESP-VoCat 实测6 个 PCB
| 板 | nets | footprints | segments | vias | edge_cuts |
|---|---:|---:|---:|---:|---:|
| BaseBoard | 38 | 59 | 391 | 126 | 4 |
| CoreBoard | 84 | 87 | 1131 | 262 | 4 |
| MicBoard | 13 | 17 | 88 | 47 | 16 |
| LCD-BD | 7 | 5 | 66 | 35 | 2 |
| Mainboard | 24 | 33 | 340 | 181 | 4 |
| Sub-board | 4 | 5 | 10 | 0 | 4 |
**6/6 都过 `kicad-cli pcb export svg`**——文件解析无 error、SVG 输出正常。footprint/track/via/edge cuts 全部可见。
`kicad-cli pcb drc` 对最小的 MicBoard 跑145 violations + 75 unconnected。分布
- 28 clearancetrace/pad 间距)+ 19 track_dangling + 39 via_dangling + 75 unconnected — 真连接问题,部分是 EPRO2 源里靠 POUR 解决但我们 Phase 1 没导
- 21 silk_overlap + 8×2 edge_clearance + 3 invalid_outline — 边角 + silk 美化
- 17 lib_footprint_issues — facere 库没注册cosmetic跟 schematic 一样)
### Phase 1 砍掉的 / 下一轮再做
- **POUR / POURED** — 铜皮覆铜,导了 75 unconnected 的大头会消掉GND 大面积在 POUR 里走,没导出来 trace 当然报 unconnected
- **FILL** — 元件下方手工 fill 块
- **ARC 圆心/半径解析** — 现在按弦近似invalid_outline 警告就这来的
- **TEARDROP** — pad/via 接 trace 处的圆角,纯美化
- **STRING / IMAGE** — 板上文字和 logo 图
### 决策Why
- **板子原点平移到 (100, 100) mm**EPRO2 板能在任意坐标含负KiCad 画布原点在 (0,0),不平移开 KiCad 时板子可能根本不在视野里。100mm 是经验值,留够 silkscreen 边距。
- **footprint inline 不走外部 .kicad_mod**:跟 schematic lib_symbol 同思路,自包含,能直接 `kicad-cli` 处理;缺点是同款 footprint 重复写多份,但 EPRO2 一个 SCH 内的 FOOTPRINT 数量级(几十)摊开来文件膨胀也不大。
- **ELLIPSE 当 circle 不当 oval**:实测 ELLIPSE 的 width/height 经常相等(=圆形 padKiCad 没有真椭圆 pad typecircle 取 max(w,h) 比 oval 更接近圆。
- **ARC 暂时按弦**:算 EPRO2 ARC 圆心要先反推三点定圆浮点精度敏感。Phase 2 单独处理。
### 测试
52 → 65 单测全过pcb_writer 8 个layer map / net id / segment / via / outline POLY / 零长 skip / 非 PCB doc reject+ footprint_writer 5 个SMD pad layers / 圆 vs slot drill / pad net 跨 doc 解析 / bottom-layer 翻转 / unresolved skip
### 下一步建议
- **POUR/POURED 导 zone**(中工作量):消掉大部分 75 unconnectedCoreBoard 这种 4-layer + 大量 GND/POWER 覆铜的板真实连通性会贴近完整。
- **schematic + PCB 同时跑** 一个 export 命令(`--all`),生成完整 KiCad project 目录。
- **Phase 4: KiCad project 文件 .kicad_pro**:让用户双击就能在 KiCad GUI 里打开 schematic + PCB 配对。
---
## 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