@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/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
|