@iasiv5/dsh-quota-watch 0.0.1
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/LICENSE +21 -0
- package/README.md +48 -0
- package/cordis.patch.yml +6 -0
- package/lib/client.js +407 -0
- package/package.json +60 -0
- package/src/access.mjs +67 -0
- package/src/client.mjs +369 -0
- package/src/core/adapters.mjs +163 -0
- package/src/host.mjs +311 -0
- package/src/shared.mjs +25 -0
package/src/client.mjs
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { CLIENT_POLL_INTERVAL_MS, CLIENT_ROUTES } from './shared.mjs'
|
|
2
|
+
|
|
3
|
+
export const name = 'quota-watch-client'
|
|
4
|
+
export const inject = []
|
|
5
|
+
|
|
6
|
+
const CARD_SELECTOR = '[data-dsh-quota-watch-card]'
|
|
7
|
+
const COLLAPSE_KEY = 'dsh-quota-watch:collapsed'
|
|
8
|
+
const FETCH_TIMEOUT_MS = 15_000
|
|
9
|
+
|
|
10
|
+
const COPY = {
|
|
11
|
+
zh: {
|
|
12
|
+
title: '套餐监控',
|
|
13
|
+
refresh: '刷新',
|
|
14
|
+
collapse: '收起',
|
|
15
|
+
expand: '展开',
|
|
16
|
+
loading: '读取中',
|
|
17
|
+
updated: '更新',
|
|
18
|
+
notConfigured: '未配置',
|
|
19
|
+
noPlan: '暂无套餐窗口',
|
|
20
|
+
failed: '查询失败',
|
|
21
|
+
stale: '数据可能已过期',
|
|
22
|
+
requestFailed: '刷新失败,显示上次成功数据',
|
|
23
|
+
available: '可用额度',
|
|
24
|
+
used: '本期已用',
|
|
25
|
+
balanceUnavailable: '余额暂不可用',
|
|
26
|
+
reset: '重置',
|
|
27
|
+
windows: { '5h': '5 小时', week: '每周', month: '每月' },
|
|
28
|
+
},
|
|
29
|
+
en: {
|
|
30
|
+
title: 'Quota Watch',
|
|
31
|
+
refresh: 'Refresh',
|
|
32
|
+
collapse: 'Collapse',
|
|
33
|
+
expand: 'Expand',
|
|
34
|
+
loading: 'Loading',
|
|
35
|
+
updated: 'Updated',
|
|
36
|
+
notConfigured: 'Not configured',
|
|
37
|
+
noPlan: 'No plan windows',
|
|
38
|
+
failed: 'Query failed',
|
|
39
|
+
stale: 'Data may be stale',
|
|
40
|
+
requestFailed: 'Refresh failed; showing last successful data',
|
|
41
|
+
available: 'Available credits',
|
|
42
|
+
used: 'Used this cycle',
|
|
43
|
+
balanceUnavailable: 'Balance unavailable',
|
|
44
|
+
reset: 'Resets',
|
|
45
|
+
windows: { '5h': '5 hours', week: 'Weekly', month: 'Monthly' },
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const STYLE_TEXT = `
|
|
50
|
+
[data-dsh-quota-watch-card] { box-sizing: border-box; width: 100%; margin: 0 0 8px; color: var(--dsw-alias-label-primary, inherit); font: inherit; }
|
|
51
|
+
[data-dsh-quota-watch-card] *, [data-dsh-quota-watch-card] *::before, [data-dsh-quota-watch-card] *::after { box-sizing: border-box; }
|
|
52
|
+
.dqw-card { border: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.35)); border-radius: 10px; background: var(--dsw-alias-bg-base, rgba(128,128,128,.08)); padding: 8px; display: flex; flex-direction: column; gap: 7px; }
|
|
53
|
+
.dqw-head { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
|
54
|
+
.dqw-title { flex: 1; min-width: 0; margin: 0; font-size: 12px; font-weight: 600; line-height: 18px; }
|
|
55
|
+
.dqw-button { border: 0; border-radius: 6px; background: transparent; color: inherit; cursor: pointer; font: inherit; font-size: 11px; line-height: 18px; padding: 1px 4px; }
|
|
56
|
+
.dqw-button:hover { background: var(--dsw-alias-bg-hover, rgba(128,128,128,.16)); }
|
|
57
|
+
.dqw-button:disabled { cursor: wait; opacity: .6; }
|
|
58
|
+
.dqw-meta { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
|
|
59
|
+
.dqw-provider { border-top: 1px solid var(--dsw-alias-border-secondary, rgba(128,128,128,.25)); padding-top: 6px; display: flex; flex-direction: column; gap: 4px; }
|
|
60
|
+
.dqw-provider-title { margin: 0; font-size: 11px; font-weight: 600; line-height: 16px; }
|
|
61
|
+
.dqw-row { display: flex; flex-direction: column; gap: 2px; }
|
|
62
|
+
.dqw-row-head { display: flex; justify-content: space-between; gap: 5px; font-size: 10px; line-height: 14px; }
|
|
63
|
+
.dqw-bar { display: block; height: 4px; overflow: hidden; border-radius: 4px; background: var(--dsw-alias-bg-tertiary, rgba(128,128,128,.2)); }
|
|
64
|
+
.dqw-bar-fill { display: block; height: 100%; border-radius: inherit; background: var(--dsw-alias-button-primary-fill, #5b8def); }
|
|
65
|
+
.dqw-muted { margin: 0; font-size: 10px; line-height: 14px; opacity: .7; }
|
|
66
|
+
.dqw-error { margin: 0; font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-warning, #b46900); }
|
|
67
|
+
.dqw-credit { margin: 0; font-size: 10px; line-height: 14px; font-variant-numeric: tabular-nums; }
|
|
68
|
+
`
|
|
69
|
+
|
|
70
|
+
function localeFor(win) {
|
|
71
|
+
return String(win?.navigator?.language ?? 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function formatNumber(value, locale) {
|
|
75
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
76
|
+
return new Intl.NumberFormat(locale === 'zh' ? 'zh-CN' : 'en-US', { maximumFractionDigits: 1 }).format(value)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatPercent(value, locale) {
|
|
80
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
81
|
+
const clamped = Math.max(0, Math.min(100, value))
|
|
82
|
+
const rounded = clamped >= 10 ? Math.round(clamped) : Number(clamped.toFixed(1))
|
|
83
|
+
return `${formatNumber(rounded, locale)}%`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatTime(value, locale) {
|
|
87
|
+
if (typeof value !== 'string' || value === '') return ''
|
|
88
|
+
const date = new Date(value)
|
|
89
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
90
|
+
return new Intl.DateTimeFormat(locale === 'zh' ? 'zh-CN' : 'en-US', {
|
|
91
|
+
dateStyle: 'short',
|
|
92
|
+
timeStyle: 'short',
|
|
93
|
+
}).format(date)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function text(doc, tag, className, value) {
|
|
97
|
+
const node = doc.createElement(tag)
|
|
98
|
+
if (className) node.className = className
|
|
99
|
+
node.textContent = String(value ?? '')
|
|
100
|
+
return node
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function progress(doc, label, percent, locale) {
|
|
104
|
+
const row = doc.createElement('div')
|
|
105
|
+
row.className = 'dqw-row'
|
|
106
|
+
const head = doc.createElement('div')
|
|
107
|
+
head.className = 'dqw-row-head'
|
|
108
|
+
head.append(text(doc, 'span', '', label), text(doc, 'span', '', formatPercent(percent, locale)))
|
|
109
|
+
const bar = doc.createElement('span')
|
|
110
|
+
bar.className = 'dqw-bar'
|
|
111
|
+
bar.setAttribute('role', 'progressbar')
|
|
112
|
+
bar.setAttribute('aria-valuemin', '0')
|
|
113
|
+
bar.setAttribute('aria-valuemax', '100')
|
|
114
|
+
bar.setAttribute('aria-valuenow', String(Math.max(0, Math.min(100, percent))))
|
|
115
|
+
const fill = doc.createElement('span')
|
|
116
|
+
fill.className = 'dqw-bar-fill'
|
|
117
|
+
fill.style.width = `${Math.max(0, Math.min(100, percent))}%`
|
|
118
|
+
bar.append(fill)
|
|
119
|
+
row.append(head, bar)
|
|
120
|
+
return row
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function providerSection(doc, key, heading) {
|
|
124
|
+
const section = doc.createElement('section')
|
|
125
|
+
section.className = 'dqw-provider'
|
|
126
|
+
section.dataset.provider = key
|
|
127
|
+
section.append(text(doc, 'h4', 'dqw-provider-title', heading))
|
|
128
|
+
const content = doc.createElement('div')
|
|
129
|
+
content.dataset.role = 'content'
|
|
130
|
+
content.className = 'dqw-content'
|
|
131
|
+
section.append(content)
|
|
132
|
+
return section
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function appendStaleError(doc, content, provider, copy) {
|
|
136
|
+
if (provider.status === 'stale') content.append(text(doc, 'p', 'dqw-error', `${copy.stale}: ${provider.error ?? ''}`))
|
|
137
|
+
else if (provider.status === 'error') content.append(text(doc, 'p', 'dqw-error', provider.error ?? copy.failed))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderGlm(doc, section, provider, copy, locale) {
|
|
141
|
+
const content = section.querySelector('[data-role="content"]')
|
|
142
|
+
content.replaceChildren()
|
|
143
|
+
appendStaleError(doc, content, provider, copy)
|
|
144
|
+
if (provider.status === 'missing') {
|
|
145
|
+
content.append(text(doc, 'p', 'dqw-muted', copy.notConfigured))
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
if (provider.status === 'loading') {
|
|
149
|
+
content.append(text(doc, 'p', 'dqw-muted', copy.loading))
|
|
150
|
+
return
|
|
151
|
+
}
|
|
152
|
+
if (!provider.plan?.windows?.length) {
|
|
153
|
+
if (provider.status !== 'error') content.append(text(doc, 'p', 'dqw-muted', copy.noPlan))
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
const planName = provider.plan.planName
|
|
157
|
+
if (planName) content.append(text(doc, 'p', 'dqw-muted', planName))
|
|
158
|
+
for (const window of provider.plan.windows) {
|
|
159
|
+
const label = window.name || copy.windows[window.key] || window.key
|
|
160
|
+
if (typeof window.percent === 'number' && Number.isFinite(window.percent)) {
|
|
161
|
+
content.append(progress(doc, label, window.percent, locale))
|
|
162
|
+
} else {
|
|
163
|
+
content.append(text(doc, 'p', 'dqw-muted', label))
|
|
164
|
+
}
|
|
165
|
+
const reset = formatTime(window.resetsAt, locale)
|
|
166
|
+
if (reset) content.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderCopilot(doc, section, provider, copy, locale) {
|
|
171
|
+
const content = section.querySelector('[data-role="content"]')
|
|
172
|
+
content.replaceChildren()
|
|
173
|
+
appendStaleError(doc, content, provider, copy)
|
|
174
|
+
if (provider.status === 'missing') {
|
|
175
|
+
content.append(text(doc, 'p', 'dqw-muted', copy.notConfigured))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if (provider.status === 'loading') {
|
|
179
|
+
content.append(text(doc, 'p', 'dqw-muted', copy.loading))
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const quota = provider.quota
|
|
183
|
+
if (quota?.planName) content.append(text(doc, 'p', 'dqw-muted', quota.planName))
|
|
184
|
+
if (quota?.balanceAvailable === true) {
|
|
185
|
+
content.append(text(doc, 'p', 'dqw-credit', `${copy.available}: ${formatNumber(quota.remaining, locale)} / ${formatNumber(quota.entitlement, locale)}`))
|
|
186
|
+
if (typeof quota.percentRemaining === 'number') {
|
|
187
|
+
content.append(progress(doc, copy.available, quota.percentRemaining, locale))
|
|
188
|
+
}
|
|
189
|
+
} else {
|
|
190
|
+
content.append(text(doc, 'p', 'dqw-muted', copy.balanceUnavailable))
|
|
191
|
+
}
|
|
192
|
+
if (typeof quota?.creditsUsed === 'number') {
|
|
193
|
+
content.append(text(doc, 'p', 'dqw-muted', `${copy.used}: ${formatNumber(quota.creditsUsed, locale)}`))
|
|
194
|
+
}
|
|
195
|
+
const reset = formatTime(quota?.resetsAt, locale)
|
|
196
|
+
if (reset) content.append(text(doc, 'p', 'dqw-muted', `${copy.reset}: ${reset}`))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function footArea(doc) {
|
|
200
|
+
const sidebar = doc.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
201
|
+
return sidebar?.querySelector('[class*="footArea"]') ?? undefined
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function readCollapsed(win) {
|
|
205
|
+
try { return win.localStorage.getItem(COLLAPSE_KEY) === '1' } catch { return false }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function writeCollapsed(win, value) {
|
|
209
|
+
try { win.localStorage.setItem(COLLAPSE_KEY, value ? '1' : '0') } catch { /* Storage may be unavailable. */ }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function fetchJson(fetchImpl, path, method) {
|
|
213
|
+
const response = await fetchImpl(path, {
|
|
214
|
+
...(method === 'POST' ? { method: 'POST' } : {}),
|
|
215
|
+
cache: 'no-store',
|
|
216
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
217
|
+
})
|
|
218
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
219
|
+
return response.json()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Mount a self-healing sidebar card. Dependencies are injectable for DOM tests. */
|
|
223
|
+
export function mountQuotaCard({
|
|
224
|
+
doc = globalThis.document,
|
|
225
|
+
win = globalThis.window,
|
|
226
|
+
fetchImpl = globalThis.fetch,
|
|
227
|
+
pollIntervalMs = CLIENT_POLL_INTERVAL_MS,
|
|
228
|
+
} = {}) {
|
|
229
|
+
if (!doc?.body || typeof fetchImpl !== 'function') return () => {}
|
|
230
|
+
if (doc.querySelector(CARD_SELECTOR)) return () => {}
|
|
231
|
+
|
|
232
|
+
const locale = localeFor(win)
|
|
233
|
+
const copy = COPY[locale]
|
|
234
|
+
const container = doc.createElement('div')
|
|
235
|
+
container.dataset.dshQuotaWatchCard = ''
|
|
236
|
+
container.dataset.dshPlugin = 'quota-watch'
|
|
237
|
+
container.dataset.dshPart = 'sidebar-card'
|
|
238
|
+
const style = text(doc, 'style', '', STYLE_TEXT)
|
|
239
|
+
const card = doc.createElement('section')
|
|
240
|
+
card.className = 'dqw-card'
|
|
241
|
+
card.setAttribute('aria-label', copy.title)
|
|
242
|
+
const header = doc.createElement('header')
|
|
243
|
+
header.className = 'dqw-head'
|
|
244
|
+
header.append(text(doc, 'h3', 'dqw-title', copy.title))
|
|
245
|
+
const refreshButton = text(doc, 'button', 'dqw-button', '↻')
|
|
246
|
+
refreshButton.type = 'button'
|
|
247
|
+
refreshButton.title = copy.refresh
|
|
248
|
+
refreshButton.setAttribute('aria-label', copy.refresh)
|
|
249
|
+
refreshButton.dataset.action = 'refresh'
|
|
250
|
+
const collapseButton = text(doc, 'button', 'dqw-button', '−')
|
|
251
|
+
collapseButton.type = 'button'
|
|
252
|
+
collapseButton.dataset.action = 'collapse'
|
|
253
|
+
const body = doc.createElement('div')
|
|
254
|
+
body.className = 'dqw-body'
|
|
255
|
+
const lastUpdated = text(doc, 'p', 'dqw-meta', copy.loading)
|
|
256
|
+
lastUpdated.dataset.role = 'updated'
|
|
257
|
+
const glmSection = providerSection(doc, 'glm', 'GLM Coding Plan')
|
|
258
|
+
const copilotSection = providerSection(doc, 'copilot', 'GitHub Copilot')
|
|
259
|
+
body.append(lastUpdated, glmSection, copilotSection)
|
|
260
|
+
header.append(refreshButton, collapseButton)
|
|
261
|
+
card.append(header, body)
|
|
262
|
+
container.append(style, card)
|
|
263
|
+
let collapsed = readCollapsed(win)
|
|
264
|
+
let snapshot
|
|
265
|
+
let requestSequence = 0
|
|
266
|
+
let timer
|
|
267
|
+
let disposed = false
|
|
268
|
+
let queuedPlace = false
|
|
269
|
+
|
|
270
|
+
const applyCollapsed = () => {
|
|
271
|
+
body.hidden = collapsed
|
|
272
|
+
collapseButton.textContent = collapsed ? '+' : '−'
|
|
273
|
+
collapseButton.title = collapsed ? copy.expand : copy.collapse
|
|
274
|
+
collapseButton.setAttribute('aria-label', collapseButton.title)
|
|
275
|
+
collapseButton.setAttribute('aria-expanded', String(!collapsed))
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const render = (next) => {
|
|
279
|
+
snapshot = next
|
|
280
|
+
const glm = next?.providers?.find((provider) => provider.key === 'glm')
|
|
281
|
+
const copilot = next?.providers?.find((provider) => provider.key === 'copilot')
|
|
282
|
+
if (glm) renderGlm(doc, glmSection, glm, copy, locale)
|
|
283
|
+
if (copilot) renderCopilot(doc, copilotSection, copilot, copy, locale)
|
|
284
|
+
const updated = formatTime(next?.updatedAt ? new Date(next.updatedAt).toISOString() : '', locale)
|
|
285
|
+
lastUpdated.textContent = updated ? `${copy.updated}: ${updated}` : copy.loading
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const showTransportError = () => {
|
|
289
|
+
if (snapshot) lastUpdated.textContent = copy.requestFailed
|
|
290
|
+
else lastUpdated.textContent = copy.failed
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const poll = async (force) => {
|
|
294
|
+
if (disposed) return
|
|
295
|
+
const sequence = ++requestSequence
|
|
296
|
+
refreshButton.disabled = force === true
|
|
297
|
+
try {
|
|
298
|
+
const next = await fetchJson(fetchImpl, force ? CLIENT_ROUTES.refresh : CLIENT_ROUTES.overview, force ? 'POST' : 'GET')
|
|
299
|
+
if (sequence === requestSequence && !disposed) render(next)
|
|
300
|
+
} catch {
|
|
301
|
+
if (sequence === requestSequence && !disposed) showTransportError()
|
|
302
|
+
} finally {
|
|
303
|
+
if (!disposed && sequence === requestSequence) refreshButton.disabled = false
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const place = () => {
|
|
308
|
+
const foot = footArea(doc)
|
|
309
|
+
if (!foot) return
|
|
310
|
+
const settings = foot.querySelector('[class*="settingsArea"]')
|
|
311
|
+
if (settings) {
|
|
312
|
+
if (container.parentElement !== foot || container.nextElementSibling !== settings) foot.insertBefore(container, settings)
|
|
313
|
+
} else if (container.parentElement !== foot || foot.lastElementChild !== container) {
|
|
314
|
+
foot.append(container)
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const schedulePlace = () => {
|
|
319
|
+
if (queuedPlace || disposed) return
|
|
320
|
+
queuedPlace = true
|
|
321
|
+
queueMicrotask(() => {
|
|
322
|
+
queuedPlace = false
|
|
323
|
+
if (!disposed) place()
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const observer = new win.MutationObserver(schedulePlace)
|
|
328
|
+
observer.observe(doc.body, { childList: true, subtree: true })
|
|
329
|
+
place()
|
|
330
|
+
applyCollapsed()
|
|
331
|
+
|
|
332
|
+
const startPolling = () => {
|
|
333
|
+
if (timer !== undefined || doc.visibilityState === 'hidden' || disposed) return
|
|
334
|
+
void poll(false)
|
|
335
|
+
timer = win.setInterval(() => { void poll(false) }, pollIntervalMs)
|
|
336
|
+
}
|
|
337
|
+
const stopPolling = () => {
|
|
338
|
+
if (timer !== undefined) win.clearInterval(timer)
|
|
339
|
+
timer = undefined
|
|
340
|
+
}
|
|
341
|
+
const onVisibilityChange = () => {
|
|
342
|
+
if (doc.visibilityState === 'hidden') stopPolling()
|
|
343
|
+
else startPolling()
|
|
344
|
+
}
|
|
345
|
+
const onRefresh = () => { void poll(true) }
|
|
346
|
+
const onCollapse = () => {
|
|
347
|
+
collapsed = !collapsed
|
|
348
|
+
writeCollapsed(win, collapsed)
|
|
349
|
+
applyCollapsed()
|
|
350
|
+
}
|
|
351
|
+
refreshButton.addEventListener('click', onRefresh)
|
|
352
|
+
collapseButton.addEventListener('click', onCollapse)
|
|
353
|
+
doc.addEventListener('visibilitychange', onVisibilityChange)
|
|
354
|
+
startPolling()
|
|
355
|
+
|
|
356
|
+
return () => {
|
|
357
|
+
disposed = true
|
|
358
|
+
stopPolling()
|
|
359
|
+
observer.disconnect()
|
|
360
|
+
doc.removeEventListener('visibilitychange', onVisibilityChange)
|
|
361
|
+
refreshButton.removeEventListener('click', onRefresh)
|
|
362
|
+
collapseButton.removeEventListener('click', onCollapse)
|
|
363
|
+
container.remove()
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function apply(ctx) {
|
|
368
|
+
ctx.effect(() => mountQuotaCard(), 'dsh-quota-watch: sidebar')
|
|
369
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const GLM_HOSTS = Object.freeze({
|
|
2
|
+
'zai-coding-cn': 'open.bigmodel.cn',
|
|
3
|
+
zai: 'api.z.ai',
|
|
4
|
+
'zai-coding': 'api.z.ai',
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
const GLM_WINDOW_KEYS = Object.freeze({ 3: '5h', 5: 'month', 6: 'week' })
|
|
8
|
+
const COPILOT_USER_URL = 'https://api.github.com/copilot_internal/user'
|
|
9
|
+
const COPILOT_HEADERS = Object.freeze({
|
|
10
|
+
accept: 'application/json',
|
|
11
|
+
'user-agent': 'GitHubCopilotChat/0.35.0',
|
|
12
|
+
'editor-version': 'vscode/1.107.0',
|
|
13
|
+
'editor-plugin-version': 'copilot-chat/0.35.0',
|
|
14
|
+
'copilot-integration-id': 'vscode-chat',
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
function asRecord(value) {
|
|
18
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asString(value) {
|
|
22
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function asNumber(value) {
|
|
26
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
27
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
28
|
+
const parsed = Number(value)
|
|
29
|
+
return Number.isFinite(parsed) ? parsed : undefined
|
|
30
|
+
}
|
|
31
|
+
return undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clampPercent(value) {
|
|
35
|
+
return Math.max(0, Math.min(100, value))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toIso(value) {
|
|
39
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
|
40
|
+
const milliseconds = value < 1e12 ? value * 1000 : value
|
|
41
|
+
const date = new Date(milliseconds)
|
|
42
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
|
|
43
|
+
}
|
|
44
|
+
const text = asString(value)
|
|
45
|
+
if (text === undefined) return undefined
|
|
46
|
+
if (/^\d+$/.test(text)) return toIso(Number(text))
|
|
47
|
+
const date = new Date(text)
|
|
48
|
+
return Number.isNaN(date.getTime()) ? undefined : date.toISOString()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function glmPercent(row, kind) {
|
|
52
|
+
if (kind === 'CREDIT_LIMIT') {
|
|
53
|
+
const current = asNumber(row.currentValue)
|
|
54
|
+
const total = asNumber(row.usage)
|
|
55
|
+
if (current !== undefined && total !== undefined && total > 0) {
|
|
56
|
+
return clampPercent((current / total) * 100)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const reported = asNumber(row.percentage)
|
|
60
|
+
return reported === undefined ? undefined : clampPercent(reported)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Build a fixed-origin GLM Coding Plan quota request. */
|
|
64
|
+
export function buildGlmProbe({ provider, apiKey } = {}) {
|
|
65
|
+
const host = GLM_HOSTS[provider]
|
|
66
|
+
const key = asString(apiKey)
|
|
67
|
+
if (host === undefined || key === undefined) return undefined
|
|
68
|
+
return {
|
|
69
|
+
url: `https://${host}/api/monitor/usage/quota/limit`,
|
|
70
|
+
headers: {
|
|
71
|
+
authorization: key,
|
|
72
|
+
'accept-language': 'en-US,en',
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Normalize GLM's provider-defined quota windows without relying on row order. */
|
|
78
|
+
export function parseGlmPlan(status, body) {
|
|
79
|
+
if (status !== 200) return undefined
|
|
80
|
+
const root = asRecord(body)
|
|
81
|
+
if (root?.success !== true) return undefined
|
|
82
|
+
const data = asRecord(root.data)
|
|
83
|
+
if (!Array.isArray(data?.limits)) return undefined
|
|
84
|
+
|
|
85
|
+
const windows = []
|
|
86
|
+
for (const candidate of data.limits) {
|
|
87
|
+
const row = asRecord(candidate)
|
|
88
|
+
if (row === undefined) continue
|
|
89
|
+
const kind = asString(row.type)
|
|
90
|
+
if (kind === 'TIME_LIMIT') continue
|
|
91
|
+
if (kind !== 'TOKENS_LIMIT' && kind !== 'CREDIT_LIMIT') continue
|
|
92
|
+
const unit = asNumber(row.unit)
|
|
93
|
+
const key = GLM_WINDOW_KEYS[unit]
|
|
94
|
+
if (key === undefined) continue
|
|
95
|
+
const percent = glmPercent(row, kind)
|
|
96
|
+
const resetsAt = toIso(row.nextResetTime)
|
|
97
|
+
windows.push({
|
|
98
|
+
key,
|
|
99
|
+
...(percent !== undefined ? { percent } : {}),
|
|
100
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (windows.length === 0) return undefined
|
|
105
|
+
const planName = asString(data.level)
|
|
106
|
+
return { ...(planName !== undefined ? { planName } : {}), windows }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Build the Copilot user-quota request using the raw GitHub OAuth grant. */
|
|
110
|
+
export function buildCopilotProbe({ githubOAuthToken } = {}) {
|
|
111
|
+
const token = asString(githubOAuthToken)
|
|
112
|
+
if (token === undefined) return undefined
|
|
113
|
+
return {
|
|
114
|
+
url: COPILOT_USER_URL,
|
|
115
|
+
headers: {
|
|
116
|
+
authorization: `Bearer ${token}`,
|
|
117
|
+
...COPILOT_HEADERS,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Normalize the Copilot user's premium-interaction snapshot.
|
|
124
|
+
* Remaining credits are never derived from entitlement minus credits_used.
|
|
125
|
+
*/
|
|
126
|
+
export function parseCopilotUser(status, body) {
|
|
127
|
+
if (status !== 200) return undefined
|
|
128
|
+
const root = asRecord(body)
|
|
129
|
+
const snapshots = asRecord(root?.quota_snapshots)
|
|
130
|
+
const snapshot = asRecord(snapshots?.premium_interactions)
|
|
131
|
+
if (snapshot === undefined) return undefined
|
|
132
|
+
|
|
133
|
+
const tokenBasedBilling = typeof snapshot.token_based_billing === 'boolean'
|
|
134
|
+
? snapshot.token_based_billing
|
|
135
|
+
: typeof root?.token_based_billing === 'boolean'
|
|
136
|
+
? root.token_based_billing
|
|
137
|
+
: undefined
|
|
138
|
+
const entitlement = asNumber(snapshot.entitlement)
|
|
139
|
+
const reportedRemaining = asNumber(snapshot.remaining)
|
|
140
|
+
const balanceAvailable = tokenBasedBilling === true
|
|
141
|
+
&& snapshot.unlimited === false
|
|
142
|
+
&& entitlement !== undefined
|
|
143
|
+
&& entitlement > 0
|
|
144
|
+
&& reportedRemaining !== undefined
|
|
145
|
+
&& reportedRemaining >= 0
|
|
146
|
+
const percentReported = asNumber(snapshot.percent_remaining)
|
|
147
|
+
const creditsUsed = tokenBasedBilling === true ? asNumber(snapshot.credits_used) : undefined
|
|
148
|
+
const resetsAt = toIso(root?.quota_reset_date_utc) ?? toIso(root?.quota_reset_date)
|
|
149
|
+
const planName = asString(root?.copilot_plan)
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
...(planName !== undefined ? { planName } : {}),
|
|
153
|
+
...(tokenBasedBilling !== undefined ? { tokenBasedBilling } : {}),
|
|
154
|
+
balanceAvailable,
|
|
155
|
+
...(balanceAvailable ? {
|
|
156
|
+
remaining: reportedRemaining,
|
|
157
|
+
entitlement,
|
|
158
|
+
...(percentReported !== undefined ? { percentRemaining: clampPercent(percentReported) } : {}),
|
|
159
|
+
} : {}),
|
|
160
|
+
...(creditsUsed !== undefined && creditsUsed >= 0 ? { creditsUsed } : {}),
|
|
161
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
162
|
+
}
|
|
163
|
+
}
|