@iasiv5/dsh-quota-watch 0.1.21 → 0.1.23

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.23",
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,20 @@ 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
+ * Dual-channel: ctx.logger (cordis, ring-buffer only — the host registers no journal
152
+ * exporter) mirrored to console so service stdout lands in journald. */
153
+ function logTransition(key, from, to, detail) {
154
+ if (from === to) return
155
+ const line = `quota-watch: ${key} ${from}→${to}${detail ? ` (${detail})` : ''}`
156
+ const level = to === 'error' || to === 'stale' ? 'warn' : 'info'
157
+ if (level === 'warn') ctx.logger?.warn?.(line)
158
+ else ctx.logger?.info?.(line)
159
+ ;(options.logMirror ?? console)[level]?.(line)
160
+ }
161
+
142
162
  function markMissing(key, displayName) {
163
+ const from = providers.get(key)?.status
143
164
  providers.set(key, {
144
165
  key,
145
166
  displayName,
@@ -147,9 +168,11 @@ export function createQuotaService(ctx, options = {}) {
147
168
  credential: 'none',
148
169
  checkedAt: now(),
149
170
  })
171
+ logTransition(key, from, 'missing')
150
172
  }
151
173
 
152
174
  function markSuccess(key, displayName, payload) {
175
+ const from = providers.get(key)?.status
153
176
  const at = now()
154
177
  providers.set(key, {
155
178
  key,
@@ -160,6 +183,7 @@ export function createQuotaService(ctx, options = {}) {
160
183
  checkedAt: at,
161
184
  ...payload,
162
185
  })
186
+ logTransition(key, from, 'ready')
163
187
  }
164
188
 
165
189
  function markGlmPlanSuccess(plan) {
@@ -175,6 +199,7 @@ export function createQuotaService(ctx, options = {}) {
175
199
  plan,
176
200
  ...(previous?.usage !== undefined ? { usage: previous.usage } : {}),
177
201
  })
202
+ logTransition('glm', previous?.status, 'ready')
178
203
  }
179
204
 
180
205
  function markGlmUsageSuccess(usage) {
@@ -189,6 +214,7 @@ export function createQuotaService(ctx, options = {}) {
189
214
  checkedAt: at,
190
215
  usage: { status: 'ready', updatedAt: at, ...usage },
191
216
  })
217
+ logTransition('glm-usage', previous.usage?.status ?? 'loading', 'ready')
192
218
  }
193
219
 
194
220
  function markGlmUsageFailure(error) {
@@ -197,27 +223,35 @@ export function createQuotaService(ctx, options = {}) {
197
223
  const lastGood = previous.usage?.status === 'ready' || previous.usage?.status === 'stale'
198
224
  ? previous.usage
199
225
  : undefined
226
+ const { message, kind } = classifyProbeError(error)
227
+ const status = lastGood !== undefined ? 'stale' : 'error'
200
228
  providers.set('glm', {
201
229
  ...previous,
202
230
  checkedAt: now(),
203
231
  usage: {
204
232
  ...(lastGood !== undefined ? lastGood : {}),
205
- status: lastGood !== undefined ? 'stale' : 'error',
206
- error: safeProbeError(error),
233
+ status,
234
+ error: message,
235
+ errorKind: kind,
207
236
  },
208
237
  })
238
+ logTransition('glm-usage', previous.usage?.status ?? 'loading', status, message)
209
239
  }
210
240
 
211
241
  function markFailure(key, displayName, error) {
212
242
  const previous = providers.get(key)
213
243
  const checkedAt = now()
214
244
  const hasLastGood = previous?.status === 'ready' || previous?.status === 'stale'
245
+ const { message, kind } = classifyProbeError(error)
246
+ const status = hasLastGood ? 'stale' : 'error'
215
247
  providers.set(key, {
216
248
  ...(hasLastGood ? previous : { key, displayName, credential: 'configured' }),
217
- status: hasLastGood ? 'stale' : 'error',
249
+ status,
218
250
  checkedAt,
219
- error: safeProbeError(error),
251
+ error: message,
252
+ errorKind: kind,
220
253
  })
254
+ logTransition(key, previous?.status, status, message)
221
255
  }
222
256
 
223
257
  async function fetchBody(spec) {
@@ -311,18 +345,23 @@ export function createQuotaService(ctx, options = {}) {
311
345
  timer = setTimeout(async () => {
312
346
  timer = undefined
313
347
  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.
348
+ // Cold-start retry with capped exponential backoff: until at least one
349
+ // provider has reached ready/stale, a failed or still-loading round re-arms
350
+ // at coldRetryMs, doubling per consecutive retryable round and capping at
351
+ // the normal cadence — a desktop cold start with a slow network still gets
352
+ // fast early retries, but a persistently broken state (e.g. a rejected
353
+ // credential) can no longer hammer the upstream every few seconds forever.
354
+ // Any non-retryable round (success, or all-missing) resets the counter, so
355
+ // a later cold phase restarts from the short interval. (updatedAt alone
356
+ // cannot signal "cold": refresh() stamps it even when every probe failed.)
322
357
  const states = [...providers.values()].map((provider) => provider.status)
323
358
  const retryable = states.some((status) => status === 'loading' || status === 'error')
324
359
  && states.every((status) => status !== 'ready' && status !== 'stale')
325
- schedule(retryable ? (options.coldRetryMs ?? 5_000) : pollIntervalSec * 1000)
360
+ if (retryable) coldAttempts += 1
361
+ else coldAttempts = 0
362
+ schedule(retryable
363
+ ? Math.min((options.coldRetryMs ?? 5_000) * 2 ** (coldAttempts - 1), pollIntervalSec * 1000)
364
+ : pollIntervalSec * 1000)
326
365
  }, delayMs)
327
366
  timer.unref?.()
328
367
  }