dsh-subusage 0.10.11 → 0.11.1

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.
Files changed (4) hide show
  1. package/README.md +101 -51
  2. package/lib/client.js +263 -88
  3. package/lib/index.js +314 -55
  4. package/package.json +3 -3
package/lib/client.js CHANGED
@@ -58,7 +58,6 @@ window.__ModuleLoader__.load({
58
58
  resets: "重置于",
59
59
  limited: "已达限额",
60
60
  cascadeLimited: "(受更长周期限额连累,暂不可用)",
61
- retry: "立即重试",
62
61
  loading: "正在读取用量…",
63
62
  unavailable: "暂不可用",
64
63
  planLabel: "套餐",
@@ -128,19 +127,41 @@ window.__ModuleLoader__.load({
128
127
  pillNeedGrok: "需登录 —— 可在 设置 → Grok Kit(xAI OAuth)登录或运行 grok login(共享登录文件 ~/.grok/auth.json)。",
129
128
  accountLabel: "账户",
130
129
  commandcodeManaged: "凭据由 Command Code 提供方插件(设置 → Command Code)管理;本页只读、不保存凭据。用量由本插件直接拉取其计费接口,取用同一凭据来源(凭据服务 COMMANDCODE_API_KEY → 启动环境 → ~/.commandcode/auth.json)。",
131
- commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai)。月总额按已知套餐快照推算,未知套餐只显示月剩余;月池耗尽不代表已购/赠送池不可用。显示药丸弹层中选择的账户的额度(未选择时为默认账户/顶层 Key),可能与实际服务账户不同;插件里自定义 apiBase 不会被跟随。",
132
- grokManaged: "凭据由 dsh-grok-kit / Grok CLI 的 OAuth 登录文件(~/.grok/auth.json)管理;本页只读、不保存凭据、不刷新 token。用量由本插件取同一凭据直连 Grok CLI 计费代理(cli-chat-proxy.grok.com)拉取。",
130
+ 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)管理;本页不保存凭据,平时只读该文件。access token 过期时,本插件借 dsh-grok-kit 的官方登录链自动续约(同一文件、同一把写锁,不会顶掉它的登录态);未安装 dsh-grok-kit 或续约失败时才提示手动重新登录。用量由本插件取同一凭据直连 Grok CLI 计费代理(cli-chat-proxy.grok.com)拉取。",
133
132
  grokHelp: "并行读取 /v1/billing?format=credits 与 /v1/settings。creditUsagePercent 是统一用量池的已用百分比(通常为周池,重置于 currentPeriod.end,剩余 = 100 − 已用);套餐名取 subscription_tier_display;已购加量余额 prepaidBalance 以美元显示。API Key(XAI_API_KEY)路线取不到订阅周池,必须 OAuth 登录。",
134
133
  ccAccountsTitle: "Command Code 账户",
135
134
  ccAccountAuto: "自动轮换",
136
135
  ccAccountDefault: "默认账户",
137
136
  ccAccountActive: "使用中",
138
- ccAccountsHint: "切换对 Command Code 提供方插件全局生效(与其设置页同一开关);上方用量同步切换为所选账户的额度。",
137
+ ccAccountsHint: "切换对 Command Code 提供方插件全局生效(与其设置页同一开关);上方用量与药丸同步切换为所选账户的额度。设置页与药丸弹层用的是同一个开关。",
139
138
  ccAccountsUnavailable: "账户列表不可用(需要 Command Code 提供方插件在运行)",
140
139
  ccUsageAccount: "上方用量:{account}",
141
140
  ccUsageAutoNote: "自动轮换时上方用量仍是默认账户(顶层 Key),不跟随实际服务账户",
142
141
  ccSwitching: "切换中…",
143
- ccSwitchFailed: "切换失败"
142
+ ccSwitchFailed: "切换失败",
143
+ // Command Code 自动轮换池健康度(其余账户用量由 Host 并行读取,各自缓存 60 秒)。
144
+ ccPoolTitle: "轮换账户池",
145
+ ccPoolAllHealthy: "{n} 个账户均可轮换使用",
146
+ ccPoolCapacity: "{available}/{total} 个账户可用,{blocked} 个已达限额",
147
+ ccPoolUnusable: "{unusable} 个账户无可用凭据或暂时读不到",
148
+ ccPoolExhausted: "轮换账户均已达限额,新请求很可能被拒绝",
149
+ ccPoolExhaustedShort: "池已满限额",
150
+ ccPoolClickToPin: "点击可固定到此账户",
151
+ // Host 失败原因按 errorCode 本地化(Host message 为中文兜底,原文保留在节点 title 供排障)。
152
+ errNetwork: "网络请求失败或超时,请稍后重试",
153
+ errHttp: "服务暂时不可用,请稍后重试",
154
+ errRateLimit: "请求过于频繁,已按服务端要求退避,请稍后再试",
155
+ errAuth: "登录或凭据已失效,请检查或重新配置",
156
+ errCredentials: "凭据无法解析,请检查凭据来源",
157
+ errIdentityTimeout: "凭据解析超时,请稍后重试",
158
+ errResponse: "用量响应无法解析(上游接口可能已变更)",
159
+ errConfig: "本机订阅设置无法读取或保存",
160
+ errRevisionConflict: "设置已被其他窗口修改,请重新读取后再保存",
161
+ errConfigChanged: "读取期间配置已改变,请刷新",
162
+ errUnknownProvider: "本机运行中的 Host 版本较旧,不认识该提供商;重启 DSH 后生效",
163
+ errTimeout: "查询超时,请稍后重试或减少查询家数",
164
+ errDisposed: "服务已卸载"
144
165
  };
145
166
  const en = {
146
167
  nav: "Subscription usage",
@@ -182,7 +203,6 @@ window.__ModuleLoader__.load({
182
203
  resets: "Resets",
183
204
  limited: "Limit reached",
184
205
  cascadeLimited: " (blocked by a larger window's limit)",
185
- retry: "Retry now",
186
206
  loading: "Loading usage…",
187
207
  unavailable: "Unavailable",
188
208
  planLabel: "Plan",
@@ -251,19 +271,41 @@ window.__ModuleLoader__.load({
251
271
  pillNeedGrok: "Login required — sign in under Settings → Grok Kit (xAI OAuth) or run grok login (shared login file ~/.grok/auth.json).",
252
272
  accountLabel: "Account",
253
273
  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).",
254
- 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 quota of the account selected in the pill popover (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.",
255
- 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.",
274
+ 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.",
275
+ 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.",
256
276
  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.",
257
277
  ccAccountsTitle: "Command Code accounts",
258
278
  ccAccountAuto: "Automatic (rotation order)",
259
279
  ccAccountDefault: "Default account",
260
280
  ccAccountActive: "in use",
261
- ccAccountsHint: "Switching applies globally to the Command Code provider plugin (same switch as its settings page); the usage above switches to the selected account's quota.",
281
+ ccAccountsHint: "Switching applies globally to the Command Code provider plugin (same switch as its settings page); the usage above and the pill follow the selected account. This page and the pill popover share one switch.",
262
282
  ccAccountsUnavailable: "Account list unavailable (requires the Command Code provider plugin to be running)",
263
283
  ccUsageAccount: "Usage above: {account}",
264
284
  ccUsageAutoNote: "with automatic rotation the usage above stays on the default (top-level key) account, not the serving one",
265
285
  ccSwitching: "Switching…",
266
- ccSwitchFailed: "Switch failed"
286
+ ccSwitchFailed: "Switch failed",
287
+ // Command Code rotation-pool health (the Host reads the other accounts in parallel, each cached 60s).
288
+ ccPoolTitle: "Rotation account pool",
289
+ ccPoolAllHealthy: "All {n} accounts are available",
290
+ ccPoolCapacity: "{available}/{total} accounts available, {blocked} at their limit",
291
+ ccPoolUnusable: "{unusable} account(s) have no usable credentials or could not be read",
292
+ ccPoolExhausted: "Every rotation account is at its limit; new requests are likely to be rejected",
293
+ ccPoolExhaustedShort: "pool at limit",
294
+ ccPoolClickToPin: "Click to pin to this account",
295
+ // Host failure reasons localized by errorCode (the Host message stays Chinese; the raw text remains in node title for debugging).
296
+ errNetwork: "Network request failed or timed out; retry shortly",
297
+ errHttp: "The service is temporarily unavailable; retry shortly",
298
+ errRateLimit: "Rate limited; backing off as the server asked, retry shortly",
299
+ errAuth: "Sign-in or credentials were rejected; check or re-enter them",
300
+ errCredentials: "Credentials could not be resolved; check the credential source",
301
+ errIdentityTimeout: "Credential resolution timed out; retry shortly",
302
+ errResponse: "The usage response could not be parsed (the upstream API may have changed)",
303
+ errConfig: "The local subscription settings could not be read or saved",
304
+ errRevisionConflict: "Settings were changed in another window; reload before saving",
305
+ errConfigChanged: "Settings changed during the read; refresh",
306
+ errUnknownProvider: "The running Host is older and does not know this provider; restart DSH to take effect",
307
+ errTimeout: "The query timed out; retry shortly or query fewer providers",
308
+ errDisposed: "The service has been unloaded"
267
309
  };
268
310
  const NS = "settings.subusage";
269
311
  const KIND_LABEL = { "5h": "w5h", "7d": "w7d", day: "wday", week: "wweek", month: "wmonth", period: "wperiod", sub: "wsub", rolling: "wrolling" };
@@ -284,7 +326,6 @@ window.__ModuleLoader__.load({
284
326
  // SuperGrok / X Premium 订阅(dsh-grok-kit 的 xai-oauth 路由):OAuth 登录文件凭据,无继承变量。
285
327
  "xai-oauth": { label: "SuperGrok", short: "SuperGrok", envName: "", managedByPlugin: true },
286
328
  // Codex / ChatGPT 订阅:凭据是 Codex CLI 的 OAuth 登录文件,只读、不刷新、不接受本地 Key。
287
- // 默认开启:它对齐的是 DSH 内置的 openai-codex 路由,没登录时会被「默认隐藏」自动收起。
288
329
  // 默认关闭:Codex 的提供方插件自带用量药丸,两个并排只是重复信息(仍可手动开启)。
289
330
  "openai-codex": { label: "Codex (ChatGPT)", short: "Codex", envName: "", managedByPlugin: true, defaultEnabled: false },
290
331
  // 国际版默认关闭,中国版保持默认开启。
@@ -321,7 +362,7 @@ window.__ModuleLoader__.load({
321
362
  };
322
363
  // 顺序只影响展示:DeepSeek 放在末尾以免打乱既有条目位置;药丸匹配靠 provider id,与顺序无关。
323
364
  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"];
324
- // 火山方舟的 7 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
365
+ // 火山方舟的 4 条路由共用同一组 IAM AK/SK,而一般用户只持有一个套餐,所以设置页只给一张卡片、
325
366
  // 一个开关(组开关落在 Host 的 visibility.ark 上)。这些 id 不能真正合并——药丸靠 provider id 匹配路由。
326
367
  const ARK_IDS = PROVIDER_ORDER.filter(id => PROVIDER_META[id]?.volc);
327
368
  /**
@@ -351,7 +392,7 @@ window.__ModuleLoader__.load({
351
392
  if (settings?.visibility?.hideWithoutApi === false) return true;
352
393
  // 火山方舟**不按推理路由收起**:额度是账号级订阅,路由装没装与额度无关——一组 IAM AK/SK 就能
353
394
  // 查到名下所有套餐(GetCodingPlanUsage 与 GetAFPUsage 同协议同凭据)。但它**要按接口回报的
354
- // 订阅状态收起**:7 条路由一律列出来,用户根本分不清哪个是哪个,而其中多数是「未检测到该套餐订阅」。
395
+ // 订阅状态收起**:4 条路由一律列出来,用户根本分不清哪个是哪个,而其中多数是「未检测到该套餐订阅」。
355
396
  // `subscribed === undefined` 表示还没拉到数据,按「未知」放行——不能因为一次没拉到就把有订阅的藏起来。
356
397
  if (PROVIDER_META[id]?.volc) {
357
398
  if (entry?.subscribed === false) return false;
@@ -377,24 +418,6 @@ window.__ModuleLoader__.load({
377
418
  if (w.percent >= 71) return { icon: "⚠", badge: "badgeHigh", color: "#eab308", num: null, rank: 2 };
378
419
  return { icon: "✓", badge: "badgeOk", color: "#22c55e", num: null, rank: 1 };
379
420
  }
380
- /** tag 圆点色:最差窗口档位色(绿/黄/橙/红);拉取失败红。 */
381
- function tagColor(entry) {
382
- if (!entry || entry.state === "loading") return "#94a3b8";
383
- if (entry.state !== "ok") return "#ef4444";
384
- if ((entry.windows || []).some((w) => w.status === "rate-limited")) return "#ef4444";
385
- if (entry.coverage === "partial" || entry.freshness === "stale") return "#eab308";
386
- const rank = { "#22c55e": 0, "#eab308": 1, "#f97316": 2, "#ef4444": 3 };
387
- let worst = "#22c55e";
388
- for (const w of entry.windows || []) {
389
- const c = usageTier(w).color;
390
- if (rank[c] > rank[worst]) worst = c;
391
- }
392
- return worst;
393
- }
394
- /** 数字色:0–70 白(前景),其余随档位。 */
395
- function numColor(w) {
396
- return usageTier(w).rank === 1 ? "inherit" : usageTier(w).color;
397
- }
398
421
  /** 最差窗:档位最高者优先;同档时根因(非连坐)优先,再取已用更多者。 */
399
422
  function worstWindow(windows) {
400
423
  let worst = null;
@@ -487,8 +510,11 @@ window.__ModuleLoader__.load({
487
510
  if (!value || typeof value !== "object") throw new Error("Invalid dsh-subusage settings");
488
511
  if (value.visibility !== undefined) {
489
512
  const v = value.visibility;
490
- 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");
513
+ // 白名单必须与 Host 的 parseSettings 逐字对齐:火山组开关走 visibility.ark(Host 合法字段),
514
+ // 漏掉它会让两端 schema 漂移(当前网关出站不校验,但任何复用本 parser 的出站校验都会致障)。
515
+ 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");
491
516
  if (v.hideWithoutApi !== undefined && typeof v.hideWithoutApi !== "boolean") throw new Error("Invalid auto-hide switch");
517
+ if (v.ark !== undefined && typeof v.ark !== "boolean") throw new Error("Invalid Ark group switch");
492
518
  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");
493
519
  }
494
520
  return value;
@@ -634,6 +660,31 @@ window.__ModuleLoader__.load({
634
660
  if (entry.coverage === "partial") return t("statusPartial");
635
661
  return t("statusOk");
636
662
  }
663
+ /**
664
+ * Host 的 failure message 是中文兜底(含错误特有的排障细节),英文界面直接渲染会中英混杂。
665
+ * 按稳定的 errorCode 出当前语言文案;认不出的 code 或传输层错误(无 code)保留原文——
666
+ * 原文可能携带只有它才有的信息(如 HTTP 状态、火山 Action 名),调用方把它放进节点 title。
667
+ */
668
+ const ERROR_TEXT_KEY = {
669
+ "subusage/network": "errNetwork",
670
+ "subusage/http": "errHttp",
671
+ "subusage/rate-limit": "errRateLimit",
672
+ "subusage/auth": "errAuth",
673
+ "subusage/credentials": "errCredentials",
674
+ "subusage/identity-timeout": "errIdentityTimeout",
675
+ "subusage/response": "errResponse",
676
+ "subusage/config": "errConfig",
677
+ "subusage/revision-conflict": "errRevisionConflict",
678
+ "subusage/config-changed": "errConfigChanged",
679
+ "subusage/unknown-provider": "errUnknownProvider",
680
+ "subusage/timeout": "errTimeout",
681
+ "subusage/disposed": "errDisposed"
682
+ };
683
+ function localError(errorCode, raw, t) {
684
+ const key = typeof errorCode === "string" ? ERROR_TEXT_KEY[errorCode] : undefined;
685
+ if (key) return t(key);
686
+ return typeof raw === "string" && raw ? raw : t("statusError");
687
+ }
637
688
  /**
638
689
  * 需要用户行动时的内联入口类型。设置页的「状态」在上(用量面板)、「操作」在下(提供商管理),
639
690
  * 出问题时用户得先去下面找到对应厂商才能修——这里让问题旁边直接出现修复动作。
@@ -672,14 +723,6 @@ window.__ModuleLoader__.load({
672
723
  const worst = worstWindow(windows);
673
724
  return `${statusText(entry, t)}${worst ? " · " + pillText(worst, windows.length > 1, t) : " · " + t("noUsageData")}`;
674
725
  }
675
- function providerIcon(entry) {
676
- if (!entry || entry.state === "loading") return "…";
677
- if ((entry.windows || []).some((w) => w.status === "rate-limited")) return "✕";
678
- if (entry.state !== "ok") return "!";
679
- if (entry.coverage === "partial" || entry.freshness === "stale") return "?";
680
- const worst = worstWindow(entry.windows || []);
681
- return worst ? usageTier(worst).icon : "?";
682
- }
683
726
  function cacheAge(entry, t) {
684
727
  if (!entry || entry.freshness !== "stale" || !canShowUsage(entry)) return null;
685
728
  const at = Date.parse(entry.lastSuccessAt);
@@ -718,7 +761,14 @@ window.__ModuleLoader__.load({
718
761
  };
719
762
  const refresh = (ids, force = false) => {
720
763
  if (disposed) return Promise.reject(new Error("Client 已卸载"));
721
- const unique = [...new Set(ids)].filter((id) => PROVIDER_ORDER.includes(id) && providerEnabled(snapshot.settings, id) && providerRoutable(snapshot, id));
764
+ // Ark 必须豁免路由检查:它的额度是账号级管控面订阅(一组 AK/SK 查名下全部套餐),
765
+ // providerVisible 对 volc 同样不看推理路由——这里若按 providerRoutable 过滤,
766
+ // 「有套餐但本机没装对应 arkcli 路由」的条目在激活刷新、重置到期补拉与定时刷新时
767
+ // 全部静默不发请求,用户点「刷新当前」也空转,只有「刷新全部」走 read 才能更新。
768
+ const unique = [...new Set(ids)].filter((id) => {
769
+ if (!PROVIDER_ORDER.includes(id) || !providerEnabled(snapshot.settings, id)) return false;
770
+ return PROVIDER_META[id]?.volc ? true : providerRoutable(snapshot, id);
771
+ });
722
772
  const start = (id) => {
723
773
  if (inflight.has(id)) return inflight.get(id);
724
774
  const startedEpoch = epoch, request = markRequest([id]);
@@ -767,7 +817,10 @@ window.__ModuleLoader__.load({
767
817
  return value;
768
818
  };
769
819
  const setCommandCodeAccount = (id) => {
770
- const next = typeof id === "string" ? id.trim() : "";
820
+ // "default" 与空串在 Host 端是同一个账户(同一缓存键、同一指纹,见 providerCacheKey):
821
+ // 归一后就不必为「固定到默认账户」多打一次强制刷新。
822
+ const raw = typeof id === "string" ? id.trim() : "";
823
+ const next = raw === "default" ? "" : raw;
771
824
  if (disposed || ccAccount === next) return Promise.resolve(snapshot);
772
825
  ccAccount = next;
773
826
  // 账户变了:旧账户的在途/缓存数据立即失效,强制按新账户重拉。
@@ -810,12 +863,6 @@ window.__ModuleLoader__.load({
810
863
  };
811
864
  }
812
865
 
813
- // ── 用量药丸(匹配当前选中模型的 provider)──────────────────────────────
814
- function severity(percent, status) {
815
- if (status === "rate-limited" || percent >= 100) return "limited";
816
- return percent >= 80 ? "high" : void 0;
817
- }
818
-
819
866
  // ── Command Code 账户切换(对齐提供方插件的账户显示)────────────────────
820
867
  // 账户列表与 id 规则对齐 @mars-sea/dsh-commandcode-provider 的 slots():
821
868
  // 默认账户 id 固定 "default";额外账户 id 取其 apiKeyEnv(凭据引用名)。
@@ -827,11 +874,15 @@ window.__ModuleLoader__.load({
827
874
  const list = [{ id: "default", label: "default" }];
828
875
  const accounts = Array.isArray(value?.accounts) ? value.accounts : [];
829
876
  const tasks = Array.isArray(value?.accountEnrollmentTasks) ? value.accountEnrollmentTasks : [];
877
+ const seen = new Set(["default"]);
830
878
  accounts.forEach((account, index) => {
831
879
  const refName = typeof account?.apiKeyEnv === "string" && account.apiKeyEnv.trim() !== "" ? account.apiKeyEnv.trim() : void 0;
832
880
  // 与 slots() 一致:无凭据引用的槽位不显示;登记任务未完成的账户暂不可选。
833
881
  if (refName === void 0) return;
882
+ // 配置被手改出重复引用名时只列一次:否则同一账户出现两个选项,切换后读的还是同一份额度。
883
+ if (seen.has(refName)) return;
834
884
  if (tasks.some((task) => task?.ref === refName && task.phase !== "naming")) return;
885
+ seen.add(refName);
835
886
  list.push({ id: refName, label: typeof account.label === "string" && account.label.trim() !== "" ? account.label.trim() : `Account ${index + 2}` });
836
887
  });
837
888
  return list;
@@ -851,6 +902,10 @@ window.__ModuleLoader__.load({
851
902
  let disposed = false;
852
903
  const listeners = new Set();
853
904
  let state = { status: "idle", accounts: [], activeId: "", revision: null, writable: false, baseActive: "", switching: false, error: null };
905
+ // 设置页与药丸弹层共用同一个控制器,可能同时驱动它:`loadSequence` 标记「哪一次读取才是当前的」,
906
+ // `inflightLoad` 把同一代的并发 load 合并成一次 describe。没有这两样时,切换账户(mutate)在途
907
+ // 期间返回的旧 describe 会把 activeId/revision 覆盖回切换前的快照,两处界面一起显示旧账户。
908
+ let loadSequence = 0, inflightLoad = null;
854
909
  const emit = () => { for (const fn of [...listeners]) fn(state); };
855
910
  const parseRow = (row) => {
856
911
  const accounts = commandCodeAccountList(row?.value);
@@ -858,18 +913,29 @@ window.__ModuleLoader__.load({
858
913
  };
859
914
  const load = async (keepError = false) => {
860
915
  if (disposed) return state;
916
+ // 同一代的并发 load 合并:两处界面同时挂载时只发一次 describe。
917
+ if (inflightLoad?.sequence === loadSequence) return inflightLoad.promise;
918
+ const sequence = ++loadSequence;
861
919
  state = { ...state, status: "loading", error: keepError ? state.error : null }; emit();
862
- try {
863
- const response = await describe();
864
- if (!response?.ok) throw new Error(response?.error?.message || "settings describe failed");
865
- const row = response.value?.namespaces?.find((candidate) => candidate?.ns === COMMANDCODE_SETTINGS_NS);
866
- if (!row) throw new Error("Command Code settings namespace not found");
867
- state = { ...state, status: "ready", writable: response.value.writable === true, ...parseRow(row) };
868
- } catch (error) {
869
- state = { ...state, status: "unavailable", error: String(error?.message || error) };
870
- }
871
- emit();
872
- return state;
920
+ const task = (async () => {
921
+ try {
922
+ const response = await describe();
923
+ if (!response?.ok) throw new Error(response?.error?.message || "settings describe failed");
924
+ // 切换账户(mutate)或更晚的 load 已经给出更新的状态:这次的结果一律作废,
925
+ // 既不用旧快照覆盖,也不把状态留在 loading(mutate 那边已写回 ready)。
926
+ if (disposed || sequence !== loadSequence) return state;
927
+ const row = response.value?.namespaces?.find((candidate) => candidate?.ns === COMMANDCODE_SETTINGS_NS);
928
+ if (!row) throw new Error("Command Code settings namespace not found");
929
+ state = { ...state, status: "ready", writable: response.value.writable === true, ...parseRow(row) };
930
+ } catch (error) {
931
+ if (disposed || sequence !== loadSequence) return state;
932
+ state = { ...state, status: "unavailable", error: String(error?.message || error) };
933
+ }
934
+ emit();
935
+ return state;
936
+ })();
937
+ inflightLoad = { sequence, promise: task };
938
+ try { return await task; } finally { if (inflightLoad?.promise === task) inflightLoad = null; }
873
939
  };
874
940
  const switchTo = async (id) => {
875
941
  if (disposed || state.status !== "ready" || state.switching) return state;
@@ -882,7 +948,10 @@ window.__ModuleLoader__.load({
882
948
  try {
883
949
  const response = await mutate(ops, state.revision);
884
950
  if (!response?.ok) throw new Error(response?.error?.message || "settings mutate failed");
885
- state = { ...state, switching: false, ...parseRow(response.value) };
951
+ // 写入成功就是权威状态:作废在途的旧 describe,并把 status 明确拉回 ready
952
+ // (load 开始时置的 loading 不该留在这里)。
953
+ loadSequence++;
954
+ state = { ...state, status: "ready", switching: false, ...parseRow(response.value) };
886
955
  } catch (error) {
887
956
  const message = String(error?.message || error);
888
957
  state = { ...state, switching: false, error: message };
@@ -899,13 +968,20 @@ window.__ModuleLoader__.load({
899
968
  };
900
969
  }
901
970
 
902
- /** 弹层内的 Command Code 账户切换区:自动轮换 + 默认账户 + 各额外账户。 */
971
+ // 未注入 cc(提供方插件未运行)时给 useSyncExternalStore 一对稳定引用:每次都新建函数
972
+ // 会让 React 反复重新订阅。
973
+ const NO_CC_STATE = () => null;
974
+ const NO_CC_SUBSCRIBE = () => () => {};
975
+ /** 弹层与设置页共用的 Command Code 账户区:自动轮换 + 默认账户 + 各额外账户。 */
903
976
  function CommandCodeAccountSwitch({ cc, ccState, t }) {
904
977
  if (!cc || !ccState) return null;
978
+ // 状态没 ready(正在读账户配置 / 不可用)时按钮不可点:switchTo 在这种状态下会直接返回,
979
+ // 可点的按钮换来的只是「点了没反应」。
980
+ const locked = ccState.switching || ccState.status !== "ready" || !ccState.writable;
905
981
  const optionButton = (id, labelText) => h("button", {
906
982
  type: "button",
907
983
  onClick: () => void cc.switchTo(id),
908
- disabled: ccState.switching || !ccState.writable,
984
+ disabled: locked,
909
985
  "aria-pressed": ccState.activeId === id,
910
986
  style: {
911
987
  display: "flex", alignItems: "center", gap: 6, width: "100%", textAlign: "left",
@@ -913,7 +989,7 @@ window.__ModuleLoader__.load({
913
989
  border: `1px solid ${ccState.activeId === id ? "rgba(59,130,246,.55)" : "rgba(128,128,128,.3)"}`,
914
990
  background: ccState.activeId === id ? "rgba(59,130,246,.10)" : "transparent",
915
991
  color: "inherit", font: "inherit",
916
- cursor: ccState.switching || !ccState.writable ? "default" : "pointer"
992
+ cursor: locked ? "default" : "pointer"
917
993
  }
918
994
  },
919
995
  h("span", { "aria-hidden": "true" }, ccState.activeId === id ? "●" : "○"),
@@ -922,10 +998,13 @@ window.__ModuleLoader__.load({
922
998
  );
923
999
  // 账户多时这一坨会把上方的额度顶出视野:折进 details,summary 只留「当前账户 + 数量」,
924
1000
  // 展开后列表自身限高滚动,弹层高度不随账户数增长。
925
- const activeLabel = ccState.status !== "ready" ? null
926
- : ccState.activeId === "" ? t("ccAccountAuto")
1001
+ const activeName = ccState.status !== "ready" ? null
927
1002
  : ccState.activeId === "default" ? t("ccAccountDefault")
928
1003
  : (ccState.accounts.find(account => account.id === ccState.activeId)?.label ?? ccState.activeId);
1004
+ // summary 直接回答「现在是哪一个账户」:固定账户写账户名 + 使用中,自动轮换写模式名。
1005
+ const activeLabel = ccState.status !== "ready" ? null
1006
+ : ccState.activeId === "" ? t("ccAccountAuto")
1007
+ : `${activeName} · ${t("ccAccountActive")}`;
929
1008
  const optionCount = ccState.status === "ready" ? ccState.accounts.length + 1 : 0;
930
1009
  return h("details", { style: { marginTop: 10, paddingTop: 8, borderTop: "1px solid rgba(128,128,128,.25)" } },
931
1010
  h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } },
@@ -941,6 +1020,53 @@ window.__ModuleLoader__.load({
941
1020
  );
942
1021
  }
943
1022
 
1023
+ /**
1024
+ * Command Code 自动轮换池健康摘要(弹层与设置页共用):
1025
+ * 数据来自 Host 挂在 commandcode entry 上的 accountPool(auto 模式才有)。
1026
+ * 不简单取「最差账户」染红——账户间是独立配额,单个限额轮换器会避开它;
1027
+ * 只有全部有凭据账户都限额(exhausted)才是真的要 429。点账户行可固定到它。
1028
+ */
1029
+ function CommandCodePoolSummary({ pool, cc, t }) {
1030
+ if (!pool || pool.mode !== "auto" || !Array.isArray(pool.accounts) || pool.total <= 1) return null;
1031
+ const summary = pool.exhausted
1032
+ ? t("ccPoolExhausted")
1033
+ : pool.blockedCount > 0
1034
+ ? t("ccPoolCapacity").replace("{available}", pool.available).replace("{total}", pool.total).replace("{blocked}", pool.blockedCount)
1035
+ : t("ccPoolAllHealthy").replace("{n}", pool.total);
1036
+ const tone = pool.exhausted ? "#ef4444" : pool.blockedCount > 0 ? "#eab308" : "#22c55e";
1037
+ const rowFor = (account) => {
1038
+ const worst = worstWindow(account.windows || []);
1039
+ const tier = account.usable || account.blocked ? usageTier(worst || { percent: 0, status: "ok" }) : null;
1040
+ const blocked = account.blocked;
1041
+ const unusable = !account.usable && !account.blocked;
1042
+ const dot = statusGlyph(unusable ? NEUTRAL : (tier ? tier.color : NEUTRAL), unusable ? "?" : blocked ? "✕" : (tier ? tier.icon : "?"), 14);
1043
+ const detail = unusable
1044
+ ? (account.state === "error" ? t("statusError") : account.state === "disabled" ? t("statusDisabled") : account.state === "no-cookie" ? t("statusNoCookie") : t("statusNoKey"))
1045
+ : blocked
1046
+ ? t("pillLimited")
1047
+ : worst ? pillText(worst, (account.windows || []).length > 1, t)
1048
+ : t("noUsageData");
1049
+ const canPin = cc && account.id !== "default";
1050
+ return h("button", {
1051
+ key: account.id, type: "button", disabled: !canPin,
1052
+ title: canPin ? t("ccPoolClickToPin") : undefined,
1053
+ onClick: canPin ? () => void cc.switchTo(account.id) : undefined,
1054
+ 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" }
1055
+ },
1056
+ dot,
1057
+ h("span", { style: { flex: 1, minWidth: 0, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, account.id === "default" ? t("ccAccountDefault") : account.label),
1058
+ h("span", { style: { opacity: 0.75, whiteSpace: "nowrap" } }, detail));
1059
+ };
1060
+ return h("div", { style: { marginTop: 10, padding: "8px 10px", borderRadius: 10, border: `1px solid ${alpha(tone, ".35")}`, background: alpha(tone, ".07") } },
1061
+ h("div", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11.5, fontWeight: 600, color: textTone(tone) } },
1062
+ h("span", null, t("ccPoolTitle")),
1063
+ h("span", { style: { flex: 1 } }),
1064
+ h("span", { style: { fontWeight: 500, opacity: 0.9 } }, `${pool.available}/${pool.total}`)),
1065
+ h("p", { role: pool.exhausted ? "alert" : "status", style: { margin: "3px 0 0", fontSize: 11, color: textTone(tone) } }, summary),
1066
+ pool.unusableCount > 0 ? h("p", { style: { margin: "2px 0 0", fontSize: 10.5, opacity: 0.7 } }, t("ccPoolUnusable").replace("{unusable}", pool.unusableCount)) : null,
1067
+ h("div", { style: { marginTop: 4 } }, pool.accounts.map(rowFor)));
1068
+ }
1069
+
944
1070
  // ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
945
1071
  // 注意:下面这些都是**普通函数调用**而不是 React 组件元素——它们不用 Hook,直接内联展开,
946
1072
  // 测试里的 React 桩也能看到它们渲染出的文字。
@@ -1023,7 +1149,8 @@ window.__ModuleLoader__.load({
1023
1149
  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 };
1024
1150
  }
1025
1151
  if (!entry) {
1026
- return { ...base, key: "error", color: "#ef4444", icon: "!", title: t("statusError"), sub: failed || (rawEntry && rawEntry.error) || "", short: t("statusError"), tinted: true };
1152
+ const rawReason = failed || (rawEntry && rawEntry.error) || "";
1153
+ return { ...base, key: "error", color: "#ef4444", icon: "!", title: t("statusError"), sub: localError(rawEntry?.errorCode, rawReason, t), rawReason, short: t("statusError"), tinted: true };
1027
1154
  }
1028
1155
  if (degraded) {
1029
1156
  // 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
@@ -1128,7 +1255,8 @@ window.__ModuleLoader__.load({
1128
1255
  const root = react.useRef(null);
1129
1256
  const retry = react.useRef(() => {});
1130
1257
  // Command Code 账户区:cc 由装配层注入(提供方插件未运行时为 null,区块整体隐藏)。
1131
- const ccState = react.useSyncExternalStore(cc ? cc.subscribe : () => () => {}, cc ? cc.getSnapshot : () => null);
1258
+ // 兜底用模块级稳定引用(NO_CC_*):内联箭头函数每次渲染都是新引用,会让 React 反复重新订阅。
1259
+ const ccState = react.useSyncExternalStore(cc ? cc.subscribe : NO_CC_SUBSCRIBE, cc ? cc.getSnapshot : NO_CC_STATE, cc ? cc.getSnapshot : NO_CC_STATE);
1132
1260
  react.useEffect(() => { if (open && cc) void cc.load(); }, [open, cc]);
1133
1261
  // 挂载即读一次账户配置:提供方插件固定了账户时,药丸本体不必等弹层打开就跟随。
1134
1262
  react.useEffect(() => { if (providerId === "commandcode" && cc) void cc.load(); }, [providerId, cc]);
@@ -1198,14 +1326,22 @@ window.__ModuleLoader__.load({
1198
1326
  }, [open]);
1199
1327
  const rawEntry = snapshot && snapshot.entry;
1200
1328
  if (rawEntry?.visible === false || rawEntry?.state === "disabled") return null;
1329
+ // Command Code 自动轮换:默认账户健康不代表池健康。所有有凭据账户都限额(exhausted)
1330
+ // 时,用池里一条 rate-limited 窗口喂给 computeStatus,让药丸/横幅如实染红;部分受限不染红
1331
+ // (轮换器会避开该账户),只在弹层池摘要里提示容量降级。
1332
+ const pool = providerId === "commandcode" ? (rawEntry?.accountPool ?? null) : null;
1333
+ const exhaustedWindow = pool?.exhausted ? (pool.accounts.flatMap(a => a.windows || []).find(w => w.status === "rate-limited") ?? null) : null;
1334
+ const statusEntry = exhaustedWindow ? { ...rawEntry, state: "ok", coverage: "complete", freshness: "fresh", windows: [exhaustedWindow] } : rawEntry;
1201
1335
  // 整体状态由 computeStatus 一处判定:药丸、弹层横幅、设置页横幅读的是同一个结论。
1202
- const st = computeStatus(rawEntry, { failed, failState, providerId, t, getLocale });
1336
+ const st = computeStatus(statusEntry, { failed, failState, providerId, t, getLocale });
1203
1337
  const { entry, windows, worst, degraded, expiry, expiring } = st;
1204
1338
  const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
1205
1339
  // 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
1206
1340
  // 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
1207
1341
  const tip = `${label}: ${st.title}${st.key === "ok" || st.key === "low" || st.key === "tight" || st.key === "blocked" || st.key === "degraded" ? " · " + summary : ""}${expiring && st.key !== "cookie" && st.key !== "blocked" ? " · " + expiry.label : ""}`;
1208
- const glyph = windows.length && st.key !== "cookie" && st.key !== "config" ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
1342
+ // 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
1343
+ // 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
1344
+ const glyph = windows.length ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
1209
1345
  return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
1210
1346
  h("button", {
1211
1347
  type: "button",
@@ -1242,7 +1378,7 @@ window.__ModuleLoader__.load({
1242
1378
  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),
1243
1379
  // 状态横幅:先给结论(充足 / 偏少 / 即将用尽 / 已用尽 / 需配置 / 失败…)与一行依据。
1244
1380
  statusBanner(st),
1245
- failed && st.key !== "error" && !st.needConfig ? h("p", { style: { color: "#f87171", margin: "8px 0 0", whiteSpace: "pre-wrap" } }, failed) : null,
1381
+ 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,
1246
1382
  // Cookie 到期常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久(已在横幅里说明时不重复)。
1247
1383
  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,
1248
1384
  // 用量账户标注:账户区 ready 时说明上方数字属于哪个账户;自动轮换时明确不跟随实际服务账户。
@@ -1251,6 +1387,9 @@ window.__ModuleLoader__.load({
1251
1387
  t("ccUsageAccount").replace("{account}", ccState.activeId === "" || ccState.activeId === "default" ? t("ccAccountDefault") : (ccState.accounts.find((a) => a.id === ccState.activeId)?.label ?? ccState.activeId)),
1252
1388
  ccState.activeId === "" ? h("span", { style: { opacity: 0.85 } }, ` · ${t("ccUsageAutoNote")}`) : null)
1253
1389
  : null,
1390
+ // 自动轮换池健康度:默认账户之外的账户也可能已限额,池耗尽时药丸已染红,这里给出逐账户明细。
1391
+ // 直接函数调用(非 React 元素),与 UsageWindowList 等视觉原语同约定,测试桩可见展开内容。
1392
+ pool ? CommandCodePoolSummary({ pool, cc, t }) : null,
1254
1393
  // 额度线切换时插一条分组标题:AFP 的日限额只覆盖视觉 / 语音模型与 Harness,与文本模型的
1255
1394
  // 5 小时 / 周 / 月不是同一条线,混排会让人把两条线的配额当成一组来互相比较。
1256
1395
  UsageWindowList({ windows, t, getLocale, uncertain: degraded }),
@@ -1327,8 +1466,11 @@ window.__ModuleLoader__.load({
1327
1466
  }
1328
1467
  return patch;
1329
1468
  }
1330
- function SubusageSection({ usageStore, t, getLocale }) {
1469
+ function SubusageSection({ usageStore, t, getLocale, cc }) {
1331
1470
  const result = react.useSyncExternalStore(usageStore.subscribe, usageStore.getSnapshot, usageStore.getSnapshot);
1471
+ // Command Code 账户区:与弹层共用同一个控制器与组件;cc 由装配层注入,
1472
+ // 提供方插件未运行时为 undefined,区块整体隐藏。
1473
+ const ccState = react.useSyncExternalStore(cc ? cc.subscribe : NO_CC_SUBSCRIBE, cc ? cc.getSnapshot : NO_CC_STATE, cc ? cc.getSnapshot : NO_CC_STATE);
1332
1474
  const [tab, setTab] = react.useState(initialTab);
1333
1475
  const [draft, setDraft] = react.useState(() => draftFor(result.settings, tab));
1334
1476
  const [dirty, setDirty] = react.useState(false);
@@ -1352,14 +1494,14 @@ window.__ModuleLoader__.load({
1352
1494
  const text = (cn, en) => local ? cn : en;
1353
1495
  react.useEffect(() => {
1354
1496
  let alive = true;
1355
- void usageStore.readAll().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(String(error.message || error)); });
1497
+ void usageStore.readAll().then(() => { if (alive) setLoadError(null); }).catch((error) => { if (alive) setLoadError(localError(error.code, String(error.message || error), t)); });
1356
1498
  return () => { alive = false; };
1357
1499
  }, [usageStore]);
1358
1500
  react.useEffect(() => {
1359
1501
  let alive = true;
1360
1502
  const enabled = editorOpen && providerEnabled(result.settings, tab);
1361
1503
  const deactivate = enabled ? usageStore.activate(tab) : () => {};
1362
- 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)); });
1504
+ 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)); });
1363
1505
  return () => { alive = false; deactivate(); };
1364
1506
  }, [usageStore, tab, editorOpen, providerEnabled(result.settings, tab)]);
1365
1507
  react.useEffect(() => {
@@ -1420,7 +1562,7 @@ window.__ModuleLoader__.load({
1420
1562
  const refresh = async (all) => {
1421
1563
  setRefreshing(true);
1422
1564
  try { if (!result.settings) await usageStore.readAll(); else await usageStore.refresh(all ? PROVIDER_ORDER : usageId ? [usageId] : [], true); if (aliveRef.current) setLoadError(null); }
1423
- catch (error) { if (aliveRef.current) setLoadError(String(error.message || error)); }
1565
+ catch (error) { if (aliveRef.current) setLoadError(localError(error.code, String(error.message || error), t)); }
1424
1566
  finally { if (aliveRef.current) setRefreshing(false); }
1425
1567
  };
1426
1568
  const saveVisibility = async (visibility) => {
@@ -1435,7 +1577,7 @@ window.__ModuleLoader__.load({
1435
1577
  // 新启用条目先重新探测凭据;保存成功不与接口验证混为一谈。
1436
1578
  const enabled = Object.entries(visibility.providers || {}).filter(([, value]) => value).map(([id]) => id);
1437
1579
  if (enabled.length) { try { await usageStore.refresh(enabled, false); } catch { if (aliveRef.current) setLoadError(text("设置已保存;用量刷新失败,可稍后重试。", "Settings saved; usage refresh failed. Retry later.")); } }
1438
- } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: String(error.message || error) }); }
1580
+ } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: localError(error.code, String(error.message || error), t) }); }
1439
1581
  finally { if (aliveRef.current) setBusy(false); }
1440
1582
  };
1441
1583
  const onSave = async () => {
@@ -1460,16 +1602,19 @@ window.__ModuleLoader__.load({
1460
1602
  const current = verified.entries.find((e) => e.providerId === tab);
1461
1603
  if (aliveRef.current) setLoadError(current && current.state === "ok" ? null : text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + (current && current.error || statusText(current, t)));
1462
1604
  } catch (error) { if (aliveRef.current) setLoadError(text("已保存;验证获取失败:", "Saved; validation fetch failed: ") + String(error.message || error)); }
1463
- } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: String(error.message || error) }); }
1605
+ } catch (error) { if (aliveRef.current) setNotice({ ok: false, text: localError(error.code, String(error.message || error), t) }); }
1464
1606
  finally { if (aliveRef.current) setBusy(false); }
1465
1607
  };
1466
1608
  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" };
1467
1609
  const btnStyle = { padding: "6px 10px", borderRadius: 6, border: "1px solid rgba(128,128,128,.35)", background: "transparent", color: "inherit", cursor: "pointer", font: "inherit" };
1468
1610
  const primary = { ...btnStyle, background: "rgba(59,130,246,.12)", borderColor: "rgba(59,130,246,.65)" };
1469
1611
  const entries = result.entries || [];
1612
+ // 首次挂载时 settings 还没读到(readAll 在飞行中),providerVisible 对缺失一律「未知放行」,
1613
+ // 会让标签栏先闪现全部默认开启的提供商;未初始化期间标签栏、空状态与计数都不渲染。
1614
+ const initialized = !!result.settings;
1470
1615
  const entry = entries.find((e) => e.providerId === tab);
1471
1616
  // 待处理的排前面;组内保持登记顺序,所以顺序不会自己抖动。
1472
- const visibleIds = PROVIDER_ORDER
1617
+ const visibleIds = !initialized ? [] : PROVIDER_ORDER
1473
1618
  .map((id, index) => ({ id, index, rank: usageAttentionRank(entries.find(e => e.providerId === id), t) }))
1474
1619
  .filter(item => providerVisible(result.settings, item.id, entries.find(e => e.providerId === item.id), result))
1475
1620
  .sort((a, b) => b.rank - a.rank || a.index - b.index)
@@ -1486,7 +1631,11 @@ window.__ModuleLoader__.load({
1486
1631
  const usageMeta = PROVIDER_META[usageId];
1487
1632
  // 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
1488
1633
  const usageAction = usageActionKind(usageEntry, usageId, t);
1489
- const usageStatus = computeStatus(usageEntry, { failed: usageEntry && usageEntry.state !== "ok" ? usageEntry.error || usageEntry.state : null, failState: usageEntry && usageEntry.state, providerId: usageId, t, getLocale });
1634
+ // Command Code 自动轮换:池耗尽时横幅按池中一条限额窗口染红(与药丸同一结论)。
1635
+ const usagePool = usageId === "commandcode" ? (usageEntry?.accountPool ?? null) : null;
1636
+ const usageExhaustedWindow = usagePool?.exhausted ? (usagePool.accounts.flatMap(a => a.windows || []).find(w => w.status === "rate-limited") ?? null) : null;
1637
+ const usageStatusEntry = usageExhaustedWindow ? { ...usageEntry, state: "ok", coverage: "complete", freshness: "fresh", windows: [usageExhaustedWindow] } : usageEntry;
1638
+ const usageStatus = computeStatus(usageStatusEntry, { failed: usageEntry && usageEntry.state !== "ok" ? usageEntry.error || usageEntry.state : null, failState: usageEntry && usageEntry.state, providerId: usageId, t, getLocale });
1490
1639
  // 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
1491
1640
  const revealEditor = (target) => {
1492
1641
  openEditor(target);
@@ -1500,6 +1649,21 @@ window.__ModuleLoader__.load({
1500
1649
  void usageStore.refresh([usageId], false).catch(() => {});
1501
1650
  return deactivate;
1502
1651
  }, [usageStore, usageId]);
1652
+ // 设置页里的账户区与弹层共用同一个控制器:挂载即读一次账户配置,切到 Command Code
1653
+ // 那一屏就有数据可显示(读取只是一次 settings describe,不发任何计费请求)。
1654
+ react.useEffect(() => {
1655
+ if (!cc) return;
1656
+ void cc.load();
1657
+ }, [cc]);
1658
+ // 账户区 ready 后把当前固定账户同步为显示账户:设置页里切换账户后,上方面板与药丸
1659
+ // 一起改看所选账户的额度(与弹层的同步行为一致)。自动轮换(activeId 为空)同步为
1660
+ // 默认账户,store 内部对相同账户去重,不产生多余请求。
1661
+ react.useEffect(() => {
1662
+ if (usageId !== "commandcode" || !ccState || ccState.status !== "ready") return;
1663
+ // 旧 store stub / 更早版本的客户端装配没有这个方法:缺了就不同步,不影响其他功能。
1664
+ if (typeof usageStore.setCommandCodeAccount !== "function") return;
1665
+ void usageStore.setCommandCodeAccount(ccState.activeId).catch(() => {});
1666
+ }, [usageStore, usageId, ccState && ccState.status, ccState && ccState.activeId]);
1503
1667
  const meta = PROVIDER_META[tab];
1504
1668
  const source = entry && entry.keySource || "none";
1505
1669
  // auth-file 有三家(Grok / Codex / Command Code),凭据来源文件各不相同,不能只特判一家。
@@ -1582,7 +1746,7 @@ window.__ModuleLoader__.load({
1582
1746
  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")}`),
1583
1747
  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.")),
1584
1748
  // 启用的排到最前面,省得每次往下翻;组内保持 PROVIDER_ORDER 的既有顺序(sort 稳定)。
1585
- // Ark 的 7 条路由合并成一张卡片:只渲染本机装了的那条作代表(都没装就渲染第一条),
1749
+ // Ark 的 4 条路由合并成一张卡片:只渲染本机装了的那条作代表(都没装就渲染第一条),
1586
1750
  // 由它统一控制整组的检测开关与那组共享的 AK/SK。
1587
1751
  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 => {
1588
1752
  const isArk = Boolean(PROVIDER_META[id].volc), arkPresent = isArk ? ARK_IDS.filter(key => providerRoutable(result, key)) : [];
@@ -1603,22 +1767,25 @@ window.__ModuleLoader__.load({
1603
1767
  return h("div", { ref: root, style: { padding: 16, maxWidth: 720, minWidth: 0, font: "13px/1.6 sans-serif", containerType: "inline-size", containerName: "subusage" } },
1604
1768
  h("div", { style: { display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" } },
1605
1769
  h("h2", { style: { margin: 0, fontSize: 16 } }, t("title")),
1606
- h("button", {
1770
+ initialized ? h("button", {
1607
1771
  type: "button",
1608
1772
  disabled: !troubledIds.length,
1609
1773
  onClick: () => { if (troubledIds.length) chooseUsage(troubledIds[0]); },
1610
1774
  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"),
1611
1775
  "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"}`),
1612
1776
  style: { ...btnStyle, padding: "2px 8px", fontSize: 12, opacity: .85, cursor: troubledIds.length ? "pointer" : "default", color: troubledIds.length ? "#b7791f" : "inherit" }
1613
- }, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)),
1777
+ }, text(`${success}/${visibleIds.length} 家数据获取成功`, `${success}/${visibleIds.length} providers fetched`)) : null,
1614
1778
  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")),
1615
1779
  h("button", { type: "button", disabled: busy || refreshing, style: { ...btnStyle, cursor: busy || refreshing ? "default" : "pointer" }, onClick: () => refresh(true) }, refreshing ? t("refreshing") : text("刷新全部", "Refresh all"))
1616
1780
  ),
1617
1781
  loadError ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, loadError) : null,
1618
- !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,
1619
- h("div", { className: "subusage-provider-tabs", role: "tablist", "aria-label": text("模型商", "Providers"), style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 14 } },
1782
+ 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,
1783
+ initialized ? h("div", { className: "subusage-provider-tabs", role: "tablist", "aria-label": text("模型商", "Providers"), style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 14 } },
1620
1784
  visibleIds.map((id, index) => {
1621
1785
  const e = entries.find((item) => item.providerId === id), active = usageId === id;
1786
+ // 标签圆标与面板/药丸读同一个 computeStatus:缺凭据是蓝 ⚙ 而不是红 !,
1787
+ // 无窗口成功是中性 ℹ 而不是绿 ?(没有额度窗口不能默认标绿)。
1788
+ const tabStatus = computeStatus(e, { failed: e && e.state !== "ok" ? e.error || e.state : null, failState: e && e.state, providerId: id, t, getLocale });
1622
1789
  return h("button", { key: id, id: `subusage-tab-${id}`, role: "tab", type: "button", tabIndex: active || (!visibleIds.includes(usageId) && index === 0) ? 0 : -1, "aria-selected": active,
1623
1790
  "aria-controls": "subusage-provider-panel", title: `${PROVIDER_META[id].label}: ${providerDescription(e, t)}`, "aria-label": `${PROVIDER_META[id].short}: ${providerDescription(e, t)}`,
1624
1791
  onClick: () => chooseUsage(id), onKeyDown: (event) => {
@@ -1629,8 +1796,8 @@ window.__ModuleLoader__.load({
1629
1796
  event.preventDefault(); chooseUsage(visibleIds[next]);
1630
1797
  const button = event.currentTarget.parentElement.querySelectorAll('[role="tab"]')[next]; if (button) button.focus();
1631
1798
  }, 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" }
1632
- }, statusGlyph(tagColor(e), providerIcon(e), 14), PROVIDER_META[id].short);
1633
- })),
1799
+ }, statusGlyph(tabStatus.color, tabStatus.icon, 14), PROVIDER_META[id].short);
1800
+ })) : null,
1634
1801
  !!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 } },
1635
1802
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1636
1803
  h("strong", { style: { fontSize: 14 } }, usageMeta.label),
@@ -1638,18 +1805,26 @@ window.__ModuleLoader__.load({
1638
1805
  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),
1639
1806
  // 一眼结论:与药丸、弹层同一个状态判定。读取成功只说明接口健康,额度档位另有结论。
1640
1807
  statusBanner(usageStatus),
1641
- usageEntry && usageEntry.error && usageStatus.key !== "error" ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
1808
+ 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,
1642
1809
  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)" } },
1643
1810
  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.")),
1644
1811
  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,
1645
1812
  h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
1646
- canShowUsage(usageEntry) && (usageEntry.windows || []).length ? UsageWindowList({ windows: usageEntry.windows, t, getLocale, uncertain: usageEntry.coverage === "partial" || usageEntry.freshness === "stale" || usageEntry.state !== "ok" }) : h("p", { style: { opacity: .75 } }, usageEntry ? t("noUsageData") : t("loading")),
1813
+ // 自动轮换池健康度(与弹层同一函数):池耗尽时上方横幅已按池中限额窗口染红。
1814
+ usagePool ? CommandCodePoolSummary({ pool: usagePool, cc, t }) : null,
1815
+ // 无窗口时的占位只给「成功但既无窗口也无余额」:余额/加载/需配置/失败等状态横幅已经说明,不再叠加「暂无额度数据」。
1816
+ // 窗口列表用 usageStatusEntry:池耗尽时它携带合成的限额窗口(非耗尽时与 usageEntry 同一对象)。
1817
+ canShowUsage(usageStatusEntry) && (usageStatusEntry.windows || []).length ? UsageWindowList({ windows: usageStatusEntry.windows, t, getLocale, uncertain: usageStatusEntry.coverage === "partial" || usageStatusEntry.freshness === "stale" || usageStatusEntry.state !== "ok" })
1818
+ : usageStatus.key === "balance" && !(usageStatus.entry && balanceText(usageStatus.entry, t)) ? h("p", { style: { opacity: .75 } }, t("noUsageData")) : null,
1647
1819
  canShowUsage(usageEntry) && usageEntry.extras && usageEntry.extras.some((x) => x.kind !== "plan")
1648
1820
  ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
1649
1821
  usageEntry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
1650
1822
  h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
1651
1823
  h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
1652
1824
  : null,
1825
+ // 账户区与药丸弹层同一个组件、同一个控制器:设置页里也能切换 activeAccount,
1826
+ // 不必再跳去 Command Code 提供方插件的设置页。提供方插件未运行时整区降级。
1827
+ usageId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
1653
1828
  usageEntry?.lastAttemptAt ? h("p", { style: { opacity: .65 } }, `${t("lastUpdated")} ${new Date(usageEntry.lastAttemptAt).toLocaleString(getLocale())}`) : null
1654
1829
  ) : null,
1655
1830
  providerManagement
@@ -1702,7 +1877,7 @@ window.__ModuleLoader__.load({
1702
1877
  ctx.inject(["remote.settings"], (settingsCtx) => { ccSettingsNamespace = settingsCtx.remote.settings; });
1703
1878
  ctx.effect(() => () => cc.dispose(), "dsh-subusage: commandcode account switch");
1704
1879
  try {
1705
- ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "subusage", order: 22, label: () => t("nav"), inject: () => ({ usageStore, t, getLocale }) }, SubusageSection));
1880
+ ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "subusage", order: 22, label: () => t("nav"), inject: () => ({ usageStore, t, getLocale, cc }) }, SubusageSection));
1706
1881
  } catch (error) { console.error("[dsh-subusage] 设置页注册失败:", error); }
1707
1882
  ctx.inject(["modelDirectories"], (scope) => {
1708
1883
  try {