- 12px 点阵字形间只有 1 列空隙,伪粗体右偏 1px 正好填满导致左右重叠;13/24px 空隙 2 列不受影响 - 基类新增 _track():12px 且加粗时自动加 k 字距,text/rtext/ctext/_mid 统一走它,宽版映射到 12px 的 F13/F16/FPACE 同样命中 - 新增 _tw_b() 取含粗体偏移与字距的实际占宽,替换环比/同比居中、金额前后缀、状态栏推进、BTC 涨跌幅、用量行进度条右端的手写宽度 - 夹具回归:小屏 F13/F16 部分逐像素不变,各行进度条右端仍齐平 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
111 lines
45 KiB
Markdown
111 lines
45 KiB
Markdown
# 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/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**(墨迹 16~79)+ 其右一列三行、左缘对齐(F12,行距 22):① 电池图标+电量(充电中绿、≤20% 红)② 温度计+温度 | 蓝色水滴+湿度(①② 数字 F12 伪粗体、字号不变,占位 -- 不加粗,2026-09-14)③ 星期/日期(与时钟基线对齐)——设备数据来自 **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-14;Codex 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 就画 pace(time_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 绿「健康」|30~80 黑「正常」|≥ `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.0;urllib 用标准库
|
||
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)与 Codex(codex 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=pct−time_pct`(>0 超前↑ / <0 节余↓);窗口空闲(pct=0 且 `resets_at` 为 null)记 time_pct=0;故即使 `utilization` 来自缓存/推送暂存,pace 仍随时钟准确推进。5h 窗口 18000s、7d 窗口 604800s(Codex 快照带 `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 password(service `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 账号时 error「codex account authentication required to read rate limits」。**窗口归类按 `windowDurationMins`**(≤12h → five_hour,≥5 天 → seven_day),Pro 计划常只回一个窗口、另一个 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 顶部**(0–90):大时钟(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 顶部 0–90 **大时钟 F84 粗 3**(按 `getbbox` 把墨迹顶沿放到 16、底沿 79,占满整段且保住原上/左/下边距),其右 14px 起一列三行左缘对齐:电池行、温度湿度行(`_status_battery`/`_status_env`)、星期/日期 F12(按日期数字底沿与时钟**基线对齐**;三行中心约 30.5/52.5/74.5,行距 22);天气右(贴 x=790);B 用量 `_usage_block`×2(左 `usage` 10–386|竖规 x=400|右 `usage_gpt` 414–790):`_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+26~102,区块底 y0+104;D `_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 超时和风/橘喵 30s、BTC 8s)不再拖垮整图。**2026-09-14 前为串行实拉**(三路全拉约 1.1~2.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.1~1.3s(天气 0.28s、橘喵 0.06s、OKX 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.3s(Chrome 启动为主)。
|
||
- **`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-14):12px 字形之间只有 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 本机运维**:启动 `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.2~1.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}`)。**未采用**(未公开、可随时变更),仅作备选记录。
|