@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/src/host.mjs ADDED
@@ -0,0 +1,311 @@
1
+ import { credentialKey, credentialRef, isCredentialRefName } from '@deepseek-ai/dsh-credentials'
2
+ import { buildCopilotProbe, buildGlmProbe, parseCopilotUser, parseGlmPlan } from './core/adapters.mjs'
3
+ import {
4
+ DEFAULT_POLL_INTERVAL_SEC,
5
+ MAX_POLL_INTERVAL_SEC,
6
+ MIN_POLL_INTERVAL_SEC,
7
+ PROBE_TIMEOUT_MS,
8
+ PROVIDER_IDS,
9
+ ROUTES,
10
+ } from './shared.mjs'
11
+ import { isPairedOrLoopbackAllowed, isSameOriginRequest } from './access.mjs'
12
+
13
+ export const name = 'quota-watch'
14
+ export const inject = ['webServer', 'settings', 'credentials']
15
+
16
+ const COPILOT_CREDENTIAL_KEY = credentialKey('llm-pi-ai', PROVIDER_IDS.copilot)
17
+ const GLM_PROVIDER_IDS = [
18
+ PROVIDER_IDS.glmDomestic,
19
+ PROVIDER_IDS.glmInternational,
20
+ PROVIDER_IDS.glmInternationalLegacy,
21
+ ]
22
+
23
+ function asRecord(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value) ? value : undefined
25
+ }
26
+
27
+ function readPiAiProviders(ctx) {
28
+ try {
29
+ const descriptions = ctx.settings?.describe?.()
30
+ if (!Array.isArray(descriptions)) return {}
31
+ const section = descriptions.find((entry) => entry?.ns === 'llm-pi-ai')
32
+ const value = section?.value ?? section?.user
33
+ return asRecord(value?.providers) ?? {}
34
+ } catch {
35
+ return {}
36
+ }
37
+ }
38
+
39
+ function configuredGlmProvider(ctx) {
40
+ const providers = readPiAiProviders(ctx)
41
+ return GLM_PROVIDER_IDS.find((provider) => asRecord(providers[provider]) !== undefined)
42
+ }
43
+
44
+ /** Resolve the current GLM key reference anew for each probe. */
45
+ export async function resolveGlmCredential(ctx, provider) {
46
+ const profile = asRecord(readPiAiProviders(ctx)[provider])
47
+ const apiKeyEnv = profile?.apiKeyEnv
48
+ if (typeof apiKeyEnv !== 'string' || !isCredentialRefName(apiKeyEnv)) return undefined
49
+ try {
50
+ const resolved = await ctx.credentials.resolve(credentialRef(apiKeyEnv))
51
+ return typeof resolved?.value === 'string' && resolved.value !== '' ? resolved.value : undefined
52
+ } catch {
53
+ return undefined
54
+ }
55
+ }
56
+
57
+ /** Read only the original GitHub OAuth token needed by /copilot_internal/user. */
58
+ export async function resolveCopilotOAuthToken(ctx) {
59
+ try {
60
+ const record = await ctx.credentials.readRecord(COPILOT_CREDENTIAL_KEY)
61
+ if (record?.kind !== 'grant') return undefined
62
+ const payload = asRecord(record.payload)
63
+ if (payload?.type !== 'oauth') return undefined
64
+ return typeof payload.refresh === 'string' && payload.refresh !== '' ? payload.refresh : undefined
65
+ } catch {
66
+ return undefined
67
+ }
68
+ }
69
+
70
+ function resolvePollInterval(value) {
71
+ const parsed = Number(value)
72
+ const seconds = Number.isFinite(parsed) ? Math.floor(parsed) : DEFAULT_POLL_INTERVAL_SEC
73
+ return Math.max(MIN_POLL_INTERVAL_SEC, Math.min(MAX_POLL_INTERVAL_SEC, seconds))
74
+ }
75
+
76
+ function initialProvider(key, displayName) {
77
+ return { key, displayName, status: 'loading', credential: 'unknown' }
78
+ }
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 '探测失败'
85
+ }
86
+
87
+ function responseJson(res, status, body) {
88
+ res.writeHead(status, {
89
+ 'content-type': 'application/json; charset=utf-8',
90
+ 'cache-control': 'no-store',
91
+ 'referrer-policy': 'no-referrer',
92
+ })
93
+ res.end(JSON.stringify(body))
94
+ }
95
+
96
+ function methodAllowed(req, res, method) {
97
+ if (req.method === method) return true
98
+ responseJson(res, 405, { ok: false, error: 'method not allowed' })
99
+ return false
100
+ }
101
+
102
+ function requestAllowed(ctx, req, res) {
103
+ if (isPairedOrLoopbackAllowed(ctx, req) && isSameOriginRequest(req)) return true
104
+ responseJson(res, 403, { ok: false, error: 'forbidden' })
105
+ return false
106
+ }
107
+
108
+ export function createQuotaService(ctx, options = {}) {
109
+ const fetchImpl = options.fetchImpl ?? globalThis.fetch
110
+ const now = options.now ?? Date.now
111
+ const pollIntervalSec = resolvePollInterval(options.pollIntervalSec)
112
+ const providers = new Map([
113
+ ['glm', initialProvider('glm', 'GLM Coding Plan')],
114
+ ['copilot', initialProvider('copilot', 'GitHub Copilot')],
115
+ ])
116
+ let updatedAt
117
+ let pollPromise
118
+ let timer
119
+ let disposed = false
120
+
121
+ function view() {
122
+ return {
123
+ ...(updatedAt !== undefined ? { updatedAt } : {}),
124
+ providers: ['glm', 'copilot'].map((key) => {
125
+ const provider = providers.get(key)
126
+ return {
127
+ key: provider.key,
128
+ displayName: provider.displayName,
129
+ status: provider.status,
130
+ credential: provider.credential,
131
+ ...(provider.updatedAt !== undefined ? { updatedAt: provider.updatedAt } : {}),
132
+ ...(provider.checkedAt !== undefined ? { checkedAt: provider.checkedAt } : {}),
133
+ ...(provider.error !== undefined ? { error: provider.error } : {}),
134
+ ...(provider.plan !== undefined ? { plan: provider.plan } : {}),
135
+ ...(provider.quota !== undefined ? { quota: provider.quota } : {}),
136
+ }
137
+ }),
138
+ }
139
+ }
140
+
141
+ function markMissing(key, displayName) {
142
+ providers.set(key, {
143
+ key,
144
+ displayName,
145
+ status: 'missing',
146
+ credential: 'none',
147
+ checkedAt: now(),
148
+ })
149
+ }
150
+
151
+ function markSuccess(key, displayName, payload) {
152
+ const at = now()
153
+ providers.set(key, {
154
+ key,
155
+ displayName,
156
+ status: 'ready',
157
+ credential: 'configured',
158
+ updatedAt: at,
159
+ checkedAt: at,
160
+ ...payload,
161
+ })
162
+ }
163
+
164
+ function markFailure(key, displayName, error) {
165
+ const previous = providers.get(key)
166
+ const checkedAt = now()
167
+ const hasLastGood = previous?.status === 'ready' || previous?.status === 'stale'
168
+ providers.set(key, {
169
+ ...(hasLastGood ? previous : { key, displayName, credential: 'configured' }),
170
+ status: hasLastGood ? 'stale' : 'error',
171
+ checkedAt,
172
+ error: safeProbeError(error),
173
+ })
174
+ }
175
+
176
+ async function fetchBody(spec) {
177
+ const headers = new Headers(spec.headers)
178
+ headers.set('accept-encoding', 'identity')
179
+ const response = await fetchImpl(spec.url, {
180
+ headers,
181
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
182
+ })
183
+ const body = await response.json().catch(() => undefined)
184
+ if (!response.ok) {
185
+ const error = new Error()
186
+ error.status = response.status
187
+ throw error
188
+ }
189
+ return { status: response.status, body }
190
+ }
191
+
192
+ async function probeGlm() {
193
+ const provider = configuredGlmProvider(ctx)
194
+ if (provider === undefined) {
195
+ markMissing('glm', 'GLM Coding Plan')
196
+ return
197
+ }
198
+ const apiKey = await resolveGlmCredential(ctx, provider)
199
+ if (apiKey === undefined) {
200
+ markMissing('glm', 'GLM Coding Plan')
201
+ return
202
+ }
203
+ try {
204
+ const spec = buildGlmProbe({ provider, apiKey })
205
+ if (spec === undefined) throw Object.assign(new Error(), { code: 'UNRECOGNIZED_RESPONSE' })
206
+ const { status, body } = await fetchBody(spec)
207
+ const plan = parseGlmPlan(status, body)
208
+ if (plan === undefined) throw Object.assign(new Error(), { code: 'UNRECOGNIZED_RESPONSE' })
209
+ markSuccess('glm', 'GLM Coding Plan', { plan })
210
+ } catch (error) {
211
+ markFailure('glm', 'GLM Coding Plan', error)
212
+ }
213
+ }
214
+
215
+ async function probeCopilot() {
216
+ const githubOAuthToken = await resolveCopilotOAuthToken(ctx)
217
+ if (githubOAuthToken === undefined) {
218
+ markMissing('copilot', 'GitHub Copilot')
219
+ return
220
+ }
221
+ try {
222
+ const spec = buildCopilotProbe({ githubOAuthToken })
223
+ if (spec === undefined) throw Object.assign(new Error(), { code: 'UNRECOGNIZED_RESPONSE' })
224
+ const { status, body } = await fetchBody(spec)
225
+ const quota = parseCopilotUser(status, body)
226
+ if (quota === undefined) throw Object.assign(new Error(), { code: 'UNRECOGNIZED_RESPONSE' })
227
+ markSuccess('copilot', 'GitHub Copilot', { quota })
228
+ } catch (error) {
229
+ markFailure('copilot', 'GitHub Copilot', error)
230
+ }
231
+ }
232
+
233
+ function refresh() {
234
+ if (pollPromise !== undefined) return pollPromise
235
+ if (disposed) return Promise.resolve(view())
236
+ pollPromise = Promise.all([probeGlm(), probeCopilot()])
237
+ .then(() => {
238
+ updatedAt = now()
239
+ return view()
240
+ })
241
+ .finally(() => { pollPromise = undefined })
242
+ return pollPromise
243
+ }
244
+
245
+ function schedule(delayMs) {
246
+ if (disposed) return
247
+ timer = setTimeout(async () => {
248
+ timer = undefined
249
+ await refresh()
250
+ schedule(pollIntervalSec * 1000)
251
+ }, delayMs)
252
+ timer.unref?.()
253
+ }
254
+
255
+ return {
256
+ overview: view,
257
+ refresh,
258
+ start() {
259
+ if (disposed || timer !== undefined) return
260
+ schedule(2_000)
261
+ },
262
+ stop() {
263
+ disposed = true
264
+ if (timer !== undefined) clearTimeout(timer)
265
+ timer = undefined
266
+ },
267
+ pollIntervalSec,
268
+ }
269
+ }
270
+
271
+ export function makeOverviewRoute(ctx, service) {
272
+ return {
273
+ kind: 'exact',
274
+ path: ROUTES.overview,
275
+ handler: (req, res) => {
276
+ if (!requestAllowed(ctx, req, res) || !methodAllowed(req, res, 'GET')) return
277
+ responseJson(res, 200, service.overview())
278
+ },
279
+ }
280
+ }
281
+
282
+ export function makeRefreshRoute(ctx, service) {
283
+ return {
284
+ kind: 'exact',
285
+ path: ROUTES.refresh,
286
+ handler: async (req, res) => {
287
+ if (!requestAllowed(ctx, req, res) || !methodAllowed(req, res, 'POST')) return
288
+ try {
289
+ await service.refresh()
290
+ responseJson(res, 200, service.overview())
291
+ } catch {
292
+ responseJson(res, 500, { ok: false, error: 'refresh failed' })
293
+ }
294
+ },
295
+ }
296
+ }
297
+
298
+ export function apply(ctx, config = {}) {
299
+ const service = createQuotaService(ctx, { pollIntervalSec: config.pollIntervalSec })
300
+ const disposers = [
301
+ ctx.webServer.register(makeOverviewRoute(ctx, service)),
302
+ ctx.webServer.register(makeRefreshRoute(ctx, service)),
303
+ ]
304
+ service.start()
305
+ ctx.effect(() => () => {
306
+ service.stop()
307
+ for (const dispose of disposers) {
308
+ try { dispose() } catch { /* Route fiber may already be down. */ }
309
+ }
310
+ }, 'dsh-quota-watch: runtime')
311
+ }
package/src/shared.mjs ADDED
@@ -0,0 +1,25 @@
1
+ export const API_PREFIX = '/api/dsh-quota-watch'
2
+
3
+ export const ROUTES = Object.freeze({
4
+ overview: `${API_PREFIX}/overview`,
5
+ refresh: `${API_PREFIX}/refresh`,
6
+ })
7
+
8
+ // The Web UI is served under <base href="./">; clients must use document-relative paths.
9
+ export const CLIENT_ROUTES = Object.freeze({
10
+ overview: `${API_PREFIX.slice(1)}/overview`,
11
+ refresh: `${API_PREFIX.slice(1)}/refresh`,
12
+ })
13
+
14
+ export const PROVIDER_IDS = Object.freeze({
15
+ glmDomestic: 'zai-coding-cn',
16
+ glmInternational: 'zai',
17
+ glmInternationalLegacy: 'zai-coding',
18
+ copilot: 'github-copilot',
19
+ })
20
+
21
+ export const DEFAULT_POLL_INTERVAL_SEC = 60
22
+ export const MIN_POLL_INTERVAL_SEC = 30
23
+ export const MAX_POLL_INTERVAL_SEC = 3600
24
+ export const PROBE_TIMEOUT_MS = 10_000
25
+ export const CLIENT_POLL_INTERVAL_MS = 30_000