@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 +9 -4
- package/package.json +1 -1
- package/src/client.mjs +11 -4
- package/src/host.mjs +53 -18
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
|
-
|
|
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
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|