dsh-subusage 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -122,13 +122,24 @@ window.__ModuleLoader__.load({
122
122
  pillCookieSoon: "Cookie {rest}后到期",
123
123
  mimoCookieExpiresAt: "{time} 到期",
124
124
  noUsageData: "暂无额度数据",
125
+ statusPaygo: "可按量续用",
126
+ statusPaygoSub: "已购余额或计费接口支持按量续用;实际请求与余额仍以服务端为准",
127
+ statusIncludedAdvice: "基础额度已用尽,接口仍允许订阅续用",
128
+ statusAvailabilityUnknown: "续用条件待确认",
129
+ statusRestricted: "账户或预算已受限",
130
+ statusScopeLimited: "部分能力额度已用尽",
131
+ statusPoolAvailable: "轮换池仍有可用账户", statusPoolAvailableShort: "池可用", scopeAllShort: "共享额度",
132
+ statusPoolUnknown: "轮换池可用性待确认",
133
+ budgetKey: "Key 预算", budgetUser: "用户预算", budgetTeam: "团队预算", budgetScope: "上级预算",
134
+ noteLabel: "说明", creditStatusLabel: "Credits 状态", statusUnlimited: "已报告的套餐窗口不设限",
135
+ statusPaygoShort: "按量续用", statusIncludedShort: "仍可续用", statusUnknownShort: "续用待确认", statusScopeShort: "部分能力用尽", statusRestrictedShort: "已受限",
125
136
  providerUnavailable: "用量暂不可用",
126
137
  pillNeedCommandcode: "需配置 —— 可在 设置 → Command Code 登录、运行 cmd login,或导出 COMMANDCODE_API_KEY 后重试。",
127
138
  pillNeedGrok: "需登录 —— 可在 设置 → Grok Kit(xAI OAuth)登录或运行 grok login(共享登录文件 ~/.grok/auth.json)。",
128
139
  accountLabel: "账户",
129
140
  commandcodeManaged: "凭据由 Command Code 提供方插件(设置 → Command Code)管理;本页只读、不保存凭据。用量由本插件直接拉取其计费接口,取用同一凭据来源(凭据服务 COMMANDCODE_API_KEY → 启动环境 → ~/.commandcode/auth.json)。",
130
141
  commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai)。月总额按已知套餐快照推算,未知套餐只显示月剩余;月池耗尽不代表已购/赠送池不可用。显示选中账户的额度(未选择时为默认账户/顶层 Key),可能与实际服务账户不同;插件里自定义 apiBase 不会被跟随。账户切换就在下方「Command Code 账户」区,与药丸弹层同一开关;模型查询额度工具时会同时给出全部账户的余量与当前激活项。",
131
- grokManaged: "凭据由 dsh-grok-kit / Grok CLI 的 OAuth 登录文件(~/.grok/auth.json)管理;本页只读、不保存凭据、不刷新 token。用量由本插件取同一凭据直连 Grok CLI 计费代理(cli-chat-proxy.grok.com)拉取。",
142
+ grokManaged: "凭据由 dsh-grok-kit / Grok CLI 的 OAuth 登录文件(~/.grok/auth.json)管理;本页不保存凭据,平时只读该文件。access token 过期时,本插件借 dsh-grok-kit 的官方登录链自动续约(同一文件、同一把写锁,不会顶掉它的登录态);未安装 dsh-grok-kit 或续约失败时才提示手动重新登录。用量由本插件取同一凭据直连 Grok CLI 计费代理(cli-chat-proxy.grok.com)拉取。",
132
143
  grokHelp: "并行读取 /v1/billing?format=credits 与 /v1/settings。creditUsagePercent 是统一用量池的已用百分比(通常为周池,重置于 currentPeriod.end,剩余 = 100 − 已用);套餐名取 subscription_tier_display;已购加量余额 prepaidBalance 以美元显示。API Key(XAI_API_KEY)路线取不到订阅周池,必须 OAuth 登录。",
133
144
  ccAccountsTitle: "Command Code 账户",
134
145
  ccAccountAuto: "自动轮换",
@@ -139,7 +150,29 @@ window.__ModuleLoader__.load({
139
150
  ccUsageAccount: "上方用量:{account}",
140
151
  ccUsageAutoNote: "自动轮换时上方用量仍是默认账户(顶层 Key),不跟随实际服务账户",
141
152
  ccSwitching: "切换中…",
142
- ccSwitchFailed: "切换失败"
153
+ ccSwitchFailed: "切换失败",
154
+ // Command Code 自动轮换池健康度(其余账户用量由 Host 并行读取,各自缓存 60 秒)。
155
+ ccPoolTitle: "轮换账户池",
156
+ ccPoolAllHealthy: "{n} 个账户均可轮换使用",
157
+ ccPoolCapacity: "{available}/{total} 个账户可用,{blocked} 个已达限额",
158
+ ccPoolUnusable: "{unusable} 个账户无可用凭据或暂时读不到",
159
+ ccPoolExhausted: "轮换账户均已达限额,新请求很可能被拒绝",
160
+ ccPoolExhaustedShort: "池已满限额",
161
+ ccPoolClickToPin: "点击可固定到此账户",
162
+ // Host 失败原因按 errorCode 本地化(Host message 为中文兜底,原文保留在节点 title 供排障)。
163
+ errNetwork: "网络请求失败或超时,请稍后重试",
164
+ errHttp: "服务暂时不可用,请稍后重试",
165
+ errRateLimit: "请求过于频繁,已按服务端要求退避,请稍后再试",
166
+ errAuth: "登录或凭据已失效,请检查或重新配置",
167
+ errCredentials: "凭据无法解析,请检查凭据来源",
168
+ errIdentityTimeout: "凭据解析超时,请稍后重试",
169
+ errResponse: "用量响应无法解析(上游接口可能已变更)",
170
+ errConfig: "本机订阅设置无法读取或保存",
171
+ errRevisionConflict: "设置已被其他窗口修改,请重新读取后再保存",
172
+ errConfigChanged: "读取期间配置已改变,请刷新",
173
+ errUnknownProvider: "本机运行中的 Host 版本较旧,不认识该提供商;重启 DSH 后生效",
174
+ errTimeout: "查询超时,请稍后重试或减少查询家数",
175
+ errDisposed: "服务已卸载"
143
176
  };
144
177
  const en = {
145
178
  nav: "Subscription usage",
@@ -244,13 +277,24 @@ window.__ModuleLoader__.load({
244
277
  pillCookieSoon: "Cookie expires in {rest}",
245
278
  mimoCookieExpiresAt: "Expires {time}",
246
279
  noUsageData: "No quota data",
280
+ statusPaygo: "Paid usage available",
281
+ statusPaygoSub: "Purchased balance or billing advice permits paid usage; the server and actual balance remain authoritative",
282
+ statusIncludedAdvice: "Base quota used up; subscription usage still advised",
283
+ statusAvailabilityUnknown: "Continuation conditions not confirmed",
284
+ statusRestricted: "Account or budget restricted",
285
+ statusScopeLimited: "Some capability quotas used up",
286
+ statusPoolAvailable: "Rotation pool still has usable accounts", statusPoolAvailableShort: "pool usable", scopeAllShort: "shared quota",
287
+ statusPoolUnknown: "Rotation pool availability not confirmed",
288
+ budgetKey: "Key budget", budgetUser: "User budget", budgetTeam: "Team budget", budgetScope: "Parent budget",
289
+ noteLabel: "Note", creditStatusLabel: "Credits status", statusUnlimited: "Reported plan windows are unlimited",
290
+ statusPaygoShort: "paid usage", statusIncludedShort: "usage advised", statusUnknownShort: "check continuation", statusScopeShort: "some quotas used up", statusRestrictedShort: "restricted",
247
291
  providerUnavailable: "Usage unavailable",
248
292
  pillNeedCommandcode: "Setup required — sign in under Settings → Command Code, run cmd login, or export COMMANDCODE_API_KEY and retry.",
249
293
  pillNeedGrok: "Login required — sign in under Settings → Grok Kit (xAI OAuth) or run grok login (shared login file ~/.grok/auth.json).",
250
294
  accountLabel: "Account",
251
295
  commandcodeManaged: "Credentials are managed by the Command Code provider plugin (Settings → Command Code); this page is read-only and stores nothing. Usage is fetched directly by this plugin from its billing endpoint, using the same credential sources (credential service COMMANDCODE_API_KEY → launch environment → ~/.commandcode/auth.json).",
252
296
  commandcodeHelp: "Parallel requests to /alpha/billing/credits and /alpha/billing/subscriptions (default API base https://api.commandcode.ai). Monthly totals use a known-plan snapshot; unknown plans show remaining credits only. Monthly-pool depletion does not imply purchased/free credits are unusable. Shows the selected account's quota (the default/top-level key account when none is selected), which may differ from the serving account; a custom apiBase in the provider plugin is not followed. Account switching lives in the “Command Code accounts” block below — the same switch as the pill popover; the quota tool reports every account plus which one is active.",
253
- grokManaged: "Credentials are the OAuth login file shared by dsh-grok-kit / Grok CLI (~/.grok/auth.json); this page is read-only — it stores nothing and never refreshes the token. Usage is fetched directly by this plugin from the Grok CLI billing proxy (cli-chat-proxy.grok.com) with the same credential.",
297
+ grokManaged: "Credentials are the OAuth login file shared by dsh-grok-kit / Grok CLI (~/.grok/auth.json); this page stores nothing and only reads the file. When the access token expires, this plugin renews it through dsh-grok-kit's (≥0.2.0) official sign-in chain — same file, same write lock, so the shared login cannot be invalidated by this renewal; it only asks for a manual re-login when dsh-grok-kit is absent or renewal fails. Usage is fetched directly from the Grok CLI billing proxy (cli-chat-proxy.grok.com) with the same credential.",
254
298
  grokHelp: "Parallel requests to /v1/billing?format=credits and /v1/settings. creditUsagePercent is the USED percentage of the unified usage pool (usually weekly, resetting at currentPeriod.end; remaining = 100 − used). The plan name comes from subscription_tier_display; the purchased top-up balance prepaidBalance is shown in USD. An API key (XAI_API_KEY) cannot read the subscription pool — OAuth sign-in is required.",
255
299
  ccAccountsTitle: "Command Code accounts",
256
300
  ccAccountAuto: "Automatic (rotation order)",
@@ -261,7 +305,29 @@ window.__ModuleLoader__.load({
261
305
  ccUsageAccount: "Usage above: {account}",
262
306
  ccUsageAutoNote: "with automatic rotation the usage above stays on the default (top-level key) account, not the serving one",
263
307
  ccSwitching: "Switching…",
264
- ccSwitchFailed: "Switch failed"
308
+ ccSwitchFailed: "Switch failed",
309
+ // Command Code rotation-pool health (the Host reads the other accounts in parallel, each cached 60s).
310
+ ccPoolTitle: "Rotation account pool",
311
+ ccPoolAllHealthy: "All {n} accounts are available",
312
+ ccPoolCapacity: "{available}/{total} accounts available, {blocked} at their limit",
313
+ ccPoolUnusable: "{unusable} account(s) have no usable credentials or could not be read",
314
+ ccPoolExhausted: "Every rotation account is at its limit; new requests are likely to be rejected",
315
+ ccPoolExhaustedShort: "pool at limit",
316
+ ccPoolClickToPin: "Click to pin to this account",
317
+ // Host failure reasons localized by errorCode (the Host message stays Chinese; the raw text remains in node title for debugging).
318
+ errNetwork: "Network request failed or timed out; retry shortly",
319
+ errHttp: "The service is temporarily unavailable; retry shortly",
320
+ errRateLimit: "Rate limited; backing off as the server asked, retry shortly",
321
+ errAuth: "Sign-in or credentials were rejected; check or re-enter them",
322
+ errCredentials: "Credentials could not be resolved; check the credential source",
323
+ errIdentityTimeout: "Credential resolution timed out; retry shortly",
324
+ errResponse: "The usage response could not be parsed (the upstream API may have changed)",
325
+ errConfig: "The local subscription settings could not be read or saved",
326
+ errRevisionConflict: "Settings were changed in another window; reload before saving",
327
+ errConfigChanged: "Settings changed during the read; refresh",
328
+ errUnknownProvider: "The running Host is older and does not know this provider; restart DSH to take effect",
329
+ errTimeout: "The query timed out; retry shortly or query fewer providers",
330
+ errDisposed: "The service has been unloaded"
265
331
  };
266
332
  const NS = "settings.subusage";
267
333
  const KIND_LABEL = { "5h": "w5h", "7d": "w7d", day: "wday", week: "wweek", month: "wmonth", period: "wperiod", sub: "wsub", rolling: "wrolling" };
@@ -282,7 +348,6 @@ window.__ModuleLoader__.load({
282
348
  // SuperGrok / X Premium 订阅(dsh-grok-kit 的 xai-oauth 路由):OAuth 登录文件凭据,无继承变量。
283
349
  "xai-oauth": { label: "SuperGrok", short: "SuperGrok", envName: "", managedByPlugin: true },
284
350
  // Codex / ChatGPT 订阅:凭据是 Codex CLI 的 OAuth 登录文件,只读、不刷新、不接受本地 Key。
285
- // 默认开启:它对齐的是 DSH 内置的 openai-codex 路由,没登录时会被「默认隐藏」自动收起。
286
351
  // 默认关闭:Codex 的提供方插件自带用量药丸,两个并排只是重复信息(仍可手动开启)。
287
352
  "openai-codex": { label: "Codex (ChatGPT)", short: "Codex", envName: "", managedByPlugin: true, defaultEnabled: false },
288
353
  // 国际版默认关闭,中国版保持默认开启。
@@ -319,7 +384,7 @@ window.__ModuleLoader__.load({
319
384
  };
320
385
  // 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
321
386
  const PROVIDER_ORDER = ["zai-coding-cn", "kimi-coding", "xiaomi-token-plan-cn", "opencode-go", "commandcode", "openai-codex", "xai-oauth", "minimax", "minimax-cn", "zai-coding", "synthetic", "nanogpt", "siliconflow", "deepseek", "openrouter", "novita", "hyperbolic", "deepinfra", "chutes", "ollama-cloud", "vercel-ai-gateway", "zenmux", "litellm", "arkcli-agent-plan", "arkcli-coding-plan", "arkcli-agent-plan-team", "arkcli-coding-plan-team"];
322
- // 火山方舟的 7 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
387
+ // 火山方舟的 4 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
323
388
  // 一个开关(组开关落在 Host 的 visibility.ark 上)。这些 id 不能真正合并——药丸靠 provider id 匹配路由。
324
389
  const ARK_IDS = PROVIDER_ORDER.filter(id => PROVIDER_META[id]?.volc);
325
390
  /**
@@ -349,7 +414,7 @@ window.__ModuleLoader__.load({
349
414
  if (settings?.visibility?.hideWithoutApi === false) return true;
350
415
  // 火山方舟**不按推理路由收起**:额度是账号级订阅,路由装没装与额度无关——一组 IAM AK/SK 就能
351
416
  // 查到名下所有套餐(GetCodingPlanUsage 与 GetAFPUsage 同协议同凭据)。但它**要按接口回报的
352
- // 订阅状态收起**:7 条路由一律列出来,用户根本分不清哪个是哪个,而其中多数是「未检测到该套餐订阅」。
417
+ // 订阅状态收起**:4 条路由一律列出来,用户根本分不清哪个是哪个,而其中多数是「未检测到该套餐订阅」。
353
418
  // `subscribed === undefined` 表示还没拉到数据,按「未知」放行——不能因为一次没拉到就把有订阅的藏起来。
354
419
  if (PROVIDER_META[id]?.volc) {
355
420
  if (entry?.subscribed === false) return false;
@@ -403,12 +468,12 @@ window.__ModuleLoader__.load({
403
468
  }
404
469
  /** extras 标签:plan / account / balance 三类,未知类型回退为余额。 */
405
470
  function extraLabel(x, t) {
406
- return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : x.kind === "spend" ? t("spendLabel") : t("balanceLabel");
471
+ return x.kind === "plan" ? t("planLabel") : x.kind === "account" ? t("accountLabel") : x.kind === "monthly-balance" ? t("monthlyBalanceLabel") : ["note", "quota-status"].includes(x.kind) ? t("noteLabel") : x.kind === "credit-status" ? t("creditStatusLabel") : x.kind === "spend" || x.kind.endsWith("-spend") ? t("spendLabel") : t("balanceLabel");
407
472
  }
408
473
  /** 余额兜底文案:没有额度窗口但接口报告了余额时(余额型 provider、或额度接口暂时只给余额),余额就是唯一信息。 */
409
474
  function balanceText(entry, t) {
410
- const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") : undefined;
411
- return row ? `${t("balanceLabel")} ${row.value}` : null;
475
+ const row = entry && Array.isArray(entry.extras) ? entry.extras.find(x => x.kind === "balance") ?? entry.extras.find(x => x.kind === "spend") : undefined;
476
+ return row ? `${extraLabel(row, t)} ${row.value}` : null;
412
477
  }
413
478
  /** 时长紧凑文案:窗口重置倒计时与 Cookie 剩余有效期共用同一档位。 */
414
479
  function durationText(ms, t) {
@@ -467,8 +532,11 @@ window.__ModuleLoader__.load({
467
532
  if (!value || typeof value !== "object") throw new Error("Invalid dsh-subusage settings");
468
533
  if (value.visibility !== undefined) {
469
534
  const v = value.visibility;
470
- if (!v || typeof v !== "object" || Array.isArray(v) || Object.keys(value).some(k => !["visibility", "expectedRevision"].includes(k)) || typeof value.expectedRevision !== "string" || !value.expectedRevision || !Object.keys(v).length || Object.keys(v).some(k => !["providers", "hideWithoutApi"].includes(k))) throw new Error("Invalid visibility patch");
535
+ // 白名单必须与 Host 的 parseSettings 逐字对齐:火山组开关走 visibility.ark(Host 合法字段),
536
+ // 漏掉它会让两端 schema 漂移(当前网关出站不校验,但任何复用本 parser 的出站校验都会致障)。
537
+ if (!v || typeof v !== "object" || Array.isArray(v) || Object.keys(value).some(k => !["visibility", "expectedRevision"].includes(k)) || typeof value.expectedRevision !== "string" || !value.expectedRevision || !Object.keys(v).length || Object.keys(v).some(k => !["providers", "hideWithoutApi", "ark"].includes(k))) throw new Error("Invalid visibility patch");
471
538
  if (v.hideWithoutApi !== undefined && typeof v.hideWithoutApi !== "boolean") throw new Error("Invalid auto-hide switch");
539
+ if (v.ark !== undefined && typeof v.ark !== "boolean") throw new Error("Invalid Ark group switch");
472
540
  if (v.providers !== undefined && (!v.providers || typeof v.providers !== "object" || Array.isArray(v.providers) || !Object.keys(v.providers).length || Object.entries(v.providers).some(([id, enabled]) => !PROVIDER_ORDER.includes(id) || typeof enabled !== "boolean"))) throw new Error("Invalid provider switches");
473
541
  }
474
542
  return value;
@@ -614,6 +682,31 @@ window.__ModuleLoader__.load({
614
682
  if (entry.coverage === "partial") return t("statusPartial");
615
683
  return t("statusOk");
616
684
  }
685
+ /**
686
+ * Host 的 failure message 是中文兜底(含错误特有的排障细节),英文界面直接渲染会中英混杂。
687
+ * 按稳定的 errorCode 出当前语言文案;认不出的 code 或传输层错误(无 code)保留原文——
688
+ * 原文可能携带只有它才有的信息(如 HTTP 状态、火山 Action 名),调用方把它放进节点 title。
689
+ */
690
+ const ERROR_TEXT_KEY = {
691
+ "subusage/network": "errNetwork",
692
+ "subusage/http": "errHttp",
693
+ "subusage/rate-limit": "errRateLimit",
694
+ "subusage/auth": "errAuth",
695
+ "subusage/credentials": "errCredentials",
696
+ "subusage/identity-timeout": "errIdentityTimeout",
697
+ "subusage/response": "errResponse",
698
+ "subusage/config": "errConfig",
699
+ "subusage/revision-conflict": "errRevisionConflict",
700
+ "subusage/config-changed": "errConfigChanged",
701
+ "subusage/unknown-provider": "errUnknownProvider",
702
+ "subusage/timeout": "errTimeout",
703
+ "subusage/disposed": "errDisposed"
704
+ };
705
+ function localError(errorCode, raw, t) {
706
+ const key = typeof errorCode === "string" ? ERROR_TEXT_KEY[errorCode] : undefined;
707
+ if (key) return t(key);
708
+ return typeof raw === "string" && raw ? raw : t("statusError");
709
+ }
617
710
  /**
618
711
  * 需要用户行动时的内联入口类型。设置页的「状态」在上(用量面板)、「操作」在下(提供商管理),
619
712
  * 出问题时用户得先去下面找到对应厂商才能修——这里让问题旁边直接出现修复动作。
@@ -639,13 +732,11 @@ window.__ModuleLoader__.load({
639
732
  */
640
733
  function usageAttentionRank(entry, t) {
641
734
  if (!entry) return 0;
642
- if (entry.state === "error") return 3;
643
- const expiry = cookieExpiry(entry, t);
644
- if (expiry && expiry.expired) return 3;
645
- if ((entry.windows || []).some(w => w.status === "rate-limited")) return 2;
646
- if (expiry && expiry.rank >= 2) return 2;
647
- if (entry.freshness === "stale" || entry.coverage === "partial") return 1;
648
- return 0;
735
+ const status = computeStatus(entry, { providerId: entry.providerId, t, getLocale: () => undefined });
736
+ if (status.key === "error" || status.key === "cookie" && status.expiry?.expired) return 3;
737
+ if (["blocked", "restricted", "cookie"].includes(status.key)) return 2;
738
+ if (["paygo", "included", "unknown", "scope", "pool-unknown", "degraded"].includes(status.key)) return 1;
739
+ return status.key === "pool" && (entry.accountPool.blockedCount || entry.accountPool.unusableCount) ? 1 : 0;
649
740
  }
650
741
  function providerDescription(entry, t) {
651
742
  const windows = canShowUsage(entry) ? entry.windows || [] : [];
@@ -690,7 +781,14 @@ window.__ModuleLoader__.load({
690
781
  };
691
782
  const refresh = (ids, force = false) => {
692
783
  if (disposed) return Promise.reject(new Error("Client 已卸载"));
693
- const unique = [...new Set(ids)].filter((id) => PROVIDER_ORDER.includes(id) && providerEnabled(snapshot.settings, id) && providerRoutable(snapshot, id));
784
+ // Ark 必须豁免路由检查:它的额度是账号级管控面订阅(一组 AK/SK 查名下全部套餐),
785
+ // providerVisible 对 volc 同样不看推理路由——这里若按 providerRoutable 过滤,
786
+ // 「有套餐但本机没装对应 arkcli 路由」的条目在激活刷新、重置到期补拉与定时刷新时
787
+ // 全部静默不发请求,用户点「刷新当前」也空转,只有「刷新全部」走 read 才能更新。
788
+ const unique = [...new Set(ids)].filter((id) => {
789
+ if (!PROVIDER_ORDER.includes(id) || !providerEnabled(snapshot.settings, id)) return false;
790
+ return PROVIDER_META[id]?.volc ? true : providerRoutable(snapshot, id);
791
+ });
694
792
  const start = (id) => {
695
793
  if (inflight.has(id)) return inflight.get(id);
696
794
  const startedEpoch = epoch, request = markRequest([id]);
@@ -785,12 +883,6 @@ window.__ModuleLoader__.load({
785
883
  };
786
884
  }
787
885
 
788
- // ── 用量药丸(匹配当前选中模型的 provider)──────────────────────────────
789
- function severity(percent, status) {
790
- if (status === "rate-limited" || percent >= 100) return "limited";
791
- return percent >= 80 ? "high" : void 0;
792
- }
793
-
794
886
  // ── Command Code 账户切换(对齐提供方插件的账户显示)────────────────────
795
887
  // 账户列表与 id 规则对齐 @mars-sea/dsh-commandcode-provider 的 slots():
796
888
  // 默认账户 id 固定 "default";额外账户 id 取其 apiKeyEnv(凭据引用名)。
@@ -948,6 +1040,53 @@ window.__ModuleLoader__.load({
948
1040
  );
949
1041
  }
950
1042
 
1043
+ /**
1044
+ * Command Code 自动轮换池健康摘要(弹层与设置页共用):
1045
+ * 数据来自 Host 挂在 commandcode entry 上的 accountPool(auto 模式才有)。
1046
+ * 不简单取「最差账户」染红——账户间是独立配额,单个限额轮换器会避开它;
1047
+ * 只有全部有凭据账户都限额(exhausted)才是真的要 429。点账户行可固定到它。
1048
+ */
1049
+ function CommandCodePoolSummary({ pool, cc, t }) {
1050
+ if (!pool || pool.mode !== "auto" || !Array.isArray(pool.accounts) || pool.total <= 1) return null;
1051
+ const summary = pool.exhausted
1052
+ ? t("ccPoolExhausted")
1053
+ : pool.blockedCount > 0 || pool.unusableCount > 0
1054
+ ? t("ccPoolCapacity").replace("{available}", pool.available).replace("{total}", pool.total).replace("{blocked}", pool.blockedCount)
1055
+ : t("ccPoolAllHealthy").replace("{n}", pool.total);
1056
+ const tone = pool.exhausted ? "#ef4444" : pool.blockedCount > 0 || pool.unusableCount > 0 ? "#eab308" : "#22c55e";
1057
+ const rowFor = (account) => {
1058
+ const worst = worstWindow(account.windows || []);
1059
+ const tier = account.availability === "paygo" ? { color: "#eab308", icon: "ℹ" } : account.usable || account.blocked ? usageTier(worst || { percent: 0, status: "ok" }) : null;
1060
+ const blocked = account.blocked;
1061
+ const unusable = !account.usable && !account.blocked;
1062
+ const dot = statusGlyph(unusable ? NEUTRAL : (tier ? tier.color : NEUTRAL), unusable ? "?" : blocked ? "✕" : (tier ? tier.icon : "?"), 14);
1063
+ const detail = unusable
1064
+ ? (account.availability === "unknown" ? t("statusUnknownShort") : account.state === "error" ? t("statusError") : account.state === "disabled" ? t("statusDisabled") : account.state === "no-cookie" ? t("statusNoCookie") : t("statusNoKey"))
1065
+ : blocked
1066
+ ? t("pillLimited")
1067
+ : account.availability === "paygo" ? t("statusPaygoShort") : worst ? pillText(worst, (account.windows || []).length > 1, t)
1068
+ : t("noUsageData");
1069
+ const canPin = cc && account.id !== "default";
1070
+ return h("button", {
1071
+ key: account.id, type: "button", disabled: !canPin,
1072
+ title: canPin ? t("ccPoolClickToPin") : undefined,
1073
+ onClick: canPin ? () => void cc.switchTo(account.id) : undefined,
1074
+ style: { display: "flex", alignItems: "center", gap: 6, width: "100%", textAlign: "left", marginTop: 4, padding: "3px 6px", borderRadius: 6, border: "1px solid rgba(128,128,128,.25)", background: "transparent", color: "inherit", font: "inherit", fontSize: 11, cursor: canPin ? "pointer" : "default" }
1075
+ },
1076
+ dot,
1077
+ h("span", { style: { flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, account.id === "default" ? t("ccAccountDefault") : account.label),
1078
+ h("span", { style: { opacity: 0.75, whiteSpace: "nowrap" } }, detail));
1079
+ };
1080
+ return h("div", { style: { marginTop: 10, padding: "8px 10px", borderRadius: 10, border: `1px solid ${alpha(tone, ".35")}`, background: alpha(tone, ".07") } },
1081
+ h("div", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11.5, fontWeight: 600, color: textTone(tone) } },
1082
+ h("span", null, t("ccPoolTitle")),
1083
+ h("span", { style: { flex: 1 } }),
1084
+ h("span", { style: { fontWeight: 500, opacity: 0.9 } }, `${pool.available}/${pool.total}`)),
1085
+ h("p", { role: pool.exhausted ? "alert" : "status", style: { margin: "3px 0 0", fontSize: 11, color: textTone(tone) } }, summary),
1086
+ pool.unusableCount > 0 ? h("p", { style: { margin: "2px 0 0", fontSize: 10.5, opacity: 0.7 } }, t("ccPoolUnusable").replace("{unusable}", pool.unusableCount)) : null,
1087
+ h("div", { style: { marginTop: 4 } }, pool.accounts.map(rowFor)));
1088
+ }
1089
+
951
1090
  // ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
952
1091
  // 注意:下面这些都是**普通函数调用**而不是 React 组件元素——它们不用 Hook,直接内联展开,
953
1092
  // 测试里的 React 桩也能看到它们渲染出的文字。
@@ -1007,7 +1146,7 @@ window.__ModuleLoader__.load({
1007
1146
  // 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时主文案仍是失败原因;
1008
1147
  // 例外是已过期——它本身就是「请重新登录」的行动项。
1009
1148
  const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || expiry.expired);
1010
- const windowName = (w) => t(KIND_LABEL[w.kind] ?? "wall");
1149
+ const windowName = (w) => w.budgetScope ? t({ key: "budgetKey", user: "budgetUser", team: "budgetTeam", scope: "budgetScope" }[w.budgetScope] ?? "budgetScope") : t(KIND_LABEL[w.kind] ?? "wall") + (w.detail?.scope === "all" ? ` · ${t("scopeAllShort")}` : "");
1011
1150
  const windowShort = (w) => multi ? `${t(KIND_SHORT[w.kind] ?? "srolling")} ` : "";
1012
1151
  const windowSub = (w) => {
1013
1152
  const head = t(multi ? "statusWorst" : "statusOnly").replace("{w}", windowName(w)).replace("{p}", String(windowLeft(w)));
@@ -1019,6 +1158,17 @@ window.__ModuleLoader__.load({
1019
1158
  const base = { entry, windows, worst, worstTier, multi, degraded, expiry, expiring, expiryActionable, needConfig };
1020
1159
  if ((!rawEntry && !failed && !failState) || state === "loading") return { ...base, key: "loading", color: NEUTRAL, icon: "…", title: t("loading"), sub: "", short: "", tinted: false };
1021
1160
  if (state === "disabled") return { ...base, key: "disabled", color: NEUTRAL, icon: "–", title: t("statusDisabled"), sub: "", short: t("statusDisabled"), tinted: false };
1161
+ const pool = rawEntry?.accountPool;
1162
+ if (pool?.mode === "auto" && pool.available > 0) return { ...base, key: "pool", color: pool.blockedCount || pool.unusableCount ? "#eab308" : "#22c55e", icon: "✓", title: t("statusPoolAvailable"), sub: `${pool.available}/${pool.total}`, short: pool.blockedCount || pool.unusableCount ? `${pool.available}/${pool.total} ${t("statusPoolAvailableShort")}` : "", tinted: !!(pool.blockedCount || pool.unusableCount) };
1163
+ if (pool?.mode === "auto" && !pool.exhausted && pool.accounts?.some(a => a.state === "error" || a.availability === "unknown")) return { ...base, key: "pool-unknown", color: "#eab308", icon: "?", title: t("statusPoolUnknown"), sub: "", short: t("statusPoolUnknown"), tinted: true };
1164
+ const freshAdvice = rawEntry?.state === "ok" && rawEntry.freshness !== "stale";
1165
+ if (freshAdvice && rawEntry.availability === "blocked" && (rawEntry.availabilityReason !== "quota-exhausted" || !worstTier || worstTier.rank < 4)) return { ...base, key: "restricted", color: "#ef4444", icon: "✕", title: t("statusRestricted"), sub: "", short: t("statusRestrictedShort"), tinted: true };
1166
+ if (freshAdvice && (["paygo", "unknown"].includes(rawEntry.availability) || rawEntry.availability === "included" && worstTier?.rank >= 4)) {
1167
+ const title = t(rawEntry.availability === "paygo" ? "statusPaygo" : rawEntry.availability === "included" ? "statusIncludedAdvice" : "statusAvailabilityUnknown");
1168
+ return { ...base, key: rawEntry.availability, color: "#eab308", icon: rawEntry.availability === "unknown" ? "?" : "ℹ", title, sub: rawEntry.availability === "paygo" ? t("statusPaygoSub") : worst ? windowSub(worst) : "", short: t(rawEntry.availability === "paygo" ? "statusPaygoShort" : rawEntry.availability === "included" ? "statusIncludedShort" : "statusUnknownShort"), tinted: true };
1169
+ }
1170
+ const scopeLimited = worstTier?.rank >= 4 && windows.some(w => w.status !== "rate-limited" && w.detail?.scope && w.detail.scope !== "all") && windows.filter(w => w.status === "rate-limited").every(w => w.detail?.scope && w.detail.scope !== "all");
1171
+ if (scopeLimited) return { ...base, key: "scope", color: "#eab308", icon: "⚠", title: t("statusScopeLimited"), sub: windowSub(worst), short: t("statusScopeShort"), tinted: true };
1022
1172
  if (worstTier && worstTier.rank >= 4) {
1023
1173
  const title = t(worst.cascade ? "statusBlocked" : "statusExhausted");
1024
1174
  return { ...base, key: "blocked", color: worstTier.color, icon: "✕", title, sub: windowSub(worst), short: windowShort(worst) + t(worst.cascade ? "pillBlocked" : "pillLimited"), tinted: true };
@@ -1030,7 +1180,8 @@ window.__ModuleLoader__.load({
1030
1180
  return { ...base, key: "config", color: INFO, icon: "⚙", title: t(state === "no-key" ? "statusNoKey" : "statusNoCookie"), sub: needConfig, short: t(state === "no-key" ? "needConfigShort" : "statusNoCookie"), tinted: true };
1031
1181
  }
1032
1182
  if (!entry) {
1033
- return { ...base, key: "error", color: "#ef4444", icon: "!", title: t("statusError"), sub: failed || (rawEntry && rawEntry.error) || "", short: t("statusError"), tinted: true };
1183
+ const rawReason = failed || (rawEntry && rawEntry.error) || "";
1184
+ return { ...base, key: "error", color: "#ef4444", icon: "!", title: t("statusError"), sub: localError(rawEntry?.errorCode, rawReason, t), rawReason, short: t("statusError"), tinted: true };
1034
1185
  }
1035
1186
  if (degraded) {
1036
1187
  // 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
@@ -1044,6 +1195,7 @@ window.__ModuleLoader__.load({
1044
1195
  if (worstTier.rank === 2) return { ...base, key: "low", color: worstTier.color, icon: "⚠", title: t("statusLow"), sub, short: windowShort(worst) + t(worstTier.badge), tinted: true };
1045
1196
  return { ...base, key: "ok", color: worstTier.color, icon: "✓", title: t("statusPlenty"), sub, short: "", tinted: false };
1046
1197
  }
1198
+ if (entry.extras?.some(e => e.kind === "quota-status" && e.value === "unlimited")) return { ...base, key: "unlimited", color: NEUTRAL, icon: "ℹ", title: t("statusUnlimited"), sub: "", short: t("statusUnlimited"), tinted: false };
1047
1199
  // 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
1048
1200
  const balance = balanceText(entry, t);
1049
1201
  return { ...base, key: "balance", color: NEUTRAL, icon: "ℹ", title: balance ?? t("noUsageData"), sub: "", short: balance ?? t("noUsageData"), tinted: false };
@@ -1098,7 +1250,7 @@ window.__ModuleLoader__.load({
1098
1250
  style: { marginTop: 8, padding: "10px 12px", borderRadius: 10, background: blocked ? alpha(tier.color, ".09") : "rgba(128,128,128,.07)" }
1099
1251
  },
1100
1252
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1101
- h("span", { style: { fontSize: 12.5, fontWeight: 600 } }, t(KIND_LABEL[w.kind] ?? "wall")),
1253
+ h("span", { style: { fontSize: 12.5, fontWeight: 600 } }, w.budgetScope ? t({ key: "budgetKey", user: "budgetUser", team: "budgetTeam", scope: "budgetScope" }[w.budgetScope] ?? "budgetScope") : t(KIND_LABEL[w.kind] ?? "wall") + (w.detail?.scope === "all" ? ` · ${t("scopeAllShort")}` : "")),
1102
1254
  alert ? statusChip(t(tier.badge), tier.color) : null,
1103
1255
  h("span", { style: { flex: 1 } }),
1104
1256
  h("strong", { style: { fontSize: 18, lineHeight: 1.1, color: alert ? textTone(tier.color) : "inherit", whiteSpace: "nowrap" } }, `${left}%`),
@@ -1119,7 +1271,7 @@ window.__ModuleLoader__.load({
1119
1271
  * 弹层与设置页共用,避免只有其中一处带分组。
1120
1272
  */
1121
1273
  function UsageWindowList({ windows, t, getLocale, uncertain = false }) {
1122
- return (windows || []).map((w, index) => h(react.Fragment, { key: w.kind },
1274
+ return (windows || []).map((w, index) => h(react.Fragment, { key: `${w.budgetScope ?? w.kind}:${index}` },
1123
1275
  w.groupLabel && w.groupLabel !== windows[index - 1]?.groupLabel
1124
1276
  ? h("p", { role: "note", style: { margin: index ? "14px 0 0" : "6px 0 0", fontSize: 11, opacity: 0.6 } }, w.groupLabel)
1125
1277
  : null,
@@ -1206,8 +1358,14 @@ window.__ModuleLoader__.load({
1206
1358
  }, [open]);
1207
1359
  const rawEntry = snapshot && snapshot.entry;
1208
1360
  if (rawEntry?.visible === false || rawEntry?.state === "disabled") return null;
1361
+ // Command Code 自动轮换:默认账户健康不代表池健康。所有有凭据账户都限额(exhausted)
1362
+ // 时,用池里一条 rate-limited 窗口喂给 computeStatus,让药丸/横幅如实染红;部分受限不染红
1363
+ // (轮换器会避开该账户),只在弹层池摘要里提示容量降级。
1364
+ const pool = providerId === "commandcode" ? (rawEntry?.accountPool ?? null) : null;
1365
+ const exhaustedWindow = pool?.exhausted ? (pool.accounts.flatMap(a => a.windows || []).find(w => w.status === "rate-limited") ?? null) : null;
1366
+ const statusEntry = exhaustedWindow ? { ...rawEntry, state: "ok", coverage: "complete", freshness: "fresh", windows: [exhaustedWindow] } : rawEntry;
1209
1367
  // 整体状态由 computeStatus 一处判定:药丸、弹层横幅、设置页横幅读的是同一个结论。
1210
- const st = computeStatus(rawEntry, { failed, failState, providerId, t, getLocale });
1368
+ const st = computeStatus(statusEntry, { failed, failState, providerId, t, getLocale });
1211
1369
  const { entry, windows, worst, degraded, expiry, expiring } = st;
1212
1370
  const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
1213
1371
  // 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
@@ -1252,7 +1410,7 @@ window.__ModuleLoader__.load({
1252
1410
  planName(entry) ? h("span", { style: { fontSize: 11, fontWeight: 600, padding: "1px 8px", borderRadius: 999, background: "rgba(128,128,128,.16)", whiteSpace: "nowrap" } }, planName(entry)) : null),
1253
1411
  // 状态横幅:先给结论(充足 / 偏少 / 即将用尽 / 已用尽 / 需配置 / 失败…)与一行依据。
1254
1412
  statusBanner(st),
1255
- failed && st.key !== "error" && !st.needConfig ? h("p", { style: { color: "#f87171", margin: "8px 0 0", whiteSpace: "pre-wrap" } }, failed) : null,
1413
+ failed && st.key !== "error" && !st.needConfig ? h("p", { role: "alert", title: failed, style: { color: "#f87171", margin: "8px 0 0", whiteSpace: "pre-wrap" } }, localError(rawEntry?.errorCode, failed, t)) : null,
1256
1414
  // Cookie 到期常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久(已在横幅里说明时不重复)。
1257
1415
  expiry && st.key !== "cookie" ? h("p", { role: "status", style: { margin: "8px 0 0", fontSize: 11, color: expiring ? textTone(expiry.color) : "inherit", opacity: expiring ? 1 : 0.75 } }, `${expiry.label} · ${t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale()))}`) : null,
1258
1416
  // 用量账户标注:账户区 ready 时说明上方数字属于哪个账户;自动轮换时明确不跟随实际服务账户。
@@ -1261,6 +1419,9 @@ window.__ModuleLoader__.load({
1261
1419
  t("ccUsageAccount").replace("{account}", ccState.activeId === "" || ccState.activeId === "default" ? t("ccAccountDefault") : (ccState.accounts.find((a) => a.id === ccState.activeId)?.label ?? ccState.activeId)),
1262
1420
  ccState.activeId === "" ? h("span", { style: { opacity: 0.85 } }, ` · ${t("ccUsageAutoNote")}`) : null)
1263
1421
  : null,
1422
+ // 自动轮换池健康度:默认账户之外的账户也可能已限额,池耗尽时药丸已染红,这里给出逐账户明细。
1423
+ // 直接函数调用(非 React 元素),与 UsageWindowList 等视觉原语同约定,测试桩可见展开内容。
1424
+ pool ? CommandCodePoolSummary({ pool, cc, t }) : null,
1264
1425
  // 额度线切换时插一条分组标题:AFP 的日限额只覆盖视觉 / 语音模型与 Harness,与文本模型的
1265
1426
  // 5 小时 / 周 / 月不是同一条线,混排会让人把两条线的配额当成一组来互相比较。
1266
1427
  UsageWindowList({ windows, t, getLocale, uncertain: degraded }),
@@ -1365,14 +1526,14 @@ window.__ModuleLoader__.load({
1365
1526
  const text = (cn, en) => local ? cn : en;
1366
1527
  react.useEffect(() => {
1367
1528
  let alive = true;
1368
- void usageStore.readAll().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(String(error.message || error)); });
1529
+ void usageStore.readAll().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
1369
1530
  return () => { alive = false; };
1370
1531
  }, [usageStore]);
1371
1532
  react.useEffect(() => {
1372
1533
  let alive = true;
1373
1534
  const enabled = editorOpen && providerEnabled(result.settings, tab);
1374
1535
  const deactivate = enabled ? usageStore.activate(tab) : () => {};
1375
- void usageStore.readAll().then(() => { if (alive && enabled && providerEnabled(usageStore.getSnapshot().settings, tab)) return usageStore.refresh([tab], false); }).catch((error) => { if (alive) setLoadError(String(error.message || error)); });
1536
+ void usageStore.readAll().then(() => { if (alive && enabled && providerEnabled(usageStore.getSnapshot().settings, tab)) return usageStore.refresh([tab], false); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
1376
1537
  return () => { alive = false; deactivate(); };
1377
1538
  }, [usageStore, tab, editorOpen, providerEnabled(result.settings, tab)]);
1378
1539
  react.useEffect(() => {
@@ -1433,7 +1594,7 @@ window.__ModuleLoader__.load({
1433
1594
  const refresh = async (all) => {
1434
1595
  setRefreshing(true);
1435
1596
  try { if (!result.settings) await usageStore.readAll(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
1436
- catch (error) { if (aliveRef.current) setLoadError(String(error.message || error)); }
1597
+ catch (error) { if (aliveRef.current) setLoadError(localError(error.code, String(error.message || error), t)); }
1437
1598
  finally { if (aliveRef.current) setRefreshing(false); }
1438
1599
  };
1439
1600
  const saveVisibility = async (visibility) => {
@@ -1448,7 +1609,7 @@ window.__ModuleLoader__.load({
1448
1609
  // 新启用条目先重新探测凭据;保存成功不与接口验证混为一谈。
1449
1610
  const enabled = Object.entries(visibility.providers || {}).filter(([, value]) => value).map(([id]) => id);
1450
1611
  if (enabled.length) { try { await usageStore.refresh(enabled, false); } catch { if (aliveRef.current) setLoadError(text("设置已保存;用量刷新失败,可稍后重试。", "Settings saved; usage refresh failed. Retry later.")); } }
1451
- } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: String(error.message || error) }); }
1612
+ } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: localError(error.code, String(error.message || error), t) }); }
1452
1613
  finally { if (aliveRef.current) setBusy(false); }
1453
1614
  };
1454
1615
  const onSave = async () => {
@@ -1473,16 +1634,19 @@ window.__ModuleLoader__.load({
1473
1634
  const current = verified.entries.find((e) => e.providerId === tab);
1474
1635
  if (aliveRef.current) setLoadError(current && current.state === "ok" ? null : text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + (current && current.error || statusText(current, t)));
1475
1636
  } catch (error) { if (aliveRef.current) setLoadError(text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + String(error.message || error)); }
1476
- } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: String(error.message || error) }); }
1637
+ } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: localError(error.code, String(error.message || error), t) }); }
1477
1638
  finally { if (aliveRef.current) setBusy(false); }
1478
1639
  };
1479
1640
  const inputStyle = { width: "100%", minWidth: 0, boxSizing: "border-box", padding: "6px 8px", borderRadius: 6, border: "1px solid rgba(128,128,128,.4)", background: "transparent", color: "inherit", font: "inherit" };
1480
1641
  const btnStyle = { padding: "6px 10px", borderRadius: 6, border: "1px solid rgba(128,128,128,.35)", background: "transparent", color: "inherit", cursor: "pointer", font: "inherit" };
1481
1642
  const primary = { ...btnStyle, background: "rgba(59,130,246,.12)", borderColor: "rgba(59,130,246,.65)" };
1482
1643
  const entries = result.entries || [];
1644
+ // 首次挂载时 settings 还没读到(readAll 在飞行中),providerVisible 对缺失一律「未知放行」,
1645
+ // 会让标签栏先闪现全部默认开启的提供商;未初始化期间标签栏、空状态与计数都不渲染。
1646
+ const initialized = !!result.settings;
1483
1647
  const entry = entries.find((e) => e.providerId === tab);
1484
1648
  // 待处理的排前面;组内保持登记顺序,所以顺序不会自己抖动。
1485
- const visibleIds = PROVIDER_ORDER
1649
+ const visibleIds = !initialized ? [] : PROVIDER_ORDER
1486
1650
  .map((id, index) => ({ id, index, rank: usageAttentionRank(entries.find(e => e.providerId === id), t) }))
1487
1651
  .filter(item => providerVisible(result.settings, item.id, entries.find(e => e.providerId === item.id), result))
1488
1652
  .sort((a, b) => b.rank - a.rank || a.index - b.index)
@@ -1499,7 +1663,11 @@ window.__ModuleLoader__.load({
1499
1663
  const usageMeta = PROVIDER_META[usageId];
1500
1664
  // 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
1501
1665
  const usageAction = usageActionKind(usageEntry, usageId, t);
1502
- const usageStatus = computeStatus(usageEntry, { failed: usageEntry && usageEntry.state !== "ok" ? usageEntry.error || usageEntry.state : null, failState: usageEntry && usageEntry.state, providerId: usageId, t, getLocale });
1666
+ // Command Code 自动轮换:池耗尽时横幅按池中一条限额窗口染红(与药丸同一结论)。
1667
+ const usagePool = usageId === "commandcode" ? (usageEntry?.accountPool ?? null) : null;
1668
+ const usageExhaustedWindow = usagePool?.exhausted ? (usagePool.accounts.flatMap(a => a.windows || []).find(w => w.status === "rate-limited") ?? null) : null;
1669
+ const usageStatusEntry = usageExhaustedWindow ? { ...usageEntry, state: "ok", coverage: "complete", freshness: "fresh", windows: [usageExhaustedWindow] } : usageEntry;
1670
+ const usageStatus = computeStatus(usageStatusEntry, { failed: usageEntry && usageEntry.state !== "ok" ? usageEntry.error || usageEntry.state : null, failState: usageEntry && usageEntry.state, providerId: usageId, t, getLocale });
1503
1671
  // 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
1504
1672
  const revealEditor = (target) => {
1505
1673
  openEditor(target);
@@ -1610,7 +1778,7 @@ window.__ModuleLoader__.load({
1610
1778
  h("button", { type: "button", role: "switch", "aria-checked": hideWithoutApi, disabled: busy || dirty || !result.settings?.revision, "aria-label": text("没有检测到API的默认隐藏", "Hide providers without detected API credentials"), style: hideWithoutApi ? primary : btnStyle, onClick: () => saveVisibility({ hideWithoutApi: !hideWithoutApi }) }, `${hideWithoutApi ? "✓ " : "○ "}${text("没有检测到API的默认隐藏", "Hide providers without detected API credentials")}`),
1611
1779
  h("p", { style: { fontSize: 12, opacity: .75 } }, text("检测依据为可用 Key 或 MiMo Cookie 是否存在;认证失败和网络错误不会当成未配置 API。隐藏的提供商仍可在这里配置。", "Detection checks for a Key or MiMo Cookie, not request success. Auth/network errors are not treated as missing API credentials. Hidden providers remain configurable here.")),
1612
1780
  // 启用的排到最前面,省得每次往下翻;组内保持 PROVIDER_ORDER 的既有顺序(sort 稳定)。
1613
- // Ark 的 7 条路由合并成一张卡片:只渲染本机装了的那条作代表(都没装就渲染第一条),
1781
+ // Ark 的 4 条路由合并成一张卡片:只渲染本机装了的那条作代表(都没装就渲染第一条),
1614
1782
  // 由它统一控制整组的检测开关与那组共享的 AK/SK。
1615
1783
  h("div", { style: { display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 240px), 1fr))", gap: 8 } }, [...PROVIDER_ORDER].filter(id => !PROVIDER_META[id]?.volc || id === (ARK_IDS.find(key => providerRoutable(result, key)) ?? ARK_IDS[0])).sort((a, b) => Number(!providerEnabled(result.settings, a)) - Number(!providerEnabled(result.settings, b))).map(id => {
1616
1784
  const isArk = Boolean(PROVIDER_META[id].volc), arkPresent = isArk ? ARK_IDS.filter(key => providerRoutable(result, key)) : [];
@@ -1631,20 +1799,20 @@ window.__ModuleLoader__.load({
1631
1799
  return h("div", { ref: root, style: { padding: 16, maxWidth: 720, minWidth: 0, font: "13px/1.6 sans-serif", containerType: "inline-size", containerName: "subusage" } },
1632
1800
  h("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" } },
1633
1801
  h("h2", { style: { margin: 0, fontSize: 16 } }, t("title")),
1634
- h("button", {
1802
+ initialized ? h("button", {
1635
1803
  type: "button",
1636
1804
  disabled: !troubledIds.length,
1637
1805
  onClick: () => { if (troubledIds.length) chooseUsage(troubledIds[0]); },
1638
1806
  title: troubledIds.length ? text(`跳到 ${PROVIDER_META[troubledIds[0]].short}(共 ${troubledIds.length} 家待处理)`, `Jump to ${PROVIDER_META[troubledIds[0]].short} (${troubledIds.length} need attention)`) : text("全部获取成功", "All fetched"),
1639
1807
  "aria-label": text(`${success}/${visibleIds.length} 家数据获取成功;${troubledIds.length ? `点击跳到 ${PROVIDER_META[troubledIds[0]].short}` : "没有待处理项"}`, `${success}/${visibleIds.length} providers fetched; ${troubledIds.length ? `click to jump to ${PROVIDER_META[troubledIds[0]].short}` : "nothing needs attention"}`),
1640
1808
  style: { ...btnStyle, padding: "2px 8px", fontSize: 12, opacity: .85, cursor: troubledIds.length ? "pointer" : "default", color: troubledIds.length ? "#b7791f" : "inherit" }
1641
- }, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)),
1809
+ }, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)) : null,
1642
1810
  h("button", { type: "button", disabled: busy || refreshing || !usageId, style: { ...primary, cursor: busy || refreshing || !usageId ? "default" : "pointer" }, onClick: () => refresh(false) }, refreshing ? t("refreshing") : text("刷新当前", "Refresh current")),
1643
1811
  h("button", { type: "button", disabled: busy || refreshing, style: { ...btnStyle, cursor: busy || refreshing ? "default" : "pointer" }, onClick: () => refresh(true) }, refreshing ? t("refreshing") : text("刷新全部", "Refresh all"))
1644
1812
  ),
1645
1813
  loadError ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, loadError) : null,
1646
- !visibleIds.length ? h("p", { role: "status" }, text("没有可显示的提供商,请展开下方「提供商管理」开启、配置 API,或关闭默认隐藏。", "No visible providers. Expand Provider management below to enable a provider, configure API credentials, or turn off auto-hide.")) : null,
1647
- h("div", { className: "subusage-provider-tabs", role: "tablist", "aria-label": text("模型商", "Providers"), style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 14 } },
1814
+ initialized && !visibleIds.length ? h("p", { role: "status" }, text("没有可显示的提供商,请展开下方「提供商管理」开启、配置 API,或关闭默认隐藏。", "No visible providers. Expand Provider management below to enable a provider, configure API credentials, or turn off auto-hide.")) : null,
1815
+ initialized ? h("div", { className: "subusage-provider-tabs", role: "tablist", "aria-label": text("模型商", "Providers"), style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 14 } },
1648
1816
  visibleIds.map((id, index) => {
1649
1817
  const e = entries.find((item) => item.providerId === id), active = usageId === id;
1650
1818
  // 标签圆标与面板/药丸读同一个 computeStatus:缺凭据是蓝 ⚙ 而不是红 !,
@@ -1661,7 +1829,7 @@ window.__ModuleLoader__.load({
1661
1829
  const button = event.currentTarget.parentElement.querySelectorAll('[role="tab"]')[next]; if (button) button.focus();
1662
1830
  }, style: { ...btnStyle, display: "inline-flex", alignItems: "center", gap: 6, minWidth: 0, padding: "5px 12px 5px 8px", whiteSpace: "nowrap", fontSize: 12, fontWeight: active ? 600 : 400, borderRadius: 999, border: "1px solid transparent", background: active ? alpha(INFO, ".16") : "transparent" }
1663
1831
  }, statusGlyph(tabStatus.color, tabStatus.icon, 14), PROVIDER_META[id].short);
1664
- })),
1832
+ })) : null,
1665
1833
  !!usageId ? h("section", { role: "tabpanel", id: "subusage-provider-panel", "aria-label": usageMeta.label, style: { marginTop: 12, border: "1px solid rgba(128,128,128,.25)", borderRadius: 14, padding: 16 } },
1666
1834
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1667
1835
  h("strong", { style: { fontSize: 14 } }, usageMeta.label),
@@ -1669,13 +1837,16 @@ window.__ModuleLoader__.load({
1669
1837
  planName(usageEntry) ? h("span", { style: { fontSize: 11, fontWeight: 600, padding: "1px 8px", borderRadius: 999, background: "rgba(128,128,128,.16)", whiteSpace: "nowrap" } }, planName(usageEntry)) : null),
1670
1838
  // 一眼结论:与药丸、弹层同一个状态判定。读取成功只说明接口健康,额度档位另有结论。
1671
1839
  statusBanner(usageStatus),
1672
- usageEntry && usageEntry.error && usageStatus.key !== "error" ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
1840
+ usageEntry && usageEntry.error && usageStatus.key !== "error" ? h("p", { role: "alert", title: usageEntry.error, style: { color: "#dc2626", overflowWrap: "anywhere" } }, localError(usageEntry.errorCode, usageEntry.error, t)) : null,
1673
1841
  usageAction ? h("div", { role: "group", "aria-label": text("建议操作", "Suggested action"), style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", margin: "10px 0", padding: "8px 10px", border: "1px solid rgba(234,179,8,.45)", borderRadius: 8, background: "rgba(234,179,8,.08)" } },
1674
1842
  h("span", { style: { flex: 1, minWidth: 150, fontSize: 12 } }, usageAction === "mimo-relogin" ? text("Cookie 已过期,重新登录即可恢复。", "Cookie expired — sign in again to restore.") : usageAction === "mimo-login" ? text("需要登录 MiMo(或 Cookie 即将到期)。", "MiMo sign-in needed (or the cookie is about to expire).") : text("这家还没有可用的凭据。", "No usable credentials for this provider.")),
1675
1843
  usageAction !== "credentials" ? h("button", { type: "button", style: primary, disabled: busy || loginActive, onClick: startLogin }, usageAction === "mimo-relogin" ? text("重新登录", "Sign in again") : text("登录并自动导入", "Sign in & import")) : null,
1676
1844
  h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
1845
+ // 自动轮换池健康度(与弹层同一函数):池耗尽时上方横幅已按池中限额窗口染红。
1846
+ usagePool ? CommandCodePoolSummary({ pool: usagePool, cc, t }) : null,
1677
1847
  // 无窗口时的占位只给「成功但既无窗口也无余额」:余额/加载/需配置/失败等状态横幅已经说明,不再叠加「暂无额度数据」。
1678
- canShowUsage(usageEntry) && (usageEntry.windows || []).length ? UsageWindowList({ windows: usageEntry.windows, t, getLocale, uncertain: usageEntry.coverage === "partial" || usageEntry.freshness === "stale" || usageEntry.state !== "ok" })
1848
+ // 窗口列表用 usageStatusEntry:池耗尽时它携带合成的限额窗口(非耗尽时与 usageEntry 同一对象)。
1849
+ canShowUsage(usageStatusEntry) && (usageStatusEntry.windows || []).length ? UsageWindowList({ windows: usageStatusEntry.windows, t, getLocale, uncertain: usageStatusEntry.coverage === "partial" || usageStatusEntry.freshness === "stale" || usageStatusEntry.state !== "ok" })
1679
1850
  : usageStatus.key === "balance" && !(usageStatus.entry && balanceText(usageStatus.entry, t)) ? h("p", { style: { opacity: .75 } }, t("noUsageData")) : null,
1680
1851
  canShowUsage(usageEntry) && usageEntry.extras && usageEntry.extras.some((x) => x.kind !== "plan")
1681
1852
  ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },