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/README.md +13 -3
- package/README.zh.md +13 -3
- package/package.json +1 -1
- package/src/client.js +478 -164
- package/src/index.js +111 -10
- package/test/client.test.cjs +11 -8
- package/test/host.test.cjs +20 -1
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
|
-
//
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
131
|
+
// 进度色用 Anthropic 官方柔和蓝 #6A9BCC: 低饱和, 深色底上不刺眼,
|
|
132
|
+
// 又天然和告警的琥珀 / 红分开。不能用 --dsw-alias-brand-primary
|
|
133
|
+
// (它只是中性色阶别名, 换肤后会整条变黑)。
|
|
134
|
+
brand: "#6A9BCC",
|
|
122
135
|
};
|
|
123
136
|
|
|
124
|
-
//
|
|
125
|
-
|
|
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:
|
|
155
|
+
borderRadius: 14,
|
|
140
156
|
background: T.bg,
|
|
141
157
|
border: `1px solid ${T.border}`,
|
|
142
|
-
boxShadow: "0
|
|
143
|
-
|
|
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
|
-
//
|
|
257
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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, ${
|
|
276
|
-
|
|
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
|
-
|
|
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
|
|
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: "
|
|
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",
|
|
297
|
-
React.createElement("
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
React.createElement("span", { style: { fontSize:
|
|
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(
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
React.createElement("div", { style: {
|
|
307
|
-
|
|
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: "
|
|
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
|
|
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
|
-
|
|
357
|
-
|
|
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
|
-
|
|
372
|
-
|
|
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:
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
lineHeight: 1.5,
|
|
522
|
+
border: `1px solid ${T.hairline}`,
|
|
523
|
+
padding: "12px 13px",
|
|
524
|
+
marginBottom: i === 2 ? 0 : 9,
|
|
381
525
|
},
|
|
382
|
-
},
|
|
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 (
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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) =>
|
|
604
|
+
}, `⏱ ${staleList.map((s) => ENDPOINT_NAMES[s.endpoint] || s.endpoint).join("、")} 本次请求超时,显示的是上次成功获取的数据(点右上角 ⟳ 重试)`));
|
|
443
605
|
}
|
|
444
606
|
|
|
445
|
-
//
|
|
607
|
+
// 余额 + 账户信息: 状态灯与「本月剩余」同排, 其余信息独占整行 ——
|
|
608
|
+
// 灯放右边单独占一行会把"订阅开通"挤到换行。
|
|
446
609
|
rows.push(React.createElement("div", {
|
|
447
|
-
key: "
|
|
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",
|
|
455
|
-
React.createElement("
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
523
|
-
rows.push(React.createElement("div", {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
//
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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: "
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
740
|
-
React.createElement(
|
|
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 -----------------------------------------------------------------------
|