feat: E1002 六色版改版为 AI 用量看板+Claude 用量新增 Fable 7d 行与重置时刻

- 版面重排:主体左 Claude Usage(5h/7d/Fable 7d 三行),右 ChatGPT Usage(目前 mock,标「示例数据」);每行为一张用量卡:标签+F24 百分比+pace/全宽进度条/「重置 HH:MM」+「已过 NN%」
- 移除上一版的橘喵今日实时与 30 天趋势图及 trend totals 合计(业务数据留给 400×300 小屏),绿色暂不再使用
- data 用量结构新增 scoped(按模型限定额度)、reset(重置时刻文本)、updated(快照落盘时刻),新增 _usage_gpt mock 数据源,接入真实数据时保持结构替换即可
- usage_local 读取 statusline 新增的 seven_day_fable 与 updated_at;400×300 小屏 render 跳过 scoped 行
- 同步更新 CLAUDE.md 分支说明

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 14:43:31 +08:00
parent d7ae193f56
commit 110f710ff0
5 changed files with 176 additions and 144 deletions

106
data.py
View File

@@ -163,23 +163,26 @@ def _mao(now):
except Exception as e:
print(f"[警告] 今日经营拉取失败:{e}")
cols = [_placeholder_col(k) for k in ("单量", "流水", "毛利")]
# 近30天趋势按日期缓存每天只拉一次合计每次现算,不进缓存(缓存结构保持不变)
trend = dict(_trend(now))
trend["totals"] = _trend_totals(trend)
return {"upd": upd, "cols": cols, "trend": trend}
# 近30天趋势按日期缓存每天只拉一次宽版另用其最后 7 天做「近7天」数据表
trend = _trend(now)
return {"upd": upd, "cols": cols, "trend": trend, "week": _recent_days(trend, 7)}
def _trend_totals(trend):
"""近30天三指标合计 → [(名,文本)]供宽版E1002趋势图标题行展示无数据返回空列表
单量按千分位计数,流水/毛利按金额紧凑格式≥1万 显示 X.XX万"""
out = []
for s in trend.get("series") or []:
data = s.get("data") or []
if not data:
def _recent_days(trend, n):
"""趋势的最后 n 天 → 表格数据 {dates:[MM/DD…], rows:[(名, [文本…], 合计文本)]}供宽版「橘喵·近7天」表使用
recent-days 不含今日,故表头日期到昨天为止。单量按千分位流水/毛利按紧凑金额格式;无数据返回空 dict"""
dates, series = trend.get("dates") or [], trend.get("series") or []
if not dates or not series:
return {}
dates = dates[-n:]
rows = []
for s in series:
data = (s.get("data") or [])[-n:]
if len(data) != len(dates):
continue
total = sum(data)
out.append((s["k"], _fmt_count(total) if s["k"] == "单量" else _fmt_money(total)))
return out
fmt = _fmt_count if s["k"] == "单量" else _fmt_money
rows.append((s["k"], [fmt(v) for v in data], fmt(sum(data))))
return {"dates": dates, "rows": rows}
# ---------- Claude Code 用量 ----------
@@ -206,35 +209,75 @@ def _window_time_pct(resets_at, window, now):
return int(round((window - remaining) / window * 100))
def _usage(now):
"""Claude Code 用量5h / 7d——只读本地 statusline 生产的快照(见 usage_local.py**不发任何请求**。
def _fmt_reset(resets_at, window):
"""重置时刻 → 文本≤1 天的窗口只给 HH:MM更长的窗口带 MM/DD。解析失败返回 None。"""
ts = _parse_reset_ts(resets_at)
if ts is None:
return None
dt = datetime.fromtimestamp(ts)
return dt.strftime("%H:%M") if window <= 86400 else dt.strftime("%m/%d %H:%M")
utilization 来自 statusline 从 Claude Code stdin 落盘的权威实时值;
# Claude 用量窗口快照键行标签窗口长度秒scoped。scoped=True 为「按模型限定」的额度
# Fable 7d来自 /api/oauth/usage 的 limits[],由 statusline 写入快照 seven_day_fable
# 400x300 三色小屏版面放不下第三行,只在 800x480 宽版展示。
USAGE_WINDOWS = (
("five_hour", "5h", FIVE_HOUR, False),
("seven_day", "7d", SEVEN_DAY, False),
("seven_day_fable", "Fable 7d", SEVEN_DAY, True),
)
def _usage(now):
"""Claude Code 用量5h / 7d / Fable 7d——只读本地 statusline 生产的快照(见 usage_local.py**不发任何请求**。
utilization 来自 statusline 从 Claude Code stdin 落盘的权威实时值Fable 行来自其转存的 API limits
time_pct(时间已流逝%) 与 pace 每次按当前时间**现算**resets_at 为绝对时间)。
文件缺失/损坏 → 回退占位 pct=Nonerenderer 显示 "--")。
结构契约renderer 依赖):{title, warn, bars:[{k, pct(0~100 或 None), time_pct(0~100)?}]}
pct ≥ warn 时该条进度条与百分比转红pace = pct time_pct>0 超前↑ / <0 节余↓)
文件缺失/损坏 → 回退占位 pct=Nonerenderer 显示 "--";快照里没有某窗口(如旧版 statusline 无 Fable同样 "--"
结构契约renderer 依赖):{title, warn, caution, updated?, bars:[{k, pct(0~100 或 None), time_pct?, reset?, scoped}]}
pct ≥ warn 时该条进度条与百分比转红pace = pct time_pct>0 超前↑ / <0 节余↓)
reset 为重置时刻文本updated 为快照落盘时刻 HH:MM活跃使用时持续刷新空闲时停在最后一次
"""
wins = {}
raw = {}
try:
raw = usage_local.get_usage()
for key, label, win in (("five_hour", "5h", FIVE_HOUR), ("seven_day", "7d", SEVEN_DAY)):
seg = raw.get(key) or {}
wins[label] = {"pct": int(round(float(seg.get("utilization", 0)))),
"resets_at": seg.get("resets_at"), "win": win}
except Exception as e:
print(f"[警告] Claude 用量读取失败:{e}")
bars = []
for label in ("5h", "7d"):
w = wins.get(label) or {}
pct = w.get("pct")
bar = {"k": label, "pct": pct}
tp = _window_time_pct(w.get("resets_at"), w.get("win"), now)
for key, label, win, scoped in USAGE_WINDOWS:
seg = raw.get(key) or {}
util = seg.get("utilization")
pct = int(round(float(util))) if util is not None else None
bar = {"k": label, "pct": pct, "scoped": scoped}
tp = _window_time_pct(seg.get("resets_at"), win, now)
if pct is not None and tp is not None:
bar["time_pct"] = tp
rs = _fmt_reset(seg.get("resets_at"), win)
if rs:
bar["reset"] = rs
bars.append(bar)
return {"title": "Claude Usage", "warn": config.USAGE_WARN, "caution": config.USAGE_CAUTION, "bars": bars}
out = {"title": "Claude Usage", "warn": config.USAGE_WARN, "caution": config.USAGE_CAUTION, "bars": bars}
upd = _parse_reset_ts(raw.get("updated_at"))
if upd:
out["updated"] = datetime.fromtimestamp(upd).strftime("%H:%M")
return out
def _usage_gpt(now):
"""ChatGPT 用量——**目前为 mock 占位**mock=Truerenderer 在标题右侧标「示例数据」)。
结构与 _usage 完全一致,后续接入真实数据源时只需替换本函数、保持返回结构不变。
mock 取值刻意覆盖多种配色档5h 42%、7d 71%黄「注意」、Codex 7d 55%scoped 与左侧 Fable 行对称)。"""
bars = []
for label, pct, win, remain, scoped in (("5h", 42, FIVE_HOUR, 2 * 3600, False),
("7d", 71, SEVEN_DAY, 3 * 86400, False),
("Codex 7d", 55, SEVEN_DAY, 3 * 86400, True)):
reset_ts = now.timestamp() + remain
bars.append({"k": label, "pct": pct, "scoped": scoped,
"time_pct": _window_time_pct(reset_ts, win, now),
"reset": _fmt_reset(reset_ts, win)})
return {"title": "ChatGPT Usage", "warn": config.USAGE_WARN, "caution": config.USAGE_CAUTION,
"bars": bars, "mock": True}
def get_dashboard_data(now=None):
@@ -249,4 +292,5 @@ def get_dashboard_data(now=None):
"weather": _weather(now),
"mao": _mao(now),
"usage": _usage(now),
"usage_gpt": _usage_gpt(now),
}