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:
2026-09-14 14:08:21 +08:00
parent 2015947812
commit 7378d6bb88
15 changed files with 561 additions and 108 deletions

View File

@@ -8,7 +8,7 @@ reTerminal E1002 渲染层 —— 复用 renderer.DashboardRenderer 的组件(
版式k=1点阵字体且**只用整数倍字号 12/24/36/48/84**,不放大;四段,三道横规):
A 顶部090 **大时钟 F84**(墨迹 1679占满上下边距内的整段高度 星期/日期 F12 紧随其右、与时钟数字**基线对齐**(左)
|天气(右,贴 790——天气组件与三色版同尺寸。
B 用量96206 :左半 **Claude Usage**all 5h / all 7d / fable 7d)|竖规 x=400右半 **ChatGPT Usage**mockall 5h / all 7d / codex 7d)。
B 用量96206 :左半 **Claude Usage**两行5h 合并行 7d——一个大边框内上下两条实心 = 总 7d / fable 7d右侧数字取较大者`_usage_row_split`)|竖规 x=400右半 **Codex Usage**5h / 7dChatGPT 订阅的 Codex 额度池;未推送时 mock 标「示例数据」)。
行组件沿用三色版的紧凑单行 `_usage_row`(行距 31**三处文字统一 12px**(基类 F13/F16/FPACE 都映射到 12px
标签**常规字重、在标签列内右对齐**(贴进度条),百分比与 pace 数字**粗体**(曾试过 24px用户嫌大
标签用 bar["label"]statusline 同款写法),列宽按最长标签自适应;标题右侧「上次更新 HH:MM」与今日实时同款。
@@ -51,7 +51,7 @@ class E1002Renderer(DashboardRenderer):
PCT_TRACK = 0 # 百分比是整数倍字号(自带侧边距),不再手动加字符间距
LABEL_BOLD = False # 标签常规字重(数值才加粗,层级:数值 > 标签)
LABEL_ALIGN = "r" # 标签右对齐贴进度条,长短标签(all 5h / fable 7d右缘齐平
LABEL_ALIGN = "r" # 标签右对齐贴进度条,长短标签(5h / 7d右缘齐平
def __init__(self, font_path=config.FONT_PATH):
super().__init__(font_path, size=(config.E1002_WIDTH, config.E1002_HEIGHT), k=1)
@@ -66,13 +66,29 @@ class E1002Renderer(DashboardRenderer):
# ---------- 配色钩子(三档,顺序固定 绿 → 黑 → 红;用量与 pace 各自独立计算)----------
_ok, _pace_tol = _cfg.USAGE_OK, _cfg.PACE_TOL # 由 _usage_block 按数据覆盖
def _usage_row(self, x0, x1, y, bar, warn, label_w=None):
"""宽版pct 为 None接口未返回该窗口如 Codex Plus 的 5h时不画 "--",而是按 0 画——百分比 "00%"、pace "00"
空条、全黑无数据的中性色不落绿档——使各行进度条右端对齐、宽度一致2026-09-14 用户要求;"--" 会让该行条变短)。"""
if not isinstance(bar.get("pct"), (int, float)):
bar = dict(bar, pct=0, time_pct=0, _null=True)
return super()._usage_row(x0, x1, y, bar, warn, label_w=label_w)
def _pace(self, xr, cy, diff):
"""宽版diff=0持平/无数据)不画三角,但**仍预留三角位**6 + 间隙 4使每行 pace 列等宽、进度条右端齐平。"""
w = super()._pace(xr, cy, diff)
return w if diff != 0 else w + 10 * self.k
def _usage_colors(self, ok, pct, warn, bar):
"""进度条填充、边框、百分比数字三者同色pct ≤ ok 绿「健康」其间黑「正常」pct ≥ warn 红「危险」。"""
if not ok:
"""进度条填充、边框、百分比数字三者同色pct ≤ ok 绿「健康」其间黑「正常」pct ≥ warn 红「危险」无数据_null"""
if not ok or bar.get("_null"):
return BLACK, BLACK, BLACK
c = RED if pct >= warn else GREEN if pct <= self._ok else BLACK
return c, c, c
def _cmp_color(self, direction, fg=BLACK):
"""环比/同比 涨绿 / 跌红(与底部 K 线「涨绿跌红」一致2026-09-14 用户要求);无方向沿用 fg。"""
return GREEN if direction == "up" else RED if direction == "down" else fg
def _pace_color(self, diff):
"""pace = 用量% 时间%(与用量档位独立):≤ tol 绿「节余」|(tol, +tol) 黑「持平」|≥ +tol 红「偏快」。"""
if diff >= self._pace_tol:
@@ -101,14 +117,48 @@ class E1002Renderer(DashboardRenderer):
self._label(y0, usage.get("title", "Usage"), right=right, x0=x0, x1=x1)
warn = usage.get("warn", 80)
self._ok, self._pace_tol = usage.get("ok", _cfg.USAGE_OK), usage.get("pace_tol", _cfg.PACE_TOL)
bars = [dict(b, k=b.get("label") or b["k"]) for b in usage.get("bars", [])] # 宽版用长标签(all 5h / fable 7d
label_w = max([self._tw(b["k"], self.F13) for b in bars] + [26]) + self.LABEL_GAP
y = y0 + 27
bars = [dict(b, k=b.get("label") or b["k"]) for b in usage.get("bars", [])] # 宽版用长标签(5h / 7d
# scoped 行fable 7d并入紧邻其前的非 scoped 行,成为该行的下半细条 → 左右两块都是两行,对称
rows = []
for b in bars:
self._usage_row(x0, x1, y, b, warn, label_w=label_w)
if b.get("scoped") and rows and rows[-1][1] is None:
rows[-1] = (rows[-1][0], b)
else:
rows.append((b, None))
label_w = max([self._tw(m["k"], self.F13) for m, _ in rows] + [26]) + self.LABEL_GAP
y = y0 + 27
for m, sub in rows:
if sub is None:
self._usage_row(x0, x1, y, m, warn, label_w=label_w)
else:
self._usage_row_split(x0, x1, y, m, sub, warn, label_w)
y += 31
return y - 31 + 18 # 末行进度条底部cy=y+10条高 15 → 底 y+17
def _usage_row_split(self, x0, x1, y, main, sub, warn, label_w):
"""合并行「7d」2026-09-14 定稿):**一个大边框**、整行高度与单行相同H=15边框内部上下**精确对半**两条**实心**填充
(各 7px、紧挨无空隙——上 = main总 7d下 = sub模型限定 fable 7d。右侧百分比与 pace **只显示两者中较大者**
(同一窗口 time_pct 相同pct 大者 pace 也大);边框与文字颜色随该较大者的档位,两条填充各按自身档位取色。
实现:以「较大者」合成一条 bar 交给基类 _usage_row 画边框/标签/文字,再把边框内部刷白、重绘两条半高填充。"""
k = self.k
def _p(b):
v = (b or {}).get("pct")
return int(v) if isinstance(v, (int, float)) else None
pm, ps = _p(main), _p(sub)
dom = sub if (ps is not None and (pm is None or ps > pm)) else main
bx0, bx1, by, H = self._usage_row(x0, x1, y, dict(dom, k=main["k"]), warn, label_w=label_w)
# 边框内部刷白(保留 1px 边框),再画上下两条实心
self.d.rectangle([bx0 + k, by + k, bx1 - k, by + H - k], fill=WHITE)
halves = ((pm, main, by + k, by + 7 * k), (ps, sub, by + 8 * k, by + 14 * k)) # 边框内 14px 精确对半:上 by+1..by+7下 by+8..by+14无空隙
for pct, b, top, bot in halves:
if pct is None:
continue
pct = max(0, min(100, pct))
fill = self._usage_colors(True, pct, warn, b or {})[0]
fw = (bx1 - bx0) * pct / 100.0
if fw >= 2:
self.d.rectangle([bx0 + k, top, min(bx0 + fw, bx1 - k), bot], fill=fill)
def _realtime(self, y0, mao, x0=8, x1=None):
"""橘喵·今日实时五列(宽版):探店单量 / 探店流水 / 团购单量 / 团购流水 / 毛利,列内四行逐行水平居中:
指标名 F12y0+21→ 值 F24 粗y0+38→ 环比y0+72→ 同比y0+88行距 16按墨迹框算标题↔名 ~9px原 ~5px 太挤,整列下移 4px
@@ -229,7 +279,7 @@ class E1002Renderer(DashboardRenderer):
self._weather_category(wt.get("icon")), s=s, accent=RED, wet=BLUE)
self.hdash(8, self.W - 8, DIV_Y)
# ---- B. 用量:左 Claude 右 ChatGPT(紧凑单行,与三色版同款)----
# ---- B. 用量:左 Claude 右 Codex(紧凑单行,与三色版同款)----
MID = self.W // 2 # 400
y_l = self._usage_block(10, MID - 14, DIV_Y + 6, usage)
y_r = self._usage_block(MID + 14, R, DIV_Y + 6, gpt) if gpt else y_l