feat: 用量改读 statusline 生产的本地快照(生产者/消费者),实时准确

原先读 statusline 的 /tmp API 兜底缓存;新版 Claude Code 走 stdin、该缓存不再刷新
→ 数字过期(14% vs 实际 60%+)。改为消费「打过补丁的 claude-statusline 从 stdin
落盘的权威实时快照」:
- USAGE_LOCAL_PATH 默认改指 ~/.claude/usage-snapshot.json
- 修复 resets_at 解析:Claude Code stdin 给的是 epoch 数字(非 ISO),_parse_reset_ts
  现兼容 epoch/ISO,否则算不出 time_pct、pace 会消失
- 仍不发任何网络请求;快照缺失/损坏 → "--"
- 同步更新 CLAUDE.md / README.md 的数据源说明(生产者/消费者)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-06 19:22:03 +08:00
parent 14359c8726
commit 21426e13bc
5 changed files with 35 additions and 26 deletions

View File

@@ -35,11 +35,11 @@ ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py
`main.py` 串联三层,各层职责单一、低耦合: `main.py` 串联三层,各层职责单一、低耦合:
- **`data.py`** — 唯一数据入口 `get_dashboard_data()`,返回固定结构的 dict`date`/`weather`/`mao`/`usage`。日期时间用真实系统时间;橘喵今日经营由 `_mao()``jm_api`、天气由 `_weather()``weather_api`和风定位南京·江宁、Claude 用量由 `_usage()``usage_local`**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可renderer 依赖其 key`weather.icon` 为和风图标代码;`mao.cols[].hb/tb` 为环比/同比 `(direction, text)`direction 为 `None` 时不画三角;`usage.bars[]``{k, pct(或 None), time_pct?}``usage.warn` 为转红阈值)。注:`_mao` 仍会拉 `mao.trend`但趋势图当前隐藏、renderer 不使用它。 - **`data.py`** — 唯一数据入口 `get_dashboard_data()`,返回固定结构的 dict`date`/`weather`/`mao`/`usage`。日期时间用真实系统时间;橘喵今日经营由 `_mao()``jm_api`、天气由 `_weather()``weather_api`和风定位南京·江宁、Claude 用量由 `_usage()``usage_local`**任何异常都回退占位 `"--"`,不回退 mock**。改数据保持返回结构不变即可renderer 依赖其 key`weather.icon` 为和风图标代码;`mao.cols[].hb/tb` 为环比/同比 `(direction, text)`direction 为 `None` 时不画三角;`usage.bars[]``{k, pct(或 None), time_pct?}``usage.warn` 为转红阈值)。注:`_mao` 仍会拉 `mao.trend`但趋势图当前隐藏、renderer 不使用它。
- **缓存**(为 cron 每分钟独立进程而设,必须落盘):天气 `.weather_cache.json`TTL=`WEATHER_TTL`,默认 15min、趋势 `.trend_cache.json`(按日期 key每天只拉一次两者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 落盘的本地文件(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore` - **缓存**(为 cron 每分钟独立进程而设,必须落盘):天气 `.weather_cache.json`TTL=`WEATHER_TTL`,默认 15min、趋势 `.trend_cache.json`(按日期 key每天只拉一次两者拉取失败沿用旧缓存。**用量不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`(见 `usage_local`),失败即 `"--"`。两个缓存文件已在 `.gitignore`
- **用量的 pace时间维度对比**`_usage()` 每次按当前时间**现算** `time_pct=(窗口已流逝/窗口长)``pace=pcttime_pct`>0 超前↑ / <0 节余↓);故即使 `utilization` 来自可能滞后的本地缓存pace 仍随时钟准确推进5h 窗口 18000s7d 窗口 604800s - **用量的 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` 轮换 - **`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` 未配置时直接抛错天气区显示 `"--"` - **`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` 未配置时直接抛错天气区显示 `"--"`
- **`usage_local.py`** Claude Code 用量本地读取**不发任何网络请求**只读 `config.USAGE_LOCAL_PATH`默认 `/tmp/claude/statusline-usage-cache.json`由已安装的 claude-statusline 落盘 `five_hour`/`seven_day` `utilization`(已用%) `resets_at`(重置时间)新版 Claude Code stdin statusline该磁盘缓存刷新不频繁、**utilization 可能滞后** `resets_at` 是绝对时间 `data._usage` 现算的 pace 始终准文件缺失/损坏用量区 `"--"`。(曾评估直调 Anthropic OAuth 用量接口 `/api/oauth/usage`按需求改为读本地。) - **`usage_local.py`** Claude Code 用量本地读取**不发任何网络请求****生产者/消费者**模式只读 `config.USAGE_LOCAL_PATH`默认 `~/.claude/usage-snapshot.json`)。**生产者 = 打过补丁的 claude-statusline** `bin/statusline.sh` 把每次 Claude Code stdin 喂来的**权威实时**额度落盘为该快照schema `/api/oauth/usage``five_hour`/`seven_day` `utilization` + `resets_at`本模块作消费者读取活跃使用时快照几乎持续刷新准确空闲时停在最后一次文件缺失/损坏用量区 `"--"``resets_at` 可能是 **epoch 数字**Claude Code stdin 给的就是 epoch ISO `data._parse_reset_ts` 两者兼容。(曾评估直调 `/api/oauth/usage`改为读本地快照以免网络请求。)
- **`renderer.py`** `DashboardRenderer.render(data)` 返回 PIL `'RGB'` 模式、**仅黑//红三色**`render_to_file()` 落盘布局硬编码为**上中下三段**两道 `hdash` 横规分隔y=90 / 200 - **`renderer.py`** `DashboardRenderer.render(data)` 返回 PIL `'RGB'` 模式、**仅黑//红三色**`render_to_file()` 落盘布局硬编码为**上中下三段**两道 `hdash` 横规分隔y=90 / 200
- **A 顶部**090大时钟(F48)+日期/ 天气红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。 - **A 顶部**090大时钟(F48)+日期/ 天气红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。
- **B 中部主角 Claude Usage**`_label` 标题 + 两条 `_usage_row`每条单行 = 左标签 `5h`/`7d`(F13) + 粗进度条 + 百分比(F16) + 时间维度对比 pace(F13`_pace`)`pct≥warn`该条进度条(含边框)与百分比转红预警pace 用上/下三角替代 +/超前 / 节余↓)。整行标签·进度条·百分比·三角 `_mid()` 按墨迹竖直中心对齐到同一中线 - **B 中部主角 Claude Usage**`_label` 标题 + 两条 `_usage_row`每条单行 = 左标签 `5h`/`7d`(F13) + 粗进度条 + 百分比(F16) + 时间维度对比 pace(F13`_pace`)`pct≥warn`该条进度条(含边框)与百分比转红预警pace 用上/下三角替代 +/超前 / 节余↓)。整行标签·进度条·百分比·三角 `_mid()` 按墨迹竖直中心对齐到同一中线

View File

@@ -24,7 +24,7 @@ eink-push/
├── data.py # 数据层:拉真实数据并映射,失败回退占位 "--"(不回退 mock天气/趋势缓存 ├── data.py # 数据层:拉真实数据并映射,失败回退占位 "--"(不回退 mock天气/趋势缓存
├── jm_api.py # jm-devops 后端统计接口客户端(设备密钥 client_secret 授权 + token 缓存) ├── jm_api.py # jm-devops 后端统计接口客户端(设备密钥 client_secret 授权 + token 缓存)
├── weather_api.py # 和风天气QWeather客户端实时天气 + 今日温区) ├── weather_api.py # 和风天气QWeather客户端实时天气 + 今日温区)
├── usage_local.py # Claude Code 用量:只读本地 statusline 缓存文件(不发网络请求) ├── usage_local.py # Claude Code 用量:只读 statusline 生产的本地快照(不发网络请求)
├── renderer.py # 渲染层DashboardRenderer 生成 400×300 三色(黑/白/红)图片 ├── renderer.py # 渲染层DashboardRenderer 生成 400×300 三色(黑/白/红)图片
├── pusher.py # 推送层:按 .env 设备 MAC 直接推送图片(标准库 urllib ├── pusher.py # 推送层:按 .env 设备 MAC 直接推送图片(标准库 urllib
├── run.sh # cron 包装脚本本机专用git 忽略) ├── run.sh # cron 包装脚本本机专用git 忽略)
@@ -55,8 +55,8 @@ ZECTRIX_API_KEY=zt_xxx ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 main.py
# jm-devops 后端 / 和风天气 # jm-devops 后端 / 和风天气
JM_BASE_API=... JM_CLIENT_ID=... JM_BASE_API=... JM_CLIENT_ID=...
QWEATHER_HOST=... QWEATHER_KEY=... WEATHER_LOCATION=经度,纬度 WEATHER_TTL=900 QWEATHER_HOST=... QWEATHER_KEY=... WEATHER_LOCATION=经度,纬度 WEATHER_TTL=900
# Claude Code 用量(只读本地 statusline 缓存,不发请求) # Claude Code 用量(只读 statusline 生产的本地快照,不发请求)
USAGE_LOCAL_PATH=/tmp/claude/statusline-usage-cache.json USAGE_WARN=80 USAGE_LOCAL_PATH=~/.claude/usage-snapshot.json USAGE_WARN=80
``` ```
## 定时刷新cron ## 定时刷新cron
@@ -77,9 +77,11 @@ USAGE_LOCAL_PATH=/tmp/claude/statusline-usage-cache.json USAGE_WARN=80
- `GET /system/statistics/recent-days?days=30` —— 近30天每日数据趋势图当前隐藏仍会拉取并缓存 - `GET /system/statistics/recent-days?days=30` —— 近30天每日数据趋势图当前隐藏仍会拉取并缓存
- **天气**接入真实和风天气QWeather`weather_api.py`),定位固定南京·江宁。 - **天气**接入真实和风天气QWeather`weather_api.py`),定位固定南京·江宁。
实时天气取 `now.text`/`now.temp`/`now.icon`,今日温区取 `/3d``daily[0]`。需配置 `QWEATHER_HOST` + `QWEATHER_KEY` 实时天气取 `now.text`/`now.temp`/`now.icon`,今日温区取 `/3d``daily[0]`。需配置 `QWEATHER_HOST` + `QWEATHER_KEY`
- **Claude Code 用量****只读本地**`usage_local.py`,不发请求)——读 claude-statusline 落盘的 - **Claude Code 用量****只读本地**`usage_local.py`,不发请求)**生产者/消费者**模式——
`/tmp/claude/statusline-usage-cache.json`,取 5h/7d 的 `utilization``resets_at` 生产者是**打过补丁的 claude-statusline**:它把每次 Claude Code 经 stdin 喂来的**权威实时**额度
注:新版 Claude Code 经 stdin 喂 statusline该缓存 `utilization` 可能滞后;`resets_at` 为绝对时间,故 pace 时间对比始终准确。 落盘为快照 `~/.claude/usage-snapshot.json``five_hour`/`seven_day` `utilization` + `resets_at`
本项目作消费者读取。活跃使用时快照持续刷新、准确;空闲时停在最后一次。**需先安装补丁版 statusline**
(见其仓库 `bin/statusline.sh` 的 usage snapshot 段 + `node bin/install.js`)。
- **日期 / 时间 / 上次更新**:真实系统时间。 - **日期 / 时间 / 上次更新**:真实系统时间。
> `data.py` 负责把响应映射成渲染结构;**任何接口失败都回退占位 `"--"`,不回退 mock**。 > `data.py` 负责把响应映射成渲染结构;**任何接口失败都回退占位 `"--"`,不回退 mock**。
@@ -92,7 +94,7 @@ USAGE_LOCAL_PATH=/tmp/claude/statusline-usage-cache.json USAGE_WARN=80
- **趋势缓存** `.trend_cache.json`:按日期为 key当天命中即不再调 `recent-days`,每天只拉一次。 - **趋势缓存** `.trend_cache.json`:按日期为 key当天命中即不再调 `recent-days`,每天只拉一次。
两者拉取失败均**沿用旧缓存**避免天气区整片 `"--"`,仍无缓存才回退占位。想强制重拉删掉对应缓存文件即可(均已 git 忽略)。 两者拉取失败均**沿用旧缓存**避免天气区整片 `"--"`,仍无缓存才回退占位。想强制重拉删掉对应缓存文件即可(均已 git 忽略)。
Claude 用量**不自建缓存**——直接读 statusline 落盘的本地文件,文件缺失/损坏即显示 `"--"` Claude 用量**不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`,文件缺失/损坏即显示 `"--"`
## 设计说明 ## 设计说明

View File

@@ -72,8 +72,9 @@ WEATHER_CACHE_PATH = os.path.join(BASE_DIR, ".weather_cache.json")
TREND_CACHE_PATH = os.path.join(BASE_DIR, ".trend_cache.json") TREND_CACHE_PATH = os.path.join(BASE_DIR, ".trend_cache.json")
# ---- Claude Code 用量Claude Usage 模块)数据源 ---- # ---- Claude Code 用量Claude Usage 模块)数据源 ----
# 只读本地:读取 claude-statusline 落盘的用量缓存文件,**不发任何网络请求**(详见 usage_local.py # 只读本地**不发任何网络请求**(详见 usage_local.py——生产者/消费者模式:
# 注意:新版 Claude Code 经 stdin 喂 statusline该磁盘缓存刷新不频繁、utilization 可能滞后; # 生产者 = 打过补丁的 claude-statusline把每次从 Claude Code stdin 拿到的**权威实时**额度
# resets_at 为绝对时间,故 pace(时间维度对比)始终准确。文件缺失时用量区显示 "--" # five_hour/seven_day 的 utilization + resets_at落盘到 ~/.claude/usage-snapshot.json本项目读它
USAGE_LOCAL_PATH = os.environ.get("USAGE_LOCAL_PATH", "/tmp/claude/statusline-usage-cache.json") # 活跃使用时该快照几乎持续刷新、准确;空闲时停在最后一次。文件缺失时用量区显示 "--"。
USAGE_LOCAL_PATH = os.environ.get("USAGE_LOCAL_PATH", os.path.expanduser("~/.claude/usage-snapshot.json"))
USAGE_WARN = int(os.environ.get("USAGE_WARN", "80")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警 USAGE_WARN = int(os.environ.get("USAGE_WARN", "80")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警

19
data.py
View File

@@ -169,10 +169,14 @@ def _mao(now):
# ---------- Claude Code 用量 ---------- # ---------- Claude Code 用量 ----------
def _parse_iso_ts(s): def _parse_reset_ts(s):
"""ISO 时间串 → epoch 秒兼容结尾 'Z'。失败返回 None。""" """resets_at → epoch 秒兼容三种epoch 数字Claude Code stdin 给的就是 epoch
if not s: ISO 时间串(含结尾 'Z')、空。失败返回 None。"""
if s is None or s == "":
return None return None
s = str(s)
if s.replace(".", "", 1).isdigit(): # 纯数字 → 当作 epoch 秒
return float(s)
try: try:
return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp() return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
except Exception: except Exception:
@@ -181,7 +185,7 @@ def _parse_iso_ts(s):
def _window_time_pct(resets_at, window, now): def _window_time_pct(resets_at, window, now):
"""该滚动窗口「已流逝时间百分比」= (window 距重置剩余) / window ×100。失败返回 None。""" """该滚动窗口「已流逝时间百分比」= (window 距重置剩余) / window ×100。失败返回 None。"""
reset = _parse_iso_ts(resets_at) reset = _parse_reset_ts(resets_at)
if reset is None: if reset is None:
return None return None
remaining = max(0, min(window, reset - now.timestamp())) remaining = max(0, min(window, reset - now.timestamp()))
@@ -189,10 +193,11 @@ def _window_time_pct(resets_at, window, now):
def _usage(now): def _usage(now):
"""Claude Code 用量5h / 7d——只读本地 statusline 缓存(见 usage_local.py**不发任何请求**。 """Claude Code 用量5h / 7d——只读本地 statusline 生产的快照(见 usage_local.py**不发任何请求**。
time_pct(时间已流逝%) 与 pace 每次按当前时间**现算**resets_at 为绝对时间,始终准确) utilization 来自 statusline 从 Claude Code stdin 落盘的权威实时值
utilization 取自本地缓存、可能滞后。文件缺失/损坏 → 回退占位 pct=Nonerenderer 显示 "--")。 time_pct(时间已流逝%) 与 pace 每次按当前时间**现算**resets_at 为绝对时间)。
文件缺失/损坏 → 回退占位 pct=Nonerenderer 显示 "--")。
结构契约renderer 依赖):{title, warn, bars:[{k, pct(0~100 或 None), time_pct(0~100)?}]} 结构契约renderer 依赖):{title, warn, bars:[{k, pct(0~100 或 None), time_pct(0~100)?}]}
pct ≥ warn 时该条进度条与百分比转红pace = pct time_pct>0 超前↑ / <0 节余↓)。 pct ≥ warn 时该条进度条与百分比转红pace = pct time_pct>0 超前↑ / <0 节余↓)。
""" """

View File

@@ -1,14 +1,15 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Claude Code 用量本地读取 —— 只读 claude-statusline 落盘的用量缓存**不发任何网络请求**。 Claude Code 用量本地读取 —— 只读 claude-statusline 生产的用量快照**不发任何网络请求**。
数据文件config.USAGE_LOCAL_PATH默认 /tmp/claude/statusline-usage-cache.json 生产者/消费者模式:生产者 = 打过补丁的 claude-statusline(见其仓库 bin/statusline.sh 的
由已安装的 claude-statusline 写入。结构含 five_hour / seven_day各有 「Usage snapshot for external consumers」段它把每次从 Claude Code stdin 拿到的**权威实时**额度
utilization(已用百分比) 与 resets_at(ISO 重置时间,供上层算「时间已流逝%」做 pace 对比) 落盘到 config.USAGE_LOCAL_PATH默认 ~/.claude/usage-snapshot.json本模块作为消费者读取
注意:新版 Claude Code 经 stdin 把额度喂给 statusline该磁盘缓存刷新不频繁、utilization 可能滞后; 结构含 five_hour / seven_day各有 utilization(已用百分比) 与 resets_at(ISO 重置时间,供上层算
resets_at 是绝对时间,故上层的 pace(时间维度对比)始终准确。文件缺失/损坏时抛错 → 上层回退占位 "--" 「时间已流逝%」做 pace 对比)。活跃使用时快照几乎持续刷新、准确;空闲时停在最后一次
文件缺失/损坏时抛错 → 上层回退占位 "--"
""" """
import json import json