@goodandready/dsh-subscriptions 0.6.8 → 0.6.10
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/README.md +23 -0
- package/README.ru.md +23 -0
- package/README.zh.md +23 -0
- package/lib/accounts.js +4 -19
- package/lib/adapter.js +6 -5
- package/lib/client.js +113 -37
- package/lib/http.js +7 -0
- package/lib/index.js +18 -16
- package/lib/mask.js +1 -1
- package/lib/relative-time.js +0 -8
- package/lib/routes/proxy.js +1 -1
- package/lib/routes/status.js +22 -17
- package/lib/routes.js +9 -2
- package/lib/subscriptions.js +5 -5
- package/lib/updater.js +260 -0
- package/lib/usage.js +14 -14
- package/lib/utils.js +25 -0
- package/lib/vendor-factory.js +3 -2
- package/lib/vendors/grok.js +1 -1
- package/lib/wire.js +3 -1
- package/package.json +10 -7
package/lib/mask.js
CHANGED
|
@@ -30,7 +30,7 @@ export function cleanErrorMessage(raw) {
|
|
|
30
30
|
if (parsed.error && parsed.error.message) text = parsed.error.message
|
|
31
31
|
else if (parsed.message) text = parsed.message
|
|
32
32
|
else if (parsed.code) text = parsed.code
|
|
33
|
-
} catch {}
|
|
33
|
+
} catch (err) { /* ignore JSON parse failure */ }
|
|
34
34
|
}
|
|
35
35
|
const firstLine = text.split('\n')[0].trim()
|
|
36
36
|
if (firstLine.length > 160) {
|
package/lib/relative-time.js
CHANGED
package/lib/routes/proxy.js
CHANGED
|
@@ -21,7 +21,7 @@ export function registerProxyRoutes(ctx, state) {
|
|
|
21
21
|
}
|
|
22
22
|
const raw = await readBody(req, 128 * 1024).catch(() => Buffer.alloc(0))
|
|
23
23
|
let body = {}
|
|
24
|
-
try { body = JSON.parse(raw.toString('utf8') || '{}') } catch {}
|
|
24
|
+
try { body = JSON.parse(raw.toString('utf8') || '{}') } catch (err) { body = {} }
|
|
25
25
|
const events = Array.isArray(body && body.events) ? body.events : []
|
|
26
26
|
const analysis = analyzeSessionEvents(events)
|
|
27
27
|
writeJson(res, 200, { ok: true, analysis })
|
package/lib/routes/status.js
CHANGED
|
@@ -95,7 +95,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
95
95
|
expires[slot.provider] = Math.max(expires[slot.provider] || 0, slot.expiresAt)
|
|
96
96
|
labels[slot.provider] = pmL(slot.label || info.label || slot.provider)
|
|
97
97
|
}
|
|
98
|
-
} catch {}
|
|
98
|
+
} catch (err) { /* ignore status inspection error */ }
|
|
99
99
|
}
|
|
100
100
|
// #69/#72: active subscription = last successful request (newest first).
|
|
101
101
|
let active = null
|
|
@@ -118,7 +118,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
118
118
|
.filter((w) => w && typeof w.usedPercent === 'number')
|
|
119
119
|
.map((w) => ({ id: w.id || w.en || w.ru, label: w.en || w.ru || w.id, usedPercent: w.usedPercent }))
|
|
120
120
|
}
|
|
121
|
-
} catch {}
|
|
121
|
+
} catch (err) { /* ignore status inspection error */ }
|
|
122
122
|
active = {
|
|
123
123
|
provider: lastRow.provider,
|
|
124
124
|
index,
|
|
@@ -177,26 +177,27 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
177
177
|
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
178
178
|
return
|
|
179
179
|
}
|
|
180
|
-
const provider = payload.provider
|
|
180
|
+
const provider = payload.provider || payload.vendor
|
|
181
|
+
const index = payload.index || (payload.accountIndex != null ? payload.accountIndex + 1 : 1)
|
|
181
182
|
if (!isProvider(provider)) {
|
|
182
183
|
writeJson(res, 200, { ok: false, provider, error: { code: 'provider', message: 'unknown provider' } })
|
|
183
184
|
return
|
|
184
185
|
}
|
|
185
186
|
const t0 = Date.now()
|
|
186
|
-
const ref = oauthRef(provider,
|
|
187
|
+
const ref = oauthRef(provider, index)
|
|
187
188
|
const info = await store.describeRef(ref)
|
|
188
189
|
if (!info.configured) {
|
|
189
|
-
writeJson(res, 200, { ok: false, provider, index:
|
|
190
|
+
writeJson(res, 200, { ok: false, provider, index: index, ref, error: { code: 'not_connected', message: 'not connected' } })
|
|
190
191
|
return
|
|
191
192
|
}
|
|
192
193
|
let blob
|
|
193
194
|
try { blob = await store.loadBlob(ref) } catch (e) {
|
|
194
|
-
writeJson(res, 200, { ok: false, provider, index:
|
|
195
|
+
writeJson(res, 200, { ok: false, provider, index: index, ref, error: { code: 'auth', message: String(e && e.message || e) } })
|
|
195
196
|
return
|
|
196
197
|
}
|
|
197
198
|
try { blob = await store.ensureFresh(provider, blob, ref) } catch (e) {
|
|
198
199
|
writeJson(res, 200, {
|
|
199
|
-
ok: false, provider, index:
|
|
200
|
+
ok: false, provider, index: index, ref,
|
|
200
201
|
email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
|
|
201
202
|
quota: info.quota || null,
|
|
202
203
|
error: { code: 'refresh', message: String(e && e.message || e) },
|
|
@@ -205,25 +206,27 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
205
206
|
}
|
|
206
207
|
const vendor = getVendor(provider)
|
|
207
208
|
if (typeof vendor.check !== 'function') {
|
|
208
|
-
writeJson(res, 200, { ok: true, provider, index:
|
|
209
|
+
writeJson(res, 200, { ok: true, provider, index: index, ref, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: info.quota || null })
|
|
209
210
|
return
|
|
210
211
|
}
|
|
211
212
|
let capturedQuota = null
|
|
212
|
-
const probeFetch = async (url, init) => {
|
|
213
|
-
const
|
|
213
|
+
const probeFetch = async (url, init = {}) => {
|
|
214
|
+
const timeoutSignal = AbortSignal.timeout(15_000)
|
|
215
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
216
|
+
const res2 = await fetch(url, { ...init, signal })
|
|
214
217
|
try {
|
|
215
218
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
216
219
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
|
217
|
-
} catch {}
|
|
220
|
+
} catch (err) { /* ignore status inspection error */ }
|
|
218
221
|
return res2
|
|
219
222
|
}
|
|
220
223
|
try {
|
|
221
224
|
await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
|
|
222
225
|
const latencyMs = Date.now() - t0;
|
|
223
|
-
writeJson(res, 200, { ok: true, provider, index:
|
|
226
|
+
writeJson(res, 200, { ok: true, provider, index: index, ref, latencyMs, email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null, quota: capturedQuota || info.quota || null, usagePercent: info.usagePercent ?? null })
|
|
224
227
|
} catch (e) {
|
|
225
228
|
writeJson(res, 200, {
|
|
226
|
-
ok: false, provider, index:
|
|
229
|
+
ok: false, provider, index: index, ref,
|
|
227
230
|
latencyMs: Date.now() - t0,
|
|
228
231
|
email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
|
|
229
232
|
quota: capturedQuota || info.quota || null,
|
|
@@ -280,7 +283,7 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
280
283
|
return
|
|
281
284
|
}
|
|
282
285
|
let payload = {}
|
|
283
|
-
try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch {}
|
|
286
|
+
try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch (err) { /* ignore status inspection error */ }
|
|
284
287
|
|
|
285
288
|
let provider = payload.provider
|
|
286
289
|
let index = payload.index || 1
|
|
@@ -321,12 +324,14 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
321
324
|
const vendor = getVendor(provider)
|
|
322
325
|
if (typeof vendor.check === 'function') {
|
|
323
326
|
let capturedQuota = null
|
|
324
|
-
const probeFetch = async (url, init) => {
|
|
325
|
-
const
|
|
327
|
+
const probeFetch = async (url, init = {}) => {
|
|
328
|
+
const timeoutSignal = AbortSignal.timeout(15_000)
|
|
329
|
+
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
|
|
330
|
+
const res2 = await fetch(url, { ...init, signal })
|
|
326
331
|
try {
|
|
327
332
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
328
333
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
|
329
|
-
} catch {}
|
|
334
|
+
} catch (err) { /* ignore status inspection error */ }
|
|
330
335
|
return res2
|
|
331
336
|
}
|
|
332
337
|
try {
|
package/lib/routes.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { publicConfig } from './config-schema.js'
|
|
2
2
|
import { PROVIDERS, displayName } from './refs.js'
|
|
3
|
-
import { escapeHtml, writeHtml, writeJson } from './http.js'
|
|
3
|
+
import { escapeHtml, isLoopback, writeHtml, writeJson } from './http.js'
|
|
4
4
|
import { registerStatusRoutes } from './routes/status.js'
|
|
5
5
|
import { registerOauthRoutes } from './routes/oauth.js'
|
|
6
6
|
import { registerAccountsRoutes } from './routes/accounts.js'
|
|
7
7
|
import { registerProxyRoutes } from './routes/proxy.js'
|
|
8
|
+
import { registerPluginUpdater } from './updater.js'
|
|
8
9
|
|
|
9
10
|
export function registerRoutes(ctx, state) {
|
|
10
11
|
const {
|
|
@@ -22,6 +23,11 @@ export function registerRoutes(ctx, state) {
|
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
26
|
+
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
27
|
+
endpoint: '/dsh-subscriptions/update',
|
|
28
|
+
packageName: '@goodandready/dsh-subscriptions',
|
|
29
|
+
manifestUrl: new URL('../package.json', import.meta.url),
|
|
30
|
+
}), 'dsh-subscriptions: /update')
|
|
25
31
|
|
|
26
32
|
ctx.effect(() => ctx.webServer.register({
|
|
27
33
|
kind: 'exact',
|
|
@@ -32,7 +38,8 @@ export function registerRoutes(ctx, state) {
|
|
|
32
38
|
return
|
|
33
39
|
}
|
|
34
40
|
const host = (req.headers.host || '').split(':')[0]
|
|
35
|
-
|
|
41
|
+
const remote = req.socket?.remoteAddress
|
|
42
|
+
if (!isLoopback(host) || (remote && !isLoopback(remote))) {
|
|
36
43
|
writeHtml(res, 403, '<!doctype html><meta charset="utf-8"><p>Subscriptions overview is localhost-only.</p>')
|
|
37
44
|
return
|
|
38
45
|
}
|
package/lib/subscriptions.js
CHANGED
|
@@ -20,7 +20,7 @@ export function isAllowed(provider, path) {
|
|
|
20
20
|
p = p.split("?")[0].split("#")[0]
|
|
21
21
|
try {
|
|
22
22
|
if (p.startsWith("http://") || p.startsWith("https://")) p = new URL(p).pathname
|
|
23
|
-
} catch {}
|
|
23
|
+
} catch (err) { /* ignore subscriptions error */ }
|
|
24
24
|
if (!p.startsWith("/")) p = "/" + p
|
|
25
25
|
for (const allowed of list) {
|
|
26
26
|
const a = allowed.split("?")[0]
|
|
@@ -140,17 +140,17 @@ export function createSubscriptionsService(deps) {
|
|
|
140
140
|
try {
|
|
141
141
|
const snap = quotaSnapshot(provider, res.headers, null, Date.now())
|
|
142
142
|
if (snap && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap)
|
|
143
|
-
} catch {}
|
|
143
|
+
} catch (err) { /* ignore subscriptions error */ }
|
|
144
144
|
if (!res.ok && res.clone) {
|
|
145
145
|
try {
|
|
146
146
|
const txt = await res.clone().text()
|
|
147
147
|
let j = null
|
|
148
|
-
try { j = JSON.parse(txt) } catch {}
|
|
148
|
+
try { j = JSON.parse(txt) } catch (err) { /* ignore subscriptions error */ }
|
|
149
149
|
if (j) {
|
|
150
150
|
const snap2 = quotaSnapshot(provider, res.headers, j, Date.now())
|
|
151
151
|
if (snap2 && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap2)
|
|
152
152
|
}
|
|
153
|
-
} catch {}
|
|
153
|
+
} catch (err) { /* ignore subscriptions error */ }
|
|
154
154
|
}
|
|
155
155
|
if (!res.ok) {
|
|
156
156
|
const txt = await res.text().catch(() => "")
|
|
@@ -174,7 +174,7 @@ export function createSubscriptionsService(deps) {
|
|
|
174
174
|
ms: Date.now() - t0,
|
|
175
175
|
kind: "request",
|
|
176
176
|
})
|
|
177
|
-
} catch {}
|
|
177
|
+
} catch (err) { /* ignore subscriptions error */ }
|
|
178
178
|
}
|
|
179
179
|
return res
|
|
180
180
|
} catch (err) {
|
package/lib/updater.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { basename, dirname, isAbsolute, resolve } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { writeJson } from './http.js'
|
|
8
|
+
|
|
9
|
+
const UPDATE_HEADER = 'x-dsh-plugin-update'
|
|
10
|
+
const UPDATE_TIMEOUT_MS = 10 * 60_000
|
|
11
|
+
const VERSION_CACHE_MS = 5 * 60_000
|
|
12
|
+
let latestCache
|
|
13
|
+
|
|
14
|
+
function header(request, name) {
|
|
15
|
+
const value = request.headers?.[name]
|
|
16
|
+
return Array.isArray(value) ? value[0] : value
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isLoopback(value) {
|
|
20
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
21
|
+
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
22
|
+
|| address?.startsWith('127.') === true
|
|
23
|
+
|| address?.startsWith('::ffff:127.') === true
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isTrustedUpdateRequest(request) {
|
|
27
|
+
const customHeader = header(request, UPDATE_HEADER)
|
|
28
|
+
if (customHeader !== '1') {
|
|
29
|
+
// In browser fetch, allow if sec-fetch-site is same-origin or same-site
|
|
30
|
+
const site = header(request, 'sec-fetch-site')
|
|
31
|
+
if (site === 'cross-site') return false
|
|
32
|
+
}
|
|
33
|
+
const remote = request.socket?.remoteAddress
|
|
34
|
+
if (remote && !isLoopback(remote)) return false
|
|
35
|
+
const origin = header(request, 'origin')
|
|
36
|
+
const host = header(request, 'host')
|
|
37
|
+
if (origin && host) {
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(origin)
|
|
40
|
+
if (url.host !== host && !isLoopback(url.hostname)) return false
|
|
41
|
+
} catch {
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function validProfileName(value) {
|
|
49
|
+
return typeof value === 'string' && value !== '' && value !== '.' && value !== '..'
|
|
50
|
+
&& !value.includes('/') && !value.includes('\\') && !/[\0-\x1f\x7f]/.test(value)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function profileNameFromArgv(argv) {
|
|
54
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
55
|
+
if (argv[index] === '--profile') return argv[index + 1]
|
|
56
|
+
if (argv[index]?.startsWith('--profile=')) return argv[index].slice('--profile='.length)
|
|
57
|
+
}
|
|
58
|
+
return argv[2] === 'web' ? 'web' : undefined
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findDshCliEntry() {
|
|
62
|
+
const value = process.argv[1]
|
|
63
|
+
if (value === undefined || value === '') return undefined
|
|
64
|
+
const entry = value.startsWith('file:') ? fileURLToPath(value) : resolve(process.cwd(), value)
|
|
65
|
+
if (!existsSync(entry)) return undefined
|
|
66
|
+
for (let directory = dirname(entry); ; directory = dirname(directory)) {
|
|
67
|
+
const manifestPath = resolve(directory, 'package.json')
|
|
68
|
+
if (existsSync(manifestPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
|
|
71
|
+
const bin = typeof manifest.bin === 'string'
|
|
72
|
+
? manifest.bin
|
|
73
|
+
: typeof manifest.bin === 'object' && manifest.bin !== null
|
|
74
|
+
? manifest.bin.dsh
|
|
75
|
+
: undefined
|
|
76
|
+
if (manifest.name === '@deepseek-ai/dsh' && typeof bin === 'string'
|
|
77
|
+
&& !isAbsolute(bin) && resolve(directory, bin) === resolve(entry)) return entry
|
|
78
|
+
} catch {
|
|
79
|
+
// Continue searching parent package directories.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const parent = dirname(directory)
|
|
83
|
+
if (parent === directory) return undefined
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getRuntime() {
|
|
88
|
+
const profileDir = resolve(process.env.DSH_PROFILE_DIR
|
|
89
|
+
?? resolve(homedir(), '.dsh', 'profiles', 'web'))
|
|
90
|
+
const selected = profileNameFromArgv(process.argv)
|
|
91
|
+
const profileName = validProfileName(selected)
|
|
92
|
+
? selected
|
|
93
|
+
: validProfileName(basename(profileDir)) ? basename(profileDir) : 'web'
|
|
94
|
+
const cliEntry = findDshCliEntry()
|
|
95
|
+
return cliEntry === undefined ? { profileName, profileDir } : { profileName, profileDir, cliEntry }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseSemver(value) {
|
|
99
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value)
|
|
100
|
+
if (match === null) return undefined
|
|
101
|
+
return {
|
|
102
|
+
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
103
|
+
prerelease: match[4]?.split('.') ?? [],
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function comparePrerelease(left, right) {
|
|
108
|
+
if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
|
|
109
|
+
const length = Math.max(left.length, right.length)
|
|
110
|
+
for (let index = 0; index < length; index += 1) {
|
|
111
|
+
const a = left[index]
|
|
112
|
+
const b = right[index]
|
|
113
|
+
if (a === undefined || b === undefined) return a === b ? 0 : a === undefined ? -1 : 1
|
|
114
|
+
if (a === b) continue
|
|
115
|
+
const aNumeric = /^\d+$/.test(a)
|
|
116
|
+
const bNumeric = /^\d+$/.test(b)
|
|
117
|
+
if (aNumeric && bNumeric) {
|
|
118
|
+
const aNumber = BigInt(a)
|
|
119
|
+
const bNumber = BigInt(b)
|
|
120
|
+
if (aNumber !== bNumber) return aNumber > bNumber ? 1 : -1
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
|
|
124
|
+
return a > b ? 1 : -1
|
|
125
|
+
}
|
|
126
|
+
return 0
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function isNewerVersion(currentValue, candidateValue) {
|
|
130
|
+
const current = parseSemver(currentValue)
|
|
131
|
+
const candidate = parseSemver(candidateValue)
|
|
132
|
+
if (current === undefined || candidate === undefined) return false
|
|
133
|
+
for (let index = 0; index < 3; index += 1) {
|
|
134
|
+
if (candidate.core[index] !== current.core[index]) return candidate.core[index] > current.core[index]
|
|
135
|
+
}
|
|
136
|
+
return comparePrerelease(candidate.prerelease, current.prerelease) > 0
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function fetchLatestVersion(packageName, registry = 'https://registry.npmjs.org') {
|
|
140
|
+
if (latestCache?.packageName === packageName && latestCache.registry === registry && Date.now() < latestCache.expiresAt) {
|
|
141
|
+
return latestCache.version
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const response = await fetch(`${registry.replace(/\/$/, '')}/${encodeURIComponent(packageName)}/latest`, {
|
|
145
|
+
signal: AbortSignal.timeout(8_000),
|
|
146
|
+
})
|
|
147
|
+
if (!response.ok) return undefined
|
|
148
|
+
const value = await response.json()
|
|
149
|
+
if (typeof value?.version !== 'string' || value.version === '') return undefined
|
|
150
|
+
latestCache = { packageName, registry, version: value.version, expiresAt: Date.now() + VERSION_CACHE_MS }
|
|
151
|
+
return value.version
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function readCurrentVersion(manifestUrl) {
|
|
158
|
+
const value = JSON.parse(await readFile(manifestUrl, 'utf8'))
|
|
159
|
+
if (typeof value?.version !== 'string' || value.version === '') throw new Error('Cannot read current plugin version.')
|
|
160
|
+
return value.version
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function getUpdaterStatus(options, target = getRuntime()) {
|
|
164
|
+
const current = await readCurrentVersion(options.manifestUrl)
|
|
165
|
+
const latest = await fetchLatestVersion(options.packageName, options.registry ?? 'https://registry.npmjs.org')
|
|
166
|
+
return {
|
|
167
|
+
ok: true,
|
|
168
|
+
packageName: options.packageName,
|
|
169
|
+
currentVersion: current,
|
|
170
|
+
...(latest === undefined ? {} : { latestVersion: latest }),
|
|
171
|
+
latestCheckFailed: latest === undefined,
|
|
172
|
+
updateAvailable: latest !== undefined && isNewerVersion(current, latest),
|
|
173
|
+
profileName: target.profileName,
|
|
174
|
+
canAutoUpdate: target.cliEntry !== undefined,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function installExactPackage(target, packageSpec, options) {
|
|
179
|
+
if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.')
|
|
180
|
+
await new Promise((resolvePromise, reject) => {
|
|
181
|
+
const child = spawn(process.execPath, [
|
|
182
|
+
target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
|
|
183
|
+
'--config.minimumReleaseAge=0', packageSpec,
|
|
184
|
+
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
185
|
+
], {
|
|
186
|
+
cwd: target.profileDir,
|
|
187
|
+
windowsHide: true,
|
|
188
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
189
|
+
env: { ...process.env, NO_COLOR: '1' },
|
|
190
|
+
})
|
|
191
|
+
let detail = ''
|
|
192
|
+
child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
|
|
193
|
+
child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000) })
|
|
194
|
+
const timer = setTimeout(() => {
|
|
195
|
+
child.kill()
|
|
196
|
+
reject(new Error('Update timed out; use the normal DSH update flow.'))
|
|
197
|
+
}, UPDATE_TIMEOUT_MS)
|
|
198
|
+
child.once('error', error => { clearTimeout(timer); reject(error) })
|
|
199
|
+
child.once('exit', code => {
|
|
200
|
+
clearTimeout(timer)
|
|
201
|
+
if (code === 0) resolvePromise()
|
|
202
|
+
else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`))
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function registerPluginUpdater(ctx, options) {
|
|
208
|
+
let installing = false
|
|
209
|
+
return ctx.webServer.register({
|
|
210
|
+
kind: 'exact',
|
|
211
|
+
path: options.endpoint,
|
|
212
|
+
handler: async (request, response) => {
|
|
213
|
+
try {
|
|
214
|
+
const target = getRuntime()
|
|
215
|
+
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
216
|
+
const payload = await getUpdaterStatus(options, target)
|
|
217
|
+
writeJson(response, 200, payload)
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
if (request.method !== 'POST') {
|
|
221
|
+
response.writeHead(405, { allow: 'GET, HEAD, POST' })
|
|
222
|
+
response.end()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
if (!isTrustedUpdateRequest(request)) {
|
|
226
|
+
writeJson(response, 403, { ok: false, error: { code: 'forbidden', message: 'Rejected non-local or cross-origin update request.' } })
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
if (installing) {
|
|
230
|
+
writeJson(response, 409, { ok: false, error: { code: 'busy', message: 'This plugin is already updating.' } })
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
installing = true
|
|
234
|
+
try {
|
|
235
|
+
const before = await getUpdaterStatus(options, target)
|
|
236
|
+
if (before.latestVersion === undefined) {
|
|
237
|
+
writeJson(response, 503, { ok: false, error: { code: 'unavailable', message: 'The latest version is temporarily unavailable.' } })
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
if (!before.updateAvailable) {
|
|
241
|
+
writeJson(response, 200, { ok: true, ...before })
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
await installExactPackage(target, `${options.packageName}@${before.latestVersion}`, options)
|
|
245
|
+
writeJson(response, 200, {
|
|
246
|
+
ok: true,
|
|
247
|
+
...before,
|
|
248
|
+
updatedVersion: before.latestVersion,
|
|
249
|
+
restartRequired: true,
|
|
250
|
+
})
|
|
251
|
+
} finally {
|
|
252
|
+
installing = false
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`)
|
|
256
|
+
writeJson(response, 503, { ok: false, error: { code: 'update_failed', message: String(error?.message || error) } })
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
}
|
package/lib/usage.js
CHANGED
|
@@ -52,22 +52,22 @@ export function asUsageSnapshot(percent) {
|
|
|
52
52
|
// Named limit windows reported by vendors (five_hour, seven_day, primary...).
|
|
53
53
|
// Label map is a data table: extend it when a vendor adds a window name.
|
|
54
54
|
const WINDOW_LABELS = {
|
|
55
|
-
five_hour: { zh: "5小时",
|
|
56
|
-
seven_day: { zh: "7天",
|
|
57
|
-
seven_day_oauth_apps: { zh: "7天应用",
|
|
58
|
-
weekly_limit_7_days: { zh: "7天",
|
|
59
|
-
primary: { zh: "主配额",
|
|
60
|
-
secondary: { zh: "次配额",
|
|
55
|
+
five_hour: { zh: "5小时", en: "5h" },
|
|
56
|
+
seven_day: { zh: "7天", en: "7d" },
|
|
57
|
+
seven_day_oauth_apps: { zh: "7天应用", en: "7d apps" },
|
|
58
|
+
weekly_limit_7_days: { zh: "7天", en: "7d" },
|
|
59
|
+
primary: { zh: "主配额", en: "primary" },
|
|
60
|
+
secondary: { zh: "次配额", en: "secondary" },
|
|
61
61
|
// codex: the vendor API returns primary_window/secondary_window - surfaced as 5h/7d in the UI
|
|
62
|
-
primary_window: { zh: "5小时",
|
|
63
|
-
secondary_window: { zh: "7天",
|
|
64
|
-
monthly: { zh: "月度",
|
|
65
|
-
credits: { zh: "点数",
|
|
62
|
+
primary_window: { zh: "5小时", en: "5h" },
|
|
63
|
+
secondary_window: { zh: "7天", en: "7d" },
|
|
64
|
+
monthly: { zh: "月度", en: "month" },
|
|
65
|
+
credits: { zh: "点数", en: "credits" },
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function windowLabel(id) {
|
|
69
69
|
const known = WINDOW_LABELS[id]
|
|
70
|
-
return known || { zh: id,
|
|
70
|
+
return known || { zh: id, en: id }
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Walk vendor usage JSON; every node carrying a utilization-style number
|
|
@@ -83,19 +83,19 @@ export function usageWindows(obj) {
|
|
|
83
83
|
if (pct != null && key && !seen.has(key)) {
|
|
84
84
|
seen.add(key)
|
|
85
85
|
const label = windowLabel(key)
|
|
86
|
-
out.push({ id: key,
|
|
86
|
+
out.push({ id: key, zh: label.zh, en: label.en, usedPercent: pct })
|
|
87
87
|
}
|
|
88
88
|
const frac = numberOrNull(value.remainingFraction)
|
|
89
89
|
if (frac != null && key && !seen.has(key)) {
|
|
90
90
|
seen.add(key)
|
|
91
91
|
const label = windowLabel(key)
|
|
92
|
-
out.push({ id: key,
|
|
92
|
+
out.push({ id: key, zh: label.zh, en: label.en, usedPercent: (1 - frac) * 100 })
|
|
93
93
|
}
|
|
94
94
|
const ufrac = numberOrNull(value.usedFraction)
|
|
95
95
|
if (ufrac != null && key && !seen.has(key)) {
|
|
96
96
|
seen.add(key)
|
|
97
97
|
const label = windowLabel(key)
|
|
98
|
-
out.push({ id: key,
|
|
98
|
+
out.push({ id: key, zh: label.zh, en: label.en, usedPercent: ufrac * 100 })
|
|
99
99
|
}
|
|
100
100
|
for (const [k, child] of Object.entries(value)) {
|
|
101
101
|
if (child && typeof child === "object") walk(child, k, depth + 1)
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes a function safely, capturing any error and logging it at debug level
|
|
3
|
+
* if a logger is provided, then returning the fallback value.
|
|
4
|
+
*
|
|
5
|
+
* Used across the plugin to eliminate empty catch blocks per dsh-plugin-preflight.
|
|
6
|
+
*/
|
|
7
|
+
export function bestEffort(label, fn, fallback = undefined, logger = null) {
|
|
8
|
+
try {
|
|
9
|
+
const res = fn()
|
|
10
|
+
if (res && typeof res.then === 'function') {
|
|
11
|
+
return res.catch((err) => {
|
|
12
|
+
if (logger && typeof logger.debug === 'function') {
|
|
13
|
+
logger.debug(`[bestEffort] ${label}: ${err && err.message || err}`)
|
|
14
|
+
}
|
|
15
|
+
return fallback
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
return res
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (logger && typeof logger.debug === 'function') {
|
|
21
|
+
logger.debug(`[bestEffort] ${label}: ${err && err.message || err}`)
|
|
22
|
+
}
|
|
23
|
+
return fallback
|
|
24
|
+
}
|
|
25
|
+
}
|
package/lib/vendor-factory.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fetchWithTimeout } from "./http.js"
|
|
1
2
|
// Vendor factory from a declarative profile.
|
|
2
3
|
//
|
|
3
4
|
// Covers the OpenAI-Responses-compatible subscription family: PKCE auth,
|
|
@@ -87,9 +88,9 @@ export function createVendorFromProfile(profile) {
|
|
|
87
88
|
const catalog = defaultModels(cfg.models || profile.models || [])
|
|
88
89
|
if (!profile.modelsPath) return catalog
|
|
89
90
|
try {
|
|
90
|
-
const res = await (fetchImpl || fetch
|
|
91
|
+
const res = await fetchWithTimeout(fetchImpl || fetch, `${base}${profile.modelsPath}`, {
|
|
91
92
|
headers: identityHeaders(blob, { Accept: "application/json" }),
|
|
92
|
-
})
|
|
93
|
+
}, { timeoutMs: 15000 })
|
|
93
94
|
const json = await readJson(res)
|
|
94
95
|
const rows = []
|
|
95
96
|
for (const entry of json.data || json.models || []) {
|
package/lib/vendors/grok.js
CHANGED
|
@@ -220,7 +220,7 @@ export async function usage(blob, cfg, fetchImpl) {
|
|
|
220
220
|
const decoded = decodeGrokCreditsFrame(Buffer.from(buf))
|
|
221
221
|
if (decoded != null) pct = decoded
|
|
222
222
|
}
|
|
223
|
-
} catch {}
|
|
223
|
+
} catch (err) { /* ignore grok usage capture error */ }
|
|
224
224
|
}
|
|
225
225
|
const snap = asUsageSnapshot(pct)
|
|
226
226
|
if (snap) snap.windows = usageWindows(json)
|
package/lib/wire.js
CHANGED
|
@@ -199,12 +199,14 @@ export async function formTokenRequest(url, params, fetchImpl, headers, { timeou
|
|
|
199
199
|
return readJson(res)
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
export async function jsonTokenRequest(url, body, fetchImpl, headers) {
|
|
202
|
+
export async function jsonTokenRequest(url, body, fetchImpl, headers, { timeoutMs = 25000 } = {}) {
|
|
203
203
|
const impl = fetchImpl || fetch
|
|
204
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
204
205
|
const res = await impl(url, {
|
|
205
206
|
method: 'POST',
|
|
206
207
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(headers || {}) },
|
|
207
208
|
body: JSON.stringify(body),
|
|
209
|
+
signal: timeoutSignal,
|
|
208
210
|
})
|
|
209
211
|
return readJson(res)
|
|
210
212
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.10",
|
|
4
4
|
"description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -34,13 +34,20 @@
|
|
|
34
34
|
"bugs": {
|
|
35
35
|
"url": "https://github.com/GooDAnDReaDY/dsh-subscriptions/issues"
|
|
36
36
|
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "eslint lib/ && node --test test/*.test.mjs",
|
|
39
|
+
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
40
|
+
},
|
|
37
41
|
"dsh": {
|
|
38
42
|
"bundle": {
|
|
39
43
|
"patch": "./cordis.patch.yml"
|
|
40
44
|
},
|
|
41
45
|
"client": {
|
|
42
46
|
"platform": "web",
|
|
43
|
-
"inject": [
|
|
47
|
+
"inject": [
|
|
48
|
+
"@deepseek-ai/dsh-client-locale",
|
|
49
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
50
|
+
]
|
|
44
51
|
}
|
|
45
52
|
},
|
|
46
53
|
"peerDependencies": {
|
|
@@ -62,9 +69,5 @@
|
|
|
62
69
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
63
70
|
"@types/node": "^22.20.2",
|
|
64
71
|
"typescript": "^7.0.2"
|
|
65
|
-
},
|
|
66
|
-
"scripts": {
|
|
67
|
-
"test": "eslint lib/ && node --test test/*.test.mjs",
|
|
68
|
-
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
69
72
|
}
|
|
70
|
-
}
|
|
73
|
+
}
|