diff --git a/internal/gui/assets/app.js b/internal/gui/assets/app.js index 1b5dbbd4..f1d33589 100644 --- a/internal/gui/assets/app.js +++ b/internal/gui/assets/app.js @@ -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"); diff --git a/internal/gui/assets/i18n.js b/internal/gui/assets/i18n.js index f306c68a..e7b1547c 100644 --- a/internal/gui/assets/i18n.js +++ b/internal/gui/assets/i18n.js @@ -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", diff --git a/internal/gui/tests/README.md b/internal/gui/tests/README.md index 92e652b7..b883bee8 100644 --- a/internal/gui/tests/README.md +++ b/internal/gui/tests/README.md @@ -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 diff --git a/internal/gui/tests/balance-fix.test.cjs b/internal/gui/tests/balance-fix.test.cjs index 4bc46f4c..7707ca4a 100644 --- a/internal/gui/tests/balance-fix.test.cjs +++ b/internal/gui/tests/balance-fix.test.cjs @@ -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, []);