From 6bc3ff8bbd078ad5410f576299436cf6a91c2652 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Sun, 4 Oct 2026 08:30:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(plugins):=20ccquota=20=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E6=AF=8F=E6=9C=88=E9=A2=9D=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page showed a monthly BALANCE ($0.08 left) but no monthly ALLOWANCE, so "how much do I get and how much of it is gone" had no answer — the most useful number on a plan that is nearly exhausted. The vendor's credits endpoint reports what is left and never the cap, so a percentage is not derivable from it. Two things supply the missing half: - planId from subscriptions (here individual-goat), mapped to the caps the vendor publishes ($10 Go, $70 GOAT/Pro/Max). - totalMonthlyCredits from usage/summary, which counts over the BILLING PERIOD and so pairs with the monthly allowance rather than with the rolling 5h and weekly windows. Because the cap is a table lookup and not an API fact, it is cross-checked against monthly_used + balance: if the two disagree by more than 2% + $0.50, the card says the figure may be off rather than confidently printing a wrong percentage. A planId absent from the table renders the balance with no bar at all — inventing a denominator would be worse than admitting there isn't one. Also surfaces what the endpoints were already returning and the page was dropping: period_end (with a countdown), cancel_at_period_end, plan name, and the vendor's own belowThreshold flag, which is distinct from "balance > 0" — an account can hold $3 and still be under their threshold. Verified on production: both accounts show 每月 100% ($69.94/$70, red bar), 5 小时 0/14, 每周 0.803/35 on one; plan goat, 周期至 2026-10-17 / 2026-10-19. Co-Authored-By: ModelRouter --- internal/lua/plugins/ccquota.lua | 85 +++++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/internal/lua/plugins/ccquota.lua b/internal/lua/plugins/ccquota.lua index b37cd11..f9ecb8e 100644 --- a/internal/lua/plugins/ccquota.lua +++ b/internal/lua/plugins/ccquota.lua @@ -46,6 +46,26 @@ local REQUIRED_HEADERS = { -- key store). The kernel deliberately does NOT let a plugin read the config, so -- this is the only channel — which also means the key never sits in Lua state -- or in the persisted plugin file. +-- Monthly credit caps per planId. +-- +-- The credits endpoint reports what is LEFT but never the cap, so a percentage +-- is not derivable from it alone. The caps below come from the vendor's own +-- pricing page: every plan costs $10/month and grants a different credit pool +-- ($10 Go, $70 GOAT, $70 Pro, $70 Max). totalMonthlyCredits + monthlyCredits +-- also sums to the cap, so this table is a CROSS-CHECK on the plan, not the only +-- source — if the two disagree, PLAN_CAPS_WAIVED shows it instead of silently +-- displaying a wrong percentage. +-- +-- A planId absent from this table renders the balance with no percentage and no +-- bar. Guessing a cap for an unknown plan would be worse than showing none. +local PLAN_CAPS = { + ["individual-go"] = 10, + ["individual-goat"] = 70, + ["individual-pro"] = 70, + ["individual-max"] = 70, + ["team-goat"] = 70, +} + local ACCOUNTS = {} -- ---------------------------------------------------------------- helpers @@ -125,6 +145,11 @@ local function collectAccount(acct) report.purchased = tonum(c.purchasedCredits) report.free = tonum(c.freeCredits) report.total = report.monthly + report.purchased + report.free + -- belowThreshold is the vendor's own "running low" flag, distinct from + -- whether any balance remains: an account can hold $3 and still be below + -- their configured threshold. + report.below_threshold = c.belowThreshold == true + report.threshold = tonum(c.creditThreshold) report.limited = wl.limited == true report.exceeded = wl.exceeded report.fiveHour = wl.fiveHour @@ -137,7 +162,11 @@ local function collectAccount(acct) report.failures[#report.failures + 1] = "subscriptions: " .. tostring(serr) elseif type(subs) == "table" and type(subs.data) == "table" then report.plan_status = subs.data.status - report.plan_price = subs.data.metadata and nil + report.plan_id = subs.data.planId + report.plan_cap = PLAN_CAPS[tostring(subs.data.planId or "")] + report.period_start = subs.data.currentPeriodStart + report.period_end = subs.data.currentPeriodEnd + report.cancel_at_period_end = subs.data.cancelAtPeriodEnd == true local md = subs.data.metadata if type(md) == "table" then report.plan_name = md.plan or md.name or md.tier @@ -152,6 +181,23 @@ local function collectAccount(acct) report.spent = tonum(usage.totalCost) report.tokens = tonum(usage.totalTokens) report.success_rate = tonum(usage.successRate) + -- usage/summary counts over the BILLING PERIOD, so this pairs with the + -- monthly allowance rather than with the rolling 5h/weekly windows. + report.monthly_used = tonum(usage.totalMonthlyCredits) + report.period_basis = usage.periodBasis + end + + -- Cross-check the configured cap against what the vendor spent plus what is + -- left. A mismatch means PLAN_CAPS drifted from the real plan (a price change, + -- or a planId this table does not know) — surfaced rather than papered over, + -- because a confidently wrong "99% used" is the worst possible reading here. + if report.plan_cap and report.monthly_used and report.monthly_used > 0 then + local implied = report.monthly_used + report.total + local drift = math.abs(implied - report.plan_cap) + if drift > (report.plan_cap * 0.02 + 0.5) then + report.cap_mismatch = true + report.implied_cap = implied + end end -- An account whose every endpoint failed is an auth problem, not a data @@ -213,6 +259,8 @@ PLUGIN.ui = { .ccq-meta{display:flex;gap:14px;flex-wrap:wrap;font-size:11.5px;opacity:.7;margin-top:10px} .ccq-fail{font-size:11.5px;color:#b91c1c;margin-top:8px;word-break:break-word} .ccq-empty{opacity:.6;font-size:13px;padding:8px 0} +.ccq-win-nodat{opacity:.5} +.ccq-warn-line{font-size:11px;color:#b45309;margin-top:3px}