diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 3e78c04e..fd54160c 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -1155,6 +1155,9 @@ export const jaOverrides = { 'quota.service.window.sevenDayOpus': '7 日間 Opus 枠', 'quota.service.window.sevenDaySonnet': '7 日間 Sonnet 枠', 'quota.service.window.sevenDayCowork': '7 日間 Cowork 枠', + 'quota.service.window.sevenDayScoped': '7 日間 {model} 枠', + 'quota.service.window.fiveHourScoped': '5 時間 {model} 枠', + 'quota.service.activeLimit': '現在有効な制限', 'quota.service.window.duration': '{duration}枠', 'quota.service.extraUsage': '追加使用量', 'quota.service.usedOf': '使用済み {used} / {limit}', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 4a9f7f44..2c53cad4 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1205,6 +1205,9 @@ export const en: Record = { 'quota.service.window.sevenDayOpus': '7-day Opus window', 'quota.service.window.sevenDaySonnet': '7-day Sonnet window', 'quota.service.window.sevenDayCowork': '7-day Cowork window', + 'quota.service.window.sevenDayScoped': '7-day {model} window', + 'quota.service.window.fiveHourScoped': '5-hour {model} window', + 'quota.service.activeLimit': 'Active limit', 'quota.service.window.duration': '{duration} window', 'quota.service.extraUsage': 'Extra usage', 'quota.service.usedOf': 'Used {used} / {limit}', diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 49188a33..abb82694 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -1203,6 +1203,9 @@ export const zhCN = { 'quota.service.window.sevenDayOpus': '7 天 Opus 窗口', 'quota.service.window.sevenDaySonnet': '7 天 Sonnet 窗口', 'quota.service.window.sevenDayCowork': '7 天 Cowork 窗口', + 'quota.service.window.sevenDayScoped': '7 天 {model} 窗口', + 'quota.service.window.fiveHourScoped': '5 小时 {model} 窗口', + 'quota.service.activeLimit': '当前生效限制', 'quota.service.window.duration': '{duration}窗口', 'quota.service.extraUsage': '额外用量', 'quota.service.usedOf': '已用 {used} / {limit}', diff --git a/src/services/quotaService.ts b/src/services/quotaService.ts index 37916181..bbf76d61 100644 --- a/src/services/quotaService.ts +++ b/src/services/quotaService.ts @@ -315,6 +315,31 @@ export const codexResetCreditDetailsFor = ( }; }; +const claudeScopedLimitRows = (value: Record): QuotaRow[] => { + const limits = Array.isArray(value.limits) ? value.limits : []; + const windowKeyByKind: Record[1]> = { + weekly_scoped: 'quota.service.window.sevenDayScoped', + session_scoped: 'quota.service.window.fiveHourScoped', + }; + return limits.flatMap((raw): QuotaRow[] => { + if (!isRecord(raw)) return []; + const windowKey = windowKeyByKind[readString(raw, 'kind')]; + if (!windowKey) return []; + const scope = isRecord(raw.scope) ? raw.scope : null; + const model = isRecord(scope?.model) ? scope.model : null; + const name = readString(model, 'display_name', 'displayName', 'id') || readString(scope, 'surface'); + if (!name) return []; + return [{ + label: quotaText(windowKey, { model: name }), + remainingPercent: remainingFromUsedPercent(raw.percent ?? raw.utilization), + reset: absoluteResetLabel(raw.resets_at ?? raw.resetsAt), + detail: booleanValue(raw.is_active ?? raw.isActive) === true + ? quotaText('quota.service.activeLimit') + : undefined, + }]; + }); +}; + export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRow[] => { const value = parseBody(payload); if (!isRecord(value)) return []; @@ -342,6 +367,8 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo }; }) .filter((row): row is QuotaRow => row !== null); + // Model-scoped limits (e.g. Fable) are reported only in `limits`, not as named windows. + rows.push(...claudeScopedLimitRows(value)); const extraUsage = isRecord(value.extra_usage) ? value.extra_usage : isRecord(value.extraUsage) diff --git a/tests/quotaService.test.ts b/tests/quotaService.test.ts index 4593e220..5baa458f 100644 --- a/tests/quotaService.test.ts +++ b/tests/quotaService.test.ts @@ -123,6 +123,30 @@ describe('quotaRowsFor', () => { expect(claude[0].remainingPercent).toBeCloseTo(99.56); }); + it('显示 Claude 按模型限定的周窗口(如 Fable)', () => { + const rows = quotaRowsFor('claude', { + five_hour: { utilization: 15, resets_at: '2027-01-01T00:00:00Z' }, + seven_day: { utilization: 18, resets_at: '2027-01-02T00:00:00Z' }, + seven_day_opus: null, + limits: [ + { kind: 'session', group: 'session', percent: 15, is_active: false }, + { kind: 'weekly_all', group: 'weekly', percent: 18, is_active: false }, + { + kind: 'weekly_scoped', + group: 'weekly', + scope: { model: { id: null, display_name: 'Fable' }, surface: null }, + percent: 36, + is_active: true, + resets_at: '2027-01-02T00:00:00Z', + }, + ], + }); + + expect(rows.map((row) => row.label)).toEqual(['5 小时窗口', '7 天窗口', '7 天 Fable 窗口']); + expect(rows[2]).toMatchObject({ remainingPercent: 64, detail: '当前生效限制' }); + expect(rows[2].reset).toBeTruthy(); + }); + it('显示 Claude 已启用的额外用量', () => { const rows = quotaRowsFor('claude', { five_hour: { utilization: 20, resets_at: '2027-01-01T00:00:00Z' },