Files
eink-push/CLAUDE.md
YANG JIANKUAN c836b0d6bc docs: CLAUDE.md 记录 server.py/cloudflared 改由 launchd 常驻及运维命令
- 说明 nohup 启动会随 Claude Code 会话回收被 SIGTERM、重启后不自起的事故原因
- 记录两个 LaunchAgent 的 label、kickstart/bootout 操作,原手工命令降级为应急参考

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 10:59:12 +08:00

112 lines
46 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.

# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目定位
`eink-push` 是 JM monorepo 中一个**独立的 Python 工具**,与其余 Java/Vue 子项目技术栈无关,不受父级 CLAUDE.md 的 jm-cloud/uniapp/admin 开发约束。
作用把日期天气、Claude Code 用量、橘喵今日经营合成为一张 **400×300 三色(黑/白/红BWR** PNG推送到局域网墨水屏设备喵喵固件开源https://gitee.com/gxp666111/miaomiao。纯 Python + Pillow**无浏览器依赖**,适合 cron 定时运行。
> **分支 `feature/colorful`**:新设备在黑白外支持红色,本分支把渲染升级为黑/白/红三色。当前版式为**上中下三段**A 大时钟+日期 / 天气定位南京·江宁B **Claude Usage** 主角5h/7d 用量进度条 + 时间维度对比 pace用量吃紧转红预警C 橘喵今日实时三列。红作**语义强调**(用量预警、涨、定位、天气太阳/闪电),仍**无真灰阶、不抖动**全图只三种纯色。近30天趋势图当前**隐藏**`renderer._trend_chart` 及依赖保留、未调用,随时可挂回)。
> **分支 `feature/reterminal-e1002`**(基于 `feature/colorful`):新增第二个目标设备 **Seeed reTerminal E1002**7.3 寸 **800×480**E Ink **Spectra 6 原生六色**:黑/白/黄/红/绿/蓝,真机已验证六色量化干净)。该设备不接受局域网直推,而是由 **SenseCraft HMI** 云端的 **HTML 控件**按设备刷新间隔(最小 5 分钟)**每周期重新抓取**一个公网 HTTPS URL、无头浏览器截图后量化下发不支持 ETag/304内容不变也会整屏重绘全刷约 17 秒)。**真机验证2026-09**Image 控件只在填写 URL 时抓一次快照、此后不再更新HTML 控件填同一 URL 则每周期重抓——所以对外只用 HTML 控件。当前主形态是 **`server.py` 动态服务**(部署公网 HTTPS局域网地址/明文 HTTP 云端抓不到):三条恒定 URL 并行供对比——方案一 `IMAGE_PATH`(默认 `/e1002.png`)每次请求**内存中现渲染 PNG**Pillow 点阵版);方案二 `HTML_PATH`(默认 `/e1002.html`)每次请求生成 **800×480 现代网页**`html_renderer.py`,系统/网络字体 + 内联 SVG K 线,无 JS由平台截图——**真机实拍:文字抗锯齿灰边被平台抖动成毛边、小字与复杂汉字发糊**;方案三 `HTML_PNG_PATH`(默认 `/e1002-web.png`)同一网页由**服务端无头 Chrome 截图后按最近色(不抖动)固化为六色 PNG**`html_shot.py`),平台拿到纯色图无从抖动,需服务器装 Chrome。**结论2026-09-10方案一为主力**——方案二/三的矢量字体在 125 PPI 下小字必然毛糙(抗锯齿灰边无论由平台还是我们二值化都只能变成锯齿/断笔,见 `output/e1002/font_compare.png`:矢量字体单色渲染 ≈ 抗锯齿后二值化都不如点阵Canvas 逐像素画也改变不了这点Pillow 本身就是逐像素画)。方案二/三端点保留供对比,不再投入。宽版**只用点阵字体的整数倍字号 12/24/36/48/72/84**`E1002Renderer.__init__` 把基类的 F13/F16/FPACE 都映射到 12px——用量行 标签常规字重、右对齐贴进度条(基类类属性 `LABEL_BOLD`/`LABEL_ALIGN`/`LABEL_GAP`),百分比与 pace 数字粗体24px 试过被嫌大;另加 F84 给顶部大时钟),可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC/设备遥测 走**服务端内存缓存 + 有效期**`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`/`DEVICE_TTL`,默认 900/60/300/300 秒0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**Claude/Codex 用量为**账户级、形态 A**:本机 `main.py --target push` 采集Claude 复用 Claude Code 钥匙串登录态调 `/api/oauth/usage`Codex 经 `codex app-server` RPC后推送原始快照`X-Push-Token` 鉴权),服务端内存暂存、**不持任何凭证**、渲染时现算 pace。静态形态 `main.py --target e1002``output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**四段版式A 大时钟 **F84**(墨迹 1679 其右一列三行、左缘对齐F12行距 22① 电池图标电量充电中绿、≤20% 红)② 温度计+温度 蓝色水滴+湿度(①② 数字 F12 伪粗体、字号不变,占位 -- 不加粗2026-09-14③ 星期/日期(与时钟基线对齐);**右侧天气2026-09-14从左到右**:两行左缘对齐(定位水滴+城市 / 天气·温区,底行与时钟同基线、行距 22 天气图标(竖直居中)| **实时温度 F72 粗 3**(比时钟小一号——先做过 F84 与时钟同高被嫌大;与时钟同基线 79、数字墨迹 2579、右贴页边 790「°」不用大字号——F84 下 35px 宽且比数字高 7px——改 F36 粗 2 作上标、墨迹顶沿对齐数字顶沿、补回字形右侧 3px 空白使墨迹右缘贴页边;此前为「城市 / F24 温度 / 天气·温区」三行右对齐图标在左再之前短暂为「图标两行F84 温度」)——设备数据来自 **SenseCraft 设备遥测** `sensecraft_api.py`2026-09-14 新增;先做过「状态栏横排在时钟上方+时钟缩 F60」被否/天气B 左 **Claude Usage 两行5h 合并行 7d——整行高度不变、**一个大边框**,边框内上下精确对半两条 7px **实心**填充紧挨、无空隙2026-09-14 用户要求去掉白隔):上=总 7d、下fable 7d各按自身档位取色右侧百分比与 pace **只显示两者中较大者**,边框与文字随其档位取色;`E1002Renderer._usage_row_split` 以较大者合成一条交给基类 `_usage_row`(已返回条几何)画边框/文字后在内部重绘两条半高实心。2026-09-14 为与右块对称而合并,先做过「两条各带边框的细条+两行数字」版被否;标签去掉 all 前缀)**、右 **Codex Usage 两行5h / 7d——ChatGPT 订阅的 Codex 额度池;服务端未收到推送时 mock 标「示例数据」)**。宽版用量行的**空值不显示 "--"**pct 为 None 按 0 画("00%"、pace "00"、空条、全黑中性色),且 pace 列在 diff=0 时三角位画一个等宽等号「=」2026-09-14 用户要求,原为留空、后短暂用过「-」;已下沉到基类 `_pace`小屏同样——各行进度条右端齐平、等宽2026-09-14Codex Plus 接口 `secondary_window` 就是 null原始 `wham/usage` 已核对5h 行为空是数据如此)。**真 0 的窗口也必须有 pace 列**Claude 5h 空闲时接口回 `utilization: 0.0, resets_at: null`,算不出 time_pct早先渲染器遇「有 pct 无 time_pct」整列跳过该行条就比别行长——现 `data.usage_from_raw` 对 pct=0 且无重置时刻的窗口记 time_pct=0窗口未开始基类 `_usage_row` 只要有 pct 就画 pacetime_pct 缺失按 0 差值画 "00"紧凑单行样式曾试过撑满高度的大卡片被否C **橘喵·今日实时**五列全宽探店单量探店流水团购单量团购流水毛利2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px此前「值 F36 粗 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线故改竖排更早试过名与值同行、左上角角标均已否D **BTC/USDT 时 K 蜡烛图(近 48 小时 = 2 天2026-09-14 由 72 根改 48 根)**免费公开行情OKX 主源、Coinbase/Huobi 备源,`btc_api.py`5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换。六色语义用量进度条含边框、百分比数字三者同色——2026-09-14 用户要求百分比随条色)与 pace 各自**三档、颜色顺序固定 绿→黑→红、两者独立取色**:用量 ≤ `USAGE_OK` 30 绿「健康」3080 黑「正常」|≥ `USAGE_WARN` 80 红「危险」pace ≤ `PACE_TOL` 10 绿「节余」±10 内黑「持平」|≥ +10 红「偏快」。**黄不用**2026-09-14 曾按要求加过 60%80% 黄档(含 `USAGE_NOTICE`/`PACE_WARN` 两个阈值),同日被否并整体移除,别再加回。**今日实时的环比/同比「三角 + 数值」涨绿跌红**2026-09-14 改,与 K 线一致;基类钩子 `_cmp_color`,三色小屏仍涨红);红另用于定位/太阳闪电,蓝=雨雪。
## 命令
```bash
pip install -r requirements.txt # 仅 Pillow>=10.0urllib 用标准库
cp .env.example .env # 首次:复制模板并填入真实凭证 + 设备 host
python3 main.py # 取数据 → 渲染 → 推送(设备 host 取自 .env 的 EPD_HOST
python3 main.py --render-only # 只渲染到 output/dashboard.png不推送本地调试渲染时用这个
python3 main.py --target e1002 # reTerminal E1002 静态形态:渲染 800x480 六色图 + 固定尺寸 index.html 到 output/e1002/,不推送
python3 server.py # reTerminal E1002 动态服务部署公网GET /e1002.png内存现渲染 PNG/ GET /e1002.html现代网页/ GET /e1002-web.png网页截图固化六色/ /health
python3 main.py --target push # 本机:采集 Claude钥匙串登录态→/api/oauth/usage与 Codexcodex app-server RPC账户级用量并推送到 server.py需 .env 的 PUSH_URL + PUSH_TOKEN
python3 claude_usage.py [--force] # 单独调试 Claude 采集(--force 跳过 TTL 缓存)
python3 codex_usage.py [--force] # 单独调试 Codex 采集;需 Codex 已用 ChatGPT 账号 `codex login`(默认 ~/.codex
python3 probe_server.py # 验证工具:同一 URL 是否被平台周期重抓(带访问日志)
sh run.sh # cron 包装脚本cd 项目目录 + vfox python 绝对路径 + 追加日志到 output/cron.log
# 临时覆盖:真实环境变量优先级高于 .env
EPD_HOST=epd400a44.local python3 main.py
```
无测试、无 lint。调试渲染效果就跑 `--render-only``output/dashboard.png`
**配置统一走 `.env`**:所有凭证/参数放同目录 `.env`(已在 `.gitignore`,本机专用;模板见已提交的 `.env.example`)。`config.py` 顶部用零依赖加载器 `_load_dotenv()``.env` 写入 `os.environ`,再由各 `os.environ.get(...)` 读取;采用 `setdefault`,故**真实环境变量cron/命令行注入)优先级高于 `.env`**。`config.py` 内不再保留任何明文凭证fallback 为空串)。可配置项:`EPD_HOST`(局域网设备 mDNS host默认 `epd400a44.local`)、`JM_BASE_API`/`JM_CLIENT_ID`/`JM_CLIENT_SECRET`/`JM_GRANT_TYPE`/`JM_TENANT_ID``QWEATHER_HOST`/`QWEATHER_KEY`/`WEATHER_LOCATION`(默认南京·江宁)/`WEATHER_TTL`、设备遥测 `SENSECRAFT_API_KEY`/`SENSECRAFT_DEVICE_EUI`/`DEVICE_TTL`、服务端 `FETCH_BUDGET``USAGE_WARN`、Claude 采集 `CLAUDE_USAGE_TTL`(快照有效期,语义同 `WEATHER_TTL`0=每次实拉)/`CLAUDE_USAGE_BACKOFF`/`CLAUDE_USAGE_SCOPE_MODEL`、Codex 采集 `CODEX_BIN`/`CODEX_USAGE_HOME`/`CODEX_USAGE_TTL`/`CODEX_USAGE_BACKOFF`、仅宽版的 `USAGE_OK`/`PACE_TOL`/`BTC_BAR`/`BTC_LIMIT`/`BTC_TTL`/`MAO_TTL``E1002_COLOR_STRIP`
**定时部署**:由 macOS **launchd LaunchAgent** 触发 `run.sh`(不再用 cron——cron 在现代 macOS 上易被 TCC/权限静默拦截launchd 是系统原生调度器。LaunchAgent plist 装在 `~/Library/LaunchAgents/com.jm.eink-push.plist`(机器专用,未入库,等价于 `run.sh`/`.env` 的本机产物),`StartInterval=300` + `RunAtLoad=true`:登录/重启后立即跑一次,此后每 5 分钟一次。用 LaunchAgent非 LaunchDaemon是因为脚本依赖当前用户的登录钥匙串Claude Code 凭证)、`~/.codex`Codex 登录态)、`.env`,必须在用户会话下运行——机器需已登录(或开了自动登录)才能保证开机即触发。`run.sh` 内部逻辑不变cron 不继承登录环境,故脚本内 python 写死 vfox 绝对路径、并 `cd` 到项目目录后追加日志到 `output/cron.log``cd``config.py` 才能就近找到 `.env`launchd 自身的 stdout/stderr 另落 `output/launchd.log`(正常应为空,非空说明进程本身起不来,而非业务报错)。常用排查:`launchctl list | grep com.jm.eink-push` 看是否常驻及最近退出码,`launchctl kickstart -k gui/$(id -u)/com.jm.eink-push` 手动立即触发一次;改 plist 后需 `launchctl bootout` + `bootstrap` 重新加载才生效(热改文件不会自动生效)。每 5 分钟触发:业务数据每次都刷新,天气靠缓存、两路用量各有 `CLAUDE_USAGE_TTL`/`CODEX_USAGE_TTL` 有效期缓存(默认 300s同一分钟内小屏渲染与上报两个进程只打一次接口避免高频打接口`data.py`)。
## 架构(数据 → 渲染 → 推送 三层)
`main.py` 串联三层,各层职责单一、低耦合:
- **`data.py`** — 两种形态:本机 `get_dashboard_data()`(带文件缓存,两路用量经本机采集器);服务端用纯拉取函数 `weather_fetch()`/`mao_fetch()`/`btc_fetch()` + `usage_from_raw(now, raw, windows, title)``_usage(now, raw=)`/`_usage_gpt(now, raw=, allow_local=)` 都基于它raw=None 时前者经 `claude_usage` 本机采集、后者经 `codex_usage` 本机采集Codex 采集失败或服务端 `allow_local=False` 时用 `gpt_mock_raw()` 并标 mock。所有「现在」用 `now_tz()`、时间戳格式化用 `_local()``TZ=ZoneInfo(config.TZ_NAME)`),本机与境外服务端结果一致。返回固定结构的 dict`date`/`weather`/`mao`/`usage`/`usage_gpt`/`btc`/`device``device` = `{battery, charging, temp, humidity}`,由 `_device(now)``sensecraft_api` 拉取、文件缓存 `.device_cache.json` TTL=`DEVICE_TTL`,服务端用纯拉取 `device_fetch()`/占位 `device_empty()`)。日期时间用真实系统时间;橘喵今日经营由 `_mao()``jm_api`、天气由 `_weather()``weather_api`和风定位南京·江宁、Claude 用量由 `_usage()``claude_usage`、Codex 用量由 `_usage_gpt()``codex_usage`**Claude 任何异常都回退占位 `"--"`,不回退 mock**Codex 无数据时回退 mock 并显式标「示例数据」。改数据保持返回结构不变即可renderer 依赖其 key`weather.icon` 为和风图标代码;`mao.cols[]``{k, v, vp, hb, tb, wide}``vp` 为数值三段 `(前缀, 主数字, 后缀)`,金额为 `("¥", "2.14", "万")`/不足万 `("¥", "2590", "")`、单量前后缀为空,`v` 是三段拼接的整串供网页版用2026-09-14 用户要求 ¥ 与 万 字号小一点),五列由 `REALTIME_COLS`(标签、接口字段前缀、格式化、是否仅宽版)驱动,`_col_from()``.get()``today{Base}`/`{base}WowRate`/`{base}YoyRate`,字段缺失(旧后端未上团购)该列占位 `--``wide=True`(团购两列)小屏 `render()` 跳过、宽版全显;`hb/tb` 为环比/同比 `(direction, text)`direction ∈ `up`/`down`/`flat`/`None``flat` 为增长率恰等于 0三角位画等号「=」2026-09-14 用户要求,与用量行 pace 持平一致,此前 0 也画上三角)、`None` 不画三角;接口文档见 `../docs/首页看板接口文档-20260911.md`(毛利口径已含团购、金额与增长率为字符串、增长率可 `null``usage.bars[]``{k(短标签,小屏用), label(长标签 5h/7d/fable 7d宽版用), pct(或 None), time_pct?, reset?, scoped}`(宽版把 scoped 行并入紧邻其前的非 scoped 行作同一边框内的下半实心)、`usage.warn`/`usage.ok`/`usage.pace_tol` 为用量与 pace 的三档阈值(小屏只用 warn`usage.updated` 为快照采集时刻;`usage_gpt``usage` 同构、标题「Codex Usage」、只有 5h/7d 两行(快照各窗口可带 `window` 真实秒数pace 优先按它算),服务端未收到推送时为 mock`mock: True`88/22 覆盖红/绿两端)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不用它。宽版另有 `btc``_btc()``btc_api.get_candles(BTC_BAR, BTC_LIMIT)` 取 K 线(默认 1H×48结构 `{symbol, bar, src, candles:[{t, o, h, l, c}], last, chg, chg_label}`chg 时 K 取相对 24 根前收盘24h、日 K 取相对上一根;缓存 `.btc_cache.json`TTL=`BTC_TTL` 默认 5 分钟,周期变更即失效,失败沿用旧缓存,仍无则 candles 为空 → 面板 `--`)。
- **缓存**(为 launchd 每 5 分钟独立进程而设,必须落盘):天气 `.weather_cache.json`TTL=`WEATHER_TTL`,默认 15min、趋势 `.trend_cache.json`(按日期 key每天只拉一次、BTC K 线 `.btc_cache.json`TTL=`BTC_TTL`,默认 5min三者拉取失败沿用旧缓存。两路用量各有采集缓存 `.claude_usage_cache.json`/`.codex_usage_cache.json``usage_common.cached_snapshot`:距上次成功 <TTL`.env` `CLAUDE_USAGE_TTL`/`CODEX_USAGE_TTL`直接用缓存失败沿用旧快照并退避 BACKOFF 只存快照与时间戳、**不存凭证**)。以上缓存文件均已在 `.gitignore`
- **用量的 pace时间维度对比**`usage_from_raw()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)``pace=pcttime_pct`>0 超前↑ / <0 节余↓);窗口空闲pct=0 `resets_at` null time_pct=0故即使 `utilization` 来自缓存/推送暂存pace 仍随时钟准确推进5h 窗口 18000s7d 窗口 604800sCodex 快照带 `window` 时用真实值)。
- **`jm_api.py`** jm-devops 后端统计接口客户端**设备密钥授权**`grant_type=client_secret`免验证码)。先用 `CLIENT_ID + CLIENT_SECRET` `POST /auth/login` token进程内缓存 `expire_in` 提前刷新再带 `Authorization: Bearer <token>` + `clientid` 头调 `/system/statistics/{realtime,recent-days}` 401 token 重登一次重试token 字段为蛇形 `access_token`/`expire_in`历史上曾免鉴权直调更早曾用账号密码+验证码 OCR 登录ddddocr 不稳定现统一为 client_secret 授权——密钥即长期凭证务必走 HTTPS优先用 `JM_CLIENT_SECRET` 环境变量可在后端 `sys_client` 轮换
- **`weather_api.py`** 和风天气QWeather客户端**用户专属 API Host**`QWEATHER_HOST`+ `X-QW-Api-Key` 头鉴权响应 **gzip 压缩** magic number 手动解压)、返回 `code` **字符串**。`get_now()` 取实时`now.text` 中文天气 / `now.temp` / `now.icon` 图标代码`get_today()` `/3d` `daily[0]` 今日温区`QWEATHER_HOST`/`QWEATHER_KEY` 未配置时直接抛错天气区显示 `"--"`
- **`sensecraft_api.py`** SenseCraft HMI 设备遥测客户端`GET {SENSECRAFT_API_BASE}/api/v1/user/device/iot_data/{SENSECRAFT_DEVICE_EUI}` `api-key`用户在 SenseCraft 后台生成 `.env`**2026-09-14 key 曾出现在对话里建议轮换** `result.battery.level/charging``result.sensor.temp/humidity`设备每 `dataaccess.interval`=300s 上报一次 `DEVICE_TTL` 默认 300直连不走代理超时 10s`code!=200` 抛错 状态栏 `--`
- **`btc_api.py`** 比特币 K 线客户端免费公开接口无需密钥`get_candles(bar, limit)`周期代号 `BARS = {"1H", "1D"}` 映射到各源写法逐源兜底 `SOURCES = (okx, coinbase, huobi)`OKX `/api/v5/market/candles?instId=BTC-USDT&bar=`主源本地直连可达且最新)、Coinbase `/products/BTC-USD/candles?granularity=`Huobi `/market/history/kline?period=`各源字段顺序不同OKX/Huobi 最新在前Coinbase [time, low, high, open, close]统一归一为升序 `{ts, o, h, l, c}`Binance 在本地区返回 restricted location不列入
- **`claude_usage.py`** Claude **账户级**用量采集(形态 A跑在本机**复用 Claude Code 自身的 OAuth 登录态**直接 GET `https://api.anthropic.com/api/oauth/usage` `Authorization: Bearer <accessToken>` + `anthropic-beta: oauth-2025-04-20`)—— Claude Code `/usage`claude.ai 用量页同源CLI/桌面 App/网页消耗都算**不依赖 Claude Code 进程在跑不依赖 statusline 或任何插件**2026-09-14 起替代旧的statusline 补丁落盘 `~/.claude/usage-snapshot.json`本项目读文件方案`usage_local.py` 已删)。token 来源按序环境变量 `CLAUDE_CODE_OAUTH_TOKEN` macOS 钥匙串 generic passwordservice `Claude Code-credentials`JSON `claudeAiOauth.accessToken/expiresAt(ms)`)→ `~/.claude/.credentials.json`尊重 `CLAUDE_CONFIG_DIR`)。**铁律**:① **只读、绝不刷新 token**refresh token 每次使用即轮换第三方刷新不写回会让 Claude Code 强制重登多个官方 issue 证实access token 8 小时有效过期抛 `TokenUnavailable` 沿用旧快照 Claude Code 下次启动自动刷新;② **有效期 `CLAUDE_USAGE_TTL` 内不再调、建议 ≥300 秒**突发调用 429 会封约 24 小时并连带 Claude Code `/usage`claude.ai 一起看不到);③ 直连不走代理`normalize()` 优先解析 `limits[]``kind`=session/weekly_all/weekly_scoped模型限定周额度按 `scope.model.display_name == CLAUDE_USAGE_SCOPE_MODEL`Fable识别——2026 年中起顶层 `seven_day_opus/sonnet` 已置 null只能这样取顶层 `five_hour/seven_day` 兜底输出与旧快照同构 `{five_hour, seven_day, seven_day_fable|None, updated_at, source}``get_usage(force=)` `usage_common.cached_snapshot` 带缓存/最小间隔/退避`status` fetched/cache/stale/backoff官方合规页禁止第三方收集存储中介claude.ai 凭证未专门点名此端点个人自用只读监控属灰色地带无封号报告——所以凭证只留本机服务端不碰
- **`codex_usage.py`** Codex **账户级**用量采集(形态 A跑在本机经官方 **`codex app-server` stdio JSON-RPC** ChatGPT 订阅的 **Codex 额度池**CLI/IDE 扩展/Codex Web/ChatGPT 桌面 App Codex 共用)。**调研结论2026-09-14**ChatGPT 订阅的**普通聊天**额度没有任何可编程的账户级读取途径私有端点在 Sentinel/Cloudflare 门后且只在被限流时才有值官方明确聊天与 Codex 分开计量故右侧块如实命名Codex Usage」。协议本机 codex-cli 0.154 验证`initialize` 通知 `initialized` `account/rateLimits/read`params `{excludeResetCreditDetails:true}`响应 `rateLimits.primary/secondary{usedPercent, windowDurationMins, resetsAt(epoch 秒)}` + `planType` + 多桶 `rateLimitsByLimitId`未登录 ChatGPT 账号时 errorcodex account authentication required to read rate limits」。**窗口归类按 `windowDurationMins`**(≤12h five_hour,≥5 seven_dayPro 计划常只回一个窗口另一个 null不能按 primary/secondary 位置判断快照各窗口带 `window` 真实秒数供 pace 为什么不直调 `chatgpt.com/backend-api/wham/usage`token 刷新由 Codex CLI 代管refresh token 轮换外部自刷会踢掉 CLI 登录官方 CI 文档也明示不要自己调刷新接口」)。**登录**2026-09-14 用户直接在默认 `~/.codex` ChatGPT 账号 `codex login``auth_mode=chatgpt` API Key 仍保留在 auth.json`config.toml` 的第三方中转 base_url 只影响推理不影响额度读取 `CODEX_USAGE_HOME` 默认留空跟随默认目录若要隔离可设独立 `CODEX_HOME`目录不存在会自动建并在其下 `codex login`。**`CODEX_BIN` `config._which_codex()` 解析为绝对路径**launchd PATH 不含 `/opt/homebrew/bin`曾报找不到 codex 可执行文件进入退避`.env` 已写死 `/opt/homebrew/bin/codex`)。子进程剥掉代理环境变量直连chatgpt.com 本地直连可达)。**真实数据Plus 计划**`rateLimits.primary` 只有周窗口`windowDurationMins=10080`)、`secondary=null``rateLimitsByLimitId` 仅一个桶 `codex`—— 5h 行为 `--`7d 行有值`buckets` 原样附带若将来出现第二个桶再决定是否加第三行
- **`usage_common.py`** 两个采集器共用的骨架 `cached_snapshot(path, ttl, backoff, fetch, force)`距上次成功 <ttl 直接返回缓存ttl 来自 `.env` `WEATHER_TTL`/`MAO_TTL`/`BTC_TTL` 同语义0=每次实拉launchd 5 分钟先后起小屏渲染」「上报两个进程只打一次接口失败沿用旧快照并退避 backoff 期内不再尝试从未成功且失败则抛出缓存文件 `.claude_usage_cache.json`/`.codex_usage_cache.json` 只存快照与时间戳**不存凭证**。
- **`renderer.py`** `DashboardRenderer.render(data)` 返回 PIL `'RGB'` 模式、**仅黑//红三色**组件层提供可覆写钩子/参数供宽版复用`_usage_colors()`用量行 填充/边框/文字 三色元组)、`_pace_color()`pace 数值与三角颜色)、`_usage_row(label_w=)``_weather_icon(wet=)`降水元素颜色)、`_label(x0,x1)``_cmp_at()`环比/同比左对齐版`_cmp_line` 居中版基于它)、`_cmp_color(direction, fg)`三角与数值颜色三色版涨红宽版覆写涨绿跌红`flat`/`None` 两版都黑数值 F12 伪粗体字号不变占位 `--` 不加粗——加粗会粘成一条长横2026-09-14)、`_eq(cx, cy, w)`等号「=」,pace 持平与环比/同比 `flat` 共用)、`_value(cx, y, col)`今日实时数值主数字 F24 前缀 ¥/后缀 F12 小号伪粗体按数字墨迹底沿基线对齐整体居中两版共用字体另有 `F36`3 倍整数缩放)。`render_to_file()` 落盘布局硬编码为**上中下三段**两道 `hdash` 横规分隔y=90 / 200
- **A 顶部**090大时钟(F48)+日期/ 天气红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。
- **B 中部主角 Claude Usage**`_label` 标题 + 两条 `_usage_row`每条单行 = 左标签 `5h`/`7d`(F13) + 粗进度条 + 百分比(F16) + 时间维度对比 pace(F13`_pace`)`pct≥warn`该条进度条(含边框)与百分比转红预警pace 用上/下三角替代 +/超前 / 节余↓)。整行标签·进度条·百分比·三角 `_mid()` 按墨迹竖直中心对齐到同一中线
- **C 底部 橘喵今日实时**`_label` + 三列——过滤掉 `wide` 只显示探店单量/探店流水/毛利版式对齐主分支`■ 橘喵·今日实时` + `上次更新`列内 (F12)/(`_value`F24 粗主数字 小号 ¥/)/环比·同比(`_cmp_line`)相对标题偏移 +18/+34/+64/+82底部留 ~11px 下边距
- **红色点缀系统**改配色务必克制模块顶部 `BLACK/WHITE/RED``RED=(255,0,0)` 是给设备红通道的明确信号红只用在:① 定位水滴`_loc_pin(fg=RED)`)② 天气太阳/闪电`_weather_icon(accent=RED)`////月仍黑)③ 用量进度条与百分比(`pct≥warn`) + pace 超前(↑) 环比/同比的三角与数值`_cmp_line`/标签仍黑)。**配色铁律**红与黑同属深色红叠黑对比极低故红只压白底或作大块实心白字压红**绝不红字压黑底**。加红先问是否承载语义」。
- **`_trend_chart`当前隐藏**近30天三指标折线 `_styled_polyline()` solid/dashed/dotted 区分主指标(流水)红线各指标按自身极值独立归一化已保留但 `render()` 未调用想恢复即在 C 前挂回
- **天气图标全矢量手绘**无图片资源`_weather_category()` 把和风 icon 代码归并为 8 sunny/clear_night/cloudy/overcast/thunder/rain/snow/fog识别不了回退 cloudy再由 `_weather_icon()` `_sun`/`_cloud` 等基元按 1bit 描边画出改天气展示从这里动
- **`pusher.py`** 直连局域网墨水屏设备喵喵固件开源https://gitee.com/gxp666111/miaomiao`web/` 即设备自带管理页可浏览器打开 `http://<EPD_HOST>/` 调试同一套接口)。`push_image()` 先调 `GET /images` 拿画廊已存图片名单逐个 `POST /delete_image?name=`清空单张删除失败不阻塞后续 `POST /upload``Content-Type: image/png`body 为原始 PNG 字节 multipart触发设备转换+刷屏。**清空画廊是刻意设计**设备把每次 `/upload` 都追加存一份到画廊`GET /images` 可见不清理会越推越多且设备当前 `current_mode` slideshow 会轮播到旧图先清后传使画廊恒为 1 等价于始终静态展示最新一张。**清理非核心**拉取画廊列表本身失败也不阻塞后续 `/upload`跳过清理直接推)。`_request()` 对连接类异常`urllib.error.URLError` mDNS `.local` 域名解析瞬时失败内置重试`RETRY_TIMES=2`间隔 `RETRY_DELAY=2s`)——mDNS 基于组播 UDP偶发丢包很正常单次失败不该让整轮推送直接跳过历史上曾走 Zectrix 公网 API`POST /devices/{id}/display/image` + 设备 MAC + `ZECTRIX_API_KEY`现统一为局域网直连
- **`renderer_e1002.py`** reTerminal E1002 目标`E1002Renderer(DashboardRenderer)` **k=1** 复用组件字号 12/24/48 与三色版一致只重写版式与配色钩子基类抽出类属性 `PCT_TRACK`小屏 F16 百分比的手动字距宽版置 0—— F16 映射到 12px加粗后由 `_track()` 自动加 1px 字距基类另抽 `FPACE` pace 字体与标签 F13 解耦)。四段三道横规各段 y 由上一段实际底部推算A 顶部 090 **大时钟 F84 粗 3** `getbbox` 把墨迹顶沿放到 16底沿 79占满整段且保住原上//下边距其右 14px 起一列三行左缘对齐电池行温度湿度行`_status_battery`/`_status_env`)、星期/日期 F12按日期数字底沿与时钟**基线对齐**三行中心约 30.5/52.5/74.5行距 22天气右定位城市 / 天气·温区两行 图标 温度 F72+「°」F36 上标右贴 x=790见分支说明B 用量 `_usage_block`×2 `usage` 10386竖规 x=400 `usage_gpt` 414790`_label` 标题 + 右侧上次更新 HH:MM」(与今日实时同款文案 mock 示例数据」,行用基类紧凑 `_usage_row`行距 31宽版把 `bar["label"]` 长标签代入 `k``label_w` 按本块最长标签自适应C `_realtime` 今日实时五列全宽`mao.cols` 全部 `wide` 列内四行 `ctext`/`_cmp_line` 逐行水平居中—— F12 y0+21 / `_value` F24 小号 ¥/ y0+38 / 环比 y0+72 / 同比 y0+88按墨迹框标题 ~9px——2026-09-14 整列下移 4px ~5px 用户觉得挤 ~9px环比 ~6px竖规 y0+26102区块底 y0+104D `_btc_chart` BTC K 线`data.btc`标题按 `bar` 显示时K/日K」:右侧最新价 F12 + 口径 `chg_label`24h/1d+ 涨跌三角 + 涨跌幅F12 2026-09-14 加粗涨绿跌红每根蜡烛 1px 影线 + 实心实体槽位 60% 取奇数)、 绿 否则 右侧 66px 标签列放最高/最低/最新价最新价横贯一道点规标签避让顶底底部基线 + //末时间标签 `t`无数据居中 `--`)。用量块 `_usage_block` 返回末条进度条底部B/C 分割线取其 +16px用户反馈太挤)。配色钩子覆写`_usage_colors` 三档(≥`USAGE_WARN` |≤`USAGE_OK` 绿其间黑填充边框百分比文字三者同色)、`_pace_color` 三档(≥+`PACE_TOL` |≤−`PACE_TOL` 绿其间黑阈值由 `usage.ok`/`usage.pace_tol`来自 config `_usage_block` 内注入天气图标 `wet=BLUE` 让雨滴/雪花着蓝可选六色测试色条 `_color_strip``E1002_COLOR_STRIP=1` 打开默认关)。`write_page()` 写出固定 800×480 `index.html`零边距`image-rendering: pixelated`PNG 带时间戳参数防缓存)。**为何固定尺寸而非自适应**云端截图视口/DPR 不可控自适应会重排+抗锯齿产生中间色再被平台抖动固定页里的 1:1 PNG 每个像素本就是六色之一截图零损失——已用无头 Chrome `--window-size=800,480` 截图验证与 PNG 逐像素一致设备端 HTML 控件须铺满整个画布历史均已弃用k=2 整体放大版;「左用量/右今日实时/ 30 天折线;「用量大卡片撑满
- **`server.py`** E1002 动态服务stdlib `ThreadingHTTPServer`无第三方依赖)。`build_data()` `(data, 各路状态)`日期按 `config.TZ_NAME` 时区现算四路上游天气/橘喵/BTC/设备遥测 `_cached(key, ttl, fetch)`——`STATE.last_good[key]` 未过期`time - STATE.fetched_at[key] < ttl`直接用过期的**并行**提交到线程池 `_POOL``_refresh` `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`成功更新数据与时间戳`build_data` `concurrent.futures.wait` 整体最多等 **`FETCH_BUDGET`** 默认 5`.env` 可调再由 `_settle` 结算完成`fetched(0.27s)`带耗时/失败沿用旧值 `stale`/从未成功占位 `none`**未完成 `slow→stale`用上一次成功值 `slow→none`占位**拉取线程继续跑完写入缓存供下次请求stale-while-revalidate`STATE.inflight[key]` 保证同一路进行中不重复实拉响应上限 预算 + 渲染任一路上游卡死socket 超时和风/橘喵 30sBTC 8s不再拖垮整图。**2026-09-14 前为串行实拉**三路全拉约 1.12.6s单路卡住即整体超时改并行后三路全拉约 0.75s取最慢一路)。日志每行末尾 `[weather:cache(12s) mao:fetched(0.06s) btc:slow→stale]` 可直接看命中/耗时/超预算情况`/health` `cache_age_s`/`ttl_s`/`fetch_budget_s`/`inflight`用量取 `STATE.raw`推送暂存)→ `data._usage(now, raw=…)`/`data._usage_gpt(now, raw=…)`未推送时 Claude `--`ChatGPT mock端点`GET IMAGE_PATH` `render_png_bytes()``_RENDER_LOCK` 串行化Pillow 渲染器非线程安全`GET HTML_PATH` `html_renderer.render_html()``GET HTML_PNG_PATH` `html_shot.screenshot_html()`+`quantize6()``?raw=1` 跳过量化`GET /health` JSON`POST /push/usage|/push/usage_gpt`body=原始快照 JSON,≤64KB`X-Push-Token`==`PUSH_TOKEN`,为空拒绝);`GET /push/*` 查看暂存同令牌)。所有响应 `Cache-Control: no-store`每请求打一行日志 `CF-Connecting-IP`)。`PUSH_STATE_PATH` 非空时推送数据落盘原子替换重启恢复图片永不落盘实测三路全实拉约 1.11.3s天气 0.28s橘喵 0.06sOKX 0.7s 串行全部命中缓存约 0.02s渲染 20ms + 编码 14ms)。
- **`html_shot.py`** 方案三`screenshot_html(html)` 用无头 Chrome`find_chrome()` 自动探测或 `CHROME_BIN`Linux root `CHROME_EXTRA_ARGS=--no-sandbox` HTML 截成 800×480临时目录用后即删`_SHOT_LOCK` 串行化**切勿传 `--user-data-dir` 指向全新目录**——新版 `--headless=new` 对空 profile 首启会卡死到超时实测 A/B/D/E 全超时只有不带它的 C 2 秒完成)。`quantize6(img)`每通道二值化阈值 `HTML_PNG_THRESHOLD` 默认 160偏向保墨迹细笔画浅灰边归黑而非丢白)→ Pillow 调色板 `quantize(dither=NONE)` 映射到 `SPECTRA6`输出保证只含六色服务端 `?raw=1` 返回未量化截图供对照实测单次约 3.3sChrome 启动为主)。
- **`html_renderer.py`** 方案二网页版 HTML+CSS+内联 SVG JS数据服务端写死字体栈 PingFang/微软雅黑/Noto Sans SC + Google Fonts 兜底平台截图机若无中文字体会掉字能否加载取决于其出网能力颜色只用六色纯值配色规则与 PNG 版一致用量/pace 三档涨红跌黑K 线涨绿跌红版式同构K 线标题右侧价格用 `<strong>``h3 b::before` 的方块装饰只给标题)。
- **`push_client.py`** 本机推送端`SOURCES` 两路——`claude_usage.get_usage()` `POST {PUSH_URL}/push/usage``codex_usage.get_usage()` `POST {PUSH_URL}/push/usage_gpt`推前去掉 `status` 两路彼此独立一路失败只打日志两路都失败才非零退出直连不走代理连接类异常重试 2 4xx/5xx 直接报错
- **`probe_server.py`** 验证工具不参与正常流程极简 HTTP 服务`/probe.png` `--every` 秒重绘一张带大号时刻+序号的 800×480 探针图响应 `Cache-Control: no-store`每次请求打印时间/UA/来源 IP `CF-Connecting-IP`)。用途配合 `cloudflared tunnel --url http://localhost:8787` 暴露成公网 HTTPS填入 SenseCraft Image 控件**访问日志**判断平台对同一 URL 是否每个刷新周期重抓成立则可直接覆盖同一 URL 的图片省掉 HTML 页那一层)。**结论已验证**Image 控件不重抓HTML 控件重抓见分支说明
- **`config.py`** 顶部 `_load_dotenv()` 先加载同目录 `.env`再定义 API设备画布尺寸字体路径推送参数凭证 fallback 为空串真实值来自 `.env`/环境变量
## 墨水屏渲染约束(改 renderer.py 必读)
设备是 **三色(黑/白/红)、无真灰阶**——灰阶只能抖动成网点难看所以底色只用黑白 + 红作稀缺强调靠构图/字号/留白建立层级**不要引入灰色填充或抖动**
- **红只作语义强调**方向)、警示闪电)、强调定位/主折线红占墨保持很小<10% 墨迹滥用会稀释注意力破坏层级整图**只允许三种纯色** `(0,0,0)/(255,255,255)/(255,0,0)`——渲染后可用 `img.getcolors()` 自查出现第四种色多为抗锯齿灰边即为 bug矢量图元(line/ellipse/polygon/arc)默认不抗锯齿故为纯色文字靠 `fontmode="1"` 保持纯色
- **`DashboardRenderer(k=…)` 缩放系数**所有组件`_label`/`_cmp_line`/`_usage_row`/`_pace`/`tri` 的像素常量与伪粗体偏移都乘 `k`字体按 `12*k/24*k/48*k` 创建`k=1` 400×300 原版改组件后务必用固定夹具比对 `k=1` 输出逐像素不变E1002 版当前用 `k=1`k=2 整体放大在 800×480 上字过大已否决k 参数保留供更高分辩率设备使用字体属性名`F12/F24/F48`表达层级而非绝对像素
- 字体固定用 **Fusion Pixel 12px 点阵字体**`fonts/`OFL 协议首选整数倍尺寸 **12/24/48**`F12/F24/F48`最锐利用量区为拿到合适的字号层级另用了 **F13/F16**1.08x/1.33x 非整数倍笔画略不均属有意取舍 `d.fontmode = "1"` 关抗锯齿仍保持纯色不糊跨字号竖直居中统一用 `_mid()` `getbbox` 墨迹中心对齐)。
- 加粗用伪粗体」:`_draw()` 1~bold px 水平偏移叠绘笔画点阵字体只有单一字重)。**12px 加粗自动加 1px 字距**`_track()`2026-09-1412px 字形之间只有 1 列空隙伪粗体右偏 1px 正好填满相邻字粘连13/24px 空隙 2 列不受影响`text/rtext/ctext/_mid` 内部都走它宽版映射到 12px F13/F16/FPACE 同样命中手动推进 x 或居中时用 `_tw_b(s, ft, bold)` 取含粗体偏移与字距的实际占宽别再手写 `_tw + bold`
- 推送时 `DITHER = False`硬阈值纯黑白图最锐利勿改成 `true`
- 坐标字号均为像素网格上的硬编码常量调布局时注意各区分隔线 `hdash` y 值与下方组件位置联动
## 注意
- 所有凭证均在 `.env`不提交`config.py` 不再内置明文 fallback分享代码时给出 `.env.example` 即可真实 `.env` 切勿入库
- `.gitignore` 已忽略`output/``__pycache__/``*.pyc`缓存文件 `.weather_cache.json`/`.trend_cache.json`/`.btc_cache.json`/`.device_cache.json`/`.claude_usage_cache.json`/`.codex_usage_cache.json`本机专用的 `run.sh`含凭证的 `.env`模板 `.env.example` 入库)。改缓存逻辑后想强制重拉删掉对应缓存文件即可
## 验证与运维手法(会话沉淀)
- **像素回归** `renderer.py` 组件后用固定夹具比对 HEAD 版与工作区版 `DashboardRenderer` k=1 输出逐像素相同
`git show HEAD:renderer.py > /tmp/orig/renderer.py`切换 `sys.path` 分别 import 渲染`ImageChops.difference(a,b).getbbox() is None`
六色版渲染后 `set(img.getcolors())` 必须 `SPECTRA6`夹具需覆盖pct=None、≥warn 红档、≤ok 绿档direction=None、空 candles
- **HTML截图逐像素核验**`"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 --window-size=800,480 --screenshot=/tmp/s.png file://…`**勿加指向空目录的 `--user-data-dir`**卡死到超时)。
- **server.py / cloudflared 常驻2026-09-15 起走 launchd**`~/Library/LaunchAgents/com.jm.eink-server.plist`vfox python 绝对路径跑 `server.py` `com.jm.eink-cloudflared.plist``cloudflared tunnel --url http://127.0.0.1:8790 --no-autoupdate --protocol http2` `RunAtLoad`+`KeepAlive`日志仍落 `output/server.log`/`output/cloudflared.log`机器专用未入库。**为什么**此前两者在 Claude Code 工具 shell `nohup … &` 启动会话回收时被同一秒 SIGTERM2026-09-15 10:11 两进程同时退出电脑重启后也不会自起当天早上重启即中断)。重启/查看`launchctl kickstart -k gui/$(id -u)/com.jm.eink-cloudflared`隧道地址会变需更新 SenseCraft)、`launchctl list | grep com.jm.eink`改代码后 kickstart server 即生效 plist `launchctl bootout gui/$(id -u)/<label>` `bootstrap`以下手工命令仅作应急参考
- **server.py 手工启动应急**`nohup env -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u ALL_PROXY python3 server.py >> output/server.log 2>&1 &`
重启前按 PID 杀干净`for p in $(lsof -nP -iTCP:8790 -sTCP:LISTEN -t); do kill $p; done`——`pkill -f` 曾漏杀 `*:8790` `127.0.0.1:8790` 两个监听可并存旧代码继续应答表现为新端点 404)。
- **cloudflared 快速隧道**免账号地址每次重启都变需到 SenseCraft 更新 URL
`nohup env -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u ALL_PROXY cloudflared tunnel --url http://127.0.0.1:8790 --no-autoupdate --protocol http2 > output/cloudflared.log 2>&1 &`
地址 `grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' output/cloudflared.log`已写入 `output/tunnel_url.txt`)。
**快速隧道会被 Cloudflare 侧静默回收**2026-09-13 一条跑了约 2 天后失效进程仍在server 仍正常但日志循环报 `Unauthorized: Tunnel not found`公网地址 530
设备收不到数据先看 `output/cloudflared.log` 而不是 server修复只能重启 cloudflared 拿新地址并更新 SenseCraft
重启要点旧进程对 SIGTERM 退出很慢数秒确认 `ps aux | grep "cloudflared tunnel"` 已清空再起新的默认 QUIC 注册曾 `context deadline exceeded` 后自行退出 `--protocol http2` 一次成功
- **shell **本机是 fish——`env -u …` 不能存进变量再当命令执行含中文的 Python heredoc `python3 -X utf8 - <<'EOF'`否则 stdin 解码报错`gh` 未登录且 GitHub API 匿名限流取源码走 `curl raw.githubusercontent.com`
- **用量采集验证**`python3 claude_usage.py --force` 应打出 `status: fetched` `seven_day_fable` 非空紧接着不带 `--force` 再跑应为 `cache`缓存文件里不得出现 token`python3 codex_usage.py --force` 未登录时报authentication required请先执行 CODEX_HOME= codex login」,登录后应有 `five_hour`/`seven_day`历史statusline 补丁`~/.claude/statusline.sh`仍在本机产出 `~/.claude/usage-snapshot.json`但本项目已不读它
- **定时任务**`run.sh` 末尾已追加 `main.py --target push`vfox python 绝对路径`|| true`Claude/Codex 两路账户级用量随喵喵推送每 5 分钟采集并同步到 server.py推送暂存落盘 `output/push_state.json`
- **上游配额**server 三路上游已有 TTL 内存缓存天气默认 15 分钟 和风约 192 /改有效期只动 `.env`不动代码本机 `.env` 当前 `MAO_TTL=0`/`BTC_TTL=0`每次请求实拉)。
- **「平台抓取失败排查顺序**:① `output/cloudflared.log` 隧道是否被回收;② `output/server.log` 该时刻有无该请求平台 IP 13.91.x.x ~5m45s 一次)——没有记录说明请求根本没到我们这里问题在平台/隧道;③ 有记录看末尾状态串与耗时`slow→` 表示某路超 `FETCH_BUDGET`本次用了旧值2026-09-14 一次怀疑多路 TTL 同时到期导致超时」,核查日志所有平台请求均 200最慢 4.23s 且该次天气还是缓存三路同时实拉的两次仅 1.21.5s——猜想不成立MAO/BTC TTL=0 本就每次同时实拉真正的隐患是当时串行拉取 + 30s socket 超时已改并行 + 预算
- **SenseCraft 未公开后端**从网页前端 bundle 提取`https://sensecraft-hmi-api.seeed.cc` `Authorization: <token>`token localStorage `user-storage`
`POST /api/v1/oss/file/upload`multipart `file`+`type=image``file_url`)→ `POST /api/v2/user/page``{pages:[{name,type:img|url|layout,data,dither,thumbnail,resolution:"800x480"}]}`
`POST /api/v2/user/playlist/upsert_pages` `POST /api/v2/user/device/down_link``{mac_address,playlist_id,type,refresh_interval?,deep_sleep_enabled?}`
另有 `POST /api/v2/user/device/config``POST /render/preview``{url,img_format,resolution,dither}`)。**未采用**未公开可随时变更仅作备选记录