From a16cb11c7d37e0593f0d33e8f5342578cee68c51 Mon Sep 17 00:00:00 2001 From: Zhang Jiahao Date: Fri, 24 Apr 2026 00:11:32 +0800 Subject: [PATCH] =?UTF-8?q?Add=20easyeda=5Fpro=5Fsource.md:=20Pro=20?= =?UTF-8?q?=E5=B7=A5=E7=A8=8B=E6=BA=90=E5=AE=8C=E6=95=B4=E9=93=BE=20+=20EP?= =?UTF-8?q?RO2=20=E6=A0=BC=E5=BC=8F=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: - pro.lceda.cn (立创 EDA 专业版) 的工程源抓取链已经打通:4 步 API + AES-128-GCM 解密 + gzip 解压 + EPRO2 消息流解析,所有信息需要落成 文档独立保留,避免丢失;也为后续实现 EPRO2 → KiCad 转换器/选型铺路。 - 与 oshwhub.md(Std 版)并列成为独立调研文档 —— Pro 和 Std 是两套 独立编辑器,cookie/API/格式都不同,混在一起反而乱。 What: - docs/sources/easyeda_pro_source.md: * TL;DR 表 + §1 Std vs Pro 对照 * §2 4 步 API 链 + 必需 headers (Editor-Version/path/Referer/Cookie) + Python 解密代码 + 实测数据(2.7 MB 源流 / 8357 条消息) * §3 EPRO2 格式完整分类:40 种 message type 按功能分组 (零件/几何/PCB/层/规则/...) + 每类样例 * §4 安全合规(风控 / license / 密钥泄漏语义) * §5 接入 Forge (OSHWHUB_INGEST_SPEC.md) 的 gap 表 * §6 已知未验证 7 条 * 附录 A 一键重跑命令 - pyproject.toml: + pycryptodome>=3.23.0(AES-GCM 解密依赖) - log.md: 本次会话记录 Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/sources/easyeda_pro_source.md | 294 +++++++++++++++++++++++++++++ log.md | 70 +++++++ pyproject.toml | 1 + 3 files changed, 365 insertions(+) create mode 100644 docs/sources/easyeda_pro_source.md diff --git a/docs/sources/easyeda_pro_source.md b/docs/sources/easyeda_pro_source.md new file mode 100644 index 0000000..9668c72 --- /dev/null +++ b/docs/sources/easyeda_pro_source.md @@ -0,0 +1,294 @@ +# 立创 EDA Pro 工程源 (pro.lceda.cn) — 数据源调研 + +**定位**:立创 EDA **专业版**(EDA Pro,域名 `pro.lceda.cn`)的工程源抓取链。与 EasyEDA **标准版**(`u.lceda.cn` / `oshwhub.com`,见 `oshwhub.md`)并列。 +**首版调研**:2026-04-24 +**状态**:API 链路 + 加密 + 解压 + 格式 **全部打通**;Schematic 文档、对他人公开 Pro 工程、批量化仍待验证。 + +--- + +## TL;DR + +| 事项 | 结论 | +|---|---| +| 登录态 | **必须**(`pro.lceda.cn` 有独立 session,与 `u.lceda.cn` 不共享) | +| 核心 API | `/api/v4/projects//branches//histories/` (GET) | +| 源文件分发 | `https://modules.lceda.cn/projects/histories/`(私有 CDN,**不需 cookie**,但只在 API 返回上下文中可下) | +| 加密 | **AES-128-GCM**(`{key, iv}` 由 API 响应给出),16-byte auth tag 附在 ciphertext 末尾 | +| 压缩 | 解密后是 **gzip**,gunzip 后得到源流 | +| 数据格式 | **EPRO2** —— 自定义消息流(`\n` 分隔消息,`||` 分字段),40+ 种 message types | +| 一次工程完整源 | 2.7 MB (示例 `无界PLUS`) / 8 357 条消息 / 含 PART / PAD / NET / WIRE / LAYER / RULE / 等全要素 | +| Editor version snapshot | `3.2.127` (2026-04,会随 pro 编辑器升级变化,见 `Editor-Version` header) | + +--- + +## 1. 与标准版 (EasyEDA Std) 的区别 + +| 维度 | EasyEDA Std (`u.lceda.cn`) | EasyEDA Pro (`pro.lceda.cn`) | +|---|---|---| +| 编辑器入口 | `lceda.cn/editor` | `pro.lceda.cn/editor?entry=mgr-project-worker.js` | +| Cookie | `lceda_session`(hostOnly `u.lceda.cn`) | `lceda_pro_session`(hostOnly `pro.lceda.cn`) **独立** | +| 项目 API | `/api/projects/`(一次返回全部) | `/api/v4/projects/`(只返 metadata,需 4 步拿源) | +| 版本控制 | 单版本,无 branch 概念 | **Git 风格**:branches + histories(含 `parent`/`snapshot`) | +| 源数据加密 | 未完整探(见 `oshwhub.md` §3.5,`modules.lceda.cn/histories/.json` 也是 403 AccessDenied,推测同样有签名机制) | **AES-128-GCM**(确认) | +| 源格式 | 早期 EasyEDA JSON(扁平结构) | **EPRO2** 消息流(事件溯源式) | +| oshwhub 标记 | 项目 `origin: "std"` | 项目 `origin: "pro"` | +| 转 KiCad 成熟度 | `easyeda2kicad.py` 等第三方工具 | **无现成工具**,需自写转换器或用 Pro 编辑器内置"导出 KiCad"功能(需研究其 API) | + +--- + +## 2. 抓取 4 步链 + +### 2.1 URL 链路 + +``` +1. GET /api/v4/projects/ + → 返回 metadata + `branch_uuid`(默认分支) + +2. GET /api/v4/projects//branches/ + → 返回该分支详情 + `history_uuid`(当前 HEAD history) + ── 或走列表: + GET /api/v4/projects//branches?getStartNode=true&isNotPage=yes + +3. GET /api/v4/projects//branches//histories/ + → 返回 `[{ 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/` + +4. GET + → 返回加密 blob(Content-Type 未声明,二进制流) +``` + +### 2.2 必需 HTTP Headers + +``` +Cookie: <含 lceda_pro_session 的 pro.lceda.cn cookie> +Editor-Version: 3.2.127 +Referer: https://pro.lceda.cn/editor +path: # 自定义头,用做 CSRF 类校验 +User-Agent: Mozilla/5.0 (...) Chrome/147.0.0.0 ... +Accept: application/json +``` + +缺 `Editor-Version` 或 `path` 头会返回 `{"success":false,"code":1111111,"message":"没有权限操作"}`。 + +### 2.3 解密流程(Python) + +```python +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 会失败) +blob_url = api_resp["dataStrUrl"] + +# 4: 下载 +blob = httpx.get(blob_url).content +ciphertext, tag = blob[:-16], blob[-16:] # WebCrypto convention: tag 附末 + +# 5: AES-GCM decrypt_and_verify +plain_gz = AES.new(key, AES.MODE_GCM, nonce=iv).decrypt_and_verify(ciphertext, tag) + +# 6: gunzip +source_stream = gzip.decompress(plain_gz) # EPRO2 消息流 +``` + +### 2.4 实测数据(示例项目 `无界PLUS`, `37879d790e1e450dad3375232bd5110f`) + +| 步骤 | 输入 | 输出 | +|---|---|---| +| blob 下载 | `modules.lceda.cn/.../` | 416 839 bytes | +| AES-GCM 解密 | blob | 416 823 bytes (16B tag 验证通过) | +| gzip 解压 | 加密载荷 | **2 712 282 bytes** (源消息流) | +| EPRO2 切分 | 按 `\n` 分行 | **8 358 条消息** | + +--- + +## 3. EPRO2 源格式(消息流) + +### 3.1 总体结构 + +解压后是 UTF-8 文本流,按 `\n` 分成 N 条消息,每条形如: + +``` +{"type":"X","ticket":N,"id":"..."}||{payload JSON}||{optional extra fields} +``` + +- `type`:消息类型(见 §3.2) +- `ticket`:单调递增的事件序号(可视为版本 diff 的编号) +- `id`:对象的 stable ID(PART id、网络 id、等) +- `payload`:消息具体内容(JSON 对象) +- `||`:字段分隔符;消息内最多见 2-3 段 + +本格式是**事件溯源 / 增量操作日志**风格:一个工程的完整状态 = 这一串消息按序重放的结果。 + +### 3.2 消息类型分布(示例 `无界PLUS` — docType=`BOARD`) + +| 类别 | types(按数量) | +|---|---| +| 文档 & 元 | `EDIT_HEAD` (1) · `DOCHEAD` (369) · `META` (138) · `CANVAS` (93) · `PREFERENCE` (1) · `PANELIZE` (1) | +| 零件 / 符号 | **`ATTR` (2 059)** · **`PART` (62)** · **`COMPONENT` (181)** · `PIN` (240) | +| 几何图元 | `LINE` (737) · `POLY` (385) · `RECT` (39) · `CIRCLE` (8) · `ARC` (7) · `ELLIPSE` (2) · `TEXT` (17) · `STRING` (19) | +| PCB 专属 | `PAD` (204) · `VIA` (59) · **`WIRE` (128)** · **`NET` (52)** · `PAD_NET` (259) · `TEARDROP` (289) | +| 铺铜 | `FILL` (314) · `POUR` (2) · `POURED` (2) | +| 层堆叠 | **`LAYER` (1 572)** · `LAYER_PHYS` (54) · `ACTIVE_LAYER` (50) · `SILK_OPTS` (2) | +| 设计规则 | `RULE` (16) · `RULE_SELECTOR` (53) · `RULE_TEMPLATE` (1) · `PRIMITIVE` (37) | +| 其他 | `FONT` (6) · `IMAGE` (2) · `BLOB` (1) · `TABLE` (1) · `OBJ` (1) · `ELE_PLACEHOLDER` (894) | + +### 3.3 各主要类型样例(简化) + +```jsonc +// EDIT_HEAD — 编辑者上下文(一条) +{"type":"EDIT_HEAD"} || {"uuid":"...", "username":"...", "updateTime":1776959670248} + +// DOCHEAD — 文档头(每个 document 一条,ticket 等于当前版本号) +{"type":"DOCHEAD","ticket":2} || {"docType":"BOARD","uuid":"...","version":"...","editVersion":"3.2.127","user":{...}} + +// META — 文档元 +{"type":"META","ticket":1,"id":"META"} || {"title":"无界PLUS","zIndex":1} + +// CANVAS — 画布原点 +{"type":"CANVAS","ticket":1,"id":"CANVAS"} || {"originX":0,"originY":0} + +// PART — 元件封装定义 +{"type":"PART","ticket":2,"id":"0603WAF1002T5E.1"} || {"BBOX":[-10,-4,10,4],"title":"0603WAF1002T5E.1"} + +// COMPONENT — 元件放置实例 +{"type":"COMPONENT","ticket":2,"id":"e1"} || {"partId":"pid8a...", "x":0,"y":0,"rotation":0,"isMirror":false,"attrs":{}} + +// ATTR — 元件/对象属性(value, designator, rotation, color...) +{"type":"ATTR","ticket":3,"id":"e1"} || {"partId":"0603WAF1002T5E.1","key":"Symbol","value":"0603WAF1002T5E","x":null,...} + +// PIN — 元件引脚 +{"type":"PIN","ticket":6,"id":"e3"} || {"partId":"...","x":20,"y":0,"length":10,"rotation":180,"pinShape":"NONE",...} + +// NET — 电气网络 +{"type":"NET","ticket":123,"id":"[\"NET\",\"+12V\"]"} || {"netType":null,"retLine":true,...} + +// PAD_NET — 焊盘与网络的归属 +{"type":"PAD_NET","ticket":1230,"id":"[\"PAD_NET\",\"e793\",\"1\",\"e18\"]"} || {"padNet":"+12V","padLen":0} + +// 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},...} + +// LAYER — 叠层定义(每层一条) +{"type":"LAYER","ticket":2,"id":"[\"LAYER\",1]"} || {"layerType":"TOP","layerName":"Top Layer","activeColor":"#ff0000",...} + +// WIRE + LINE — 走线(WIRE 是总线,LINE 是组成它的线段,通过 lineGroup 关联) +{"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,...} + +// POUR / POURED — 铺铜定义 / 铺铜渲染结果 +{"type":"POUR",...} || {"netName":"GND","layerId":1,"pourType":{"pourType":"SOLID","fineness":8},...} + +// RULE / RULE_SELECTOR — 设计规则 +{"type":"RULE","ticket":225,"id":"[\"RULE\",\"SAFE\",\"copperThickness1oz\"]"} || {"ruleContext":{"unit":"mm","safeSpacing":[...]}} +``` + +### 3.4 docType 取值 + +| docType | 含义 | +|---|---| +| `BOARD` | PCB 布线文档(本次解出来的示例) | +| `SCHEMATIC` | 原理图文档(**未验证**,需要含 schematic 的项目样本) | +| 其它可能 | 待探:`SYMBOL` / `FOOTPRINT` / `3D_MODEL` 等 | + +**关键待验证**:一个 Project 下多个 document 的关系 —— 每个 document 是否有独立的 branch/history 链?还是所有 document 共享一条分支?从目前 1 个项目的样本看,API 里一个 project → 一个 branch → 一条 history → 一坨 blob,blob 内含 **1 个 `DOCHEAD`**(本例 `BOARD`)。如果原理图是另一个 document,说明需要: + +- 先在 API 里枚举项目的所有 documents(需找对应端点,目前 HAR 里未见) +- 然后每个 document 走独立的 4 步链 + +--- + +## 4. 安全与合规考量 + +### 4.1 登录态与账号 + +- **必须合法账号登录**;我方主号测试已通过(见 `docs/infra.md`)。风控风险:主号被封影响大 +- 登录后建议**立即登出 + 重登**一次,让测试期间暴露过的 session 失效(见 CLAUDE.md §登录态) +- 放量时 **QPS ≤ 0.1**(10 秒一次请求)以降低风控概率;多账号轮询作为后备 + +### 4.2 抓取范围的合法性 + +- 仅抓**公开**(`public: true`)且作者有明确 license 声明的项目 +- License whitelist(见 `OSHWHUB_INGEST_SPEC.md` §2.1):MIT / BSD / Apache-2.0 / CC0 / CC-BY-4.0 / CERN-OHL-P / Unlicense +- **拒抓**:CC-BY-SA(立创默认选项,占比可能 50%+)、GPL 系列、"未声明"、NC(禁商用)变种 +- 项目 `permissions` 字段可反查本号是否有导出权限(见 `project.file.export`) + +### 4.3 加密密钥泄漏 + +`key` / `iv` 随每次 API 响应明文返回 —— 这不是保密凭据,而是服务端控制**谁能拿到这对值**(通过登录态 + 签名签发)。我们把解密后的明文落盘即可,不需要保护 key/iv。 + +但**解密后的工程源是作者的知识产权**: +- 不要将他人工程源上传到 Gitea 公开仓库,先过 license 审核 +- `~/.secrets/` 之外的本地存储也要做权限控制 +- 后续建索引时,每条记录的 `license_verified` 字段必须为 true 才可进入 Forge 交付批次 + +--- + +## 5. 接入 Forge 的 gap + +按 `OSHWHUB_INGEST_SPEC.md`(消费侧)要求:每项目三件套 `schematic.kicad_sch` + `manifest.json` + `source/`。本章列出从 EPRO2 到 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 单独交付(非 BOARD) | 本次样本是 BOARD | 需要找原理图 document 的 API 入口(待验证) | +| `manifest.json` 完整字段 | `metadata.json` 已覆盖多数字段 | 补:`components_used`(可从 ATTR / COMPONENT 聚合)、`kicad_sch_version`(转换产物)、`file_checksum_sha256`、`converted_from`、`conversion_tool`、`license_source` | +| 目录命名 `oshwhub__` | 当前 `data/raw/oshwhub//` | 需额外生成 `batch_/oshwhub__/` 结构(建议放 `data/processed/batches/`,保留 raw 不动) | +| License whitelist | 当前 whitelist 较宽(含 GPL / CC-BY-SA) | 按 §4.2 收紧;下游过滤器写进 `scripts/filter_for_forge.py` | +| 自测 4 项检查 | 无 | 写 `scripts/forge_batch_check.py`(见 SPEC §6) | + +--- + +## 6. 已知未解决 / 待补 + +| # | 项 | 优先级 | 行动 | +|---|---|---|---| +| 1 | 对**他人**公开 Pro 工程能否跑通同样 4 步链? | 高 | 下一批 HAR:挑一个 oshwhub 里 `origin: pro` 的公开项目,复现链 | +| 2 | `SCHEMATIC` docType 是否在独立 document?API 入口? | 高 | 打开含原理图的 Pro 工程录 HAR | +| 3 | 多 document 枚举端点(project → documents 列表) | 高 | 同上 | +| 4 | Pro 编辑器的"导出 KiCad"功能走什么端点?产物格式? | 高 | 在编辑器里手动点一次导出,录 HAR | +| 5 | 批量化对风控的敏感度 | 中 | 先从 10 → 100 → 1000 阶梯放量,监控 403/429/1111111 | +| 6 | Std 版 `u.lceda.cn` 的同构源 API(如果有) | 中 | 用 Std 编辑器打开 oshwhub 上 `origin: std` 项目录 HAR | +| 7 | EPRO2 → KiCad 转换完整性 | 中 | 选完路线(§5 gap)后逐 type 实现,自测对比 Pro 编辑器渲染 | + +--- + +## 附录 A — 重跑一次的完整命令 + +```bash +# 登录态(cookie 已备好在 ~/.secrets/pro-lceda-cookie-header.txt) +# PROJ_UUID / BRANCH / HIST 从 HAR 或 API 响应中提取 +uv run python - <<'PY' +import json, gzip, httpx +from Crypto.Cipher import AES + +cookie = open('/home/ubuntu/.secrets/pro-lceda-cookie-header.txt').read().strip() +headers = { + 'User-Agent': 'Mozilla/5.0 Chrome/147.0.0.0', + 'Referer': 'https://pro.lceda.cn/editor', + 'Editor-Version': '3.2.127', + 'Accept': 'application/json', + 'Cookie': cookie, +} +PROJ = 'YOUR_PROJECT_UUID' + +with httpx.Client(headers=headers, timeout=30) as c: + p = c.get(f'https://pro.lceda.cn/api/v4/projects/{PROJ}', headers={'path': PROJ}).json()['result'] + br = p['branch_uuid'] + h = c.get(f'https://pro.lceda.cn/api/v4/projects/{PROJ}/branches/{br}', headers={'path': PROJ}).json()['result'][0] + blob = c.get(h['dataStrUrl']).content + 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) + stream = gzip.decompress(pt) + +print(f'messages: {stream.count(chr(10).encode())}') +PY +``` + +## 附录 B — 变更历史 + +| 日期 | 变更 | +|---|---| +| 2026-04-24 | 首版:4 步 API 链路 + AES-128-GCM + gzip + EPRO2 消息流格式完整解析(40 种 types 覆盖 BOARD 全要素);与 Std 版、Forge SPEC 的 gap 列出 | diff --git a/log.md b/log.md index 6170c42..92916e1 100644 --- a/log.md +++ b/log.md @@ -4,6 +4,76 @@ --- +## 2026-04-24 00:25 打通 pro.lceda.cn 工程源完整链 + EPRO2 格式解析 + +**Claude 会话** + +核心成果:**立创 EDA Pro 工程源的 API + 加密 + 格式三层全打通**。 + +### 完整链路 + +``` +1. GET /api/v4/projects/ → branch_uuid +2. GET /api/v4/projects//branches/ → history_uuid +3. GET /api/v4/projects//branches//histories/ → {key, iv, dataStrUrl} +4. GET (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: ` / `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_NET(PCB 电气) +- 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/` | 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 在聊天里粘过两次 cookie(u.lceda.cn 一次 + pro.lceda.cn 一次),已写入 dev1 `~/.secrets/`。 +当前会话 transcript 含明文 —— 本轮验证完 Charles 应登出再重登一次,让测试期间暴露过的 session invalidate。 + +--- + ## 2026-04-23 20:10 策略大调:登录内容入场 + 云服务器 + EDA→KiCad 转换 **Claude 会话** diff --git a/pyproject.toml b/pyproject.toml index 9c58d9a..713ac27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ requires-python = ">=3.10" dependencies = [ "httpx[http2]>=0.27", "jsonschema>=4.26.0", + "pycryptodome>=3.23.0", ] [tool.ruff]