Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions Shared/UsageViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,17 @@ struct StatsSection: View {

struct UsageBar: View {
@Environment(\.palette) private var palette
@Environment(\.widgetRenderingMode) private var renderingMode
let percent: Int
var height: CGFloat = 6
var body: some View {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule().fill(palette.track)
Capsule().fill(severityColor(percent))
// 单色模式下只用同一灰阶的透明度做层次:深色轨道 + 浅色填充。
// 不放入 accent group,避免它在不同桌面色调下重新变成实心色块。
Capsule().fill(renderingMode == .fullColor ? palette.track : Color.white.opacity(0.24))
Capsule()
.fill(renderingMode == .fullColor ? severityColor(percent) : Color.white.opacity(0.92))
.frame(width: max(geo.size.width * CGFloat(min(max(percent, 0), 100)) / 100.0,
percent > 0 ? 3 : 0))
}
Expand All @@ -201,19 +205,30 @@ struct UsageBar: View {

struct ProductHeader: View {
@Environment(\.palette) private var palette
@Environment(\.widgetRenderingMode) private var renderingMode
let name: String
let color: Color
var plan: String? = nil
var body: some View {
HStack(spacing: 6) {
// 产品色点在全部模式中保留,方便快速区分 Claude Code 与 Codex。
Circle().fill(color).frame(width: 7, height: 7)
Text(name).font(.system(size: 12, weight: .medium)).foregroundColor(palette.textPrimary)
if let plan = plan {
Text(plan.capitalized)
.font(.system(size: 9, weight: .medium))
.foregroundColor(palette.secondary)
.padding(.horizontal, 5).padding(.vertical, 1)
.background(Capsule().fill(palette.track))
if let plan, !plan.isEmpty {
if renderingMode == .fullColor {
Text(plan.capitalized)
.font(.system(size: 9, weight: .medium))
.foregroundColor(palette.secondary)
.padding(.horizontal, 5).padding(.vertical, 1)
.background(Capsule().fill(palette.track))
} else {
// vibrant 会把文字和不透明胶囊都映射成同一灰阶,文字随即消失。
// 单色状态改为无底色的明确文本,保留套餐/消耗数据的可读性。
Text(plan.capitalized)
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(.primary)
.opacity(0.88)
}
}
Spacer(minLength: 0)
}
Expand Down
14 changes: 10 additions & 4 deletions Widget/UsageWidget.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ struct UsageProvider: TimelineProvider {
struct UsageWidgetEntryView: View {
@Environment(\.widgetFamily) private var family
@Environment(\.colorScheme) private var systemScheme
@Environment(\.widgetRenderingMode) private var renderingMode
let entry: UsageEntry

private var palette: Palette {
Expand All @@ -67,10 +68,15 @@ struct UsageWidgetEntryView: View {
}
Button(intent: RefreshUsageIntent()) {
Image(systemName: "arrow.clockwise")
.font(.system(size: 9, weight: .semibold))
.foregroundColor(palette.secondary)
.padding(5)
.background(Circle().fill(palette.track))
.font(.system(size: 10, weight: .semibold))
.foregroundStyle(renderingMode == .fullColor ? palette.secondary : .primary)
.frame(width: 22, height: 22)
.background {
if renderingMode == .fullColor {
Circle().fill(palette.track)
}
}
.contentShape(Circle())
}
.buttonStyle(.plain)
.padding(7)
Expand Down
53 changes: 45 additions & 8 deletions agent/usage-agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,23 @@ def iso_to_unix(s):
CODEX_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage"

def read_codex(opener):
"""优先调实时接口(当前账号权威值),失败再回退本地文件扫描。"""
"""优先使用完整的实时窗口;不完整响应才回退本地一周快照。"""
live = _codex_live(opener)
return live if live is not None else _codex_from_files()
# 未能从实时接口识别出一周窗口时,才使用本地兜底。_codex_live 会根据
# limit_window_seconds 处理「primary 本身就是一周」的账号响应。
if live is not None and live.get("five_hour") is not None and live.get("seven_day") is not None:
return live

fallback = _codex_from_files()
if fallback.get("ok"):
if live and live.get("plan"):
fallback["plan"] = live["plan"]
return fallback

if live is not None:
live["five_hour"] = {"percent": 0, "resets_at": None}
return live
return fallback


def _codex_account_id(auth):
Expand Down Expand Up @@ -106,10 +120,21 @@ def _codex_live(opener):
req = urllib.request.Request(CODEX_USAGE_URL, headers=headers)
j = _open_json(opener, req)
rl = j.get("rate_limit") or {}
primary = rl.get("primary_window")
secondary = rl.get("secondary_window")

# 有些账号只返回一个 7 天窗口,却放在 primary_window(secondary 为 null)。
# 不能按字段名把它展示成 5 小时;按窗口时长映射到一周,并将 5 小时归零。
if _codex_window_seconds(primary) >= 6 * 24 * 3600:
five_hour = {"percent": 0, "resets_at": None}
seven_day = _codex_live_win(primary)
else:
five_hour = _codex_live_win(primary)
seven_day = _codex_live_win(secondary)
return {"ok": True,
"plan": j.get("plan_type"),
"five_hour": _codex_live_win(rl.get("primary_window")),
"seven_day": _codex_live_win(rl.get("secondary_window"))}
"five_hour": five_hour,
"seven_day": seven_day}
except Exception:
return None

Expand All @@ -122,13 +147,25 @@ def _codex_live_win(w):
"resets_at": int(ra) if ra else None}


def _codex_window_seconds(w):
if not isinstance(w, dict):
return 0
try:
return int(w.get("limit_window_seconds") or 0)
except (TypeError, ValueError):
return 0


def _codex_from_files():
"""回退:全局取「事件时间戳最新」的可用 primary/secondary
"""回退:只使用本地快照中的一周窗口

多个并发会话时,文件 mtime 最新的那条可能是额度耗尽写的 primary/secondary 全 null
的退化记录(limit_id="premium"),或比别的会话更旧。所以按每条 rate_limits 事件自身
的 timestamp 全局取最新。注意:本地文件不带账号 id,多账号时可能串到别的账号,
仅作为实时接口失败时的兜底。
的 timestamp 全局取最新。注意:本地文件不带账号 id,多账号时可能串到别的账号。

primary(5 小时)是 CLI 写入的临时限额快照,实时接口不可用时不能可靠代表当前
窗口;此降级路径固定返回 0%,避免把过期的临时用量误报为实时消耗。secondary
(一周)仍按最新可用快照显示。
"""
pattern = os.path.expanduser("~/.codex/sessions/**/*.jsonl")
files = sorted(glob.glob(pattern, recursive=True), key=os.path.getmtime, reverse=True)
Expand Down Expand Up @@ -158,7 +195,7 @@ def _codex_from_files():
if best_p[1] is None and best_s[1] is None:
return {"ok": False, "error": "无数据(还没用过 Codex?)"}
return {"ok": True,
"five_hour": _codex_win(best_p[1]),
"five_hour": {"percent": 0, "resets_at": None},
"seven_day": _codex_win(best_s[1]),
"plan": _codex_plan()}

Expand Down