feat(ai-usage): 优化额度与重置进度展示

This commit is contained in:
ysicing committed 2026-09-23 22:28:55 +08:00
1 parent c7e4a34e91
commit 9a7e0cc1e0
4 files changed
+31 -11

No files matched your search

@@ -17,6 +17,7 @@ extension DS {
static let success = Color.dynamic(light: 0x16A34A, dark: 0x22C55E)
static let warning = Color.dynamic(light: 0xD97706, dark: 0xF59E0B)
static let error = Color.dynamic(light: 0xDC2626, dark: 0xF87171)
static let critical = Color.dynamic(light: 0x991B1B, dark: 0xEF4444)
// 文字
static let textPrimary = Color.dynamic(light: 0x111827, dark: 0xF3F4F6)
@@ -139,21 +139,19 @@ private struct LocalUsageContent: View {
.foregroundStyle(DS.Palette.textPrimary)
}
.dsFont(.xs)
ProgressView(value: window.remainingPercent, total: 100)
.tint(DS.Palette.primary)
ProgressTrack(fraction: window.remainingPercent / 100,
color: progressTint(for: window.remainingPercent))
.accessibilityLabel("\(quotaTitle(window.kind)) \(tr("剩余")) \(Int(window.remainingPercent.rounded()))%")
if let reset = window.resetsAt {
Text(tr("重置:") + " " + reset.formatted(Date.FormatStyle(date: .abbreviated, time: .shortened, locale: L10n.locale)))
.dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
TimelineView(.periodic(from: .now, by: 60)) { context in
if let fraction = window.remainingTimeFraction(at: context.date) {
HStack(spacing: DS.Space.s2) {
Text(tr("距重置"))
.dsFont(.xs).foregroundStyle(DS.Palette.textTertiary)
ProgressView(value: fraction, total: 1)
.tint(DS.Palette.textTertiary)
.accessibilityLabel(tr("距重置"))
}
// 时间条越接近重置越满、颜色越偏绿;额度条则随剩余量减少而变红。
ProgressTrack(fraction: 1 - fraction,
color: progressTint(for: (1 - fraction) * 100))
.accessibilityLabel(tr("重置:") + " " + reset.formatted(Date.FormatStyle(date: .abbreviated, time: .shortened, locale: L10n.locale)))
.accessibilityValue("\(Int(((1 - fraction) * 100).rounded()))%")
}
}
}
@@ -186,6 +184,14 @@ private struct LocalUsageContent: View {
}
}
private func progressTint(for percent: Double) -> Color {
if percent <= 10 { return DS.Palette.critical }
if percent <= 20 { return DS.Palette.error }
if percent < 40 { return DS.Palette.warning }
if percent < 80 { return DS.Palette.primary }
return DS.Palette.success
}
private func quotaFailureText(_ failure: AIQuotaFailure?) -> String {
switch failure {
case nil: tr("暂无额度数据")
@@ -57,6 +57,20 @@ private actor StubQuotaHTTPClient: QuotaHTTPClient {
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-token")
}
@Test func codexShowsOnlyQuotaWindowsReturnedForEachPlan() throws {
let now = Date(timeIntervalSince1970: 1_799_000_000)
let plus = Data(#"{"plan_type":"plus","rate_limit":{"primary_window":{"used_percent":35,"limit_window_seconds":18000,"reset_after_seconds":3600},"secondary_window":null}}"#.utf8)
let pro = Data(#"{"plan_type":"pro","rate_limit":{"primary_window":{"used_percent":74,"limit_window_seconds":604800,"reset_after_seconds":86400},"secondary_window":null}}"#.utf8)
let plusSnapshot = try CodexQuotaProvider.parse(plus, now: now)
#expect(plusSnapshot.window(.session)?.remainingPercent == 65)
#expect(plusSnapshot.window(.weekly) == nil)
let proSnapshot = try CodexQuotaProvider.parse(pro, now: now)
#expect(proSnapshot.window(.session) == nil)
#expect(proSnapshot.window(.weekly)?.remainingPercent == 26)
}
@Test func claudeMapsFiveHourAndWeeklyBuckets() async throws {
let http = StubQuotaHTTPClient(body: #"{"five_hour":{"utilization":22.5,"resets_at":"2026-09-23T10:00:00Z"},"seven_day":{"utilization":72,"resets_at":"2026-09-29T10:00:00Z"},"seven_day_sonnet":{"utilization":81,"resets_at":"2026-09-29T10:00:00Z"}}"#)
let provider = ClaudeQuotaProvider(credentials: { "claude-test-token" }, http: http)
+1 -2
View File
@@ -78,8 +78,7 @@ SOFTWARE.
## AI Provider 契约参考
XStats 的 AI Provider 契约与状态设计参考了以下项目公开的 Provider 契约、测试样例与实现模式;
XStats 未引入这些项目的可执行程序、Sparkle 更新模块或浏览器扩展,也不读取任何登录凭据:
XStats 的 AI Provider 契约与状态设计参考了以下项目公开的 Provider 契约、测试样例与实现模式:
- [burakgon/ai-usage-menubar](https://github.com/burakgon/ai-usage-menubar),MIT License,Copyright (c) 2026 Burak Gon
- [robinebers/openusage](https://github.com/robinebers/openusage),MIT License,Copyright (c) 2026 Robin Ebers