@@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
> **分支 `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 Usage( mock, 标「示例数据」) **, 紧凑单行样式( 曾试过撑满高度的大卡片, 被否) ; 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% 黄「注意」档,已移除)。
> **分支 `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 Usage( mock, 标「示例数据」) **, 紧凑单行样式( 曾试过撑满高度的大卡片, 被否) ; 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 线替换)。六色语义:用量进度条(含边框)与 pace 各自**三档**——绿=健康(用量 ≤ `USAGE_OK` 30 / pace ≤ − `PACE_TOL` 10) 、黑=正常、红=危险(用量 ≥ `USAGE_WARN` 80 / pace ≥ +10) ; 百分比数字只在红档转红( 绿小字在白底发虚) 。红另用于涨/定位/太阳闪电,蓝=雨雪;**黄不用**——用户明确不要黄色进度条(曾有 60%~ 80% 黄「注意」档,已移除)。
## 命令
@@ -40,7 +40,7 @@ EPD_HOST=epd400a44.local python3 main.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 为空 → 面板 `--` )。
- **`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× 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) ; 三者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json` (见 `usage_local` ),失败即 `"--"` 。两个缓存文件已在 `.gitignore` 。
- **用量的 pace( 时间维度对比) **: `_usage()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)` 、`pace=pct− time_pct` ( >0 超前↑ / < 0 节余 ↓); 故即使 `utilization` 来自可能滞后的本地缓存 , pace 仍随时钟准确推进 。 5h 窗口 18000s 、 7d 窗口 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` 轮换 。
@@ -89,8 +89,11 @@ EPD_HOST=epd400a44.local python3 main.py
- ** 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 &` ,
`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` 。
- ** 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` 。