@iasiv5/dsh-quota-watch 0.1.20 → 0.1.22
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 +1 -1
- package/lib/client.js +16 -6
- package/package.json +1 -1
- package/src/client.mjs +18 -6
- package/src/host.mjs +53 -18
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
## ✨ 功能一览
|
|
16
16
|
|
|
17
17
|
- **🚀 悬浮胶囊(默认界面)**:一行「GLM 15% · Copilot 1.8%」常驻屏幕(默认右下角安全区),单击展开面板——胶囊上没有任何隐藏手势(右键 / 长按菜单已于 0.1.14 退役)。拖拽跟手(保持抓取点,transform 渲染)、任意位置停放、位置跨重启记忆、越界自动钳回视口(距边 8px 安全边距),并自动避开 Windows Desktop 壳顶部的标题栏拖拽带(0.1.20:该带按布局吞掉指针输入、无视 z-index,胶囊/面板落进去就再也点不到、拖不回——现与壳自家浮窗同款消费 `--dsh-windows-titlebar-height` 让位,历史卡死位置在启动时自动钳回带外;Web 无此变量,行为零漂移);触摸设备直接拖拽(更宽的防误拖阈值)、点击热区 44px、自动避让 iPhone 刘海 / 小黑条(safe-area)。已用超过 80% 转橙、超过 95% 转红,任一 provider 超 95% 时轻微脉动(尊重系统「减少动态效果」设置)。悬停提示与 aria-label 实时给出摘要。
|
|
18
|
-
- **🖥️
|
|
18
|
+
- **🖥️ 详情面板**:半透明毛玻璃底;总览(Token 额度)宽 `240px` 与侧边栏卡片同宽(窄屏 `min(240px, 100vw−24px)` 兜底),点进 GLM / Copilot 详情后恢复 0.0.18 内容自适应宽度(上限 `min(320px, 100vw−24px)`),靠边自动翻侧,滚动 / 缩放跟随;≤480px 窄屏自动改为底部弹层并避开 safe-area;Esc 或点击面板外关闭。概览列两个 provider 行,点击进入详情:
|
|
19
19
|
- **GLM**:今日 Tokens / 调用次数双大数字 + 五列对齐窗口网格(MCP(月)→ 5 小时 → 每月 → 每周固定排序,周窗口未下发时显示「每周额度 · ♾️(无限)」占位)+ 分模型用量行;
|
|
20
20
|
- **Copilot**:可用额度、本期已用(credits 消耗 + 弱化百分比)、重置时间三行。
|
|
21
21
|
- 窗口备注的括号按语言成对统一(zh 全角 `(…)` / en 半角 `(…)`),备注列左缘恒对齐(0.1.16)。接口未下发有效 `remaining` / `percent_remaining` 时该行不渲染,`credits_used` 绝不用于推算余额。
|
package/lib/client.js
CHANGED
|
@@ -199,6 +199,7 @@ var COPY = {
|
|
|
199
199
|
loading: "\u8BFB\u53D6\u4E2D",
|
|
200
200
|
failed: "\u67E5\u8BE2\u5931\u8D25",
|
|
201
201
|
stale: "\u6570\u636E\u53EF\u80FD\u5DF2\u8FC7\u671F",
|
|
202
|
+
authRejected: "\u51ED\u8BC1\u5DF2\u88AB\u670D\u52A1\u5546\u62D2\u7EDD\uFF0C\u8BF7\u91CD\u65B0\u6388\u6743",
|
|
202
203
|
requestFailed: "\u5237\u65B0\u5931\u8D25\uFF0C\u663E\u793A\u4E0A\u6B21\u6210\u529F\u6570\u636E",
|
|
203
204
|
available: "\u53EF\u7528\u989D\u5EA6",
|
|
204
205
|
used: "\u672C\u671F\u5DF2\u7528",
|
|
@@ -236,6 +237,7 @@ var COPY = {
|
|
|
236
237
|
loading: "Loading",
|
|
237
238
|
failed: "Query failed",
|
|
238
239
|
stale: "Data may be stale",
|
|
240
|
+
authRejected: "Credential rejected by provider \u2014 re-authorize",
|
|
239
241
|
requestFailed: "Refresh failed; showing last successful data",
|
|
240
242
|
available: "Available credits",
|
|
241
243
|
used: "Used this cycle",
|
|
@@ -311,6 +313,11 @@ var FLOAT_STYLE_TEXT = `
|
|
|
311
313
|
.dqw-panel { position: fixed; z-index: 2147483000; min-width: 166px; max-width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 24px); overflow-y: auto; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
|
|
312
314
|
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-panel { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
|
|
313
315
|
.dqw-panel--sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom, 0px)); width:auto; max-width:none; max-height:calc(100dvh - 24px); }
|
|
316
|
+
/* Overview-only pin: the capsule pop matches the sidebar card (240px); detail
|
|
317
|
+
views (GLM/Copilot) keep the 0.0.18 content-driven width above. :not() keeps
|
|
318
|
+
the bottom sheet (\u2264480px) winning over the pin. Keyed off the data attr that
|
|
319
|
+
syncPop already maintains \u2014 no JS state change. */
|
|
320
|
+
.dqw-panel[data-dsh-quota-watch-panel-overview]:not(.dqw-panel--sheet) { width: min(240px, calc(100vw - 24px)); }
|
|
314
321
|
.dqw-panel[hidden] { display: none; }
|
|
315
322
|
.dqw-panel *, .dqw-panel *::before, .dqw-panel *::after { box-sizing: border-box; }
|
|
316
323
|
.dqw-panel-header { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
@@ -330,7 +337,7 @@ var FLOAT_STYLE_TEXT = `
|
|
|
330
337
|
.dqw-label { font-size: 11px; font-weight: 600; width: 52px; flex: none; }
|
|
331
338
|
.dqw-label[data-action="refresh"] { cursor: pointer; border-radius: 4px; }
|
|
332
339
|
.dqw-label[data-action="refresh"]:hover { opacity: .75; }
|
|
333
|
-
.dqw-bar { display: block; flex:
|
|
340
|
+
.dqw-bar { display: block; flex: 1 1 auto; width: auto; min-width: 36px; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
|
|
334
341
|
.dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
|
|
335
342
|
.dqw-bar-fill.warn { background: var(--dsw-alias-label-warning, #d29922); }
|
|
336
343
|
.dqw-bar-fill.danger { background: var(--dsw-alias-label-danger, #c93c3c); }
|
|
@@ -351,7 +358,7 @@ var FLOAT_STYLE_TEXT = `
|
|
|
351
358
|
.dqw-kv { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
|
|
352
359
|
.dqw-kv-label { flex: none; opacity: .65; }
|
|
353
360
|
.dqw-kv-value { min-width: 0; text-align: right; font-weight: 600; }
|
|
354
|
-
.dqw-kv-sub { font-weight: 400; opacity: .55; }
|
|
361
|
+
.dqw-kv-sub { min-width: 0; overflow-wrap: anywhere; font-weight: 400; opacity: .55; }
|
|
355
362
|
.dqw-wins { display: grid; grid-template-columns: max-content max-content max-content max-content 1fr; column-gap: 6px; row-gap: 3px; align-items: baseline; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
|
|
356
363
|
.dqw-win-name { opacity: .65; }
|
|
357
364
|
.dqw-win-mid { opacity: .65; white-space: nowrap; }
|
|
@@ -428,7 +435,7 @@ function kvRow(doc, label, value) {
|
|
|
428
435
|
}
|
|
429
436
|
function glmRowModel(provider) {
|
|
430
437
|
if (provider?.status === "missing") return null;
|
|
431
|
-
if (provider?.status === "error") return { kind: "error", label: "GLM", error: provider.error };
|
|
438
|
+
if (provider?.status === "error") return { kind: "error", label: "GLM", error: provider.error, errorKind: provider.errorKind };
|
|
432
439
|
const windows = provider?.plan?.windows;
|
|
433
440
|
if (!Array.isArray(windows) || windows.length === 0) return null;
|
|
434
441
|
const five = windows.find((window) => window?.key === "5h");
|
|
@@ -439,12 +446,13 @@ function glmRowModel(provider) {
|
|
|
439
446
|
percent: five?.percent,
|
|
440
447
|
stale: provider.status === "stale",
|
|
441
448
|
error: provider.error,
|
|
449
|
+
errorKind: provider.errorKind,
|
|
442
450
|
extra: tokens
|
|
443
451
|
};
|
|
444
452
|
}
|
|
445
453
|
function copilotRowModel(provider) {
|
|
446
454
|
if (provider?.status === "missing") return null;
|
|
447
|
-
if (provider?.status === "error") return { kind: "error", label: "Copilot", error: provider.error };
|
|
455
|
+
if (provider?.status === "error") return { kind: "error", label: "Copilot", error: provider.error, errorKind: provider.errorKind };
|
|
448
456
|
const quota = provider?.quota;
|
|
449
457
|
if (quota?.balanceAvailable !== true) return null;
|
|
450
458
|
const used = remainingToUsed(quota.percentRemaining);
|
|
@@ -455,6 +463,7 @@ function copilotRowModel(provider) {
|
|
|
455
463
|
percent: used,
|
|
456
464
|
stale: provider.status === "stale",
|
|
457
465
|
error: provider.error,
|
|
466
|
+
errorKind: provider.errorKind,
|
|
458
467
|
extra: typeof quota.creditsUsed === "number" && quota.creditsUsed >= 0 ? quota.creditsUsed : void 0
|
|
459
468
|
};
|
|
460
469
|
}
|
|
@@ -735,7 +744,7 @@ function mountQuotaCard({
|
|
|
735
744
|
if (model.stale) {
|
|
736
745
|
const mark = text(doc, "span", "dqw-stale-mark", "\u26A0");
|
|
737
746
|
mark.dataset.dshQuotaWatchStale = "";
|
|
738
|
-
mark.title = model.error ?? copy.stale;
|
|
747
|
+
mark.title = model.errorKind === "auth" && model.error ? `${copy.authRejected}: ${model.error}` : model.error ?? copy.stale;
|
|
739
748
|
row.append(mark);
|
|
740
749
|
}
|
|
741
750
|
row.append(text(doc, "span", "dqw-pct", typeof model.percent === "number" ? formatPercent(model.percent, locale) : "\u2014"));
|
|
@@ -819,7 +828,8 @@ function mountQuotaCard({
|
|
|
819
828
|
detail.replaceChildren();
|
|
820
829
|
detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav }));
|
|
821
830
|
if (provider?.status === "stale") {
|
|
822
|
-
|
|
831
|
+
const bannerPrefix = provider.errorKind === "auth" ? copy.authRejected : copy.stale;
|
|
832
|
+
detail.append(text(doc, "p", "dqw-error", `${bannerPrefix}${provider.error ? `: ${provider.error}` : ""}`));
|
|
823
833
|
}
|
|
824
834
|
if (key === "glm") {
|
|
825
835
|
const usage = provider?.usage;
|
package/package.json
CHANGED
package/src/client.mjs
CHANGED
|
@@ -34,6 +34,7 @@ const COPY = {
|
|
|
34
34
|
loading: '读取中',
|
|
35
35
|
failed: '查询失败',
|
|
36
36
|
stale: '数据可能已过期',
|
|
37
|
+
authRejected: '凭证已被服务商拒绝,请重新授权',
|
|
37
38
|
requestFailed: '刷新失败,显示上次成功数据',
|
|
38
39
|
available: '可用额度',
|
|
39
40
|
used: '本期已用',
|
|
@@ -71,6 +72,7 @@ const COPY = {
|
|
|
71
72
|
loading: 'Loading',
|
|
72
73
|
failed: 'Query failed',
|
|
73
74
|
stale: 'Data may be stale',
|
|
75
|
+
authRejected: 'Credential rejected by provider — re-authorize',
|
|
74
76
|
requestFailed: 'Refresh failed; showing last successful data',
|
|
75
77
|
available: 'Available credits',
|
|
76
78
|
used: 'Used this cycle',
|
|
@@ -153,6 +155,11 @@ const FLOAT_STYLE_TEXT = `
|
|
|
153
155
|
.dqw-panel { position: fixed; z-index: 2147483000; min-width: 166px; max-width: min(320px, calc(100vw - 24px)); max-height: calc(100vh - 24px); overflow-y: auto; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); background: color-mix(in srgb, var(--dsw-alias-bg-base, #1f1f1f) 86%, transparent); -webkit-backdrop-filter: blur(14px) saturate(1.3); backdrop-filter: blur(14px) saturate(1.3); box-shadow: 0 8px 24px rgba(0,0,0,.25); color: var(--dsw-alias-label-primary, inherit); display: flex; flex-direction: column; gap: 8px; font: inherit; }
|
|
154
156
|
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { .dqw-panel { background: var(--dsw-alias-bg-elevated, var(--dsw-alias-bg-base, #1f1f1f)); } }
|
|
155
157
|
.dqw-panel--sheet { left:8px; right:8px; top:auto; bottom:calc(8px + env(safe-area-inset-bottom, 0px)); width:auto; max-width:none; max-height:calc(100dvh - 24px); }
|
|
158
|
+
/* Overview-only pin: the capsule pop matches the sidebar card (240px); detail
|
|
159
|
+
views (GLM/Copilot) keep the 0.0.18 content-driven width above. :not() keeps
|
|
160
|
+
the bottom sheet (≤480px) winning over the pin. Keyed off the data attr that
|
|
161
|
+
syncPop already maintains — no JS state change. */
|
|
162
|
+
.dqw-panel[data-dsh-quota-watch-panel-overview]:not(.dqw-panel--sheet) { width: min(240px, calc(100vw - 24px)); }
|
|
156
163
|
.dqw-panel[hidden] { display: none; }
|
|
157
164
|
.dqw-panel *, .dqw-panel *::before, .dqw-panel *::after { box-sizing: border-box; }
|
|
158
165
|
.dqw-panel-header { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
@@ -172,7 +179,7 @@ const FLOAT_STYLE_TEXT = `
|
|
|
172
179
|
.dqw-label { font-size: 11px; font-weight: 600; width: 52px; flex: none; }
|
|
173
180
|
.dqw-label[data-action="refresh"] { cursor: pointer; border-radius: 4px; }
|
|
174
181
|
.dqw-label[data-action="refresh"]:hover { opacity: .75; }
|
|
175
|
-
.dqw-bar { display: block; flex:
|
|
182
|
+
.dqw-bar { display: block; flex: 1 1 auto; width: auto; min-width: 36px; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
|
|
176
183
|
.dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
|
|
177
184
|
.dqw-bar-fill.warn { background: var(--dsw-alias-label-warning, #d29922); }
|
|
178
185
|
.dqw-bar-fill.danger { background: var(--dsw-alias-label-danger, #c93c3c); }
|
|
@@ -193,7 +200,7 @@ const FLOAT_STYLE_TEXT = `
|
|
|
193
200
|
.dqw-kv { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
|
|
194
201
|
.dqw-kv-label { flex: none; opacity: .65; }
|
|
195
202
|
.dqw-kv-value { min-width: 0; text-align: right; font-weight: 600; }
|
|
196
|
-
.dqw-kv-sub { font-weight: 400; opacity: .55; }
|
|
203
|
+
.dqw-kv-sub { min-width: 0; overflow-wrap: anywhere; font-weight: 400; opacity: .55; }
|
|
197
204
|
.dqw-wins { display: grid; grid-template-columns: max-content max-content max-content max-content 1fr; column-gap: 6px; row-gap: 3px; align-items: baseline; font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
|
|
198
205
|
.dqw-win-name { opacity: .65; }
|
|
199
206
|
.dqw-win-mid { opacity: .65; white-space: nowrap; }
|
|
@@ -317,7 +324,7 @@ function kvRow(doc, label, value) {
|
|
|
317
324
|
/** GLM summary row view model: fixed 5h-window percent plus today's compact tokens. */
|
|
318
325
|
function glmRowModel(provider) {
|
|
319
326
|
if (provider?.status === 'missing') return null
|
|
320
|
-
if (provider?.status === 'error') return { kind: 'error', label: 'GLM', error: provider.error }
|
|
327
|
+
if (provider?.status === 'error') return { kind: 'error', label: 'GLM', error: provider.error, errorKind: provider.errorKind }
|
|
321
328
|
const windows = provider?.plan?.windows
|
|
322
329
|
if (!Array.isArray(windows) || windows.length === 0) return null
|
|
323
330
|
const five = windows.find((window) => window?.key === '5h')
|
|
@@ -330,6 +337,7 @@ function glmRowModel(provider) {
|
|
|
330
337
|
percent: five?.percent,
|
|
331
338
|
stale: provider.status === 'stale',
|
|
332
339
|
error: provider.error,
|
|
340
|
+
errorKind: provider.errorKind,
|
|
333
341
|
extra: tokens,
|
|
334
342
|
}
|
|
335
343
|
}
|
|
@@ -337,7 +345,7 @@ function glmRowModel(provider) {
|
|
|
337
345
|
/** Copilot summary row view model: used% inverted from the reported remaining percent. */
|
|
338
346
|
function copilotRowModel(provider) {
|
|
339
347
|
if (provider?.status === 'missing') return null
|
|
340
|
-
if (provider?.status === 'error') return { kind: 'error', label: 'Copilot', error: provider.error }
|
|
348
|
+
if (provider?.status === 'error') return { kind: 'error', label: 'Copilot', error: provider.error, errorKind: provider.errorKind }
|
|
341
349
|
const quota = provider?.quota
|
|
342
350
|
if (quota?.balanceAvailable !== true) return null
|
|
343
351
|
const used = remainingToUsed(quota.percentRemaining)
|
|
@@ -348,6 +356,7 @@ function copilotRowModel(provider) {
|
|
|
348
356
|
percent: used,
|
|
349
357
|
stale: provider.status === 'stale',
|
|
350
358
|
error: provider.error,
|
|
359
|
+
errorKind: provider.errorKind,
|
|
351
360
|
extra: typeof quota.creditsUsed === 'number' && quota.creditsUsed >= 0 ? quota.creditsUsed : undefined,
|
|
352
361
|
}
|
|
353
362
|
}
|
|
@@ -690,7 +699,9 @@ export function mountQuotaCard({
|
|
|
690
699
|
if (model.stale) {
|
|
691
700
|
const mark = text(doc, 'span', 'dqw-stale-mark', '⚠')
|
|
692
701
|
mark.dataset.dshQuotaWatchStale = ''
|
|
693
|
-
mark.title = model.
|
|
702
|
+
mark.title = model.errorKind === 'auth' && model.error
|
|
703
|
+
? `${copy.authRejected}: ${model.error}`
|
|
704
|
+
: (model.error ?? copy.stale)
|
|
694
705
|
row.append(mark)
|
|
695
706
|
}
|
|
696
707
|
row.append(text(doc, 'span', 'dqw-pct', typeof model.percent === 'number' ? formatPercent(model.percent, locale) : '—'))
|
|
@@ -788,7 +799,8 @@ export function mountQuotaCard({
|
|
|
788
799
|
detail.replaceChildren()
|
|
789
800
|
detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav }))
|
|
790
801
|
if (provider?.status === 'stale') {
|
|
791
|
-
|
|
802
|
+
const bannerPrefix = provider.errorKind === 'auth' ? copy.authRejected : copy.stale
|
|
803
|
+
detail.append(text(doc, 'p', 'dqw-error', `${bannerPrefix}${provider.error ? `: ${provider.error}` : ''}`))
|
|
792
804
|
}
|
|
793
805
|
if (key === 'glm') {
|
|
794
806
|
const usage = provider?.usage
|
package/src/host.mjs
CHANGED
|
@@ -77,11 +77,17 @@ function initialProvider(key, displayName) {
|
|
|
77
77
|
return { key, displayName, status: 'loading', credential: 'unknown' }
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
if (error?.
|
|
84
|
-
|
|
80
|
+
/** Classify a probe failure into a user-facing message and a stable kind for the client. */
|
|
81
|
+
export function classifyProbeError(error) {
|
|
82
|
+
if (error?.name === 'TimeoutError' || error?.name === 'AbortError') return { message: '请求超时', kind: 'network' }
|
|
83
|
+
if (Number.isInteger(error?.status)) {
|
|
84
|
+
if (error.status === 401 || error.status === 403) return { message: `HTTP ${error.status}`, kind: 'auth' }
|
|
85
|
+
return { message: `HTTP ${error.status}`, kind: 'server' }
|
|
86
|
+
}
|
|
87
|
+
if (error?.code === 'UNRECOGNIZED_RESPONSE') return { message: '服务商响应格式无法识别', kind: 'parse' }
|
|
88
|
+
const causeCode = String(error?.cause?.code ?? '')
|
|
89
|
+
if (/^[A-Z0-9_]{3,60}$/.test(causeCode)) return { message: `探测失败 (${causeCode})`, kind: 'network' }
|
|
90
|
+
return { message: '探测失败', kind: 'network' }
|
|
85
91
|
}
|
|
86
92
|
|
|
87
93
|
function responseJson(res, status, body) {
|
|
@@ -117,6 +123,7 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
117
123
|
let pollPromise
|
|
118
124
|
let timer
|
|
119
125
|
let disposed = false
|
|
126
|
+
let coldAttempts = 0
|
|
120
127
|
|
|
121
128
|
function view() {
|
|
122
129
|
return {
|
|
@@ -131,6 +138,7 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
131
138
|
...(provider.updatedAt !== undefined ? { updatedAt: provider.updatedAt } : {}),
|
|
132
139
|
...(provider.checkedAt !== undefined ? { checkedAt: provider.checkedAt } : {}),
|
|
133
140
|
...(provider.error !== undefined ? { error: provider.error } : {}),
|
|
141
|
+
...(provider.errorKind !== undefined ? { errorKind: provider.errorKind } : {}),
|
|
134
142
|
...(provider.plan !== undefined ? { plan: provider.plan } : {}),
|
|
135
143
|
...(provider.quota !== undefined ? { quota: provider.quota } : {}),
|
|
136
144
|
...(provider.usage !== undefined ? { usage: provider.usage } : {}),
|
|
@@ -139,7 +147,16 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
|
|
150
|
+
/** Log one real state transition; degradations warn, recoveries and missing-credentials inform. */
|
|
151
|
+
function logTransition(key, from, to, detail) {
|
|
152
|
+
if (from === to) return
|
|
153
|
+
const line = `quota-watch: ${key} ${from}→${to}${detail ? ` (${detail})` : ''}`
|
|
154
|
+
if (to === 'error' || to === 'stale') ctx.logger?.warn?.(line)
|
|
155
|
+
else ctx.logger?.info?.(line)
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
function markMissing(key, displayName) {
|
|
159
|
+
const from = providers.get(key)?.status
|
|
143
160
|
providers.set(key, {
|
|
144
161
|
key,
|
|
145
162
|
displayName,
|
|
@@ -147,9 +164,11 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
147
164
|
credential: 'none',
|
|
148
165
|
checkedAt: now(),
|
|
149
166
|
})
|
|
167
|
+
logTransition(key, from, 'missing')
|
|
150
168
|
}
|
|
151
169
|
|
|
152
170
|
function markSuccess(key, displayName, payload) {
|
|
171
|
+
const from = providers.get(key)?.status
|
|
153
172
|
const at = now()
|
|
154
173
|
providers.set(key, {
|
|
155
174
|
key,
|
|
@@ -160,6 +179,7 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
160
179
|
checkedAt: at,
|
|
161
180
|
...payload,
|
|
162
181
|
})
|
|
182
|
+
logTransition(key, from, 'ready')
|
|
163
183
|
}
|
|
164
184
|
|
|
165
185
|
function markGlmPlanSuccess(plan) {
|
|
@@ -175,6 +195,7 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
175
195
|
plan,
|
|
176
196
|
...(previous?.usage !== undefined ? { usage: previous.usage } : {}),
|
|
177
197
|
})
|
|
198
|
+
logTransition('glm', previous?.status, 'ready')
|
|
178
199
|
}
|
|
179
200
|
|
|
180
201
|
function markGlmUsageSuccess(usage) {
|
|
@@ -189,6 +210,7 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
189
210
|
checkedAt: at,
|
|
190
211
|
usage: { status: 'ready', updatedAt: at, ...usage },
|
|
191
212
|
})
|
|
213
|
+
logTransition('glm-usage', previous.usage?.status ?? 'loading', 'ready')
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
function markGlmUsageFailure(error) {
|
|
@@ -197,27 +219,35 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
197
219
|
const lastGood = previous.usage?.status === 'ready' || previous.usage?.status === 'stale'
|
|
198
220
|
? previous.usage
|
|
199
221
|
: undefined
|
|
222
|
+
const { message, kind } = classifyProbeError(error)
|
|
223
|
+
const status = lastGood !== undefined ? 'stale' : 'error'
|
|
200
224
|
providers.set('glm', {
|
|
201
225
|
...previous,
|
|
202
226
|
checkedAt: now(),
|
|
203
227
|
usage: {
|
|
204
228
|
...(lastGood !== undefined ? lastGood : {}),
|
|
205
|
-
status
|
|
206
|
-
error:
|
|
229
|
+
status,
|
|
230
|
+
error: message,
|
|
231
|
+
errorKind: kind,
|
|
207
232
|
},
|
|
208
233
|
})
|
|
234
|
+
logTransition('glm-usage', previous.usage?.status ?? 'loading', status, message)
|
|
209
235
|
}
|
|
210
236
|
|
|
211
237
|
function markFailure(key, displayName, error) {
|
|
212
238
|
const previous = providers.get(key)
|
|
213
239
|
const checkedAt = now()
|
|
214
240
|
const hasLastGood = previous?.status === 'ready' || previous?.status === 'stale'
|
|
241
|
+
const { message, kind } = classifyProbeError(error)
|
|
242
|
+
const status = hasLastGood ? 'stale' : 'error'
|
|
215
243
|
providers.set(key, {
|
|
216
244
|
...(hasLastGood ? previous : { key, displayName, credential: 'configured' }),
|
|
217
|
-
status
|
|
245
|
+
status,
|
|
218
246
|
checkedAt,
|
|
219
|
-
error:
|
|
247
|
+
error: message,
|
|
248
|
+
errorKind: kind,
|
|
220
249
|
})
|
|
250
|
+
logTransition(key, previous?.status, status, message)
|
|
221
251
|
}
|
|
222
252
|
|
|
223
253
|
async function fetchBody(spec) {
|
|
@@ -311,18 +341,23 @@ export function createQuotaService(ctx, options = {}) {
|
|
|
311
341
|
timer = setTimeout(async () => {
|
|
312
342
|
timer = undefined
|
|
313
343
|
await refresh()
|
|
314
|
-
// Cold-start retry: until at least one
|
|
315
|
-
// a failed or still-loading round re-arms
|
|
316
|
-
//
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
//
|
|
344
|
+
// Cold-start retry with capped exponential backoff: until at least one
|
|
345
|
+
// provider has reached ready/stale, a failed or still-loading round re-arms
|
|
346
|
+
// at coldRetryMs, doubling per consecutive retryable round and capping at
|
|
347
|
+
// the normal cadence — a desktop cold start with a slow network still gets
|
|
348
|
+
// fast early retries, but a persistently broken state (e.g. a rejected
|
|
349
|
+
// credential) can no longer hammer the upstream every few seconds forever.
|
|
350
|
+
// Any non-retryable round (success, or all-missing) resets the counter, so
|
|
351
|
+
// a later cold phase restarts from the short interval. (updatedAt alone
|
|
352
|
+
// cannot signal "cold": refresh() stamps it even when every probe failed.)
|
|
322
353
|
const states = [...providers.values()].map((provider) => provider.status)
|
|
323
354
|
const retryable = states.some((status) => status === 'loading' || status === 'error')
|
|
324
355
|
&& states.every((status) => status !== 'ready' && status !== 'stale')
|
|
325
|
-
|
|
356
|
+
if (retryable) coldAttempts += 1
|
|
357
|
+
else coldAttempts = 0
|
|
358
|
+
schedule(retryable
|
|
359
|
+
? Math.min((options.coldRetryMs ?? 5_000) * 2 ** (coldAttempts - 1), pollIntervalSec * 1000)
|
|
360
|
+
: pollIntervalSec * 1000)
|
|
326
361
|
}, delayMs)
|
|
327
362
|
timer.unref?.()
|
|
328
363
|
}
|