#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 推送端(本机运行)—— 在本机采集两路账户级 AI 用量,POST 到 server.py;服务端暂存、渲染时现算 pace。 推什么(形态 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 末尾)。 """ import json import time import urllib.error import urllib.request import claude_usage import codex_usage import config RETRY_TIMES, RETRY_DELAY, TIMEOUT = 2, 2, 10 def _post_json(url, payload): body = json.dumps(payload, ensure_ascii=False).encode("utf-8") req = urllib.request.Request(url, data=body, method="POST", headers={ "Content-Type": "application/json; charset=utf-8", "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 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 except urllib.error.URLError as e: # 连接类:重试 last = e if i < RETRY_TIMES: time.sleep(RETRY_DELAY) 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") 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__": push_usage()