@goodandready/dsh-subscriptions 0.6.7 → 0.6.9
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 +18 -0
- package/README.ru.md +18 -0
- package/README.zh.md +18 -0
- package/lib/accounts.js +0 -17
- package/lib/client.js +502 -284
- package/lib/http.js +7 -0
- package/lib/relative-time.js +11 -3
- package/lib/routes/status.js +22 -14
- package/lib/routes.js +9 -2
- package/lib/updater.js +260 -0
- package/lib/usage.js +11 -11
- package/package.json +6 -6
package/lib/http.js
CHANGED
|
@@ -30,6 +30,13 @@ export function readBody(req, maxBytes = 64 * 1024) {
|
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function isLoopback(value) {
|
|
34
|
+
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
35
|
+
return address === 'localhost' || address === 'localhost.' || address === '::1'
|
|
36
|
+
|| address?.startsWith('127.') === true
|
|
37
|
+
|| address?.startsWith('::ffff:127.') === true
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
/**
|
|
34
41
|
* @param {import('node:http').IncomingMessage} request
|
|
35
42
|
* @returns {boolean}
|
package/lib/relative-time.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Format a future timestamp as a relative remaining duration down to the minute.
|
|
3
|
-
* Supports en and
|
|
3
|
+
* Supports en and zh.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export const RELATIVE_UNITS = {
|
|
@@ -12,6 +12,14 @@ export const RELATIVE_UNITS = {
|
|
|
12
12
|
hour: '{n}h',
|
|
13
13
|
day: '{n}d',
|
|
14
14
|
},
|
|
15
|
+
zh: {
|
|
16
|
+
soon: '刚刚',
|
|
17
|
+
prefix: '',
|
|
18
|
+
suffix: '后',
|
|
19
|
+
minute: '{n}分',
|
|
20
|
+
hour: '{n}小时',
|
|
21
|
+
day: '{n}天',
|
|
22
|
+
},
|
|
15
23
|
ru: {
|
|
16
24
|
soon: 'только что',
|
|
17
25
|
prefix: 'через ',
|
|
@@ -32,10 +40,10 @@ function fill(template, n) {
|
|
|
32
40
|
* @param {number} [now]
|
|
33
41
|
* @returns {string} human-readable remaining time, down to the minute
|
|
34
42
|
*/
|
|
35
|
-
export function formatRelativeReset(resetAt, lang = '
|
|
43
|
+
export function formatRelativeReset(resetAt, lang = 'en', now = Date.now()) {
|
|
36
44
|
if (typeof resetAt !== 'number' || !Number.isFinite(resetAt) || resetAt <= 0) return ''
|
|
37
45
|
const delta = resetAt - now
|
|
38
|
-
const units = RELATIVE_UNITS[lang] || RELATIVE_UNITS.
|
|
46
|
+
const units = RELATIVE_UNITS[lang] || RELATIVE_UNITS.en
|
|
39
47
|
if (delta <= 0) return units.soon
|
|
40
48
|
|
|
41
49
|
const totalMinutes = Math.max(1, Math.round(delta / 60_000))
|
package/lib/routes/status.js
CHANGED
|
@@ -16,8 +16,8 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
16
16
|
store,
|
|
17
17
|
history,
|
|
18
18
|
diagnosticsReport,
|
|
19
|
-
pmL,
|
|
20
|
-
pmE,
|
|
19
|
+
pmL = (s) => s,
|
|
20
|
+
pmE = (s) => s,
|
|
21
21
|
} = state
|
|
22
22
|
|
|
23
23
|
async function configResponse() {
|
|
@@ -177,25 +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
|
-
const
|
|
186
|
+
const t0 = Date.now()
|
|
187
|
+
const ref = oauthRef(provider, index)
|
|
186
188
|
const info = await store.describeRef(ref)
|
|
187
189
|
if (!info.configured) {
|
|
188
|
-
writeJson(res, 200, { ok: false, provider, index:
|
|
190
|
+
writeJson(res, 200, { ok: false, provider, index: index, ref, error: { code: 'not_connected', message: 'not connected' } })
|
|
189
191
|
return
|
|
190
192
|
}
|
|
191
193
|
let blob
|
|
192
194
|
try { blob = await store.loadBlob(ref) } catch (e) {
|
|
193
|
-
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) } })
|
|
194
196
|
return
|
|
195
197
|
}
|
|
196
198
|
try { blob = await store.ensureFresh(provider, blob, ref) } catch (e) {
|
|
197
199
|
writeJson(res, 200, {
|
|
198
|
-
ok: false, provider, index:
|
|
200
|
+
ok: false, provider, index: index, ref,
|
|
199
201
|
email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
|
|
200
202
|
quota: info.quota || null,
|
|
201
203
|
error: { code: 'refresh', message: String(e && e.message || e) },
|
|
@@ -204,12 +206,14 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
204
206
|
}
|
|
205
207
|
const vendor = getVendor(provider)
|
|
206
208
|
if (typeof vendor.check !== 'function') {
|
|
207
|
-
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 })
|
|
208
210
|
return
|
|
209
211
|
}
|
|
210
212
|
let capturedQuota = null
|
|
211
|
-
const probeFetch = async (url, init) => {
|
|
212
|
-
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 })
|
|
213
217
|
try {
|
|
214
218
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
215
219
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
|
@@ -218,10 +222,12 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
218
222
|
}
|
|
219
223
|
try {
|
|
220
224
|
await vendor.check(blob, vendorConfig(provider, live()), probeFetch)
|
|
221
|
-
|
|
225
|
+
const latencyMs = Date.now() - t0;
|
|
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 })
|
|
222
227
|
} catch (e) {
|
|
223
228
|
writeJson(res, 200, {
|
|
224
|
-
ok: false, provider, index:
|
|
229
|
+
ok: false, provider, index: index, ref,
|
|
230
|
+
latencyMs: Date.now() - t0,
|
|
225
231
|
email: pmE(blob.email), label: pmL(blob.label), expiresAt: blob.expiresAt || null,
|
|
226
232
|
quota: capturedQuota || info.quota || null,
|
|
227
233
|
error: { code: e && e.code ? e.code : 'VENDOR', message: String(e && e.message || e).slice(0, 300) },
|
|
@@ -318,8 +324,10 @@ export function registerStatusRoutes(ctx, state) {
|
|
|
318
324
|
const vendor = getVendor(provider)
|
|
319
325
|
if (typeof vendor.check === 'function') {
|
|
320
326
|
let capturedQuota = null
|
|
321
|
-
const probeFetch = async (url, init) => {
|
|
322
|
-
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 })
|
|
323
331
|
try {
|
|
324
332
|
const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
|
|
325
333
|
if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
|
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/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: { ru: "5ч", en: "5h" },
|
|
56
|
-
seven_day: { ru: "7д", en: "7d" },
|
|
57
|
-
seven_day_oauth_apps: { ru: "7д прил.", en: "7d apps" },
|
|
58
|
-
weekly_limit_7_days: { ru: "7д", en: "7d" },
|
|
59
|
-
primary: { ru: "осн.", en: "primary" },
|
|
60
|
-
secondary: { ru: "втор.", en: "secondary" },
|
|
55
|
+
five_hour: { zh: "5小时", ru: "5ч", en: "5h" },
|
|
56
|
+
seven_day: { zh: "7天", ru: "7д", en: "7d" },
|
|
57
|
+
seven_day_oauth_apps: { zh: "7天应用", ru: "7д прил.", en: "7d apps" },
|
|
58
|
+
weekly_limit_7_days: { zh: "7天", ru: "7д", en: "7d" },
|
|
59
|
+
primary: { zh: "主配额", ru: "осн.", en: "primary" },
|
|
60
|
+
secondary: { zh: "次配额", ru: "втор.", en: "secondary" },
|
|
61
61
|
// codex: the vendor API returns primary_window/secondary_window - surfaced as 5h/7d in the UI
|
|
62
|
-
primary_window: { ru: "5ч", en: "5h" },
|
|
63
|
-
secondary_window: { ru: "7д", en: "7d" },
|
|
64
|
-
monthly: { ru: "месяц", en: "month" },
|
|
65
|
-
credits: { ru: "кредиты", en: "credits" },
|
|
62
|
+
primary_window: { zh: "5小时", ru: "5ч", en: "5h" },
|
|
63
|
+
secondary_window: { zh: "7天", ru: "7д", en: "7d" },
|
|
64
|
+
monthly: { zh: "月度", ru: "месяц", en: "month" },
|
|
65
|
+
credits: { zh: "点数", ru: "кредиты", en: "credits" },
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function windowLabel(id) {
|
|
69
69
|
const known = WINDOW_LABELS[id]
|
|
70
|
-
return known || { ru: id, en: id }
|
|
70
|
+
return known || { zh: id, ru: id, en: id }
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
// Walk vendor usage JSON; every node carrying a utilization-style number
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
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,6 +34,10 @@
|
|
|
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"
|
|
@@ -62,9 +66,5 @@
|
|
|
62
66
|
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
63
67
|
"@types/node": "^22.20.2",
|
|
64
68
|
"typescript": "^7.0.2"
|
|
65
|
-
},
|
|
66
|
-
"scripts": {
|
|
67
|
-
"test": "eslint lib/ && node --test test/*.test.mjs",
|
|
68
|
-
"typecheck": "tsc -p tsconfig.checkjs.json"
|
|
69
69
|
}
|
|
70
|
-
}
|
|
70
|
+
}
|