@iasiv5/dsh-quota-watch 0.1.21 → 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/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",
@@ -433,7 +435,7 @@ function kvRow(doc, label, value) {
433
435
  }
434
436
  function glmRowModel(provider) {
435
437
  if (provider?.status === "missing") return null;
436
- 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 };
437
439
  const windows = provider?.plan?.windows;
438
440
  if (!Array.isArray(windows) || windows.length === 0) return null;
439
441
  const five = windows.find((window) => window?.key === "5h");
@@ -444,12 +446,13 @@ function glmRowModel(provider) {
444
446
  percent: five?.percent,
445
447
  stale: provider.status === "stale",
446
448
  error: provider.error,
449
+ errorKind: provider.errorKind,
447
450
  extra: tokens
448
451
  };
449
452
  }
450
453
  function copilotRowModel(provider) {
451
454
  if (provider?.status === "missing") return null;
452
- 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 };
453
456
  const quota = provider?.quota;
454
457
  if (quota?.balanceAvailable !== true) return null;
455
458
  const used = remainingToUsed(quota.percentRemaining);
@@ -460,6 +463,7 @@ function copilotRowModel(provider) {
460
463
  percent: used,
461
464
  stale: provider.status === "stale",
462
465
  error: provider.error,
466
+ errorKind: provider.errorKind,
463
467
  extra: typeof quota.creditsUsed === "number" && quota.creditsUsed >= 0 ? quota.creditsUsed : void 0
464
468
  };
465
469
  }
@@ -740,7 +744,7 @@ function mountQuotaCard({
740
744
  if (model.stale) {
741
745
  const mark = text(doc, "span", "dqw-stale-mark", "\u26A0");
742
746
  mark.dataset.dshQuotaWatchStale = "";
743
- mark.title = model.error ?? copy.stale;
747
+ mark.title = model.errorKind === "auth" && model.error ? `${copy.authRejected}: ${model.error}` : model.error ?? copy.stale;
744
748
  row.append(mark);
745
749
  }
746
750
  row.append(text(doc, "span", "dqw-pct", typeof model.percent === "number" ? formatPercent(model.percent, locale) : "\u2014"));
@@ -824,7 +828,8 @@ function mountQuotaCard({
824
828
  detail.replaceChildren();
825
829
  detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav }));
826
830
  if (provider?.status === "stale") {
827
- detail.append(text(doc, "p", "dqw-error", `${copy.stale}${provider.error ? `: ${provider.error}` : ""}`));
831
+ const bannerPrefix = provider.errorKind === "auth" ? copy.authRejected : copy.stale;
832
+ detail.append(text(doc, "p", "dqw-error", `${bannerPrefix}${provider.error ? `: ${provider.error}` : ""}`));
828
833
  }
829
834
  if (key === "glm") {
830
835
  const usage = provider?.usage;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-quota-watch",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "DSH Web sidebar monitor for GLM Coding Plan quotas and GitHub Copilot credits",
5
5
  "type": "module",
6
6
  "main": "./src/host.mjs",
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',
@@ -322,7 +324,7 @@ function kvRow(doc, label, value) {
322
324
  /** GLM summary row view model: fixed 5h-window percent plus today's compact tokens. */
323
325
  function glmRowModel(provider) {
324
326
  if (provider?.status === 'missing') return null
325
- 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 }
326
328
  const windows = provider?.plan?.windows
327
329
  if (!Array.isArray(windows) || windows.length === 0) return null
328
330
  const five = windows.find((window) => window?.key === '5h')
@@ -335,6 +337,7 @@ function glmRowModel(provider) {
335
337
  percent: five?.percent,
336
338
  stale: provider.status === 'stale',
337
339
  error: provider.error,
340
+ errorKind: provider.errorKind,
338
341
  extra: tokens,
339
342
  }
340
343
  }
@@ -342,7 +345,7 @@ function glmRowModel(provider) {
342
345
  /** Copilot summary row view model: used% inverted from the reported remaining percent. */
343
346
  function copilotRowModel(provider) {
344
347
  if (provider?.status === 'missing') return null
345
- 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 }
346
349
  const quota = provider?.quota
347
350
  if (quota?.balanceAvailable !== true) return null
348
351
  const used = remainingToUsed(quota.percentRemaining)
@@ -353,6 +356,7 @@ function copilotRowModel(provider) {
353
356
  percent: used,
354
357
  stale: provider.status === 'stale',
355
358
  error: provider.error,
359
+ errorKind: provider.errorKind,
356
360
  extra: typeof quota.creditsUsed === 'number' && quota.creditsUsed >= 0 ? quota.creditsUsed : undefined,
357
361
  }
358
362
  }
@@ -695,7 +699,9 @@ export function mountQuotaCard({
695
699
  if (model.stale) {
696
700
  const mark = text(doc, 'span', 'dqw-stale-mark', '⚠')
697
701
  mark.dataset.dshQuotaWatchStale = ''
698
- mark.title = model.error ?? copy.stale
702
+ mark.title = model.errorKind === 'auth' && model.error
703
+ ? `${copy.authRejected}: ${model.error}`
704
+ : (model.error ?? copy.stale)
699
705
  row.append(mark)
700
706
  }
701
707
  row.append(text(doc, 'span', 'dqw-pct', typeof model.percent === 'number' ? formatPercent(model.percent, locale) : '—'))
@@ -793,7 +799,8 @@ export function mountQuotaCard({
793
799
  detail.replaceChildren()
794
800
  detail.append(panelHeader({ title: copy.providerNames[key], showBack: fromPanelNav }))
795
801
  if (provider?.status === 'stale') {
796
- detail.append(text(doc, 'p', 'dqw-error', `${copy.stale}${provider.error ? `: ${provider.error}` : ''}`))
802
+ const bannerPrefix = provider.errorKind === 'auth' ? copy.authRejected : copy.stale
803
+ detail.append(text(doc, 'p', 'dqw-error', `${bannerPrefix}${provider.error ? `: ${provider.error}` : ''}`))
797
804
  }
798
805
  if (key === 'glm') {
799
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
- function safeProbeError(error) {
81
- if (error?.name === 'TimeoutError' || error?.name === 'AbortError') return '请求超时'
82
- if (Number.isInteger(error?.status)) return `HTTP ${error.status}`
83
- if (error?.code === 'UNRECOGNIZED_RESPONSE') return '服务商响应格式无法识别'
84
- return '探测失败'
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: lastGood !== undefined ? 'stale' : 'error',
206
- error: safeProbeError(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: hasLastGood ? 'stale' : 'error',
245
+ status,
218
246
  checkedAt,
219
- error: safeProbeError(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 provider has reached ready/stale,
315
- // a failed or still-loading round re-arms shortly instead of waiting a
316
- // full interval — a desktop cold start with a slow network would
317
- // otherwise stare at no capsule for up to a minute. (updatedAt alone
318
- // cannot signal "cold": refresh() stamps it even when every probe
319
- // failed.) All-'missing' (no credentials configured) and warm states
320
- // keep the normal cadence, so nobody without credentials gets
321
- // high-frequency probing.
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
- schedule(retryable ? (options.coldRetryMs ?? 5_000) : pollIntervalSec * 1000)
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
  }