diff --git a/CLAUDE.md b/CLAUDE.md index 6385147..b701fe9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,11 +35,11 @@ ZECTRIX_DEVICE_ID=AA:BB:CC:DD:EE:FF python3 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 不使用它。 - - **缓存**(为 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=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 ` + `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` 未配置时直接抛错→天气区显示 `"--"`。 -- **`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): - **A 顶部**(0–90):大时钟(F48)+日期(左)/ 天气(右:红定位水滴 + 温度 + 天气·温区 + 全矢量图标)。 - **B 中部主角 Claude Usage**(`_label` 标题 + 两条 `_usage_row`):每条单行 = 左标签 `5h`/`7d`(F13) + 粗进度条 + 百分比(F16) + 时间维度对比 pace(F13,`_pace`);`pct≥warn`→该条进度条(含边框)与百分比转红预警;pace 用上/下三角替代 +/−(超前↑ / 节余↓)。整行「标签·进度条·百分比·三角」用 `_mid()` 按墨迹竖直中心对齐到同一中线。 diff --git a/README.md b/README.md index 79683a6..461ce5e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ eink-push/ ├── data.py # 数据层:拉真实数据并映射,失败回退占位 "--"(不回退 mock);天气/趋势缓存 ├── jm_api.py # jm-devops 后端统计接口客户端(设备密钥 client_secret 授权 + token 缓存) ├── weather_api.py # 和风天气(QWeather)客户端(实时天气 + 今日温区) -├── usage_local.py # Claude Code 用量:只读本地 statusline 缓存文件(不发网络请求) +├── usage_local.py # Claude Code 用量:只读 statusline 生产的本地快照(不发网络请求) ├── renderer.py # 渲染层:DashboardRenderer 生成 400×300 三色(黑/白/红)图片 ├── pusher.py # 推送层:按 .env 设备 MAC 直接推送图片(标准库 urllib) ├── 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_BASE_API=... JM_CLIENT_ID=... QWEATHER_HOST=... QWEATHER_KEY=... WEATHER_LOCATION=经度,纬度 WEATHER_TTL=900 -# Claude Code 用量(只读本地 statusline 缓存,不发请求) -USAGE_LOCAL_PATH=/tmp/claude/statusline-usage-cache.json USAGE_WARN=80 +# Claude Code 用量(只读 statusline 生产的本地快照,不发请求) +USAGE_LOCAL_PATH=~/.claude/usage-snapshot.json USAGE_WARN=80 ``` ## 定时刷新(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天每日数据(趋势图当前隐藏,仍会拉取并缓存) - **天气**:接入真实和风天气(QWeather,`weather_api.py`),定位固定南京·江宁。 实时天气取 `now.text`/`now.temp`/`now.icon`,今日温区取 `/3d` 的 `daily[0]`。需配置 `QWEATHER_HOST` + `QWEATHER_KEY`。 -- **Claude Code 用量**:**只读本地**(`usage_local.py`,不发请求)——读 claude-statusline 落盘的 - `/tmp/claude/statusline-usage-cache.json`,取 5h/7d 的 `utilization` 与 `resets_at`。 - 注:新版 Claude Code 经 stdin 喂 statusline,该缓存 `utilization` 可能滞后;`resets_at` 为绝对时间,故 pace 时间对比始终准确。 +- **Claude Code 用量**:**只读本地**(`usage_local.py`,不发请求),**生产者/消费者**模式—— + 生产者是**打过补丁的 claude-statusline**:它把每次 Claude Code 经 stdin 喂来的**权威实时**额度 + 落盘为快照 `~/.claude/usage-snapshot.json`(`five_hour`/`seven_day` 的 `utilization` + `resets_at`); + 本项目作消费者读取。活跃使用时快照持续刷新、准确;空闲时停在最后一次。**需先安装补丁版 statusline** + (见其仓库 `bin/statusline.sh` 的 usage snapshot 段 + `node bin/install.js`)。 - **日期 / 时间 / 上次更新**:真实系统时间。 > `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`,每天只拉一次。 两者拉取失败均**沿用旧缓存**避免天气区整片 `"--"`,仍无缓存才回退占位。想强制重拉删掉对应缓存文件即可(均已 git 忽略)。 -Claude 用量**不自建缓存**——直接读 statusline 落盘的本地文件,文件缺失/损坏即显示 `"--"`。 +Claude 用量**不自建缓存**——直接读 statusline 生产的本地快照 `~/.claude/usage-snapshot.json`,文件缺失/损坏即显示 `"--"`。 ## 设计说明 diff --git a/config.py b/config.py index 342a11f..93b4441 100644 --- a/config.py +++ b/config.py @@ -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") # ---- Claude Code 用量(Claude Usage 模块)数据源 ---- -# 只读本地:读取 claude-statusline 落盘的用量缓存文件,**不发任何网络请求**(详见 usage_local.py)。 -# 注意:新版 Claude Code 经 stdin 喂 statusline,该磁盘缓存刷新不频繁、utilization 可能滞后; -# resets_at 为绝对时间,故 pace(时间维度对比)始终准确。文件缺失时用量区显示 "--"。 -USAGE_LOCAL_PATH = os.environ.get("USAGE_LOCAL_PATH", "/tmp/claude/statusline-usage-cache.json") +# 只读本地、**不发任何网络请求**(详见 usage_local.py)——生产者/消费者模式: +# 生产者 = 打过补丁的 claude-statusline,把每次从 Claude Code stdin 拿到的**权威实时**额度 +# (five_hour/seven_day 的 utilization + resets_at)落盘到 ~/.claude/usage-snapshot.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")) # 用量 ≥ 此百分比 → 进度条与百分比转红预警 diff --git a/data.py b/data.py index 11a3d7c..e1a1c4b 100644 --- a/data.py +++ b/data.py @@ -169,10 +169,14 @@ def _mao(now): # ---------- Claude Code 用量 ---------- -def _parse_iso_ts(s): - """ISO 时间串 → epoch 秒;兼容结尾 'Z'。失败返回 None。""" - if not s: +def _parse_reset_ts(s): + """resets_at → epoch 秒。兼容三种:epoch 数字(Claude Code stdin 给的就是 epoch)、 + ISO 时间串(含结尾 'Z')、空。失败返回 None。""" + if s is None or s == "": return None + s = str(s) + if s.replace(".", "", 1).isdigit(): # 纯数字 → 当作 epoch 秒 + return float(s) try: return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp() except Exception: @@ -181,7 +185,7 @@ def _parse_iso_ts(s): def _window_time_pct(resets_at, window, now): """该滚动窗口「已流逝时间百分比」= (window − 距重置剩余) / window ×100。失败返回 None。""" - reset = _parse_iso_ts(resets_at) + reset = _parse_reset_ts(resets_at) if reset is None: return None remaining = max(0, min(window, reset - now.timestamp())) @@ -189,10 +193,11 @@ def _window_time_pct(resets_at, window, 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 取自本地缓存、可能滞后。文件缺失/损坏 → 回退占位 pct=None(renderer 显示 "--")。 + utilization 来自 statusline 从 Claude Code stdin 落盘的权威实时值; + time_pct(时间已流逝%) 与 pace 每次按当前时间**现算**(resets_at 为绝对时间)。 + 文件缺失/损坏 → 回退占位 pct=None(renderer 显示 "--")。 结构契约(renderer 依赖):{title, warn, bars:[{k, pct(0~100 或 None), time_pct(0~100)?}]}; pct ≥ warn 时该条进度条与百分比转红;pace = pct − time_pct(>0 超前↑ / <0 节余↓)。 """ diff --git a/usage_local.py b/usage_local.py index 71647ad..e92ee46 100644 --- a/usage_local.py +++ b/usage_local.py @@ -1,14 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ -Claude Code 用量本地读取 —— 只读 claude-statusline 落盘的用量缓存,**不发任何网络请求**。 +Claude Code 用量本地读取 —— 只读 claude-statusline 生产的用量快照,**不发任何网络请求**。 -数据文件:config.USAGE_LOCAL_PATH(默认 /tmp/claude/statusline-usage-cache.json), -由已安装的 claude-statusline 写入。结构含 five_hour / seven_day,各有 -utilization(已用百分比) 与 resets_at(ISO 重置时间,供上层算「时间已流逝%」做 pace 对比)。 +生产者/消费者模式:生产者 = 打过补丁的 claude-statusline(见其仓库 bin/statusline.sh 的 +「Usage snapshot for external consumers」段),它把每次从 Claude Code stdin 拿到的**权威实时**额度 +落盘到 config.USAGE_LOCAL_PATH(默认 ~/.claude/usage-snapshot.json);本模块作为消费者读取。 -注意:新版 Claude Code 经 stdin 把额度喂给 statusline,该磁盘缓存刷新不频繁、utilization 可能滞后; -resets_at 是绝对时间,故上层的 pace(时间维度对比)始终准确。文件缺失/损坏时抛错 → 上层回退占位 "--"。 +结构含 five_hour / seven_day,各有 utilization(已用百分比) 与 resets_at(ISO 重置时间,供上层算 +「时间已流逝%」做 pace 对比)。活跃使用时快照几乎持续刷新、准确;空闲时停在最后一次。 +文件缺失/损坏时抛错 → 上层回退占位 "--"。 """ import json