gui: a ZCode account with nothing left to spend says so, and says what to do (#1245)

A ZCode account whose free Start Plan (体验套餐) has run out and which has
no GLM Coding Plan of its own reads as "Allowance unavailable" on the
Usage page's card, which is the words for a reading that failed — the
account's real state is that it has nothing to spend. The built-in already
names it (zcodeStartQuota, internal/provider/zcode_start.go: "this account
has no GLM Coding Plan, and ZCode's Start Plan has ended or was never
started") and the plugin's usage error is that same string byte for byte
(NO_START, magpie-community/plugins packages/zcode); quotaError matched
neither, so both fell through to the fallback.

The card now says it, and says the action on the card itself: the
"subscribe at …" wording is in the sign-in errors (zcodeSignedIn /
signedIn), which never reach this card, so there was nothing to hover for
(yetone on #1001). "has ended" would also be wrong for an account that
never started one, hence "no free Start Plan".

The match is anchored on the whole card sentence, so the sign-in errors —
which name the address themselves — and the sibling "…Start Plan could not
be read: <err>" (a reading that failed) are both left alone.

zh, zh-TW, ja and de get the new string. balance-fix.test.cjs gains both
card errors, the sign-in error and the could-not-be-read one, asserting
the first two get the card's words and the last two do not; the new string
joins its list of those needing a Chinese. Red/green: the two card
assertions fail with the mapping taken out of app.js, and the
could-not-be-read and sign-in ones fail if the match is loosened to
"Start Plan has ended" anywhere in the string. 108 tests pass across
balance-fix, gui-de, gui-ja, gui-zh-tw and i18n-keys.
This commit is contained in:
如是 authored and GitHub committed 2026-10-08 15:39:50 +08:00
1 parent 01e66e0f4a
commit 52ad2d9845
4 files changed
+41 -1

No files matched your search

+13
View File
@@ -10979,6 +10979,19 @@ function quotaError(err) {
if (/violation of Terms of Service/i.test(err)) return t("Google has suspended this account — hover for details");
if (/access token is invalid or expired|didn't take the access token/.test(err)) return t("AiHubMix didn't take the access token — paste a new one in the provider's settings");
if (/this key has no limit/.test(err)) return t("This key has no limit — add the account's access token in the provider's settings to see its balance");
// ZCode with no GLM Coding Plan and no Start Plan left: the account has
// nothing to spend, which is not a reading that failed (#1001). Both the
// built-in (zcodeStartQuota, internal/provider/zcode_start.go) and the
// plugin (NO_START, packages/zcode) put these words on the card — the
// same string, byte for byte — and neither names a way out: the
// "subscribe at …" wording is in the sign-in errors (zcodeSignedIn /
// signedIn), which never reach this card. So the action is said here, on
// the card. "has ended" would also be wrong for an account that never
// started one, hence "no free Start Plan". "could not be read" is the
// other one: that is a reading that failed. The match is anchored on the whole
// card sentence, so the sign-in errors — which name the address themselves —
// and "could not be read" are left alone.
if (/^this account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started$/.test(err)) return t("ZCode: no GLM Coding Plan, and no free Start Plan — subscribe to a GLM Coding Plan to use this account");
// a remote magpie's card (remote_quotas.go)
if (/^nothing read on that magpie yet/.test(err)) return t("Nothing read on that computer yet — refresh this card to have it read");
if (/^remote magpie doesn't share its quotas/.test(err)) return t("That computer's magpie doesn't share its quotas yet — update magpie there");
+4
View File
@@ -2302,6 +2302,7 @@ const I18N = {
"Google no longer serves personal accounts to Gemini CLI — hover for more": "Google 已停止向个人账号提供 Gemini CLI,悬停查看详情",
"Google has suspended this account — hover for details": "此账号已被 Google 停用,悬停查看详情",
"This key has no limit — add the account's access token in the provider's settings to see its balance": "此 key 未设额度,在供应商设置中填入账户访问令牌即可查看余额",
"ZCode: no GLM Coding Plan, and no free Start Plan — subscribe to a GLM Coding Plan to use this account": "ZCode:没有 GLM Coding Plan,也没有免费的体验套餐 — 请订阅 GLM Coding Plan 后使用该账号",
"AiHubMix didn't take the access token — paste a new one in the provider's settings": "AiHubMix 不认这个访问令牌,请在供应商设置里重新填入",
"Account balance": "账户余额",
"Step Plan allowances": "Step Plan 额度",
@@ -6041,6 +6042,7 @@ const I18N = {
"Google no longer serves personal accounts to Gemini CLI — hover for more": "Google 已停止向個人帳號提供 Gemini CLI,懸停檢視詳情",
"Google has suspended this account — hover for details": "此帳號已被 Google 停用,懸停檢視詳情",
"This key has no limit — add the account's access token in the provider's settings to see its balance": "此 key 未設額度,在供應商設定中填入帳戶存取權杖即可檢視餘額",
"ZCode: no GLM Coding Plan, and no free Start Plan — subscribe to a GLM Coding Plan to use this account": "ZCode:沒有 GLM Coding Plan,也沒有免費的體驗套餐 — 請訂閱 GLM Coding Plan 後使用該帳號",
"AiHubMix didn't take the access token — paste a new one in the provider's settings": "AiHubMix 不認這個存取權杖,請在供應商設定裡重新填入",
"Account balance": "帳戶餘額",
"Step Plan allowances": "Step Plan 額度",
@@ -9744,6 +9746,7 @@ const I18N = {
"Google no longer serves personal accounts to Gemini CLI — hover for more": "Google は Gemini CLI での個人アカウントの提供を終了しました — ホバーで詳細を表示",
"Google has suspended this account — hover for details": "このアカウントは Google により停止されています — ホバーで詳細を表示",
"This key has no limit — add the account's access token in the provider's settings to see its balance": "このキーには上限がありません — 残高を表示するには、プロバイダの設定にアカウントのアクセストークンを追加してください",
"ZCode: no GLM Coding Plan, and no free Start Plan — subscribe to a GLM Coding Plan to use this account": "ZCode:GLM Coding Plan がなく、無料の体験プランもない — このアカウントを使うには GLM Coding Plan を購読してください",
"AiHubMix didn't take the access token — paste a new one in the provider's settings": "AiHubMix がアクセストークンを受け付けませんでした — プロバイダの設定で新しいものを貼り付けてください",
"Account balance": "アカウント残高",
"Step Plan allowances": "Step Plan の利用枠",
@@ -13466,6 +13469,7 @@ const I18N = {
"Google no longer serves personal accounts to Gemini CLI — hover for more": "Google bedient private Konten nicht mehr über Gemini CLI – für mehr darüberfahren",
"Google has suspended this account — hover for details": "Google hat dieses Konto gesperrt – für Details darüberfahren",
"This key has no limit — add the account's access token in the provider's settings to see its balance": "Dieser Schlüssel hat kein Limit – tragen Sie das Zugriffstoken des Kontos in den Anbietereinstellungen ein, um das Guthaben zu sehen",
"ZCode: no GLM Coding Plan, and no free Start Plan — subscribe to a GLM Coding Plan to use this account": "ZCode: kein GLM Coding Plan und kein kostenloser Start Plan – abonnieren Sie einen GLM Coding Plan, um dieses Konto zu nutzen",
"AiHubMix didn't take the access token — paste a new one in the provider's settings": "AiHubMix hat das Zugriffstoken nicht akzeptiert – fügen Sie in den Anbietereinstellungen ein neues ein",
"Account balance": "Kontoguthaben",
"Step Plan allowances": "Step-Plan-Kontingente",
+4 -1
View File
@@ -535,7 +535,10 @@ beside new-api's `/api/usage/token` (and a new one with a token and no
Balance URL): the editor says so, one click moves it to `/api/user/self`
with the quota as its field, the New-Api-User header is asked for until it
is typed, Check balance asks as the form has it and says the fix plainly,
and the Usage page's card does too.
and the Usage page's card does too. It also holds the card's own words for
a ZCode account with nothing left to spend (#1001): a Start Plan that has
ended is said as that, while one that couldn't be read stays "Allowance
unavailable" — the two are not the same thing.
`balance-field-default.test.cjs` leaves a custom provider's Balance field
empty (#881): its placeholder is the field magpie reads from a Balance URL
+20
View File
@@ -118,10 +118,29 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
quotaError("the Balance URL https://relay.example.com/api/usage/token takes the API key, not the access token: set it to https://relay.example.com/api/user/self (Balance field $data.quota / 500000) for the account's balance, or remove the token for the key's own"),
quotaError("401 Unauthorized: add the header New-Api-User = your user ID (shown in the site's personal settings) to this provider's Headers; the relay said: 无权进行此操作,未提供 New-Api-User"),
quotaError("401 Unauthorized: something else"),
// ZCode with nothing left to spend: the card gets either the
// built-in's words (zcodeStartQuota, zcode_start.go) or the
// plugin's NO_START, and the two are the same string — neither
// names a way out, so the card says the action itself (#1001)
quotaError("this account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started"),
quotaError("this account has no GLM Coding Plan, and ZCode's Start Plan has ended or was never started"),
// a sign-in error, which names the address and never reaches the
// card — it must not be mistaken for the card's
quotaError("this Z.ai account has no GLM Coding Plan, of its own or a team's, and ZCode's Start Plan has ended or was never started — subscribe at z.ai/subscribe, then add it again"),
// one that couldn't be read is a reading that failed, and says so
quotaError("this Z.ai account has no GLM Coding Plan, of its own or a team's, and ZCode's Start Plan could not be read: 502 Bad Gateway"),
]);
assert.match(said[0], /set it to …\/api\/user\/self/);
assert.match(said[1], /^Add the header New-Api-User = your user ID/);
assert.equal(said[2], "Allowance unavailable");
// the card's own words for a ZCode account with nothing to spend, and
// the action on the card itself (there is none in the error to hover for)
for (const s of [said[3], said[4]]) {
assert.match(s, /^ZCode: no GLM Coding Plan, and no free Start Plan/);
assert.match(s, /subscribe to a GLM Coding Plan to use this account/);
}
assert.equal(said[5], "Allowance unavailable", "a Start Plan that couldn't be read is not a plan that ended");
assert.equal(said[6], "Allowance unavailable", "a sign-in error names the address itself and is not the card's")
const missing = await page.evaluate(() => [
"…/api/usage/token takes the API key, not this token: for the account's balance, new-api uses /api/user/self; sub2api uses /api/v1/user/profile.",
"The token needs a Balance URL: a new-api relay uses /api/user/self; a sub2api panel uses /api/v1/user/profile to read the account's balance.",
@@ -129,6 +148,7 @@ for (const engine of (process.env.BROWSER ? [process.env.BROWSER] : ["chromium",
"A new-api relay also wants the header New-Api-User = your user ID (shown in the site's personal settings): add it under Headers.",
"The Balance URL …/api/usage/token takes the API key, not the access token — set it to …/api/user/self in the provider's settings",
"Add the header New-Api-User = your user ID (shown in the site's personal settings) to the provider's Headers",
"ZCode: no GLM Coding Plan, and no free Start Plan — subscribe to a GLM Coding Plan to use this account",
].filter((k) => !I18N.zh[k]));
assert.deepEqual(missing, [], "every new string has its Chinese");
assert.deepEqual(errors, []);