feat: AI 用量改账户级本机采集(Claude 钥匙串/Codex RPC)+宽版 7d 合并行
- Claude:复用 Claude Code 钥匙串登录态直调 /api/oauth/usage,零依赖 statusline 与 Claude Code 进程;只读不刷新 token - Codex:经 codex app-server JSON-RPC 读 ChatGPT 订阅的 Codex 额度池(普通聊天额度无账户级途径,右块改名 Codex Usage) - 两路采集共用 TTL 缓存+失败退避(CLAUDE_USAGE_TTL/CODEX_USAGE_TTL,语义同 WEATHER_TTL),推送端两路独立上报 - E1002:Claude 7d 与 fable 7d 合并为一行(单边框内上下对半实心,数字取较大者)、标签去 all 前缀、空值按 00% 00 占位等宽 - E1002/网页版:今日实时环比同比改涨绿跌红,与 K 线一致 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
推送端(本机运行)—— 把本机的用量原始快照 POST 到 server.py,服务端暂存、渲染时现算 pace。
|
||||
推送端(本机运行)—— 在本机采集两路账户级 AI 用量,POST 到 server.py;服务端暂存、渲染时现算 pace。
|
||||
|
||||
推什么:
|
||||
- Claude:~/.claude/usage-snapshot.json 原文(statusline 生产,含 five_hour / seven_day / seven_day_fable / updated_at)→ POST /push/usage
|
||||
- ChatGPT:暂无本机数据源;将来有了按 data.GPT_WINDOWS 的键(five_hour / seven_day / seven_day_codex,各含 utilization + resets_at)
|
||||
组一个 dict → POST /push/usage_gpt。目前不推,服务端用 mock 并标「示例数据」。
|
||||
推什么(形态 A:凭证与采集都留在本机,服务端不持任何凭证、不读本机文件):
|
||||
- Claude:claude_usage.get_usage()(复用 Claude Code 钥匙串登录态调 /api/oauth/usage)→ POST /push/usage
|
||||
- Codex:codex_usage.get_usage()(`codex app-server` RPC,独立 CODEX_HOME 的 ChatGPT 登录)→ POST /push/usage_gpt
|
||||
两路彼此独立:一路失败不影响另一路;两路都失败才以非零码退出。采集器自带 ≥300s 最小间隔缓存,
|
||||
所以与 main.py(小屏渲染)在同一分钟内先后运行也只打一次接口。
|
||||
鉴权:请求头 X-Push-Token = PUSH_TOKEN。网络:直连(不走代理),连接类异常重试 2 次。
|
||||
调度:与喵喵设备推送同节律,由 launchd 每 5 分钟跑一次 `main.py --target push`(或加进 run.sh)。
|
||||
调度:与喵喵设备推送同节律,由 launchd 每 5 分钟跑一次 `main.py --target push`(run.sh 末尾)。
|
||||
"""
|
||||
import json
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import claude_usage
|
||||
import codex_usage
|
||||
import config
|
||||
import usage_local
|
||||
|
||||
RETRY_TIMES, RETRY_DELAY, TIMEOUT = 2, 2, 10
|
||||
|
||||
@@ -28,10 +30,11 @@ def _post_json(url, payload):
|
||||
"X-Push-Token": config.PUSH_TOKEN,
|
||||
"User-Agent": "eink-push/push_client",
|
||||
})
|
||||
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
||||
last = None
|
||||
for i in range(RETRY_TIMES + 1):
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=TIMEOUT) as resp:
|
||||
with opener.open(req, timeout=TIMEOUT) as resp:
|
||||
return json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as e: # 4xx/5xx 不重试,直接报出服务端的说明
|
||||
raise RuntimeError(f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')[:200]}") from e
|
||||
@@ -42,13 +45,29 @@ def _post_json(url, payload):
|
||||
raise RuntimeError(f"连接失败:{last}")
|
||||
|
||||
|
||||
SOURCES = (
|
||||
("Claude", claude_usage.get_usage, "/push/usage"),
|
||||
("Codex", codex_usage.get_usage, "/push/usage_gpt"),
|
||||
)
|
||||
|
||||
|
||||
def push_usage():
|
||||
if not config.PUSH_URL or not config.PUSH_TOKEN:
|
||||
raise SystemExit("[推送] 需在 .env 配置 PUSH_URL 与 PUSH_TOKEN")
|
||||
raw = usage_local.get_usage() # 缺文件直接抛错——没有快照就没什么可推
|
||||
resp = _post_json(config.PUSH_URL.rstrip("/") + "/push/usage", raw)
|
||||
print(f"[推送] Claude 用量快照已推送到 {config.PUSH_URL}:{resp}")
|
||||
return resp
|
||||
base = config.PUSH_URL.rstrip("/")
|
||||
ok = 0
|
||||
for name, getter, path in SOURCES:
|
||||
try:
|
||||
raw = getter()
|
||||
status = raw.pop("status", "")
|
||||
resp = _post_json(base + path, raw)
|
||||
print(f"[推送] {name} 用量({status})已推送到 {base}{path}:{resp}")
|
||||
ok += 1
|
||||
except Exception as e:
|
||||
print(f"[推送] {name} 用量跳过:{e}")
|
||||
if ok == 0:
|
||||
raise SystemExit(1)
|
||||
return ok
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user