From bd9a3111fa81cb8c10317b1b862dc00b5f56c91d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 23 Jun 2026 17:32:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(usage=20=E5=89=A9=E9=A4=98=E7=99=BE?= =?UTF-8?q?=E5=88=86=E6=AF=94):=20=E6=8A=BD=E5=87=BA=20calculatePercent=20?= =?UTF-8?q?=E5=85=B1=E7=94=A8=E5=87=BD=E5=BC=8F=E4=B8=A6=E4=BF=AE=E6=AD=A3?= =?UTF-8?q?=E5=89=A9=E9=A4=98=E5=80=BC=E7=BC=BA=E5=A4=B1=E6=99=82=E8=AA=A4?= =?UTF-8?q?=E7=AE=97=200%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 剩餘值為 null/undefined 時原本經 Number() 轉成 0 而算出 0%, 改由 calculatePercent 統一檢查有效性,無效值一律落到「無法計算」。 Co-Authored-By: Claude Opus 4.8 (1M context) --- app/usage.js | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/app/usage.js b/app/usage.js index 16778a0..e8e668e 100644 --- a/app/usage.js +++ b/app/usage.js @@ -211,31 +211,39 @@ function round1(n) { const RATE_KIND_LABEL = { tokens: 'token', requests: '次數' }; -/** 是否為有限正數(排除 0、負數、NaN、Infinity),避免算出 Infinity%/NaN%/負百分比。 */ -function isFinitePositive(n) { - return Number.isFinite(Number(n)) && Number(n) > 0; +/** + * 計算「剩餘百分比」= remaining / limit × 100。 + * limit 或 remaining 為 null/undefined/NaN/Infinity,或 limit ≤ 0 時回 null, + * 避免算出 Infinity%/NaN%/負百分比或除以零。 + */ +function calculatePercent(remaining, limit) { + if (remaining == null || limit == null) return null; + const rem = Number(remaining); + const lim = Number(limit); + if (!Number.isFinite(rem) || !Number.isFinite(lim) || lim <= 0) return null; + return round1((rem / lim) * 100); } /** * 計算「剩餘可用百分比」,依優先序擇一: - * 1. 帳號額度(quota 有有限正數上限)→ 剩餘 credits / 上限; - * 2. 速率配額(rate limit header,有限正數上限)→ 當前視窗剩餘 / 上限; - * 上限或剩餘為 0/負數/NaN/Infinity 等無效值時不計算,落到 { percent: null, reason }。 + * 1. 帳號額度(quota 有有效上限)→ 剩餘 credits / 上限; + * 2. 速率配額(rate limit header,有有效上限)→ 當前視窗剩餘 / 上限; + * 上限或剩餘為無效值(null/0/負數/NaN/Infinity)時跳過計算,落到 { percent: null, reason }。 */ export function resolveRemainingPercent(quota, rate) { - if (quota?.available && quota.limit != null && isFinitePositive(quota.limit)) { + if (quota?.available && quota.limit != null) { const limit = Number(quota.limit); const remaining = quota.remaining == null ? limit - num(quota.used) : Number(quota.remaining); - if (Number.isFinite(remaining)) { - return { percent: round1((remaining / limit) * 100), basis: '帳號額度', remaining, limit, unit: quota.currency || '' }; + const percent = calculatePercent(remaining, limit); + if (percent != null) { + return { percent, basis: '帳號額度', remaining, limit, unit: quota.currency || '' }; } } - if (rate?.hasData && isFinitePositive(rate.limit)) { - const limit = Number(rate.limit); - const remaining = Number(rate.remaining); - if (Number.isFinite(remaining)) { + if (rate?.hasData) { + const percent = calculatePercent(rate.remaining, rate.limit); + if (percent != null) { const kindLabel = RATE_KIND_LABEL[rate.kind] || rate.kind; - return { percent: round1((remaining / limit) * 100), basis: `速率配額(當前視窗,${kindLabel})`, remaining, limit, unit: '' }; + return { percent, basis: `速率配額(當前視窗,${kindLabel})`, remaining: Number(rate.remaining), limit: Number(rate.limit), unit: '' }; } } let reason;