Skip to content

Commit 83dd37f

Browse files
committed
fix(quota): render weekly boost (150%) and unlimited (status=3) quota
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
1 parent f42ee5e commit 83dd37f

3 files changed

Lines changed: 181 additions & 7 deletions

File tree

src/output/quota-table.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,42 @@ function displayWidth(s: string): number {
7373
const BAR_WIDTH = 16;
7474
const COMPACT_BAR_WIDTH = 10;
7575

76+
// Display ceiling. Server returns base percent (0–100) plus a `weekly_boost_permille`
77+
// multiplier; a typical boosted plan shows up to 150%, so cap the rendered value
78+
// at 200% to leave headroom and keep the bar/text readable.
79+
const MAX_DISPLAY_PCT = 200;
80+
81+
// Weekly quota is unlimited when the server reports `current_weekly_status: 3`
82+
// (per the status enum: 1=normal, 2=exhausted, 3=unlimited).
83+
function isUnweekly(status: number | undefined | null): boolean {
84+
return status === 3;
85+
}
86+
7687
function clampPct(value: number): number {
77-
return Math.max(0, Math.min(100, Math.round(value)));
88+
return Math.max(0, Math.min(MAX_DISPLAY_PCT, Math.round(value)));
7889
}
7990

80-
function remainingPct(percent: number | undefined | null, remaining: number, total: number): number {
81-
return percent !== undefined && percent !== null
82-
? clampPct(percent)
83-
: total > 0 ? clampPct((remaining / total) * 100) : 0;
91+
function boostFactor(boostPermille: number | undefined | null): number {
92+
if (boostPermille === undefined || boostPermille === null) return 1;
93+
return Math.max(0, boostPermille) / 1000;
94+
}
95+
96+
function remainingPct(
97+
percent: number | undefined | null,
98+
remaining: number,
99+
total: number,
100+
boostPermille?: number | null,
101+
): number {
102+
const factor = boostFactor(boostPermille);
103+
if (percent !== undefined && percent !== null) {
104+
return clampPct(percent * factor);
105+
}
106+
return total > 0 ? clampPct((remaining / total) * 100 * factor) : 0;
84107
}
85108

86109
function renderBar(remainingPct: number, color: boolean, barWidth: number = BAR_WIDTH, showPct: boolean = true): string {
87110
const pct = clampPct(remainingPct);
88-
const ratio = pct / 100;
111+
const ratio = Math.min(1, pct / 100);
89112
const filled = Math.round(barWidth * ratio);
90113
const empty = barWidth - filled;
91114
const pctStr = `${pct}%`.padStart(4);
@@ -98,14 +121,31 @@ function renderBar(remainingPct: number, color: boolean, barWidth: number = BAR_
98121
return showPct ? `${bar} ${fg}${B}${pctStr}${R}` : bar;
99122
}
100123

124+
const UNLIMITED_SYMBOL = '∞';
125+
const UNLIMITED_LABEL_CN = '无限';
126+
const UNLIMITED_LABEL_EN = 'unlimited';
127+
101128
function renderMetric(
102129
label: string,
103130
remaining: number,
104131
total: number,
105132
percent: number | undefined | null,
106133
color: boolean,
134+
boostPermille?: number | null,
135+
unlimited?: boolean,
136+
unlimitedLabel?: string,
107137
): string {
108-
const pct = remainingPct(percent, remaining, total);
138+
if (unlimited) {
139+
const ul = unlimitedLabel ?? UNLIMITED_SYMBOL;
140+
const ulStr = ul.padStart(4);
141+
if (color) {
142+
const bar = `${BG_GREEN}${' '.repeat(COMPACT_BAR_WIDTH)}${R}`;
143+
return `${D}${label}${R} ${bar} ${FG_GREEN}${B}${ulStr}${R}`;
144+
}
145+
const bar = `[${'█'.repeat(COMPACT_BAR_WIDTH)}]`;
146+
return `${label} ${bar} ${ulStr}`;
147+
}
148+
const pct = remainingPct(percent, remaining, total, boostPermille);
109149
const bar = renderBar(pct, color, COMPACT_BAR_WIDTH, total <= 0);
110150
if (total > 0) {
111151
const count = `${remaining.toLocaleString()} / ${total.toLocaleString()}`;
@@ -142,6 +182,9 @@ export function renderQuotaTable(models: QuotaModelRemain[], config: Config): vo
142182
m.current_weekly_total_count,
143183
m.current_weekly_remaining_percent,
144184
useColor,
185+
m.weekly_boost_permille,
186+
isUnweekly(m.current_weekly_status),
187+
config.region === 'cn' ? UNLIMITED_LABEL_CN : UNLIMITED_LABEL_EN,
145188
);
146189
const reset = `${L.resetsIn} ${formatDuration(m.remains_time, L.now)}`;
147190
return { displayName, current, weekly, reset };

src/types/api.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,16 @@ export interface QuotaModelRemain {
256256
current_weekly_total_count: number;
257257
current_weekly_usage_count: number;
258258
current_weekly_remaining_percent?: number;
259+
// Server-side status. 1 = normal (limited), 2 = exhausted, 3 = unlimited.
260+
current_interval_status?: number;
261+
current_weekly_status?: number;
259262
weekly_start_time: number;
260263
weekly_end_time: number;
261264
weekly_remains_time: number;
265+
// Weekly display multiplier in permille (1/1000). The server returns the
266+
// base weekly remaining percent and a separate boost factor; the rendered
267+
// weekly value is base × (boost_permille / 1000). 1500 ⇒ display up to 150%.
268+
weekly_boost_permille?: number;
262269
}
263270

264271
// ---- File ----

test/output/quota-table.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,128 @@ describe('renderQuotaTable', () => {
129129
expect(output).toContain('21 / 21');
130130
expect(output).not.toContain('0 / 3');
131131
});
132+
133+
it('applies weekly_boost_permille (1500 ⇒ up to 150%) when rendering weekly percent', () => {
134+
const lines: string[] = [];
135+
const originalLog = console.log;
136+
137+
console.log = (message?: unknown) => {
138+
lines.push(String(message ?? ''));
139+
};
140+
141+
try {
142+
renderQuotaTable(
143+
[
144+
{
145+
...createModel(),
146+
current_weekly_total_count: 0,
147+
current_weekly_usage_count: 0,
148+
current_weekly_remaining_percent: 100,
149+
weekly_boost_permille: 1500,
150+
},
151+
],
152+
{ ...createConfig(), noColor: true },
153+
);
154+
} finally {
155+
console.log = originalLog;
156+
}
157+
158+
const output = lines.join('\n');
159+
expect(output).toContain('Wk left [██████████] 150%');
160+
});
161+
162+
it('clamps boosted weekly percent at MAX_DISPLAY_PCT (200)', () => {
163+
const lines: string[] = [];
164+
const originalLog = console.log;
165+
166+
console.log = (message?: unknown) => {
167+
lines.push(String(message ?? ''));
168+
};
169+
170+
try {
171+
renderQuotaTable(
172+
[
173+
{
174+
...createModel(),
175+
current_weekly_total_count: 0,
176+
current_weekly_usage_count: 0,
177+
current_weekly_remaining_percent: 100,
178+
weekly_boost_permille: 3000,
179+
},
180+
],
181+
{ ...createConfig(), noColor: true },
182+
);
183+
} finally {
184+
console.log = originalLog;
185+
}
186+
187+
const output = lines.join('\n');
188+
expect(output).toContain('200%');
189+
expect(output).not.toContain('300%');
190+
});
191+
192+
it('renders "无限" for weekly when status=3 (CN region)', () => {
193+
const lines: string[] = [];
194+
const originalLog = console.log;
195+
196+
console.log = (message?: unknown) => {
197+
lines.push(String(message ?? ''));
198+
};
199+
200+
try {
201+
renderQuotaTable(
202+
[
203+
{
204+
...createModel(),
205+
current_weekly_total_count: 0,
206+
current_weekly_usage_count: 0,
207+
current_weekly_remaining_percent: 100,
208+
current_weekly_status: 3,
209+
weekly_boost_permille: 1500,
210+
},
211+
],
212+
{ ...createConfig(), region: 'cn', noColor: true },
213+
);
214+
} finally {
215+
console.log = originalLog;
216+
}
217+
218+
const output = lines.join('\n');
219+
expect(output).toContain('[██████████]');
220+
expect(output).toContain('周剩余');
221+
expect(output).toContain('无限');
222+
expect(output).not.toContain('150%');
223+
});
224+
225+
it('renders "unlimited" for weekly when status=3 (global region)', () => {
226+
const lines: string[] = [];
227+
const originalLog = console.log;
228+
229+
console.log = (message?: unknown) => {
230+
lines.push(String(message ?? ''));
231+
};
232+
233+
try {
234+
renderQuotaTable(
235+
[
236+
{
237+
...createModel(),
238+
current_weekly_total_count: 0,
239+
current_weekly_usage_count: 0,
240+
current_weekly_remaining_percent: 100,
241+
current_weekly_status: 3,
242+
},
243+
],
244+
{ ...createConfig(), noColor: true },
245+
);
246+
} finally {
247+
console.log = originalLog;
248+
}
249+
250+
const output = lines.join('\n');
251+
expect(output).toContain('[██████████]');
252+
expect(output).toContain('Wk left');
253+
expect(output).toContain('unlimited');
254+
expect(output).not.toContain('100%');
255+
});
132256
});

0 commit comments

Comments
 (0)