Files
eink-push/CLAUDE.md
YANG JIANKUAN b1a99a0e6b feat: 今日实时新增团购单量/流水字段,E1002 改五列竖排版式
今日实时由三列(单量/流水/毛利)扩展为五列,新增团购单量与团购流水;小屏
仍只显三列(跳过 wide 列),宽版 E1002 从「值 F36+右侧叠放环比同比」改为
四行竖排(名/值/环比/同比),避免五列每列 157px 放不下的问题。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 16:12:01 +08:00

103 lines
32 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**`E1002Renderer.__init__` 把基类的 F13/F16 换成 12/24px可用 `all(v % 12 == 0 for v in 字体尺寸)` 自查。天气/橘喵/BTC 走**服务端内存缓存 + 有效期**`WEATHER_TTL`/`MAO_TTL`/`BTC_TTL`,默认 900/60/300 秒0=每次实拉;请求时过期才实拉并更新时间戳,失败沿用旧值、不更新时间戳),服务**不读本机任何文件**Claude/ChatGPT 用量**由本机 `main.py --target push` 推送原始快照**`X-Push-Token` 鉴权),服务端内存暂存、渲染时现算 pace。静态形态 `main.py --target e1002``output/e1002/index.html` + `dashboard.png`)仍保留。渲染由 `renderer_e1002.py` 完成:继承 `DashboardRenderer` 组件、**k=1 字号与三色版完全相同(不放大,放大后字太大很傻)**四段版式A 时钟/天气B 左 **Claude Usage 三行all 5h / all 7d / fable 7d标签沿用 statusline 写法、小写开头)**、右 **ChatGPT Usagemock标「示例数据」**紧凑单行样式曾试过撑满高度的大卡片被否C **橘喵·今日实时**五列全宽探店单量探店流水团购单量团购流水毛利2026-09-11 接口新增团购字段后由三列改五列;列内四行逐行居中:名 F12 / 值 F24 粗 / 环比 / 同比,与小屏同构——五列每列仅 ~157px此前「值 F36 粗 右侧叠放的环比/同比」并排组约 194px 放不下、降 F24 后 156px 仍贴分隔线故改竖排更早试过名与值同行、左上角角标均已否D **BTC/USDT 时 K 蜡烛图(近 72 小时 = 3 天)**免费公开行情OKX 主源、Coinbase/Huobi 备源,`btc_api.py`5 分钟缓存;周期/根数 `BTC_BAR`/`BTC_LIMIT` 可配,日 K 也支持;**涨绿跌红**)。此前放过 30 天折线图(用户明确不要)和「近 7 天」数据表(已被 K 线替换)。六色语义:用量进度条(含边框)与 pace 各自**三档**——绿=健康(用量 ≤ `USAGE_OK` 30 / pace ≤ `PACE_TOL` 10、黑=正常、红=危险(用量 ≥ `USAGE_WARN` 80 / pace ≥ +10百分比数字只在红档转红绿小字在白底发虚。红另用于涨/定位/太阳闪电,蓝=雨雪;**黄不用**——用户明确不要黄色进度条(曾有 60%80% 黄「注意」档,已移除)。
## 命令
```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/usage-snapshot.json 推送到 server.py需 .env 的 PUSH_URL + PUSH_TOKEN
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``USAGE_LOCAL_PATH`/`USAGE_WARN`、仅宽版的 `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/usage-snapshot.json``.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 分钟触发:业务数据每次都刷新,天气靠缓存、用量读本地文件,避免高频打接口(见 `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=)` 都基于它raw=None 时前者读本地快照、后者用 `gpt_mock_raw()`)。所有「现在」用 `now_tz()`、时间戳格式化用 `_local()``TZ=ZoneInfo(config.TZ_NAME)`),本机与境外服务端结果一致。返回固定结构的 dict`date`/`weather`/`mao`/`usage`/`usage_gpt`/`btc`。日期时间用真实系统时间;橘喵今日经营由 `_mao()``jm_api`、天气由 `_weather()``weather_api`和风定位南京·江宁、Claude 用量由 `_usage()``usage_local`**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可renderer 依赖其 key`weather.icon` 为和风图标代码;`mao.cols[]``{k, v, hb, tb, wide}`,五列由 `REALTIME_COLS`(标签、接口字段前缀、格式化、是否仅宽版)驱动,`_col_from()``.get()``today{Base}`/`{base}WowRate`/`{base}YoyRate`,字段缺失(旧后端未上团购)该列占位 `--``wide=True`(团购两列)小屏 `render()` 跳过、宽版全显;`hb/tb` 为环比/同比 `(direction, text)`direction 为 `None` 时不画三角;接口文档见 `../docs/首页看板接口文档-20260911.md`(毛利口径已含团购、金额与增长率为字符串、增长率可 `null``usage.bars[]``{k(短标签,小屏用), label(长标签 all 5h/fable 7d宽版用), pct(或 None), time_pct?, reset?, scoped}``usage.warn`/`usage.ok`/`usage.pace_tol` 为红/绿/pace 阈值(小屏只用 warn`usage.updated` 为快照落盘时刻;`usage_gpt``usage` 同构,目前是 **mock**`mock: True`;三行故意取 88/71/22 以常驻红/黑/绿三档),接真实数据源时替换 `_usage_gpt()` 即可)。注:`_mao` 仍会拉 `mao.trend`,但两个版面当前都不用它。宽版另有 `btc``_btc()``btc_api.get_candles(BTC_BAR, BTC_LIMIT)` 取 K 线(默认 1H×72结构 `{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三者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore`
- **用量的 pace时间维度对比**`_usage()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)``pace=pcttime_pct`>0 超前↑ / <0 节余↓);故即使 `utilization` 来自可能滞后的本地缓存pace 仍随时钟准确推进5h 窗口 18000s7d 窗口 604800s
- **`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` 未配置时直接抛错天气区显示 `"--"`
- **`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不列入
- **`usage_local.py`** Claude Code 用量本地读取**不发任何网络请求****生产者/消费者**模式只读 `config.USAGE_LOCAL_PATH`默认 `~/.claude/usage-snapshot.json`)。**生产者 = 打过补丁的 claude-statusline**本机 `~/.claude/statusline.sh`把每次 Claude Code stdin 喂来的**权威实时**额度落盘为该快照schema `/api/oauth/usage``five_hour`/`seven_day` `utilization` + `resets_at`并额外写入 **`seven_day_fable`**Fable 模型限定的 7 天额度——stdin 里没有只在 `/api/oauth/usage` 返回的 `limits[]` `scope.model.display_name == "Fable"` 那项statusline stdin 分支也会每 5 分钟刷新一次 API 缓存 `/tmp/claude/statusline-usage-cache.json` 以拿到它 token/断网时该键为 `null``data.USAGE_WINDOWS` `scoped=True` 标记这一行400×300 小屏 `render()` 跳过 scoped 800×480 宽版显示本模块作消费者读取活跃使用时快照几乎持续刷新准确空闲时停在最后一次文件缺失/损坏用量区 `"--"``resets_at` 可能是 **epoch 数字**Claude Code stdin 给的就是 epoch ISO `data._parse_reset_ts` 两者兼容。(曾评估直调 `/api/oauth/usage`改为读本地快照以免网络请求。)
- **`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` 居中版基于它字体另有 `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)/(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 与三色版一致只重写版式与配色钩子四段三道横规各段 y 由上一段实际底部推算A 顶部 090 时钟/日期左天气右 x=790B 用量 `_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+17 / F24 y0+34 / 环比 y0+68 / 同比 y0+84按墨迹框 ~9px环比 ~6px竖规 y0+2298区块底 y0+100D `_btc_chart` BTC K 线`data.btc`标题按 `bar` 显示时K/日K」:右侧最新价 F12 + 口径 `chg_label`24h/1d+ 涨跌三角 + 涨跌幅涨绿跌红每根蜡烛 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` 时区现算三路上游经 `_cached(key, ttl, fetch, fallback)`——`STATE.last_good[key]` 未过期`time - STATE.fetched_at[key] < ttl`直接用过期才调 `data.weather_fetch()`/`data.mao_fetch(now, with_trend=False)`/`data.btc_fetch()`成功更新数据与时间戳失败沿用旧值状态 `stale`)、从未成功用占位`none`日志每行末尾 `[weather:cache(12s) mao:fetched btc:cache(3s)]` 可直接看命中情况`/health` `cache_age_s`/`ttl_s`用量取 `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`** 本机推送端 `~/.claude/usage-snapshot.json` 原文 `POST {PUSH_URL}/push/usage`直连连接类异常重试 2 4xx/5xx 直接报错ChatGPT 暂无本机源不推将来按 `data.GPT_WINDOWS` 键组 dict `/push/usage_gpt`)。
- **`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 水平偏移叠绘笔画点阵字体只有单一字重)。
- 推送时 `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`本机专用的 `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 cloudflared tunnel --url http://127.0.0.1:8790 --no-autoupdate > output/cloudflared.log 2>&1 &`
地址 `grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' output/cloudflared.log`已写入 `output/tunnel_url.txt`)。
- **shell **本机是 fish——`env -u …` 不能存进变量再当命令执行含中文的 Python heredoc `python3 -X utf8 - <<'EOF'`否则 stdin 解码报错`gh` 未登录且 GitHub API 匿名限流取源码走 `curl raw.githubusercontent.com`
- **statusline 补丁验证**`~/.claude/statusline.sh`备份 `statusline.sh.bak-*`用假 HOME stdin 不污染真实快照——
`echo '<stdin json>' | HOME=/tmp/fakehome bash ~/.claude/statusline.sh` `/tmp/fakehome/.claude/usage-snapshot.json` 是否含 `seven_day_fable`
- **定时任务**`run.sh` 末尾已追加 `main.py --target push`vfox python 绝对路径`|| true`用量随喵喵推送每 5 分钟同步到 server.py推送暂存落盘 `output/push_state.json`
- **上游配额**server 三路上游已有 TTL 内存缓存天气默认 15 分钟 和风约 192 /改有效期只动 `.env`不动代码
- **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}`)。**未采用**未公开可随时变更仅作备选记录