dsh-commandcode-quota 0.2.2 → 0.3.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.
package/src/client.js CHANGED
@@ -18,8 +18,13 @@ window.__ModuleLoader__.load({
18
18
 
19
19
  const FETCH_URL = "/__dsh-commandcode-quota/dashboard";
20
20
  const CREDENTIALS_URL = "/__dsh-commandcode-quota/credentials";
21
- // 每 3 分钟后台自动刷新一次余额(面板开/关都刷新, 打开时看到的就是最新数据)
22
- const REFRESH_MS = 3 * 60 * 1000;
21
+ // 分层轮询: credits(余额)轻量, 3 分钟一次; usage/subscriptions 是重查询, 30 分钟一次。
22
+ // 页面不可见时整轮跳过, 切回前台立即补一次 —— 免得后台白打上游接口。
23
+ const QUICK_REFRESH_MS = 3 * 60 * 1000;
24
+ const FULL_REFRESH_MS = 30 * 60 * 1000;
25
+
26
+ // 接口名 → 中文, 用于把宿主端返回的 errors 展示成人能读懂的话
27
+ const ENDPOINT_NAMES = { credits: "余额", usage: "用量", subscriptions: "订阅", whoami: "账户" };
23
28
 
24
29
  // ---- 格式化 --------------------------------------------------------------
25
30
 
@@ -28,6 +33,7 @@ window.__ModuleLoader__.load({
28
33
  return "$" + value.toFixed(digits);
29
34
  }
30
35
 
36
+ // 不设 G 档: token 计量惯例是 K/M, 2.56e9 应显示为 2560.5M 而不是 2.56G。
31
37
  function fmtNumber(value) {
32
38
  if (typeof value !== "number" || !Number.isFinite(value)) return "—";
33
39
  if (value >= 1e6) return (value / 1e6).toFixed(1) + "M";
@@ -86,7 +92,8 @@ window.__ModuleLoader__.load({
86
92
  const m = Math.floor((diff % 3600000) / 60000);
87
93
  if (days > 0) return `${days}天${h}小时`;
88
94
  if (h > 0) return `${h}小时${m}分`;
89
- return `${m}分${Math.floor((diff % 60000) / 1000)}秒`;
95
+ // 不足 1 小时时精确到秒没有意义, 让人读到"47 分钟"就够了
96
+ return `${Math.max(1, m)} 分钟`;
90
97
  }
91
98
 
92
99
  // 到期日与今天不同时补上日期, 同时回答"还有多久"和"哪天到期"
@@ -106,23 +113,33 @@ window.__ModuleLoader__.load({
106
113
 
107
114
  const T = {
108
115
  bg: "var(--dsw-alias-bg-layer-1, #232324)",
109
- bg2: "var(--dsw-alias-bg-layer-2, #2c2c2e)",
110
- bg3: "var(--dsw-alias-bg-layer-3, #353636)",
116
+ // 不要用 --dsw-alias-brand-primary: 它只是个 neutral 色阶别名,
117
+ // 卡片底色: 从 layer-1 只混 6% 主文字色 —— 混多了是一块突兀的亮灰,
118
+ // 照搬 layer-2 又可能换肤后和 layer-1 同色糊成一片。6% + 1px 边框刚好。
119
+ bg2: "color-mix(in srgb, var(--dsw-alias-bg-layer-1, #232324) 96%, var(--dsw-alias-label-primary, #f9fafb) 4%)",
111
120
  border: "var(--dsw-alias-border-l2, rgba(255,255,255,.14))",
112
121
  borderL: "var(--dsw-alias-border-l1, rgba(255,255,255,.08))",
122
+ // 卡片描边自己派生: 浅色主题下主题的 border-l1 偏重(实测 #c4c5c6),
123
+ // 1px 就能把面板描得发脏; 10% 主文字色在两种主题下都刚好是一条淡线。
124
+ hairline: "color-mix(in srgb, var(--dsw-alias-label-primary, #f9fafb) 10%, transparent)",
113
125
  label: "var(--dsw-alias-label-primary, #f9fafb)",
114
126
  label2: "var(--dsw-alias-label-secondary, #d3d5d9)",
115
127
  label3: "var(--dsw-alias-label-tertiary, #adb2b8)",
116
- // 进度条轨道在 深色主题下直接用近黑半透明 (Bar 里用 TRACK_STYLE 常量)
117
- track: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.2))",
118
128
  success: "var(--dsw-alias-state-success-primary, #22c55e)",
119
129
  warn: "var(--dsw-alias-state-warn-primary, #f59e0b)",
120
130
  error: "var(--dsw-alias-state-error-primary, #f25a5a)",
121
- brand: "var(--dsw-alias-brand-primary, #679efe)",
131
+ // 进度色用 Anthropic 官方柔和蓝 #6A9BCC: 低饱和, 深色底上不刺眼,
132
+ // 又天然和告警的琥珀 / 红分开。不能用 --dsw-alias-brand-primary
133
+ // (它只是中性色阶别名, 换肤后会整条变黑)。
134
+ brand: "#6A9BCC",
122
135
  };
123
136
 
124
- // 深色主题下进度条轨道: 近黑半透明, 与填充色形成强对比
125
- const TRACK_STYLE = "rgba(0, 0, 0, 0.35)";
137
+ // 进度条轨道: 从主文字色派生 10% 透明度 —— 深色主题得浅色轨道, 浅色主题得深色轨道,
138
+ // 不依赖 interactive-bg-hover(换肤后可能和背景同色, 轨道直接消失)。
139
+ const TRACK_STYLE = "color-mix(in srgb, var(--dsw-alias-label-primary, #f9fafb) 10%, transparent)";
140
+
141
+ // 原生 select 的下拉箭头替身(SVG data URI), 盖掉浏览器默认控件外观
142
+ const SELECT_CHEVRON = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23adb2b8' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E\")";
126
143
 
127
144
  // ---- 样式常量 -------------------------------------------------------------
128
145
 
@@ -131,16 +148,16 @@ window.__ModuleLoader__.load({
131
148
  width: 352,
132
149
  maxWidth: "calc(100vw - 40px)",
133
150
  maxHeight: "calc(100vh - 120px)",
134
- overflowY: "auto",
135
151
  pointerEvents: "auto",
136
152
  zIndex: 9999,
137
153
  fontFamily: "inherit",
138
154
  color: T.label,
139
- borderRadius: 16,
155
+ borderRadius: 14,
140
156
  background: T.bg,
141
157
  border: `1px solid ${T.border}`,
142
- boxShadow: "0 16px 56px rgba(0,0,0,.55), 0 2px 10px rgba(0,0,0,.35)",
143
- overflow: "hidden",
158
+ boxShadow: "0 18px 48px rgba(0,0,0,.5), 0 0 0 1px rgba(0,0,0,.25)",
159
+ overflowY: "auto",
160
+ overflowX: "hidden",
144
161
  animation: "ccUsagePop .22s cubic-bezier(.2,.9,.3,1.2)",
145
162
  };
146
163
 
@@ -214,6 +231,11 @@ window.__ModuleLoader__.load({
214
231
  React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
215
232
  React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" }));
216
233
 
234
+ // 齿轮: 凭据设置入口(默认收起, 不占主面板)
235
+ const IconGear = () => React.createElement("svg", { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round" },
236
+ React.createElement("circle", { cx: 12, cy: 12, r: 3 }),
237
+ React.createElement("path", { d: "M19.4 15a1.7 1.7 0 0 0 .34 1.87l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 1.55V21a2 2 0 1 1-4 0v-.09A1.7 1.7 0 0 0 9 19.4a1.7 1.7 0 0 0-1.87.34l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-1.55-1H3a2 2 0 1 1 0-4h.09A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.34-1.87l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-1.55V3a2 2 0 1 1 4 0v.09a1.7 1.7 0 0 0 1 1.55 1.7 1.7 0 0 0 1.87-.34l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.7 1.7 0 0 0 19.4 9c.12.38.4.7.78.86H21a2 2 0 1 1 0 4h-.09a1.7 1.7 0 0 0-1.55 1z" }));
238
+
217
239
  const IconClock = ({ color }) => React.createElement("svg", { width: 13, height: 13, viewBox: "0 0 24 24", fill: "none", stroke: color, strokeWidth: 2, strokeLinecap: "round" },
218
240
  React.createElement("circle", { cx: "12", cy: "12", r: "9" }),
219
241
  React.createElement("polyline", { points: "12 7 12 12 15 14" }));
@@ -253,58 +275,159 @@ window.__ModuleLoader__.load({
253
275
  sub ? React.createElement("div", { style: { fontSize: 11, color: T.label3, marginTop: 2, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" } }, sub) : null);
254
276
  }
255
277
 
256
- // 进度条: 轨道用深色 (与卡片背景强对比), 填充用 color-mix 渐变色
257
- function Bar({ pct, color }) {
278
+ // 三档状态灯: 当前档亮起, 其余压暗。
279
+ // 数据来自官方字段 credits.belowThreshold / usage.windowLimits.*.exceeded;
280
+ // 这三个人话名字是给那两条数据起的, 不是官方术语 —— 悬停每个灯都有解释。
281
+ const STATUS_LEVELS = [
282
+ { key: "ok", label: "正常", hint: "余额充足,三个额度窗口都没用满", color: T.success },
283
+ { key: "low", label: "余额低", hint: "余额已低于你设定的告警阈值(threshold)", color: T.warn },
284
+ { key: "out", label: "已用尽", hint: "有额度窗口已经用满,等它按 5 小时 / 每周 / 每月重置", color: T.error },
285
+ ];
286
+
287
+ // 可选配色: 只改"低用量起点色"; 用量一高, 仍然统一走向暖色和红。
288
+ const PALETTES = [
289
+ { key: "mist", label: "雾蓝", hex: "#6A9BCC" },
290
+ { key: "indigo", label: "靛蓝", hex: "#5E6AD2" },
291
+ { key: "steel", label: "灰蓝", hex: "#7C9CBF" },
292
+ { key: "teal", label: "青绿", hex: "#2E9E8A" },
293
+ { key: "lilac", label: "丁香", hex: "#8B7BB8" },
294
+ { key: "clay", label: "陶土", hex: "#D97757" },
295
+ ];
296
+ const PALETTE_STORAGE_KEY = "dsh-commandcode-quota:palette";
297
+
298
+ function hexToRgb(hex) {
299
+ const m = /^#?([0-9a-f]{6})$/i.exec(String(hex));
300
+ if (!m) return [106, 155, 204];
301
+ const n = parseInt(m[1], 16);
302
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
303
+ }
304
+
305
+ /** 用量 → 颜色。起点由所选配色决定; 55% 之前保持冷静, 之后升温, 100% 收在红。 */
306
+ function usageColor(pct, brandHex) {
307
+ const base = hexToRgb(brandHex);
308
+ const stops = [
309
+ [0, base],
310
+ [55, base],
311
+ [70, [217, 164, 65]],
312
+ [85, [217, 119, 87]],
313
+ [100, [201, 79, 79]],
314
+ ];
315
+ const p = Math.max(0, Math.min(100, pct));
316
+ for (let i = 1; i < stops.length; i++) {
317
+ const [p1, c1] = stops[i];
318
+ if (p <= p1) {
319
+ const [p0, c0] = stops[i - 1];
320
+ const t = p1 === p0 ? 0 : (p - p0) / (p1 - p0);
321
+ const rgb = c0.map((v, k) => Math.round(v + (c1[k] - v) * t));
322
+ return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
323
+ }
324
+ }
325
+ return "rgb(201, 79, 79)";
326
+ }
327
+
328
+ // 进度条: 颜色随用量走, 条形内部还有一段 40 个百分点的渐变 —
329
+ // 高用量时能直接看出"冷 → 暖"的推进。消耗 > 0 时至少露出 3px。
330
+ function Bar({ pct, height = 8, forceColor, brand }) {
258
331
  const clamped = Math.max(0, Math.min(100, pct || 0));
259
- const fillColor = color || T.success;
332
+ const end = forceColor || usageColor(clamped, brand);
333
+ const start = forceColor || usageColor(Math.max(0, clamped - 40), brand);
260
334
  return React.createElement("div", {
261
335
  style: {
262
- height: 10,
336
+ height,
263
337
  borderRadius: 99,
264
338
  background: TRACK_STYLE,
265
- border: "1px solid rgba(255,255,255,.06)",
266
339
  overflow: "hidden",
267
- padding: 1,
340
+ boxShadow: "inset 0 1px 2px rgba(0,0,0,.12)",
268
341
  },
269
342
  },
270
343
  React.createElement("div", {
271
344
  style: {
272
- width: clamped + "%",
345
+ width: clamped > 0 ? `max(${clamped}%, 3px)` : "0%",
273
346
  height: "100%",
274
347
  borderRadius: 99,
275
- background: `linear-gradient(90deg, ${fillColor}, color-mix(in srgb, ${fillColor} 78%, #ffffff 22%))`,
276
- boxShadow: `0 0 10px color-mix(in srgb, ${fillColor} 45%, transparent)`,
277
- transition: "width .5s cubic-bezier(.3,.8,.3,1)",
348
+ background: `linear-gradient(90deg, ${start}, ${end})`,
349
+ transition: "width .45s cubic-bezier(.3,.8,.3,1)",
278
350
  },
279
351
  }));
280
352
  }
281
353
 
282
- // 限额卡片: 标题 + 百分比 + 进度条 + 已用/上限/剩余 + 重置时间
283
- function LimitCard({ title, used, cap, resetAt }) {
354
+ // 状态徽章: 颜色由服务端告警位决定, 不再永远绿色 active。
355
+ const BADGE_TONES = { success: T.success, warn: T.warn, error: T.error };
356
+ function StatusBadge({ tone, label }) {
357
+ const color = BADGE_TONES[tone] ?? T.success;
358
+ return React.createElement("div", {
359
+ style: {
360
+ display: "flex",
361
+ alignItems: "center",
362
+ gap: 6,
363
+ background: `color-mix(in srgb, ${color} 15%, transparent)`,
364
+ border: `1px solid color-mix(in srgb, ${color} 40%, transparent)`,
365
+ color,
366
+ borderRadius: 99,
367
+ padding: "4px 11px",
368
+ fontSize: 11.5,
369
+ fontWeight: 600,
370
+ whiteSpace: "nowrap",
371
+ },
372
+ },
373
+ React.createElement("span", { style: { width: 6, height: 6, borderRadius: "50%", background: color, display: "inline-block" } }),
374
+ label);
375
+ }
376
+
377
+ // 统一错误卡片: 顶部错误态与「余额字段缺失」态共用,
378
+ // 后者以前直接 return null, 于是查询失败被渲染成一块空白面板。
379
+ function ErrorBox({ text }) {
380
+ return React.createElement("div", {
381
+ style: {
382
+ margin: "16px",
383
+ padding: "12px 14px",
384
+ whiteSpace: "pre-line",
385
+ background: "color-mix(in srgb, var(--dsw-alias-state-error-primary, #f25a5a) 14%, transparent)",
386
+ border: "1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary, #f25a5a) 38%, transparent)",
387
+ borderRadius: 12,
388
+ fontSize: 12.5,
389
+ color: T.error,
390
+ lineHeight: 1.6,
391
+ },
392
+ }, String(text));
393
+ }
394
+
395
+ // 一个额度窗口 = 一张卡: 名称 + 百分比 / 进度条 / 一行明细。
396
+ // 明细压成一行(已用/上限 · 剩余 · 重置), 信息不丢但高度减半。
397
+ function QuotaRow({ title, used, cap, resetAt, exceeded, brand, note }) {
284
398
  const capSafe = cap > 0 ? cap : (used > 0 ? used : 1);
285
399
  const pct = capSafe > 0 ? Math.min(100, (used / capSafe) * 100) : 0;
286
- const color = pct >= 90 ? T.error : pct >= 70 ? T.warn : T.success;
400
+ const exhausted = exceeded === true;
401
+ // 颜色交给进度条表达(随用量连续变化), 数字保持主文字色, 只在用尽时转红 ——
402
+ // 浅色主题下黄/橙数字对比度不够, 读起来会糊。
403
+ const pctColor = exhausted ? T.error : T.label;
287
404
  const remaining = Math.max(0, capSafe - used);
288
405
  return React.createElement("div", {
289
406
  style: {
290
407
  background: T.bg2,
291
- border: `1px solid ${T.borderL}`,
292
408
  borderRadius: 12,
293
- padding: "12px 14px",
409
+ padding: "10px 13px 11px",
410
+ // 几乎同色的底 + 一条极细边框: 靠线和间距分组, 而不是靠一块亮灰
411
+ border: `1px solid ${T.hairline}`,
294
412
  },
295
413
  },
296
- React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 9 } },
297
- React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 7, fontSize: 12.5, fontWeight: 600, color: T.label } },
298
- React.createElement(IconClock, { color }),
299
- title),
300
- React.createElement("span", { style: { fontSize: 16, fontWeight: 800, fontVariantNumeric: "tabular-nums", color } },
414
+ React.createElement("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", gap: 8, fontSize: 12, color: T.label2 } },
415
+ React.createElement("span", { style: { fontWeight: 600 } },
416
+ title,
417
+ exhausted ? React.createElement("span", { style: { marginLeft: 6, fontSize: 10.5, fontWeight: 700, color: T.error } }, "已用尽") : null),
418
+ React.createElement("span", { style: { fontSize: 15, fontWeight: 700, fontVariantNumeric: "tabular-nums", color: pctColor, letterSpacing: "-.3px" } },
301
419
  (Math.round(pct * 10) / 10) + "%")),
302
- React.createElement(Bar, { pct, color }),
303
- React.createElement("div", { style: { display: "flex", justifyContent: "space-between", marginTop: 9, fontSize: 11.5, color: T.label2, fontVariantNumeric: "tabular-nums" } },
304
- React.createElement("span", null, `已用 ${fmtCurrency(used)}`),
305
- React.createElement("span", null, `上限 ${fmtCurrency(capSafe)}`)),
306
- React.createElement("div", { style: { marginTop: 3, fontSize: 11, color: T.label3 } },
307
- `剩余 ${fmtCurrency(remaining)} · 重置 ${fmtDuration(resetAt)}${fmtResetDay(resetAt)}`));
420
+ React.createElement("div", { style: { margin: "8px 0 7px" } },
421
+ React.createElement(Bar, { pct, brand, forceColor: exhausted ? T.error : null })),
422
+ // 固定两行: 一行放"已用/上限", 一行放"剩余/重置"。
423
+ // 挤成一行时日期会被断到下一行, 比多一行更难看。
424
+ React.createElement("div", { style: { fontSize: 11, color: T.label3, lineHeight: 1.55, fontVariantNumeric: "tabular-nums" } },
425
+ `已用 ${fmtCurrency(used)} / 上限 ${fmtCurrency(capSafe)}`),
426
+ React.createElement("div", { style: { fontSize: 11, color: T.label3, lineHeight: 1.55, fontVariantNumeric: "tabular-nums" } },
427
+ `剩余 ${fmtCurrency(remaining)} · `
428
+ + (exhausted ? `${fmtDuration(resetAt)}后重置` : `重置 ${fmtDuration(resetAt)}${fmtResetDay(resetAt)}`)),
429
+ // 超额预警: 只有"按当前日均撑不到重置"时才多出这一行
430
+ note ? React.createElement("div", { style: { marginTop: 4, fontSize: 11, color: T.warn, lineHeight: 1.5 } }, `⚠ ${note}`) : null);
308
431
  }
309
432
 
310
433
  // ---- 凭据条目选择 ------------------------------------------------------------
@@ -333,7 +456,7 @@ window.__ModuleLoader__.load({
333
456
  display: "flex",
334
457
  alignItems: "center",
335
458
  gap: 8,
336
- padding: "9px 16px",
459
+ padding: "8px 16px 9px",
337
460
  borderBottom: `1px solid ${T.borderL}`,
338
461
  fontSize: 11.5,
339
462
  color: T.label3,
@@ -348,13 +471,18 @@ window.__ModuleLoader__.load({
348
471
  style: {
349
472
  flex: 1,
350
473
  minWidth: 0,
351
- padding: "4px 6px",
474
+ padding: "4px 24px 4px 8px",
352
475
  fontSize: 11.5,
353
476
  fontFamily: "inherit",
354
477
  color: T.label2,
355
478
  background: T.bg2,
356
- border: `1px solid ${T.border}`,
357
- borderRadius: 8,
479
+ backgroundImage: SELECT_CHEVRON,
480
+ backgroundRepeat: "no-repeat",
481
+ backgroundPosition: "right 7px center",
482
+ appearance: "none",
483
+ WebkitAppearance: "none",
484
+ border: `1px solid ${T.borderL}`,
485
+ borderRadius: 7,
358
486
  cursor: safe.saving ? "default" : "pointer",
359
487
  },
360
488
  },
@@ -366,33 +494,70 @@ window.__ModuleLoader__.load({
366
494
 
367
495
  // ---- 面板内容 ----------------------------------------------------------------
368
496
 
369
- function DashboardBody({ data, loading, error }) {
370
- if (error) {
371
- return React.createElement("div", { style: { padding: "16px 16px 20px" } },
372
- React.createElement("div", {
497
+ function DashboardBody({ data, loading, error, palette }) {
498
+ if (error) return React.createElement(ErrorBox, { text: error });
499
+ if (loading && !data) {
500
+ // 骨架屏: 按真实布局占位(余额块 + 三张卡), 数据回来时不会整块"跳一下"
501
+ const bone = (key, w, h, mb) => React.createElement("div", {
502
+ key,
503
+ // 占位条要比卡片底色更实一点, 否则和卡片同色会"消失"
504
+ style: { width: w, height: h, borderRadius: 6, background: T.hairline, marginBottom: mb },
505
+ });
506
+ return React.createElement("div", {
507
+ key: "skeleton",
508
+ style: { padding: "14px 16px 12px" },
509
+ "aria-busy": true,
510
+ "aria-label": "正在获取额度",
511
+ },
512
+ bone("b1", "34%", 9, 10),
513
+ bone("b2", "48%", 26, 12),
514
+ bone("b3", "72%", 9, 6),
515
+ bone("b4", "58%", 9, 6),
516
+ bone("b5", "64%", 9, 14),
517
+ [0, 1, 2].map((i) => React.createElement("div", {
518
+ key: "sk" + i,
373
519
  style: {
374
- background: "color-mix(in srgb, var(--dsw-alias-state-error-primary, #f25a5a) 14%, transparent)",
375
- border: "1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary, #f25a5a) 38%, transparent)",
520
+ background: T.bg2,
376
521
  borderRadius: 12,
377
- padding: "12px 14px",
378
- fontSize: 12.5,
379
- color: T.error,
380
- lineHeight: 1.5,
522
+ border: `1px solid ${T.hairline}`,
523
+ padding: "12px 13px",
524
+ marginBottom: i === 2 ? 0 : 9,
381
525
  },
382
- }, String(error)));
526
+ },
527
+ bone("s" + i + "a", "40%", 9, 10),
528
+ bone("s" + i + "b", "100%", 8, 9),
529
+ bone("s" + i + "c", "76%", 9, 5),
530
+ bone("s" + i + "d", "52%", 9, 0))),
531
+ React.createElement("div", { style: { marginTop: 12, textAlign: "center", fontSize: 11, color: T.label3 } }, "正在获取额度…"));
383
532
  }
384
- if (loading && !data) {
385
- return React.createElement("div", { style: { padding: "24px 16px", textAlign: "center", fontSize: 12.5, color: T.label3 } },
386
- React.createElement("div", { style: { width: 22, height: 22, margin: "0 auto 10px", border: "2px solid rgba(255,255,255,.18)", borderTopColor: T.brand, borderRadius: "50%", animation: "ccUsageSpin .8s linear infinite" } }),
387
- "正在获取余额…(Command Code 接口较慢,约需 10~20 秒)");
533
+ if (!data) return null;
534
+ // 余额字段缺失(最常见的是 credits 单接口 401/500)时必须说清原因,
535
+ // 不能再 return null —— 那会把「查询失败」伪装成一块空白面板。
536
+ if (!data.balance) {
537
+ const failed = Array.isArray(data.errors) ? data.errors : [];
538
+ const detail = failed.length > 0
539
+ ? failed.map((e) => `${ENDPOINT_NAMES[e.endpoint] ?? e.endpoint}: ${e.message}`).join("\n")
540
+ : "服务端没有返回余额数据(点右上角 ⟳ 重试)";
541
+ return React.createElement(ErrorBox, { text: `额度查询失败\n${detail}` });
388
542
  }
389
- if (!data || !data.balance) return null;
390
543
 
391
544
  const { balance, windowLimits, usage, subscription, user } = data;
392
545
 
393
546
  // 订阅开通时间: 优先 createdAt, 回退到当前周期开始
394
547
  const subSince = subscription && (subscription.createdAt || subscription.currentPeriodStart) || null;
395
548
 
549
+ // ---- 服务端告警位 --------------------------------------------------------
550
+ // API 已经把「余额低于阈值 / 窗口已用尽 / 订阅已取消续费」判好了, 之前一个都没用,
551
+ // 于是额度真的用尽时面板还是那个绿色 active 徽章 —— 在最该报警的时刻显得一切正常。
552
+ const belowThreshold = balance.belowThreshold === true;
553
+ const exhaustedNames = [];
554
+ if (windowLimits && windowLimits.fiveHour && windowLimits.fiveHour.exceeded === true) exhaustedNames.push("5 小时");
555
+ if (windowLimits && windowLimits.weekly && windowLimits.weekly.exceeded === true) exhaustedNames.push("每周");
556
+ if (windowLimits && windowLimits.exceeded === true && exhaustedNames.length === 0) exhaustedNames.push("额度");
557
+ // 状态档位: 任一窗口用尽 > 余额低于阈值 > 正常
558
+ const statusLevel = exhaustedNames.length > 0 ? "out" : belowThreshold ? "low" : "ok";
559
+ const brandHex = (palette && palette.hex) || PALETTES[0].hex;
560
+
396
561
  // 本次请求里订阅接口是否失败(接口本身慢/超时) —— 用于区分"没这条数据"和"没取到"
397
562
  const subFailed = Array.isArray(data.errors)
398
563
  && data.errors.some((e) => e && e.endpoint === "subscriptions");
@@ -406,31 +571,28 @@ window.__ModuleLoader__.load({
406
571
  : null;
407
572
  const monthReset = subscription && subscription.currentPeriodEnd ? subscription.currentPeriodEnd : null;
408
573
 
409
- const rows = [];
574
+ // 日均消耗: 本周期已用 ÷ 本周期已过天数(periodStart 由 usage 接口给出)。
575
+ // 取代原来的「成功率」格子 —— 那是整期累计的运维指标, 对「今天还该不该放开用」没有决策价值。
576
+ const periodStart = usage && usage.periodStart ? toMs(usage.periodStart) : null;
577
+ const periodDays = periodStart !== null ? Math.max(0, (Date.now() - periodStart) / 86400000) : null;
578
+ const dailyAvg = monthUsed !== null && periodDays !== null && periodDays > 0 ? monthUsed / periodDays : null;
410
579
 
411
- // 数据一致性告警 (典型: 两个账号解析到同一个 API key, 余额自然相同)
412
- const warnings = Array.isArray(data.warnings) ? data.warnings : [];
413
- if (warnings.length > 0) {
414
- rows.push(React.createElement("div", {
415
- key: "warnings",
416
- style: {
417
- margin: "12px 16px 0",
418
- padding: "10px 12px",
419
- borderRadius: 10,
420
- background: "color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 15%, transparent)",
421
- border: "1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #f59e0b) 45%, transparent)",
422
- color: T.warn,
423
- fontSize: 11.5,
424
- lineHeight: 1.55,
425
- },
426
- },
427
- React.createElement("div", { style: { fontWeight: 700, marginBottom: 4 } }, "⚠ 凭据告警"),
428
- warnings.map((w, i) => React.createElement("div", { key: i, style: { marginTop: i > 0 ? 4 : 0 } }, w))));
580
+ // 超额预警: 用本周期日均推算"剩余额度够不够撑到重置"。
581
+ // 进度条只说现在用了多少; 这条只在"按这个速度会在重置前用光"时出现。
582
+ let etaNote = null;
583
+ if (monthCap !== null && monthUsed !== null && dailyAvg !== null && dailyAvg > 0 && monthReset) {
584
+ const resetMs = toMs(monthReset);
585
+ const daysLeft = resetMs === null ? null : (resetMs - Date.now()) / 86400000;
586
+ const daysToEmpty = (monthCap - monthUsed) / dailyAvg;
587
+ if (daysLeft !== null && daysLeft > 0.5 && daysToEmpty > 0 && daysToEmpty < daysLeft) {
588
+ etaNote = `按当前日均,约 ${Math.max(1, Math.round(daysToEmpty))} 天后用完`;
589
+ }
429
590
  }
430
591
 
592
+ const rows = [];
593
+
431
594
  // 缓存兜底提示: 接口超时, 当前显示的是上次成功获取的数据
432
595
  if (staleList.length > 0) {
433
- const FIELD_NAMES = { credits: "余额", usage: "用量", subscriptions: "订阅", whoami: "账户" };
434
596
  rows.push(React.createElement("div", {
435
597
  key: "stale",
436
598
  style: {
@@ -439,91 +601,115 @@ window.__ModuleLoader__.load({
439
601
  color: T.label3,
440
602
  lineHeight: 1.55,
441
603
  },
442
- }, `⏱ ${staleList.map((s) => FIELD_NAMES[s.endpoint] || s.endpoint).join("、")} 本次请求超时,显示的是上次成功获取的数据(点右上角 ⟳ 重试)`));
604
+ }, `⏱ ${staleList.map((s) => ENDPOINT_NAMES[s.endpoint] || s.endpoint).join("、")} 本次请求超时,显示的是上次成功获取的数据(点右上角 ⟳ 重试)`));
443
605
  }
444
606
 
445
- // Hero: 余额大数字
607
+ // 余额 + 账户信息: 状态灯与「本月剩余」同排, 其余信息独占整行 ——
608
+ // 灯放右边单独占一行会把"订阅开通"挤到换行。
446
609
  rows.push(React.createElement("div", {
447
- key: "hero",
448
- style: {
449
- padding: "18px 16px 16px",
450
- background: `linear-gradient(150deg, ${T.bg3}, ${T.bg2})`,
451
- borderBottom: `1px solid ${T.borderL}`,
452
- },
610
+ key: "balance",
611
+ style: { padding: "14px 16px 13px" },
453
612
  },
454
- React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "flex-start" } },
455
- React.createElement("div", null,
456
- React.createElement("div", { style: { fontSize: 11, fontWeight: 600, color: T.label3, letterSpacing: ".7px", textTransform: "uppercase", marginBottom: 4 } }, "可用余额 Balance"),
457
- React.createElement("div", {
458
- style: {
459
- fontSize: 34,
460
- fontWeight: 800,
461
- lineHeight: 1.05,
462
- fontVariantNumeric: "tabular-nums",
463
- letterSpacing: "-1px",
464
- color: T.label,
465
- },
466
- }, fmtCurrency(balance.monthlyCredits))),
467
- React.createElement("div", {
468
- style: {
469
- display: "flex",
470
- alignItems: "center",
471
- gap: 6,
472
- background: "color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 15%, transparent)",
473
- border: "1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary, #22c55e) 40%, transparent)",
474
- color: T.success,
475
- borderRadius: 99,
476
- padding: "4px 11px",
477
- fontSize: 11.5,
478
- fontWeight: 600,
479
- },
613
+ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8, marginBottom: 3 } },
614
+ React.createElement("span", { style: { fontSize: 11, color: T.label3 } }, "本月剩余"),
615
+ // 三档状态灯: 当前档亮起, 其余压暗 —— 比单独一个 "active" 更能说明处在哪一档。
616
+ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 4 } },
617
+ STATUS_LEVELS.map((level) => {
618
+ const active = level.key === statusLevel;
619
+ return React.createElement("span", {
620
+ key: level.key,
621
+ title: level.hint,
622
+ style: {
623
+ display: "inline-flex",
624
+ alignItems: "center",
625
+ gap: 4,
626
+ fontSize: 10.5,
627
+ fontWeight: active ? 700 : 500,
628
+ color: active ? level.color : T.label3,
629
+ background: active ? `color-mix(in srgb, ${level.color} 14%, transparent)` : "transparent",
630
+ border: `1px solid ${active ? `color-mix(in srgb, ${level.color} 38%, transparent)` : "transparent"}`,
631
+ borderRadius: 99,
632
+ padding: "2px 6px",
633
+ opacity: active ? 1 : 0.5,
634
+ whiteSpace: "nowrap",
635
+ },
636
+ }, React.createElement("span", {
637
+ style: {
638
+ width: 5,
639
+ height: 5,
640
+ borderRadius: "50%",
641
+ background: active ? level.color : T.label3,
642
+ display: "inline-block",
643
+ },
644
+ }), level.label);
645
+ }))),
646
+ React.createElement("div", {
647
+ style: {
648
+ fontSize: 28,
649
+ fontWeight: 700,
650
+ lineHeight: 1.1,
651
+ letterSpacing: "-.8px",
652
+ fontVariantNumeric: "tabular-nums",
653
+ color: T.label,
480
654
  },
481
- React.createElement("span", { style: { width: 6, height: 6, borderRadius: "50%", background: T.success, display: "inline-block" } }),
482
- (subscription && subscription.status) ? subscription.status : "active")),
483
- React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 6, marginTop: 10, fontSize: 11.5, color: T.label2 } },
484
- React.createElement(IconCalendar, { color: T.brand }),
485
- React.createElement("span", null, subSince
486
- ? `开通于 ${fmtDay(subSince)} · 已使用 ${fmtElapsed(subSince)}`
487
- : (subFailed ? "订阅接口本次未响应(点右上角 ⟳ 重试)" : "订阅信息暂不可用"))),
488
- React.createElement("div", { style: { display: "flex", gap: 8, marginTop: 12 } },
489
- React.createElement(StatCard, { label: "总消耗", value: fmtCurrency(usage && usage.totalCost) }),
490
- React.createElement(StatCard, { label: "Tokens", value: fmtNumber(usage && usage.totalTokens) }),
491
- React.createElement(StatCard, { label: "成功率", value: usage ? usage.successRate + "%" : "—" }))));
492
-
493
- // 限额卡片: 5小时 / 每周 / 每月
494
- const limitCards = [];
655
+ }, fmtCurrency(balance.monthlyCredits)),
656
+ React.createElement("div", { style: { marginTop: 6, fontSize: 11, color: T.label3, lineHeight: 1.6, fontVariantNumeric: "tabular-nums" } },
657
+ `总消耗 ${fmtCurrency(usage && usage.totalCost)} Tokens ${fmtNumber(usage && usage.totalTokens)}`),
658
+ React.createElement("div", { style: { fontSize: 11, color: T.label3, lineHeight: 1.6, fontVariantNumeric: "tabular-nums" } },
659
+ `日均 ${fmtCurrency(dailyAvg)}` + (periodDays !== null && periodDays >= 1 ? ` 本周期 ${Math.floor(periodDays)} 天` : "")),
660
+ React.createElement("div", { style: { marginTop: 4, fontSize: 11, color: T.label3, lineHeight: 1.6 } },
661
+ subSince
662
+ ? `订阅开通 ${fmtDay(subSince)},已 ${fmtElapsed(subSince)}`
663
+ : (subFailed ? "订阅接口本次未响应(点右上角 ⟳ 重试)" : "订阅信息暂不可用"))));
664
+
665
+ // 订阅取消续费提示
666
+ if (subscription && subscription.cancelAtPeriodEnd === true) {
667
+ rows.push(React.createElement("div", {
668
+ key: "cancel",
669
+ style: { padding: "0 16px 10px", fontSize: 11, color: T.warn, lineHeight: 1.55 },
670
+ }, `⚠ 订阅将于 ${fmtDay(subscription.currentPeriodEnd)} 到期,不再续费`));
671
+ }
672
+
673
+ // 三个窗口: 三张卡, 统一放在一个网格容器里
674
+ const windowCards = [];
495
675
  if (windowLimits && windowLimits.fiveHour) {
496
- limitCards.push(React.createElement(LimitCard, {
676
+ windowCards.push(React.createElement(QuotaRow, {
497
677
  key: "5h",
498
678
  title: "5 小时窗口",
499
679
  used: windowLimits.fiveHour.used,
500
680
  cap: windowLimits.fiveHour.cap,
501
681
  resetAt: windowLimits.fiveHour.resetAt,
682
+ exceeded: windowLimits.fiveHour.exceeded,
683
+ brand: brandHex,
502
684
  }));
503
685
  }
504
686
  if (windowLimits && windowLimits.weekly) {
505
- limitCards.push(React.createElement(LimitCard, {
687
+ windowCards.push(React.createElement(QuotaRow, {
506
688
  key: "week",
507
689
  title: "每周窗口",
508
690
  used: windowLimits.weekly.used,
509
691
  cap: windowLimits.weekly.cap,
510
692
  resetAt: windowLimits.weekly.resetAt,
693
+ exceeded: windowLimits.weekly.exceeded,
694
+ brand: brandHex,
511
695
  }));
512
696
  }
513
697
  if (monthCap !== null) {
514
- limitCards.push(React.createElement(LimitCard, {
698
+ windowCards.push(React.createElement(QuotaRow, {
515
699
  key: "month",
516
700
  title: "每月窗口",
517
701
  used: monthUsed,
518
702
  cap: monthCap,
519
703
  resetAt: monthReset,
704
+ brand: brandHex,
705
+ note: etaNote,
520
706
  }));
521
707
  }
522
- if (limitCards.length > 0) {
523
- rows.push(React.createElement("div", { key: "limits", style: { padding: "14px 16px 4px" } },
524
- React.createElement("div", { style: { fontSize: 11, fontWeight: 700, color: T.label3, letterSpacing: ".7px", textTransform: "uppercase", marginBottom: 10 } },
525
- "窗口限额 Usage Limits"),
526
- React.createElement("div", { style: { display: "grid", gap: 10 } }, limitCards)));
708
+ if (windowCards.length > 0) {
709
+ rows.push(React.createElement("div", {
710
+ key: "windows",
711
+ style: { padding: "0 12px 12px", display: "grid", gap: 9 },
712
+ }, windowCards));
527
713
  }
528
714
 
529
715
  // 底部: 账户 + 更新时间
@@ -546,6 +732,51 @@ window.__ModuleLoader__.load({
546
732
  return React.createElement("div", null, rows);
547
733
  }
548
734
 
735
+ // 配色选择: 六个颜色分两行排列, 只影响低用量起点色, 高用量仍统一走向暖色。
736
+ function PalettePicker({ value, onChange }) {
737
+ return React.createElement("div", {
738
+ style: {
739
+ display: "flex",
740
+ alignItems: "center",
741
+ gap: 8,
742
+ padding: "8px 16px 9px",
743
+ borderBottom: `1px solid ${T.borderL}`,
744
+ fontSize: 11.5,
745
+ color: T.label3,
746
+ },
747
+ },
748
+ React.createElement("span", { style: { whiteSpace: "nowrap" } }, "进度色"),
749
+ React.createElement("div", { style: { display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: 6, flex: 1 } },
750
+ PALETTES.map((p) => {
751
+ const on = Boolean(value) && value.key === p.key;
752
+ return React.createElement("button", {
753
+ key: p.key,
754
+ type: "button",
755
+ title: p.label,
756
+ "aria-pressed": on,
757
+ onClick: () => onChange(p),
758
+ style: {
759
+ display: "inline-flex",
760
+ alignItems: "center",
761
+ justifyContent: "center",
762
+ gap: 5,
763
+ padding: "3px 6px",
764
+ borderRadius: 8,
765
+ cursor: "pointer",
766
+ fontSize: 11,
767
+ fontFamily: "inherit",
768
+ color: on ? T.label : T.label3,
769
+ background: on ? T.bg2 : "transparent",
770
+ border: `1px solid ${on ? T.hairline : "transparent"}`,
771
+ },
772
+ },
773
+ React.createElement("span", {
774
+ style: { width: 8, height: 8, borderRadius: "50%", background: p.hex, display: "inline-block", flexShrink: 0 },
775
+ }),
776
+ p.label);
777
+ })));
778
+ }
779
+
549
780
  // ---- 主组件 -------------------------------------------------------------------
550
781
 
551
782
  function UsageBadge() {
@@ -556,33 +787,53 @@ window.__ModuleLoader__.load({
556
787
  const timerRef = useRef(null);
557
788
  // 凭据条目:与 data / error 完全解耦 —— 这样面板报「没配 key」时依然能切换
558
789
  const [creds, setCreds] = useState({ entries: [], apiKeyEnv: null, saving: false, error: null });
790
+ // 凭据设置默认收起(见头部齿轮按钮)
791
+ const [gearOpen, setGearOpen] = useState(false);
792
+ // 进度色: 默认 Claude 蓝, 记住上次选择
793
+ const [palette, setPalette] = useState(() => {
794
+ try {
795
+ if (typeof localStorage === "undefined") return PALETTES[0];
796
+ const saved = localStorage.getItem(PALETTE_STORAGE_KEY);
797
+ return PALETTES.find((p) => p.key === saved) || PALETTES[0];
798
+ } catch {
799
+ return PALETTES[0];
800
+ }
801
+ });
802
+ // 外层容器引用:用来判断点击是否落在「状态栏 + 面板」之外
803
+ const rootRef = useRef(null);
559
804
 
560
- const refresh = useCallback(async () => {
561
- const fullUrl = FETCH_URL;
562
- // Command Code 的用量/订阅接口要 10~25 秒, 余额接口 1~5 秒。
563
- // 先只取余额(带上次缓存的用量/订阅)让面板立刻出内容, 再拉完整数据覆盖。
564
- const quickUrl = fullUrl + "?scope=quick";
805
+ // options.full=false → 只刷余额(轻量); options.silent=true → 后台轮询, 不动 loading 态
806
+ const refresh = useCallback(async (options = {}) => {
807
+ const wantFull = options.full !== false;
808
+ const silent = options.silent === true;
809
+ // Command Code 的用量/订阅接口比余额接口慢得多, 所以两段式:
810
+ // 先只取余额让面板立刻出内容, 再拉完整数据覆盖。
811
+ const quickUrl = FETCH_URL + "?scope=quick";
565
812
 
566
813
  const pull = (url) => fetch(url, { cache: "no-store" }).then((res) => {
567
814
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
568
815
  return res.json();
569
816
  });
570
817
 
571
- setLoading(true);
818
+ if (!silent) setLoading(true);
572
819
  try {
573
820
  const quick = await pull(quickUrl);
574
821
  if (quick) {
575
822
  setData(quick);
823
+ // 余额拿到了就清掉上次的陈旧错误; 拿不到则让面板显示具体失败原因
824
+ if (quick.balance) setError(null);
576
825
  }
577
- const full = await pull(fullUrl);
578
- if (full) {
579
- setData(full);
580
- setError(full.ok === false && full.error ? full.error : null);
826
+ if (wantFull) {
827
+ const full = await pull(FETCH_URL);
828
+ if (full) {
829
+ setData(full);
830
+ setError(full.ok === false && full.error ? full.error : null);
831
+ }
581
832
  }
582
833
  } catch (reason) {
583
834
  setError(String(reason));
584
835
  } finally {
585
- setLoading(false);
836
+ if (!silent) setLoading(false);
586
837
  }
587
838
  }, []);
588
839
 
@@ -602,6 +853,15 @@ window.__ModuleLoader__.load({
602
853
  }
603
854
  }, []);
604
855
 
856
+ const choosePalette = useCallback((next) => {
857
+ setPalette(next);
858
+ try {
859
+ if (typeof localStorage !== "undefined") localStorage.setItem(PALETTE_STORAGE_KEY, next.key);
860
+ } catch {
861
+ /* 隐私模式 / 存储不可用, 忽略 */
862
+ }
863
+ }, []);
864
+
605
865
  const pickCredential = useCallback(async (name) => {
606
866
  setCreds((prev) => ({ ...prev, saving: true, error: null }));
607
867
  try {
@@ -622,11 +882,47 @@ window.__ModuleLoader__.load({
622
882
  useEffect(() => {
623
883
  loadCredentials();
624
884
  refresh();
625
- // 每 3 分钟自动查询一次余额(面板打开时也刷新)
626
- timerRef.current = setInterval(() => refresh(), REFRESH_MS);
627
- return () => { if (timerRef.current) clearInterval(timerRef.current); };
885
+ // 分层轮询: 余额 3 分钟 / 用量+订阅 30 分钟。页面不可见时整轮跳过,
886
+ // 切回前台立刻补一次完整查询 —— 后台不白打上游接口。
887
+ const doc = typeof document !== "undefined" ? document : null;
888
+ const visible = () => !doc || doc.hidden !== true;
889
+ const quickTimer = setInterval(() => { if (visible()) refresh({ full: false, silent: true }); }, QUICK_REFRESH_MS);
890
+ const fullTimer = setInterval(() => { if (visible()) refresh({ silent: true }); }, FULL_REFRESH_MS);
891
+ const onVisibility = () => { if (visible()) refresh({ silent: true }); };
892
+ if (doc && typeof doc.addEventListener === "function") doc.addEventListener("visibilitychange", onVisibility);
893
+ timerRef.current = { quickTimer, fullTimer };
894
+ return () => {
895
+ clearInterval(quickTimer);
896
+ clearInterval(fullTimer);
897
+ if (doc && typeof doc.removeEventListener === "function") doc.removeEventListener("visibilitychange", onVisibility);
898
+ };
628
899
  }, [loadCredentials, refresh]);
629
900
 
901
+ // 点击面板外部或按 Esc 关闭 —— 与 DSH 其它浮层的交互保持一致。
902
+ // 之前只有再点一次徽章才关得掉, 点别处没反应。
903
+ useEffect(() => {
904
+ if (!readoutOpen) return undefined;
905
+ const doc = typeof document !== "undefined" ? document : null;
906
+ if (!doc) return undefined;
907
+ const onPointerDown = (event) => {
908
+ const root = rootRef.current;
909
+ if (!root) return;
910
+ const target = event.target;
911
+ const inside = typeof Node !== "undefined" && target instanceof Node && root.contains(target);
912
+ if (!inside) setReadoutOpen(false);
913
+ };
914
+ const onKeyDown = (event) => {
915
+ if (event.key === "Escape") setReadoutOpen(false);
916
+ };
917
+ // capture 阶段监听: 面板内元素即使 stopPropagation 也能正确判定
918
+ doc.addEventListener("pointerdown", onPointerDown, true);
919
+ doc.addEventListener("keydown", onKeyDown);
920
+ return () => {
921
+ doc.removeEventListener("pointerdown", onPointerDown, true);
922
+ doc.removeEventListener("keydown", onKeyDown);
923
+ };
924
+ }, [readoutOpen]);
925
+
630
926
  function pctOf(win) {
631
927
  if (!win || typeof win.used !== "number" || typeof win.cap !== "number" || win.cap <= 0) return null;
632
928
  return Math.round((win.used / win.cap) * 100);
@@ -654,9 +950,14 @@ window.__ModuleLoader__.load({
654
950
  if (monthTotal !== null && monthTotal > 0 && monthUsed !== null) {
655
951
  parts.push({ key: "mo", label: "月 " + Math.round((monthUsed / monthTotal) * 100) + "%" });
656
952
  }
657
- const readout = data
658
- ? (parts.length > 0 ? parts : [{ key: "wait", label: "额度查询中…" }])
659
- : [{ key: "idle", label: "Command Code" }];
953
+ // 状态栏也要能说人话: 查询失败时别再挂着「额度查询中…」骗人。
954
+ const readout = !data
955
+ ? [{ key: "idle", label: "Command Code" }]
956
+ : parts.length > 0
957
+ ? parts
958
+ : (Array.isArray(data.errors) && data.errors.length > 0)
959
+ ? [{ key: "err", label: "额度查询失败" }]
960
+ : [{ key: "wait", label: "额度查询中…" }];
660
961
 
661
962
  // 状态栏本体: 空心图标 + 三个占比读数。不加粗, 颜色沿用官方三级文字色。
662
963
  const badgeButton = React.createElement("button", {
@@ -699,7 +1000,7 @@ window.__ModuleLoader__.load({
699
1000
  color: T.label,
700
1001
  };
701
1002
 
702
- return React.createElement("div", { style: statusBarStyle },
1003
+ return React.createElement("div", { style: statusBarStyle, ref: rootRef },
703
1004
  badgeButton,
704
1005
  readoutOpen ? React.createElement("div", { style: { ...shellStyle, ...panelPosStyle } },
705
1006
  React.createElement("style", null, [
@@ -711,22 +1012,33 @@ window.__ModuleLoader__.load({
711
1012
  display: "flex",
712
1013
  justifyContent: "space-between",
713
1014
  alignItems: "center",
714
- padding: "10px 10px 10px 16px",
1015
+ padding: "9px 10px 9px 16px",
715
1016
  borderBottom: `1px solid ${T.borderL}`,
716
1017
  },
717
1018
  },
718
1019
  React.createElement("span", { style: { fontSize: 13, fontWeight: 700, display: "flex", alignItems: "center", gap: 8 } },
719
- React.createElement("span", { style: { width: 8, height: 8, borderRadius: "50%", background: T.brand, boxShadow: `0 0 8px ${T.brand}` } }),
1020
+ // 品牌点: 去掉发光效果(原来像"正在录音"), 改成 6px 方块
1021
+ React.createElement("span", { style: { width: 6, height: 6, borderRadius: 2, background: T.brand, display: "inline-block" } }),
720
1022
  "Command Code"),
721
- React.createElement("div", { style: { display: "flex", gap: 2 } },
1023
+ React.createElement("div", { style: { display: "flex", gap: 1 } },
722
1024
  React.createElement("button", {
723
- style: iconBtn,
724
- title: "刷新",
1025
+ style: gearOpen ? { ...iconBtn, ...iconBtnHover } : iconBtn,
1026
+ title: gearOpen ? "收起凭据设置" : "凭据设置",
1027
+ "aria-label": "凭据设置",
1028
+ "aria-expanded": gearOpen,
1029
+ onClick: () => setGearOpen(!gearOpen),
1030
+ onMouseEnter: (e) => { if (!gearOpen) Object.assign(e.currentTarget.style, iconBtnHover); },
1031
+ onMouseLeave: (e) => { if (!gearOpen) { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = T.label3; } },
1032
+ }, React.createElement(IconGear)),
1033
+ React.createElement("button", {
1034
+ style: loading ? { ...iconBtn, opacity: 0.45, cursor: "default" } : iconBtn,
1035
+ title: loading ? "刷新中…" : "刷新",
725
1036
  "aria-label": "刷新",
1037
+ "aria-busy": loading,
726
1038
  disabled: loading,
727
- onClick: refresh,
728
- onMouseEnter: (e) => Object.assign(e.currentTarget.style, iconBtnHover),
729
- onMouseLeave: (e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = T.label3; },
1039
+ onClick: () => refresh(),
1040
+ onMouseEnter: (e) => { if (!loading) Object.assign(e.currentTarget.style, iconBtnHover); },
1041
+ onMouseLeave: (e) => { if (!loading) { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = T.label3; } },
730
1042
  }, React.createElement(IconRefresh)),
731
1043
  React.createElement("button", {
732
1044
  style: iconBtn,
@@ -736,8 +1048,10 @@ window.__ModuleLoader__.load({
736
1048
  onMouseEnter: (e) => Object.assign(e.currentTarget.style, iconBtnHover),
737
1049
  onMouseLeave: (e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = T.label3; },
738
1050
  }, React.createElement(IconClose)))),
739
- React.createElement(CredentialPicker, { state: creds, onPick: pickCredential }),
740
- React.createElement(DashboardBody, { data, loading, error })) : null);
1051
+ // 凭据选择默认收起: 它是配置项, 不该常驻在数据面板的主视觉里
1052
+ gearOpen ? React.createElement(CredentialPicker, { state: creds, onPick: pickCredential }) : null,
1053
+ gearOpen ? React.createElement(PalettePicker, { value: palette, onChange: choosePalette }) : null,
1054
+ React.createElement(DashboardBody, { data, loading, error, palette })) : null);
741
1055
  }
742
1056
 
743
1057
  // ---- apply -----------------------------------------------------------------------