dsh-subusage 0.10.10 → 0.11.0

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 +91 -39
  2. package/lib/client.js +309 -149
  3. package/lib/index.js +185 -39
  4. package/package.json +3 -3
package/lib/client.js CHANGED
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
31
31
  lightOk: "全部拉取成功",
32
32
  lightFail: "存在拉取失败",
33
33
  statusOk: "数据获取成功",
34
- statusPartial: "数据不完整,额度未知",
34
+ statusPartial: "数据不完整",
35
35
  statusCache: "缓存数据",
36
36
  cacheAge: "缓存 · {n} 分钟前",
37
37
  cacheUnknown: "缓存时间未知",
@@ -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: "套餐",
@@ -91,16 +90,27 @@ window.__ModuleLoader__.load({
91
90
  restDays: "{n} 天",
92
91
  restHours: "{n} 小时",
93
92
  restMins: "{n} 分钟",
94
- badgeOk: "可用",
95
- badgeHigh: "偏高",
96
- badgeTight: "紧张",
97
- badgeLimited: "不可用",
93
+ // 档位词只描述「还剩多少」;旧的「偏高」没写清主语(用量高还是余量高),读起来自相矛盾。
94
+ // 键名 badgeHigh 保留(它对应已用 71–90% 的档位),文案改成「偏少」。
95
+ badgeOk: "充足",
96
+ badgeHigh: "偏少",
97
+ badgeTight: "即将用尽",
98
+ badgeLimited: "已用尽",
98
99
  badgeBlocked: "受连累",
100
+ statusPlenty: "额度充足",
101
+ statusLow: "余量偏低",
102
+ statusTight: "即将用尽",
103
+ statusExhausted: "额度已用尽",
104
+ statusBlocked: "暂不可用",
105
+ statusWorst: "最紧张:{w} · 剩 {p}%",
106
+ statusOnly: "{w} · 剩 {p}%",
107
+ statusResetIn: "{rest}后重置",
108
+ remainWord: "剩余",
99
109
  numUsed: "已用 {p}%",
100
110
  numLimited: "⚠ 已达限额",
101
111
  numBlocked: "⚠ 暂不可用",
102
112
  pillRemaining: "余 {p}%",
103
- pillLimited: "已达限额",
113
+ pillLimited: "已用尽",
104
114
  pillBlocked: "受连累",
105
115
  needConfigShort: "需配置",
106
116
  pillNeedKey: "未配置 API Key —— 请打开 设置 → 订阅用量 填写,或导出同名环境变量后重启。",
@@ -117,14 +127,14 @@ window.__ModuleLoader__.load({
117
127
  pillNeedGrok: "需登录 —— 可在 设置 → Grok Kit(xAI OAuth)登录或运行 grok login(共享登录文件 ~/.grok/auth.json)。",
118
128
  accountLabel: "账户",
119
129
  commandcodeManaged: "凭据由 Command Code 提供方插件(设置 → Command Code)管理;本页只读、不保存凭据。用量由本插件直接拉取其计费接口,取用同一凭据来源(凭据服务 COMMANDCODE_API_KEY → 启动环境 → ~/.commandcode/auth.json)。",
120
- commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai)。月总额按已知套餐快照推算,未知套餐只显示月剩余;月池耗尽不代表已购/赠送池不可用。显示药丸弹层中选择的账户的额度(未选择时为默认账户/顶层 Key),可能与实际服务账户不同;插件里自定义 apiBase 不会被跟随。",
130
+ commandcodeHelp: "并行读取 /alpha/billing/credits 和 /alpha/billing/subscriptions(默认 API 地址 https://api.commandcode.ai)。月总额按已知套餐快照推算,未知套餐只显示月剩余;月池耗尽不代表已购/赠送池不可用。显示选中账户的额度(未选择时为默认账户/顶层 Key),可能与实际服务账户不同;插件里自定义 apiBase 不会被跟随。账户切换就在下方「Command Code 账户」区,与药丸弹层同一开关;模型查询额度工具时会同时给出全部账户的余量与当前激活项。",
121
131
  grokManaged: "凭据由 dsh-grok-kit / Grok CLI 的 OAuth 登录文件(~/.grok/auth.json)管理;本页只读、不保存凭据、不刷新 token。用量由本插件取同一凭据直连 Grok CLI 计费代理(cli-chat-proxy.grok.com)拉取。",
122
132
  grokHelp: "并行读取 /v1/billing?format=credits 与 /v1/settings。creditUsagePercent 是统一用量池的已用百分比(通常为周池,重置于 currentPeriod.end,剩余 = 100 − 已用);套餐名取 subscription_tier_display;已购加量余额 prepaidBalance 以美元显示。API Key(XAI_API_KEY)路线取不到订阅周池,必须 OAuth 登录。",
123
133
  ccAccountsTitle: "Command Code 账户",
124
134
  ccAccountAuto: "自动轮换",
125
135
  ccAccountDefault: "默认账户",
126
136
  ccAccountActive: "使用中",
127
- ccAccountsHint: "切换对 Command Code 提供方插件全局生效(与其设置页同一开关);上方用量同步切换为所选账户的额度。",
137
+ ccAccountsHint: "切换对 Command Code 提供方插件全局生效(与其设置页同一开关);上方用量与药丸同步切换为所选账户的额度。设置页与药丸弹层用的是同一个开关。",
128
138
  ccAccountsUnavailable: "账户列表不可用(需要 Command Code 提供方插件在运行)",
129
139
  ccUsageAccount: "上方用量:{account}",
130
140
  ccUsageAutoNote: "自动轮换时上方用量仍是默认账户(顶层 Key),不跟随实际服务账户",
@@ -144,7 +154,7 @@ window.__ModuleLoader__.load({
144
154
  lightOk: "All fetches succeeded",
145
155
  lightFail: "Some fetches failed",
146
156
  statusOk: "Fetch OK",
147
- statusPartial: "Partial data; quota unknown",
157
+ statusPartial: "Partial data",
148
158
  statusCache: "Cached data",
149
159
  cacheAge: "Cached · {n} min ago",
150
160
  cacheUnknown: "Cache age unknown",
@@ -171,7 +181,6 @@ window.__ModuleLoader__.load({
171
181
  resets: "Resets",
172
182
  limited: "Limit reached",
173
183
  cascadeLimited: " (blocked by a larger window's limit)",
174
- retry: "Retry now",
175
184
  loading: "Loading usage…",
176
185
  unavailable: "Unavailable",
177
186
  planLabel: "Plan",
@@ -204,16 +213,26 @@ window.__ModuleLoader__.load({
204
213
  restDays: "{n} d",
205
214
  restHours: "{n} h",
206
215
  restMins: "{n} min",
207
- badgeOk: "OK",
208
- badgeHigh: "High",
209
- badgeTight: "Tight",
210
- badgeLimited: "Unavailable",
216
+ // Tier words describe what is LEFT; the old "High" never said high what.
217
+ badgeOk: "Plenty",
218
+ badgeHigh: "Low",
219
+ badgeTight: "Almost out",
220
+ badgeLimited: "Used up",
211
221
  badgeBlocked: "Blocked",
222
+ statusPlenty: "Plenty left",
223
+ statusLow: "Running low",
224
+ statusTight: "Almost out",
225
+ statusExhausted: "Quota used up",
226
+ statusBlocked: "Temporarily blocked",
227
+ statusWorst: "Tightest: {w} · {p}% left",
228
+ statusOnly: "{w} · {p}% left",
229
+ statusResetIn: "resets in {rest}",
230
+ remainWord: "left",
212
231
  numUsed: "Used {p}%",
213
232
  numLimited: "⚠ Limit reached",
214
233
  numBlocked: "⚠ Unavailable",
215
234
  pillRemaining: "{p}% left",
216
- pillLimited: "Limit reached",
235
+ pillLimited: "Used up",
217
236
  pillBlocked: "Blocked",
218
237
  needConfigShort: "Setup",
219
238
  pillNeedKey: "No API key — open Settings → Subscription usage to fill one in, or export the same-named env variable and restart.",
@@ -230,14 +249,14 @@ window.__ModuleLoader__.load({
230
249
  pillNeedGrok: "Login required — sign in under Settings → Grok Kit (xAI OAuth) or run grok login (shared login file ~/.grok/auth.json).",
231
250
  accountLabel: "Account",
232
251
  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).",
233
- 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.",
252
+ 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.",
234
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.",
235
254
  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.",
236
255
  ccAccountsTitle: "Command Code accounts",
237
256
  ccAccountAuto: "Automatic (rotation order)",
238
257
  ccAccountDefault: "Default account",
239
258
  ccAccountActive: "in use",
240
- 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.",
259
+ 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.",
241
260
  ccAccountsUnavailable: "Account list unavailable (requires the Command Code provider plugin to be running)",
242
261
  ccUsageAccount: "Usage above: {account}",
243
262
  ccUsageAutoNote: "with automatic rotation the usage above stays on the default (top-level key) account, not the serving one",
@@ -356,24 +375,6 @@ window.__ModuleLoader__.load({
356
375
  if (w.percent >= 71) return { icon: "⚠", badge: "badgeHigh", color: "#eab308", num: null, rank: 2 };
357
376
  return { icon: "✓", badge: "badgeOk", color: "#22c55e", num: null, rank: 1 };
358
377
  }
359
- /** tag 圆点色:最差窗口档位色(绿/黄/橙/红);拉取失败红。 */
360
- function tagColor(entry) {
361
- if (!entry || entry.state === "loading") return "#94a3b8";
362
- if (entry.state !== "ok") return "#ef4444";
363
- if ((entry.windows || []).some((w) => w.status === "rate-limited")) return "#ef4444";
364
- if (entry.coverage === "partial" || entry.freshness === "stale") return "#eab308";
365
- const rank = { "#22c55e": 0, "#eab308": 1, "#f97316": 2, "#ef4444": 3 };
366
- let worst = "#22c55e";
367
- for (const w of entry.windows || []) {
368
- const c = usageTier(w).color;
369
- if (rank[c] > rank[worst]) worst = c;
370
- }
371
- return worst;
372
- }
373
- /** 数字色:0–70 白(前景),其余随档位。 */
374
- function numColor(w) {
375
- return usageTier(w).rank === 1 ? "inherit" : usageTier(w).color;
376
- }
377
378
  /** 最差窗:档位最高者优先;同档时根因(非连坐)优先,再取已用更多者。 */
378
379
  function worstWindow(windows) {
379
380
  let worst = null;
@@ -651,14 +652,6 @@ window.__ModuleLoader__.load({
651
652
  const worst = worstWindow(windows);
652
653
  return `${statusText(entry, t)}${worst ? " · " + pillText(worst, windows.length > 1, t) : " · " + t("noUsageData")}`;
653
654
  }
654
- function providerIcon(entry) {
655
- if (!entry || entry.state === "loading") return "…";
656
- if ((entry.windows || []).some((w) => w.status === "rate-limited")) return "✕";
657
- if (entry.state !== "ok") return "!";
658
- if (entry.coverage === "partial" || entry.freshness === "stale") return "?";
659
- const worst = worstWindow(entry.windows || []);
660
- return worst ? usageTier(worst).icon : "?";
661
- }
662
655
  function cacheAge(entry, t) {
663
656
  if (!entry || entry.freshness !== "stale" || !canShowUsage(entry)) return null;
664
657
  const at = Date.parse(entry.lastSuccessAt);
@@ -746,7 +739,10 @@ window.__ModuleLoader__.load({
746
739
  return value;
747
740
  };
748
741
  const setCommandCodeAccount = (id) => {
749
- const next = typeof id === "string" ? id.trim() : "";
742
+ // "default" 与空串在 Host 端是同一个账户(同一缓存键、同一指纹,见 providerCacheKey):
743
+ // 归一后就不必为「固定到默认账户」多打一次强制刷新。
744
+ const raw = typeof id === "string" ? id.trim() : "";
745
+ const next = raw === "default" ? "" : raw;
750
746
  if (disposed || ccAccount === next) return Promise.resolve(snapshot);
751
747
  ccAccount = next;
752
748
  // 账户变了:旧账户的在途/缓存数据立即失效,强制按新账户重拉。
@@ -806,11 +802,15 @@ window.__ModuleLoader__.load({
806
802
  const list = [{ id: "default", label: "default" }];
807
803
  const accounts = Array.isArray(value?.accounts) ? value.accounts : [];
808
804
  const tasks = Array.isArray(value?.accountEnrollmentTasks) ? value.accountEnrollmentTasks : [];
805
+ const seen = new Set(["default"]);
809
806
  accounts.forEach((account, index) => {
810
807
  const refName = typeof account?.apiKeyEnv === "string" && account.apiKeyEnv.trim() !== "" ? account.apiKeyEnv.trim() : void 0;
811
808
  // 与 slots() 一致:无凭据引用的槽位不显示;登记任务未完成的账户暂不可选。
812
809
  if (refName === void 0) return;
810
+ // 配置被手改出重复引用名时只列一次:否则同一账户出现两个选项,切换后读的还是同一份额度。
811
+ if (seen.has(refName)) return;
813
812
  if (tasks.some((task) => task?.ref === refName && task.phase !== "naming")) return;
813
+ seen.add(refName);
814
814
  list.push({ id: refName, label: typeof account.label === "string" && account.label.trim() !== "" ? account.label.trim() : `Account ${index + 2}` });
815
815
  });
816
816
  return list;
@@ -830,6 +830,10 @@ window.__ModuleLoader__.load({
830
830
  let disposed = false;
831
831
  const listeners = new Set();
832
832
  let state = { status: "idle", accounts: [], activeId: "", revision: null, writable: false, baseActive: "", switching: false, error: null };
833
+ // 设置页与药丸弹层共用同一个控制器,可能同时驱动它:`loadSequence` 标记「哪一次读取才是当前的」,
834
+ // `inflightLoad` 把同一代的并发 load 合并成一次 describe。没有这两样时,切换账户(mutate)在途
835
+ // 期间返回的旧 describe 会把 activeId/revision 覆盖回切换前的快照,两处界面一起显示旧账户。
836
+ let loadSequence = 0, inflightLoad = null;
833
837
  const emit = () => { for (const fn of [...listeners]) fn(state); };
834
838
  const parseRow = (row) => {
835
839
  const accounts = commandCodeAccountList(row?.value);
@@ -837,18 +841,29 @@ window.__ModuleLoader__.load({
837
841
  };
838
842
  const load = async (keepError = false) => {
839
843
  if (disposed) return state;
844
+ // 同一代的并发 load 合并:两处界面同时挂载时只发一次 describe。
845
+ if (inflightLoad?.sequence === loadSequence) return inflightLoad.promise;
846
+ const sequence = ++loadSequence;
840
847
  state = { ...state, status: "loading", error: keepError ? state.error : null }; emit();
841
- try {
842
- const response = await describe();
843
- if (!response?.ok) throw new Error(response?.error?.message || "settings describe failed");
844
- const row = response.value?.namespaces?.find((candidate) => candidate?.ns === COMMANDCODE_SETTINGS_NS);
845
- if (!row) throw new Error("Command Code settings namespace not found");
846
- state = { ...state, status: "ready", writable: response.value.writable === true, ...parseRow(row) };
847
- } catch (error) {
848
- state = { ...state, status: "unavailable", error: String(error?.message || error) };
849
- }
850
- emit();
851
- return state;
848
+ const task = (async () => {
849
+ try {
850
+ const response = await describe();
851
+ if (!response?.ok) throw new Error(response?.error?.message || "settings describe failed");
852
+ // 切换账户(mutate)或更晚的 load 已经给出更新的状态:这次的结果一律作废,
853
+ // 既不用旧快照覆盖,也不把状态留在 loading(mutate 那边已写回 ready)。
854
+ if (disposed || sequence !== loadSequence) return state;
855
+ const row = response.value?.namespaces?.find((candidate) => candidate?.ns === COMMANDCODE_SETTINGS_NS);
856
+ if (!row) throw new Error("Command Code settings namespace not found");
857
+ state = { ...state, status: "ready", writable: response.value.writable === true, ...parseRow(row) };
858
+ } catch (error) {
859
+ if (disposed || sequence !== loadSequence) return state;
860
+ state = { ...state, status: "unavailable", error: String(error?.message || error) };
861
+ }
862
+ emit();
863
+ return state;
864
+ })();
865
+ inflightLoad = { sequence, promise: task };
866
+ try { return await task; } finally { if (inflightLoad?.promise === task) inflightLoad = null; }
852
867
  };
853
868
  const switchTo = async (id) => {
854
869
  if (disposed || state.status !== "ready" || state.switching) return state;
@@ -861,7 +876,10 @@ window.__ModuleLoader__.load({
861
876
  try {
862
877
  const response = await mutate(ops, state.revision);
863
878
  if (!response?.ok) throw new Error(response?.error?.message || "settings mutate failed");
864
- state = { ...state, switching: false, ...parseRow(response.value) };
879
+ // 写入成功就是权威状态:作废在途的旧 describe,并把 status 明确拉回 ready
880
+ // (load 开始时置的 loading 不该留在这里)。
881
+ loadSequence++;
882
+ state = { ...state, status: "ready", switching: false, ...parseRow(response.value) };
865
883
  } catch (error) {
866
884
  const message = String(error?.message || error);
867
885
  state = { ...state, switching: false, error: message };
@@ -878,13 +896,20 @@ window.__ModuleLoader__.load({
878
896
  };
879
897
  }
880
898
 
881
- /** 弹层内的 Command Code 账户切换区:自动轮换 + 默认账户 + 各额外账户。 */
899
+ // 未注入 cc(提供方插件未运行)时给 useSyncExternalStore 一对稳定引用:每次都新建函数
900
+ // 会让 React 反复重新订阅。
901
+ const NO_CC_STATE = () => null;
902
+ const NO_CC_SUBSCRIBE = () => () => {};
903
+ /** 弹层与设置页共用的 Command Code 账户区:自动轮换 + 默认账户 + 各额外账户。 */
882
904
  function CommandCodeAccountSwitch({ cc, ccState, t }) {
883
905
  if (!cc || !ccState) return null;
906
+ // 状态没 ready(正在读账户配置 / 不可用)时按钮不可点:switchTo 在这种状态下会直接返回,
907
+ // 可点的按钮换来的只是「点了没反应」。
908
+ const locked = ccState.switching || ccState.status !== "ready" || !ccState.writable;
884
909
  const optionButton = (id, labelText) => h("button", {
885
910
  type: "button",
886
911
  onClick: () => void cc.switchTo(id),
887
- disabled: ccState.switching || !ccState.writable,
912
+ disabled: locked,
888
913
  "aria-pressed": ccState.activeId === id,
889
914
  style: {
890
915
  display: "flex", alignItems: "center", gap: 6, width: "100%", textAlign: "left",
@@ -892,7 +917,7 @@ window.__ModuleLoader__.load({
892
917
  border: `1px solid ${ccState.activeId === id ? "rgba(59,130,246,.55)" : "rgba(128,128,128,.3)"}`,
893
918
  background: ccState.activeId === id ? "rgba(59,130,246,.10)" : "transparent",
894
919
  color: "inherit", font: "inherit",
895
- cursor: ccState.switching || !ccState.writable ? "default" : "pointer"
920
+ cursor: locked ? "default" : "pointer"
896
921
  }
897
922
  },
898
923
  h("span", { "aria-hidden": "true" }, ccState.activeId === id ? "●" : "○"),
@@ -901,10 +926,13 @@ window.__ModuleLoader__.load({
901
926
  );
902
927
  // 账户多时这一坨会把上方的额度顶出视野:折进 details,summary 只留「当前账户 + 数量」,
903
928
  // 展开后列表自身限高滚动,弹层高度不随账户数增长。
904
- const activeLabel = ccState.status !== "ready" ? null
905
- : ccState.activeId === "" ? t("ccAccountAuto")
929
+ const activeName = ccState.status !== "ready" ? null
906
930
  : ccState.activeId === "default" ? t("ccAccountDefault")
907
931
  : (ccState.accounts.find(account => account.id === ccState.activeId)?.label ?? ccState.activeId);
932
+ // summary 直接回答「现在是哪一个账户」:固定账户写账户名 + 使用中,自动轮换写模式名。
933
+ const activeLabel = ccState.status !== "ready" ? null
934
+ : ccState.activeId === "" ? t("ccAccountAuto")
935
+ : `${activeName} · ${t("ccAccountActive")}`;
908
936
  const optionCount = ccState.status === "ready" ? ccState.accounts.length + 1 : 0;
909
937
  return h("details", { style: { marginTop: 10, paddingTop: 8, borderTop: "1px solid rgba(128,128,128,.25)" } },
910
938
  h("summary", { style: { cursor: "pointer", fontSize: 11, opacity: 0.8 } },
@@ -920,13 +948,126 @@ window.__ModuleLoader__.load({
920
948
  );
921
949
  }
922
950
 
951
+ // ── 视觉原语:扁平 + 状态色(药丸 / 弹层 / 设置页共用同一套)────────────────
952
+ // 注意:下面这些都是**普通函数调用**而不是 React 组件元素——它们不用 Hook,直接内联展开,
953
+ // 测试里的 React 桩也能看到它们渲染出的文字。
954
+ const TRACK = "rgba(128,128,128,.22)";
955
+ const NEUTRAL = "#94a3b8";
956
+ const INFO = "#3b82f6";
957
+ /** #rrggbb → rgba(r,g,b,a):底色与描边都由状态色派生,保证同一状态到处同一个颜色。 */
958
+ function alpha(color, a) {
959
+ const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(color || "");
960
+ return m ? `rgba(${parseInt(m[1], 16)},${parseInt(m[2], 16)},${parseInt(m[3], 16)},${a})` : `rgba(128,128,128,${a})`;
961
+ }
962
+ /** 状态色实心圆上的字形色:黄色配深字,其余白字。 */
963
+ function onColor(color) { return color === "#eab308" ? "#1f1300" : "#fff"; }
964
+ /** 状态色用作文字时的版本:纯黄在浅色主题上看不清,加深一档。 */
965
+ function textTone(color) { return color === "#eab308" ? "#ca8a04" : color; }
966
+ function windowLeft(w) { return w.status === "rate-limited" ? 0 : remainingPercent(w.percent); }
967
+ /** 状态色实心圆 + 字形(✓ ⚠ ✕ ? ! ⚙ ℹ …):颜色之外还有形状,不靠颜色单独传达。 */
968
+ function statusGlyph(color, icon, size) {
969
+ return h("span", { "aria-hidden": "true", style: { flex: "none", width: size, height: size, borderRadius: "50%", background: color, color: onColor(color), display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: Math.round(size * 0.62), fontWeight: 700, lineHeight: 1 } }, icon);
970
+ }
971
+ /** 小标签:状态色圆点 + 继承前景色的文字(文字不着色,深浅主题都清晰)。 */
972
+ function statusChip(text, color) {
973
+ return h("span", { style: { display: "inline-flex", alignItems: "center", gap: 5, fontSize: 11, lineHeight: 1.5, padding: "1px 8px", borderRadius: 999, background: alpha(color, ".14"), whiteSpace: "nowrap" } },
974
+ h("span", { "aria-hidden": "true", style: { width: 6, height: 6, borderRadius: "50%", background: color } }), text);
975
+ }
976
+ /**
977
+ * 药丸里的迷你余量条:一个窗口一根横条,满 = 100% 剩余,颜色是该窗口自己的档位色。
978
+ * 条的顺序与弹层里窗口行的顺序一致,所以不用文字也能一眼对上;窗口越多条越细(最多 5 根)。
979
+ */
980
+ function miniBars(windows, muted) {
981
+ const list = windows.slice(0, 5), n = list.length;
982
+ const barH = n <= 1 ? 6 : n === 2 ? 5 : n === 3 ? 4 : n === 4 ? 3 : 2;
983
+ return h("span", { "aria-hidden": "true", style: { display: "inline-flex", flexDirection: "column", justifyContent: "center", gap: n <= 2 ? 3 : 2, width: 34, flex: "none", opacity: muted ? 0.5 : 1 } },
984
+ list.map((w, i) => {
985
+ const left = windowLeft(w);
986
+ return h("span", { key: `${w.kind}:${i}`, style: { display: "block", height: barH, borderRadius: 99, background: TRACK, overflow: "hidden" } },
987
+ h("span", { style: { display: "block", width: `${left}%`, minWidth: left > 0 ? 3 : 0, height: "100%", borderRadius: 99, background: usageTier(w).color } }));
988
+ }));
989
+ }
990
+ /**
991
+ * 订阅整体状态:一个对象回答「现在订阅怎么样」,药丸、弹层横幅、设置页横幅共用,
992
+ * 所以三处永远同一个结论、同一个颜色。优先级:额度用尽 > Cookie 到期 > 需配置 > 读取失败 > 缓存/不完整 > 额度档位。
993
+ * 返回 { key, color, icon, title, sub, short, tinted, ... }:
994
+ * title/sub 给弹层横幅;short 是药丸上的**一个词**(正常时为空——绿色余量条本身就是答案);
995
+ * tinted 为真时药丸整体染色,提醒「这里需要你看一眼」。
996
+ */
997
+ function computeStatus(rawEntry, { failed = null, failState, providerId, t, getLocale }) {
998
+ const entry = canShowUsage(rawEntry) ? rawEntry : null;
999
+ const windows = entry ? entry.windows || [] : [];
1000
+ const worst = windows.length ? worstWindow(windows) : null;
1001
+ const worstTier = worst ? usageTier(worst) : null;
1002
+ const multi = windows.length > 1;
1003
+ const degraded = !!(rawEntry && (rawEntry.state !== "ok" || rawEntry.freshness === "stale" || rawEntry.coverage === "partial"));
1004
+ const state = rawEntry ? rawEntry.state : failState;
1005
+ const expiry = cookieExpiry(rawEntry, t);
1006
+ const expiring = !!(expiry && expiry.rank >= 2);
1007
+ // 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时主文案仍是失败原因;
1008
+ // 例外是已过期——它本身就是「请重新登录」的行动项。
1009
+ const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || expiry.expired);
1010
+ const windowName = (w) => t(KIND_LABEL[w.kind] ?? "wall");
1011
+ const windowShort = (w) => multi ? `${t(KIND_SHORT[w.kind] ?? "srolling")} ` : "";
1012
+ const windowSub = (w) => {
1013
+ const head = t(multi ? "statusWorst" : "statusOnly").replace("{w}", windowName(w)).replace("{p}", String(windowLeft(w)));
1014
+ const ms = w.resetsAt ? Date.parse(w.resetsAt) - Date.now() : NaN;
1015
+ return ms > 0 ? `${head} · ${t("statusResetIn").replace("{rest}", durationText(ms, t))}` : head;
1016
+ };
1017
+ const needConfig = state === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
1018
+ : state === "no-cookie" ? t("pillNeedCookie") : null;
1019
+ const base = { entry, windows, worst, worstTier, multi, degraded, expiry, expiring, expiryActionable, needConfig };
1020
+ if ((!rawEntry && !failed && !failState) || state === "loading") return { ...base, key: "loading", color: NEUTRAL, icon: "…", title: t("loading"), sub: "", short: "", tinted: false };
1021
+ if (state === "disabled") return { ...base, key: "disabled", color: NEUTRAL, icon: "–", title: t("statusDisabled"), sub: "", short: t("statusDisabled"), tinted: false };
1022
+ if (worstTier && worstTier.rank >= 4) {
1023
+ const title = t(worst.cascade ? "statusBlocked" : "statusExhausted");
1024
+ return { ...base, key: "blocked", color: worstTier.color, icon: "✕", title, sub: windowSub(worst), short: windowShort(worst) + t(worst.cascade ? "pillBlocked" : "pillLimited"), tinted: true };
1025
+ }
1026
+ if (expiryActionable) {
1027
+ return { ...base, key: "cookie", color: expiry.color, icon: expiry.icon, title: expiry.label, sub: t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale())), short: expiry.label, tinted: true };
1028
+ }
1029
+ if (needConfig) {
1030
+ 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
+ }
1032
+ if (!entry) {
1033
+ return { ...base, key: "error", color: "#ef4444", icon: "!", title: t("statusError"), sub: failed || (rawEntry && rawEntry.error) || "", short: t("statusError"), tinted: true };
1034
+ }
1035
+ if (degraded) {
1036
+ // 过期缓存即使 state=error 也要叫「缓存数据」:读取失败是原因,用户看到的是上次的数字。
1037
+ const label = rawEntry.freshness === "stale" ? t("statusCache") : rawEntry.coverage === "partial" ? t("statusPartial") : statusText(rawEntry, t);
1038
+ const worse = worstTier && worstTier.rank >= 3;
1039
+ return { ...base, key: "degraded", color: worse ? worstTier.color : "#eab308", icon: "?", title: label, sub: [cacheAge(rawEntry, t), worst ? windowSub(worst) : null].filter(Boolean).join(" · "), short: label, tinted: true };
1040
+ }
1041
+ if (worstTier) {
1042
+ const sub = windowSub(worst);
1043
+ if (worstTier.rank >= 3) return { ...base, key: "tight", color: worstTier.color, icon: "⚠", title: t("statusTight"), sub, short: windowShort(worst) + t(worstTier.badge), tinted: true };
1044
+ 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
+ return { ...base, key: "ok", color: worstTier.color, icon: "✓", title: t("statusPlenty"), sub, short: "", tinted: false };
1046
+ }
1047
+ // 成功但没有额度窗口:只有余额(或什么都没有)。余额就是唯一信息,不画成「充足」。
1048
+ const balance = balanceText(entry, t);
1049
+ return { ...base, key: "balance", color: NEUTRAL, icon: "ℹ", title: balance ?? t("noUsageData"), sub: "", short: balance ?? t("noUsageData"), tinted: false };
1050
+ }
1051
+ /** 弹层 / 设置页顶部的状态横幅:左侧实心状态圆,右侧一句结论 + 一行依据。 */
1052
+ function statusBanner(status) {
1053
+ return h("div", { role: "status", style: { display: "flex", alignItems: "center", gap: 12, marginTop: 10, padding: "12px 14px", borderRadius: 12, background: alpha(status.color, ".10"), border: `1px solid ${alpha(status.color, ".28")}` } },
1054
+ statusGlyph(status.color, status.icon, 30),
1055
+ h("div", { style: { minWidth: 0, flex: 1 } },
1056
+ h("div", { style: { fontSize: 15, fontWeight: 700, lineHeight: 1.3, overflowWrap: "anywhere" } }, status.title),
1057
+ status.sub ? h("div", { style: { fontSize: 11.5, opacity: 0.75, marginTop: 2, overflowWrap: "anywhere", whiteSpace: "pre-wrap" } }, status.sub) : null));
1058
+ }
1059
+ /** 套餐名(extras 里 kind=plan):放在标题行右侧做成小标签,不再混在余额明细里。 */
1060
+ function planName(entry) {
1061
+ const row = entry && Array.isArray(entry.extras) ? entry.extras.find((x) => x.kind === "plan") : undefined;
1062
+ return row ? row.value : null;
1063
+ }
923
1064
  function RemainingBar({ value, color, label }) {
924
1065
  const pct = Math.max(0, Math.min(100, Number(value) || 0));
925
1066
  return h("div", {
926
1067
  role: "progressbar",
927
1068
  "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, "aria-label": label,
928
- style: { width: "100%", height: 10, marginTop: 8, borderRadius: 6, background: "rgba(148,163,184,.22)", overflow: "hidden" }
929
- }, h("div", { style: { width: `${pct}%`, height: "100%", borderRadius: 6, background: color, boxShadow: pct ? "inset 0 1px 0 rgba(255,255,255,.2)" : "none" } }));
1069
+ style: { width: "100%", height: 8, marginTop: 8, borderRadius: 99, background: TRACK, overflow: "hidden" }
1070
+ }, h("div", { style: { width: `${pct}%`, minWidth: pct > 0 ? 4 : 0, height: "100%", borderRadius: 99, background: color } }));
930
1071
  }
931
1072
  /**
932
1073
  * 单窗口行(药丸弹层与设置页共用):徽标 + 余量% 主数字 + 进度条(满格=100% 剩余,随已用扣减)+ 明细行。
@@ -950,28 +1091,22 @@ window.__ModuleLoader__.load({
950
1091
  resetLine(w, t, getLocale),
951
1092
  w.cascade ? t("cascadeLimited") + (w.detail && w.detail.blockedBy ? ` [${w.detail.blockedBy.join(", ")}]` : "") : null
952
1093
  ].filter(Boolean).join(" · ");
1094
+ // 档位词只在「需要留意」时出现:充足时绿色长条与大数字已经说明一切,再贴一个「充足」只是噪音。
1095
+ const alert = tier.rank >= 2;
1096
+ const label = blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(left));
953
1097
  return h("div", {
954
- style: {
955
- marginTop: 8, padding: "8px 10px", borderRadius: 8,
956
- background: blocked ? "rgba(239,68,68,.08)" : "transparent",
957
- border: blocked ? "1px solid rgba(239,68,68,.35)" : "1px solid transparent"
958
- }
1098
+ style: { marginTop: 8, padding: "10px 12px", borderRadius: 10, background: blocked ? alpha(tier.color, ".09") : "rgba(128,128,128,.07)" }
959
1099
  },
960
1100
  h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
961
- h("span", { style: { fontSize: 12, opacity: 0.85, minWidth: 52 } }, t(KIND_LABEL[w.kind] ?? "wall")),
962
- h("span", {
963
- style: {
964
- fontSize: 11, padding: "1px 8px", borderRadius: 999, whiteSpace: "nowrap",
965
- border: `1px solid ${tier.color}`, color: tier.color
966
- }
967
- }, `${tier.icon} ${t(tier.badge)}`),
1101
+ h("span", { style: { fontSize: 12.5, fontWeight: 600 } }, t(KIND_LABEL[w.kind] ?? "wall")),
1102
+ alert ? statusChip(t(tier.badge), tier.color) : null,
968
1103
  h("span", { style: { flex: 1 } }),
969
- h("strong", { style: { fontSize: 13, color: numColor(w), whiteSpace: "nowrap" } },
970
- blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(remainingPercent(w.percent))))
1104
+ h("strong", { style: { fontSize: 18, lineHeight: 1.1, color: alert ? textTone(tier.color) : "inherit", whiteSpace: "nowrap" } }, `${left}%`),
1105
+ h("span", { style: { fontSize: 11, opacity: 0.65, whiteSpace: "nowrap" } }, t("remainWord"))
971
1106
  ),
972
- RemainingBar({ value: left, color: tier.color, label: blocked ? t(tier.num) : t("pillRemaining").replace("{p}", String(remainingPercent(w.percent))) }),
1107
+ RemainingBar({ value: left, color: tier.color, label }),
973
1108
  usageLine || resetParts
974
- ? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 4 } },
1109
+ ? h("div", { style: { fontSize: 11, opacity: 0.7, marginTop: 6 } },
975
1110
  usageLine ? h("div", { style: { overflowWrap: "anywhere" } }, usageLine) : null,
976
1111
  resetParts ? h("div", { style: { marginTop: usageLine ? 2 : 0, overflowWrap: "anywhere" } }, resetParts) : null)
977
1112
  : null
@@ -1000,7 +1135,8 @@ window.__ModuleLoader__.load({
1000
1135
  const root = react.useRef(null);
1001
1136
  const retry = react.useRef(() => {});
1002
1137
  // Command Code 账户区:cc 由装配层注入(提供方插件未运行时为 null,区块整体隐藏)。
1003
- const ccState = react.useSyncExternalStore(cc ? cc.subscribe : () => () => {}, cc ? cc.getSnapshot : () => null);
1138
+ // 兜底用模块级稳定引用(NO_CC_*):内联箭头函数每次渲染都是新引用,会让 React 反复重新订阅。
1139
+ const ccState = react.useSyncExternalStore(cc ? cc.subscribe : NO_CC_SUBSCRIBE, cc ? cc.getSnapshot : NO_CC_STATE, cc ? cc.getSnapshot : NO_CC_STATE);
1004
1140
  react.useEffect(() => { if (open && cc) void cc.load(); }, [open, cc]);
1005
1141
  // 挂载即读一次账户配置:提供方插件固定了账户时,药丸本体不必等弹层打开就跟随。
1006
1142
  react.useEffect(() => { if (providerId === "commandcode" && cc) void cc.load(); }, [providerId, cc]);
@@ -1070,72 +1206,55 @@ window.__ModuleLoader__.load({
1070
1206
  }, [open]);
1071
1207
  const rawEntry = snapshot && snapshot.entry;
1072
1208
  if (rawEntry?.visible === false || rawEntry?.state === "disabled") return null;
1073
- const entry = canShowUsage(rawEntry) ? rawEntry : null;
1074
- const ok = !!(rawEntry && rawEntry.state === "ok" && rawEntry.freshness !== "stale" && rawEntry.coverage !== "partial");
1075
- const light = ok ? "🟢" : (snapshot !== null || failed !== null ? "🔴" : "🟡");
1076
- const windows = entry ? entry.windows || [] : [];
1077
- const worst = windows.length > 0 ? worstWindow(windows) : null;
1078
- const worstTier = worst ? usageTier(worst) : null;
1079
- const degraded = !!(rawEntry && (rawEntry.state !== "ok" || rawEntry.freshness === "stale" || rawEntry.coverage === "partial"));
1209
+ // 整体状态由 computeStatus 一处判定:药丸、弹层横幅、设置页横幅读的是同一个结论。
1210
+ const st = computeStatus(rawEntry, { failed, failState, providerId, t, getLocale });
1211
+ const { entry, windows, worst, degraded, expiry, expiring } = st;
1080
1212
  const summary = worst ? pillText(worst, windows.length > 1, t) : entry ? (balanceText(entry, t) ?? t("noUsageData")) : t("providerUnavailable");
1081
- const stateText = !ok ? (snapshot && snapshot.entry ? snapshot.entry.state : failState) : void 0;
1082
- const stateLabel = rawEntry ? statusText(rawEntry, t) : failState === "no-key" ? t("statusNoKey") : failState === "no-cookie" ? t("statusNoCookie") : failed ? t("statusError") : t("loading");
1083
- // 配置缺失类失败给可行动指引,不倒裸报错。
1084
- const needConfig = stateText === "no-key" ? (providerId === "commandcode" ? t("pillNeedCommandcode") : providerId === "xai-oauth" ? t("pillNeedGrok") : t("pillNeedKey"))
1085
- : stateText === "no-cookie" ? t("pillNeedCookie")
1086
- : null;
1087
- const isConfig = stateText === "no-key" || stateText === "no-cookie";
1088
- // 余额请求成功不代表有额度窗口(例如小米接口暂时只返回余额)。
1089
- // Cookie 临近到期是需要重新登录的行动项:除额度已用尽外,优先于普通余量与降级提示。
1090
- const expiry = cookieExpiry(rawEntry, t);
1091
- const quotaBlocked = !!(worstTier && worstTier.rank >= 4);
1092
- const expiring = !!(expiry && expiry.rank >= 2);
1093
- // 倒计时只用于展示,永远不能盖过官方接口的判定:凭据已被拒绝(state=error)时
1094
- // 主文案必须仍是失败原因,否则「还有 2 小时到期」会把重新登录推迟掉。
1095
- // 例外是已过期:它本身就是「请重新登录」的行动项,比通用错误文案更具体。
1096
- const expiryActionable = expiring && (!(rawEntry && rawEntry.state === "error") || (expiry && expiry.expired));
1097
- // 底色与文字沿用额度告警语汇:已过期与额度用尽同为红,30 分钟内为橙,2 小时内为黄。
1098
- const alertStyle = quotaBlocked || (expiry && expiry.expired) ? { background: "rgba(239,68,68,.10)", borderColor: "rgba(239,68,68,.45)" } : expiryActionable && expiry.rank >= 3 ? { background: "rgba(249,115,22,.10)", borderColor: "rgba(249,115,22,.45)" } : expiryActionable ? { background: "rgba(234,179,8,.10)", borderColor: "rgba(234,179,8,.45)" } : {};
1099
- const triggerColor = quotaBlocked ? worstTier.color : expiryActionable ? expiry.color : worstTier && worstTier.rank > 1 ? worstTier.color : null;
1100
- const triggerIcon = quotaBlocked ? worstTier.icon : expiryActionable ? expiry.icon : degraded ? "⚠" : worstTier ? worstTier.icon : ok ? "ℹ" : isConfig ? "⚙" : light;
1101
- const triggerText = quotaBlocked ? (worst ? pillText(worst, windows.length > 1, t) : entry ? t("noUsageData") : stateLabel) : expiryActionable ? expiry.label : worst ? `${degraded ? statusText(rawEntry, t) + " · " : ""}${pillText(worst, windows.length > 1, t)}` : entry ? `${degraded ? stateLabel + " · " : ""}${(balanceText(entry, t) ?? t("noUsageData"))}` : isConfig ? t("needConfigShort") : stateLabel;
1213
+ // 药丸只回答「订阅现在怎么样」:状态图形 + 至多一个词。正常时只剩彩色余量条——
1214
+ // 厂商名就是旁边的模型选择器,具体百分比点开再看;文字与数字都收进 title / aria-label。
1215
+ 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 : ""}`;
1216
+ // 有窗口就画迷你余量条:Cookie 到期等行动项通过 short 文案表达,不牺牲余量信息;
1217
+ // 只有没有任何额度窗口时(需配置 / 失败 / 仅余额)才退回状态字形。
1218
+ const glyph = windows.length ? miniBars(windows, degraded) : statusGlyph(st.color, st.icon, 16);
1102
1219
  return h("span", { ref: root, style: { position: "relative", display: "inline-flex", alignItems: "center" } },
1103
1220
  h("button", {
1104
1221
  type: "button",
1105
1222
  "aria-expanded": open,
1106
1223
  "aria-haspopup": "dialog",
1107
- "aria-label": `${label}: ${stateLabel} · ${summary}${expiring && !quotaBlocked ? " · " + expiry.label : ""}`,
1108
- title: `${label}: ${stateLabel} · ${summary}${expiring && !quotaBlocked ? " · " + expiry.label : ""}`,
1224
+ "aria-label": tip,
1225
+ title: tip,
1109
1226
  onClick: () => setOpen(!open),
1110
1227
  style: {
1111
- display: "inline-flex", alignItems: "center", gap: 6, padding: "4px 10px",
1112
- borderRadius: 999, border: "1px solid rgba(128,128,128,.35)", background: "transparent",
1113
- color: "inherit", font: "inherit", cursor: "pointer", whiteSpace: "nowrap",
1114
- ...alertStyle
1228
+ display: "inline-flex", alignItems: "center", gap: 8, padding: st.short ? "4px 11px 4px 9px" : "5px 10px", minHeight: 26, boxSizing: "border-box",
1229
+ borderRadius: 999, border: `1px solid ${st.tinted ? alpha(st.color, ".45") : "rgba(128,128,128,.3)"}`,
1230
+ background: st.tinted ? alpha(st.color, ".10") : "transparent",
1231
+ color: "inherit", font: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap"
1115
1232
  }
1116
1233
  },
1117
- h("span", { "aria-hidden": "true", style: { fontSize: 11, lineHeight: 1 } }, triggerIcon),
1118
- h("span", { style: triggerColor ? { color: triggerColor } : null }, triggerText),
1119
- failed && ok ? h("span", { style: { opacity: 0.7, fontSize: 11 } }, `(${t("unavailable")})`) : null
1234
+ glyph,
1235
+ st.short ? h("span", null, st.short) : null
1120
1236
  ),
1121
1237
  open && h("div", {
1122
1238
  role: "dialog",
1123
1239
  "aria-label": `${label} ${t("nav")}`,
1124
1240
  style: {
1125
1241
  position: "absolute", bottom: "calc(100% + 6px)", right: 0, zIndex: 50,
1126
- width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding: 12, borderRadius: 10,
1242
+ width: "min(340px, calc(100vw - 24px))", boxSizing: "border-box", overflowWrap: "anywhere", padding: 14, borderRadius: 14,
1127
1243
  background: "var(--dsh-surface, Canvas)", color: "var(--dsh-text, CanvasText)",
1128
- border: "1px solid rgba(128,128,128,.35)",
1129
- boxShadow: "0 8px 24px rgba(0,0,0,.35)", font: "12px/1.6 sans-serif"
1244
+ border: "1px solid rgba(128,128,128,.28)",
1245
+ boxShadow: "0 12px 32px rgba(0,0,0,.28)", font: "12px/1.5 sans-serif"
1130
1246
  }
1131
1247
  },
1132
- h("strong", null, label),
1133
- needConfig ? h("p", { style: { margin: "6px 0 0", opacity: 0.9 } }, needConfig) : null,
1134
- failed && !needConfig ? h("p", { style: { color: "#f87171", margin: "6px 0 0", whiteSpace: "pre-wrap" } }, failed) : null,
1135
- !ok && !snapshot && !failed ? h("p", { style: { margin: "6px 0 0", opacity: 0.8 } }, t("loading")) : null,
1136
- cacheAge(rawEntry, t) ? h("p", { style: { color: "#b7791f" } }, cacheAge(rawEntry, t)) : null,
1137
- // 到期提醒常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久。
1138
- expiry ? h("p", { role: "status", style: { margin: "6px 0 0", color: expiring ? expiry.color : "inherit" } }, `${expiry.label} · ${t("mimoCookieExpiresAt").replace("{time}", new Date(expiry.expiresAt).toLocaleString(getLocale()))}`) : null,
1248
+ // 标题行:厂商名 + 套餐名小标签。
1249
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1250
+ h("strong", { style: { fontSize: 13 } }, label),
1251
+ h("span", { style: { flex: 1 } }),
1252
+ 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
+ // 状态横幅:先给结论(充足 / 偏少 / 即将用尽 / 已用尽 / 需配置 / 失败…)与一行依据。
1254
+ statusBanner(st),
1255
+ failed && st.key !== "error" && !st.needConfig ? h("p", { style: { color: "#f87171", margin: "8px 0 0", whiteSpace: "pre-wrap" } }, failed) : null,
1256
+ // Cookie 到期常驻弹层:即使用量正常,也让用户看见 Cookie 还剩多久(已在横幅里说明时不重复)。
1257
+ 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,
1139
1258
  // 用量账户标注:账户区 ready 时说明上方数字属于哪个账户;自动轮换时明确不跟随实际服务账户。
1140
1259
  providerId === "commandcode" && ccState && ccState.status === "ready"
1141
1260
  ? h("p", { style: { margin: "6px 0 0", fontSize: 11, opacity: 0.75 } },
@@ -1145,18 +1264,24 @@ window.__ModuleLoader__.load({
1145
1264
  // 额度线切换时插一条分组标题:AFP 的日限额只覆盖视觉 / 语音模型与 Harness,与文本模型的
1146
1265
  // 5 小时 / 周 / 月不是同一条线,混排会让人把两条线的配额当成一组来互相比较。
1147
1266
  UsageWindowList({ windows, t, getLocale, uncertain: degraded }),
1148
- entry && entry.extras && entry.extras.length > 0
1149
- ? h("div", { style: { marginTop: 10, opacity: 0.85 } },
1150
- entry.extras.map((x) => h("div", { key: x.kind }, `${extraLabel(x, t)}: ${x.value}`)))
1267
+ // 余额等补充项:套餐名已在标题行,这里只剩「标签 … 数值」两列。
1268
+ entry && entry.extras && entry.extras.some((x) => x.kind !== "plan")
1269
+ ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
1270
+ entry.extras.filter((x) => x.kind !== "plan").map((x) => h("div", { key: x.kind, style: { display: "flex", justifyContent: "space-between", gap: 12, padding: "3px 0" } },
1271
+ h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
1272
+ h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
1151
1273
  : null,
1152
1274
  providerId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
1153
- snapshot ? h("div", { style: { marginTop: 10, opacity: 0.7 } }, `${t("lastUpdated")} ${new Date(snapshot.updatedAt).toLocaleString(getLocale())}`) : null,
1154
- h("button", {
1155
- type: "button",
1156
- onClick: () => retry.current(),
1157
- disabled: refreshing,
1158
- style: { marginTop: 10, padding: "4px 12px", borderRadius: 6, border: "1px solid rgba(128,128,128,.5)", background: "transparent", color: "inherit", cursor: "pointer" }
1159
- }, t(refreshing ? "refreshing" : "retry"))
1275
+ // 页脚:更新时间 + 刷新。
1276
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginTop: 12 } },
1277
+ h("span", { style: { flex: 1, minWidth: 0, fontSize: 11, opacity: 0.6 } }, snapshot ? `${t("lastUpdated")} ${new Date(snapshot.updatedAt).toLocaleString(getLocale())}` : ""),
1278
+ h("button", {
1279
+ type: "button",
1280
+ onClick: () => retry.current(),
1281
+ disabled: refreshing,
1282
+ style: { padding: "4px 12px", borderRadius: 8, border: "none", background: alpha(INFO, ".14"), color: "inherit", font: "inherit", fontWeight: 600, cursor: refreshing ? "default" : "pointer", whiteSpace: "nowrap" }
1283
+ }, t(refreshing ? "refreshing" : "refresh"))
1284
+ )
1160
1285
  )
1161
1286
  );
1162
1287
  }
@@ -1212,8 +1337,11 @@ window.__ModuleLoader__.load({
1212
1337
  }
1213
1338
  return patch;
1214
1339
  }
1215
- function SubusageSection({ usageStore, t, getLocale }) {
1340
+ function SubusageSection({ usageStore, t, getLocale, cc }) {
1216
1341
  const result = react.useSyncExternalStore(usageStore.subscribe, usageStore.getSnapshot, usageStore.getSnapshot);
1342
+ // Command Code 账户区:与弹层共用同一个控制器与组件;cc 由装配层注入,
1343
+ // 提供方插件未运行时为 undefined,区块整体隐藏。
1344
+ const ccState = react.useSyncExternalStore(cc ? cc.subscribe : NO_CC_SUBSCRIBE, cc ? cc.getSnapshot : NO_CC_STATE, cc ? cc.getSnapshot : NO_CC_STATE);
1217
1345
  const [tab, setTab] = react.useState(initialTab);
1218
1346
  const [draft, setDraft] = react.useState(() => draftFor(result.settings, tab));
1219
1347
  const [dirty, setDirty] = react.useState(false);
@@ -1371,6 +1499,7 @@ window.__ModuleLoader__.load({
1371
1499
  const usageMeta = PROVIDER_META[usageId];
1372
1500
  // 需要行动时(缺凭据/凭据被拒/Cookie 即将到期)在用量面板里内联修复入口。
1373
1501
  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 });
1374
1503
  // 行动按钮把人送到对应厂商的凭据编辑器:展开管理区、打开该行、滚动过去。
1375
1504
  const revealEditor = (target) => {
1376
1505
  openEditor(target);
@@ -1384,6 +1513,21 @@ window.__ModuleLoader__.load({
1384
1513
  void usageStore.refresh([usageId], false).catch(() => {});
1385
1514
  return deactivate;
1386
1515
  }, [usageStore, usageId]);
1516
+ // 设置页里的账户区与弹层共用同一个控制器:挂载即读一次账户配置,切到 Command Code
1517
+ // 那一屏就有数据可显示(读取只是一次 settings describe,不发任何计费请求)。
1518
+ react.useEffect(() => {
1519
+ if (!cc) return;
1520
+ void cc.load();
1521
+ }, [cc]);
1522
+ // 账户区 ready 后把当前固定账户同步为显示账户:设置页里切换账户后,上方面板与药丸
1523
+ // 一起改看所选账户的额度(与弹层的同步行为一致)。自动轮换(activeId 为空)同步为
1524
+ // 默认账户,store 内部对相同账户去重,不产生多余请求。
1525
+ react.useEffect(() => {
1526
+ if (usageId !== "commandcode" || !ccState || ccState.status !== "ready") return;
1527
+ // 旧 store stub / 更早版本的客户端装配没有这个方法:缺了就不同步,不影响其他功能。
1528
+ if (typeof usageStore.setCommandCodeAccount !== "function") return;
1529
+ void usageStore.setCommandCodeAccount(ccState.activeId).catch(() => {});
1530
+ }, [usageStore, usageId, ccState && ccState.status, ccState && ccState.activeId]);
1387
1531
  const meta = PROVIDER_META[tab];
1388
1532
  const source = entry && entry.keySource || "none";
1389
1533
  // auth-file 有三家(Grok / Codex / Command Code),凭据来源文件各不相同,不能只特判一家。
@@ -1503,6 +1647,9 @@ window.__ModuleLoader__.load({
1503
1647
  h("div", { className: "subusage-provider-tabs", role: "tablist", "aria-label": text("模型商", "Providers"), style: { display: "flex", flexWrap: "wrap", gap: 4, marginTop: 14 } },
1504
1648
  visibleIds.map((id, index) => {
1505
1649
  const e = entries.find((item) => item.providerId === id), active = usageId === id;
1650
+ // 标签圆标与面板/药丸读同一个 computeStatus:缺凭据是蓝 ⚙ 而不是红 !,
1651
+ // 无窗口成功是中性 ℹ 而不是绿 ?(没有额度窗口不能默认标绿)。
1652
+ const tabStatus = computeStatus(e, { failed: e && e.state !== "ok" ? e.error || e.state : null, failState: e && e.state, providerId: id, t, getLocale });
1506
1653
  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,
1507
1654
  "aria-controls": "subusage-provider-panel", title: `${PROVIDER_META[id].label}: ${providerDescription(e, t)}`, "aria-label": `${PROVIDER_META[id].short}: ${providerDescription(e, t)}`,
1508
1655
  onClick: () => chooseUsage(id), onKeyDown: (event) => {
@@ -1512,20 +1659,33 @@ window.__ModuleLoader__.load({
1512
1659
  else if (event.key === "Home") next = 0; else if (event.key === "End") next = visibleIds.length - 1; else return;
1513
1660
  event.preventDefault(); chooseUsage(visibleIds[next]);
1514
1661
  const button = event.currentTarget.parentElement.querySelectorAll('[role="tab"]')[next]; if (button) button.focus();
1515
- }, style: { ...btnStyle, minWidth: 0, padding: "7px 10px", whiteSpace: "nowrap", fontSize: 12, borderRadius: "6px 6px 0 0", border: "1px solid transparent", borderBottom: active ? "2px solid #3b82f6" : "2px solid transparent", background: active ? "rgba(59,130,246,.10)" : "transparent" }
1516
- }, h("span", { "aria-hidden": true, style: { display: "inline-block", width: 6, height: 6, marginRight: 4, borderRadius: 99, background: tagColor(e) } }), h("span", { "aria-hidden": true, style: { marginRight: 3 } }, providerIcon(e)), PROVIDER_META[id].short);
1662
+ }, 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
+ }, statusGlyph(tabStatus.color, tabStatus.icon, 14), PROVIDER_META[id].short);
1517
1664
  })),
1518
- !!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: 10, padding: 14 } },
1519
- h("strong", null, usageMeta.label),
1520
- h("p", { role: "status", style: { margin: "4px 0", color: usageEntry && usageEntry.state !== "ok" ? "#dc2626" : "inherit" } }, statusText(usageEntry, t)),
1521
- cacheAge(usageEntry, t) ? h("p", { style: { color: "#b7791f" } }, cacheAge(usageEntry, t)) : null,
1522
- usageEntry && usageEntry.error ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
1665
+ !!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
+ h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
1667
+ h("strong", { style: { fontSize: 14 } }, usageMeta.label),
1668
+ h("span", { style: { flex: 1 } }),
1669
+ 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
+ // 一眼结论:与药丸、弹层同一个状态判定。读取成功只说明接口健康,额度档位另有结论。
1671
+ statusBanner(usageStatus),
1672
+ usageEntry && usageEntry.error && usageStatus.key !== "error" ? h("p", { role: "alert", style: { color: "#dc2626", overflowWrap: "anywhere" } }, usageEntry.error) : null,
1523
1673
  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)" } },
1524
1674
  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.")),
1525
1675
  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,
1526
1676
  h("button", { type: "button", style: usageAction === "credentials" ? primary : btnStyle, disabled: busy, onClick: () => revealEditor(usageId) }, usageAction === "credentials" ? text("配置凭据", "Configure credentials") : text("手动填写", "Enter manually"))) : null,
1527
- 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")),
1528
- canShowUsage(usageEntry) && usageEntry.extras ? h("p", { style: { opacity: .75 } }, usageEntry.extras.map((x) => `${extraLabel(x, t)}: ${x.value}`).join(" · ")) : null,
1677
+ // 无窗口时的占位只给「成功但既无窗口也无余额」:余额/加载/需配置/失败等状态横幅已经说明,不再叠加「暂无额度数据」。
1678
+ canShowUsage(usageEntry) && (usageEntry.windows || []).length ? UsageWindowList({ windows: usageEntry.windows, t, getLocale, uncertain: usageEntry.coverage === "partial" || usageEntry.freshness === "stale" || usageEntry.state !== "ok" })
1679
+ : usageStatus.key === "balance" && !(usageStatus.entry && balanceText(usageStatus.entry, t)) ? h("p", { style: { opacity: .75 } }, t("noUsageData")) : null,
1680
+ canShowUsage(usageEntry) && usageEntry.extras && usageEntry.extras.some((x) => x.kind !== "plan")
1681
+ ? h("div", { style: { marginTop: 10, padding: "6px 12px", borderRadius: 10, background: "rgba(128,128,128,.07)" } },
1682
+ 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" } },
1683
+ h("span", { style: { opacity: 0.7 } }, extraLabel(x, t)),
1684
+ h("strong", { style: { fontWeight: 600, textAlign: "right" } }, x.value))))
1685
+ : null,
1686
+ // 账户区与药丸弹层同一个组件、同一个控制器:设置页里也能切换 activeAccount,
1687
+ // 不必再跳去 Command Code 提供方插件的设置页。提供方插件未运行时整区降级。
1688
+ usageId === "commandcode" ? h(CommandCodeAccountSwitch, { cc, ccState, t }) : null,
1529
1689
  usageEntry?.lastAttemptAt ? h("p", { style: { opacity: .65 } }, `${t("lastUpdated")} ${new Date(usageEntry.lastAttemptAt).toLocaleString(getLocale())}`) : null
1530
1690
  ) : null,
1531
1691
  providerManagement
@@ -1578,7 +1738,7 @@ window.__ModuleLoader__.load({
1578
1738
  ctx.inject(["remote.settings"], (settingsCtx) => { ccSettingsNamespace = settingsCtx.remote.settings; });
1579
1739
  ctx.effect(() => () => cc.dispose(), "dsh-subusage: commandcode account switch");
1580
1740
  try {
1581
- ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "subusage", order: 22, label: () => t("nav"), inject: () => ({ usageStore, t, getLocale }) }, SubusageSection));
1741
+ ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "subusage", order: 22, label: () => t("nav"), inject: () => ({ usageStore, t, getLocale, cc }) }, SubusageSection));
1582
1742
  } catch (error) { console.error("[dsh-subusage] 设置页注册失败:", error); }
1583
1743
  ctx.inject(["modelDirectories"], (scope) => {
1584
1744
  try {