From 83dd37f449f742084228ab710dc61437d023c485 Mon Sep 17 00:00:00 2001 From: yuanhe Date: Tue, 9 Jun 2026 20:29:43 +0800 Subject: [PATCH] fix(quota): render weekly boost (150%) and unlimited (status=3) quota MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /v1/token_plan/remains endpoint returns two signals the renderer ignored: - weekly_boost_permille (千分制, e.g. 1500 ⇒ ×1.5 ⇒ display up to 150%) - current_weekly_status === 3 ⇒ weekly quota is unlimited Previously, the weekly row always used the raw remaining percent, so a 1.5x boost plan showed 100% instead of 150%, and an unlimited plan was visually indistinguishable from a normal 100% account. Changes: - api.ts: add weekly_boost_permille?, current_interval_status?, current_weekly_status? to QuotaModelRemain - quota-table.ts: multiply weekly percent by boost_permille/1000, render '无限' / 'unlimited' for status=3, raise display ceiling to 200% to accommodate boosted values - quota-table.test.ts: cover 150% boost, 200% clamp, unlimited CN/EN --- src/output/quota-table.ts | 57 +++++++++++++-- src/types/api.ts | 7 ++ test/output/quota-table.test.ts | 124 ++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 7 deletions(-) diff --git a/src/output/quota-table.ts b/src/output/quota-table.ts index 0611318..8241c78 100644 --- a/src/output/quota-table.ts +++ b/src/output/quota-table.ts @@ -73,19 +73,42 @@ function displayWidth(s: string): number { const BAR_WIDTH = 16; const COMPACT_BAR_WIDTH = 10; +// Display ceiling. Server returns base percent (0–100) plus a `weekly_boost_permille` +// multiplier; a typical boosted plan shows up to 150%, so cap the rendered value +// at 200% to leave headroom and keep the bar/text readable. +const MAX_DISPLAY_PCT = 200; + +// Weekly quota is unlimited when the server reports `current_weekly_status: 3` +// (per the status enum: 1=normal, 2=exhausted, 3=unlimited). +function isUnweekly(status: number | undefined | null): boolean { + return status === 3; +} + function clampPct(value: number): number { - return Math.max(0, Math.min(100, Math.round(value))); + return Math.max(0, Math.min(MAX_DISPLAY_PCT, Math.round(value))); } -function remainingPct(percent: number | undefined | null, remaining: number, total: number): number { - return percent !== undefined && percent !== null - ? clampPct(percent) - : total > 0 ? clampPct((remaining / total) * 100) : 0; +function boostFactor(boostPermille: number | undefined | null): number { + if (boostPermille === undefined || boostPermille === null) return 1; + return Math.max(0, boostPermille) / 1000; +} + +function remainingPct( + percent: number | undefined | null, + remaining: number, + total: number, + boostPermille?: number | null, +): number { + const factor = boostFactor(boostPermille); + if (percent !== undefined && percent !== null) { + return clampPct(percent * factor); + } + return total > 0 ? clampPct((remaining / total) * 100 * factor) : 0; } function renderBar(remainingPct: number, color: boolean, barWidth: number = BAR_WIDTH, showPct: boolean = true): string { const pct = clampPct(remainingPct); - const ratio = pct / 100; + const ratio = Math.min(1, pct / 100); const filled = Math.round(barWidth * ratio); const empty = barWidth - filled; const pctStr = `${pct}%`.padStart(4); @@ -98,14 +121,31 @@ function renderBar(remainingPct: number, color: boolean, barWidth: number = BAR_ return showPct ? `${bar} ${fg}${B}${pctStr}${R}` : bar; } +const UNLIMITED_SYMBOL = '∞'; +const UNLIMITED_LABEL_CN = '无限'; +const UNLIMITED_LABEL_EN = 'unlimited'; + function renderMetric( label: string, remaining: number, total: number, percent: number | undefined | null, color: boolean, + boostPermille?: number | null, + unlimited?: boolean, + unlimitedLabel?: string, ): string { - const pct = remainingPct(percent, remaining, total); + if (unlimited) { + const ul = unlimitedLabel ?? UNLIMITED_SYMBOL; + const ulStr = ul.padStart(4); + if (color) { + const bar = `${BG_GREEN}${' '.repeat(COMPACT_BAR_WIDTH)}${R}`; + return `${D}${label}${R} ${bar} ${FG_GREEN}${B}${ulStr}${R}`; + } + const bar = `[${'█'.repeat(COMPACT_BAR_WIDTH)}]`; + return `${label} ${bar} ${ulStr}`; + } + const pct = remainingPct(percent, remaining, total, boostPermille); const bar = renderBar(pct, color, COMPACT_BAR_WIDTH, total <= 0); if (total > 0) { const count = `${remaining.toLocaleString()} / ${total.toLocaleString()}`; @@ -142,6 +182,9 @@ export function renderQuotaTable(models: QuotaModelRemain[], config: Config): vo m.current_weekly_total_count, m.current_weekly_remaining_percent, useColor, + m.weekly_boost_permille, + isUnweekly(m.current_weekly_status), + config.region === 'cn' ? UNLIMITED_LABEL_CN : UNLIMITED_LABEL_EN, ); const reset = `${L.resetsIn} ${formatDuration(m.remains_time, L.now)}`; return { displayName, current, weekly, reset }; diff --git a/src/types/api.ts b/src/types/api.ts index ad59e02..2a96ece 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -256,9 +256,16 @@ export interface QuotaModelRemain { current_weekly_total_count: number; current_weekly_usage_count: number; current_weekly_remaining_percent?: number; + // Server-side status. 1 = normal (limited), 2 = exhausted, 3 = unlimited. + current_interval_status?: number; + current_weekly_status?: number; weekly_start_time: number; weekly_end_time: number; weekly_remains_time: number; + // Weekly display multiplier in permille (1/1000). The server returns the + // base weekly remaining percent and a separate boost factor; the rendered + // weekly value is base × (boost_permille / 1000). 1500 ⇒ display up to 150%. + weekly_boost_permille?: number; } // ---- File ---- diff --git a/test/output/quota-table.test.ts b/test/output/quota-table.test.ts index 3c0a42c..5f24a00 100644 --- a/test/output/quota-table.test.ts +++ b/test/output/quota-table.test.ts @@ -129,4 +129,128 @@ describe('renderQuotaTable', () => { expect(output).toContain('21 / 21'); expect(output).not.toContain('0 / 3'); }); + + it('applies weekly_boost_permille (1500 ⇒ up to 150%) when rendering weekly percent', () => { + const lines: string[] = []; + const originalLog = console.log; + + console.log = (message?: unknown) => { + lines.push(String(message ?? '')); + }; + + try { + renderQuotaTable( + [ + { + ...createModel(), + current_weekly_total_count: 0, + current_weekly_usage_count: 0, + current_weekly_remaining_percent: 100, + weekly_boost_permille: 1500, + }, + ], + { ...createConfig(), noColor: true }, + ); + } finally { + console.log = originalLog; + } + + const output = lines.join('\n'); + expect(output).toContain('Wk left [██████████] 150%'); + }); + + it('clamps boosted weekly percent at MAX_DISPLAY_PCT (200)', () => { + const lines: string[] = []; + const originalLog = console.log; + + console.log = (message?: unknown) => { + lines.push(String(message ?? '')); + }; + + try { + renderQuotaTable( + [ + { + ...createModel(), + current_weekly_total_count: 0, + current_weekly_usage_count: 0, + current_weekly_remaining_percent: 100, + weekly_boost_permille: 3000, + }, + ], + { ...createConfig(), noColor: true }, + ); + } finally { + console.log = originalLog; + } + + const output = lines.join('\n'); + expect(output).toContain('200%'); + expect(output).not.toContain('300%'); + }); + + it('renders "无限" for weekly when status=3 (CN region)', () => { + const lines: string[] = []; + const originalLog = console.log; + + console.log = (message?: unknown) => { + lines.push(String(message ?? '')); + }; + + try { + renderQuotaTable( + [ + { + ...createModel(), + current_weekly_total_count: 0, + current_weekly_usage_count: 0, + current_weekly_remaining_percent: 100, + current_weekly_status: 3, + weekly_boost_permille: 1500, + }, + ], + { ...createConfig(), region: 'cn', noColor: true }, + ); + } finally { + console.log = originalLog; + } + + const output = lines.join('\n'); + expect(output).toContain('[██████████]'); + expect(output).toContain('周剩余'); + expect(output).toContain('无限'); + expect(output).not.toContain('150%'); + }); + + it('renders "unlimited" for weekly when status=3 (global region)', () => { + const lines: string[] = []; + const originalLog = console.log; + + console.log = (message?: unknown) => { + lines.push(String(message ?? '')); + }; + + try { + renderQuotaTable( + [ + { + ...createModel(), + current_weekly_total_count: 0, + current_weekly_usage_count: 0, + current_weekly_remaining_percent: 100, + current_weekly_status: 3, + }, + ], + { ...createConfig(), noColor: true }, + ); + } finally { + console.log = originalLog; + } + + const output = lines.join('\n'); + expect(output).toContain('[██████████]'); + expect(output).toContain('Wk left'); + expect(output).toContain('unlimited'); + expect(output).not.toContain('100%'); + }); });