@goodandready/dsh-subscriptions 0.6.28 → 0.6.29
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/lib/adapter-manager.js +2 -0
- package/lib/adapter.js +33 -2
- package/lib/alerts.js +10 -1
- package/lib/client.js +30 -1
- package/lib/routes.js +4 -0
- package/lib/stream-rotate.js +16 -0
- package/lib/updater.js +23 -24
- package/package.json +1 -1
package/lib/adapter-manager.js
CHANGED
|
@@ -46,6 +46,8 @@ export function createAdapterManager({
|
|
|
46
46
|
getQuota: (ref) => store.getQuota(ref),
|
|
47
47
|
refreshUsage: (provider) => store.refreshUsage(provider),
|
|
48
48
|
saveBlob: (ref, blob) => store.saveBlob(ref, blob),
|
|
49
|
+
alertThresholds: () => live().alertThresholds,
|
|
50
|
+
webhookUrl: () => live().webhookUrl,
|
|
49
51
|
recordHistory,
|
|
50
52
|
fetchImpl: fetch,
|
|
51
53
|
fetchForRef,
|
package/lib/adapter.js
CHANGED
|
@@ -4,6 +4,7 @@ import { displayName } from './refs.js'
|
|
|
4
4
|
import { getVendor } from './vendors/index.js'
|
|
5
5
|
import { modelCatalog } from './messages.js'
|
|
6
6
|
import { streamWithRotation } from './stream-rotate.js'
|
|
7
|
+
import { checkQuotaThresholds } from './alerts.js'
|
|
7
8
|
import { pickFetch } from './proxy.js'
|
|
8
9
|
import { quotaSnapshot, parseBody } from './ratelimit.js'
|
|
9
10
|
import { toTokenUsage } from './wire.js'
|
|
@@ -112,6 +113,8 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
112
113
|
cooldownMs: deps.cooldownMs(),
|
|
113
114
|
switchAtRemaining: typeof deps.switchAtRemaining === 'function' ? deps.switchAtRemaining() : (deps.switchAtRemaining ?? 0),
|
|
114
115
|
options,
|
|
116
|
+
webhookUrl: typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl,
|
|
117
|
+
fetchImpl: deps.fetchImpl || fetch,
|
|
115
118
|
onCooldown: (account) => {
|
|
116
119
|
if (typeof deps.rememberCooldown === 'function') deps.rememberCooldown(account.ref, account.cooldownUntil, account.cooldownFamilies || null)
|
|
117
120
|
if (typeof deps.recordSwitch === 'function') deps.recordSwitch(account.ref)
|
|
@@ -129,7 +132,21 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
129
132
|
const res = await fetchWithTimeout(baseFetch, url, init, { timeoutMs: 120000 })
|
|
130
133
|
try {
|
|
131
134
|
const snap = quotaSnapshot(provider, res.headers, null, Date.now())
|
|
132
|
-
if (snap && typeof deps.rememberQuota === "function")
|
|
135
|
+
if (snap && typeof deps.rememberQuota === "function") {
|
|
136
|
+
deps.rememberQuota(account.ref, snap)
|
|
137
|
+
if (snap.usedPercent != null) {
|
|
138
|
+
const th = typeof deps.alertThresholds === 'function' ? deps.alertThresholds() : deps.alertThresholds
|
|
139
|
+
const wh = typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl
|
|
140
|
+
checkQuotaThresholds({
|
|
141
|
+
ref: account.ref,
|
|
142
|
+
provider,
|
|
143
|
+
usedPercent: snap.usedPercent,
|
|
144
|
+
thresholds: th,
|
|
145
|
+
webhookUrl: wh,
|
|
146
|
+
fetchImpl: deps.fetchImpl || fetch,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
133
150
|
} catch { /* ignore quota capture failure */ }
|
|
134
151
|
if (!res.ok && res.clone) {
|
|
135
152
|
try {
|
|
@@ -139,7 +156,21 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
139
156
|
const b = parseBody(provider, j, Date.now())
|
|
140
157
|
if (b) {
|
|
141
158
|
const snap2 = quotaSnapshot(provider, res.headers, j, Date.now())
|
|
142
|
-
if (snap2)
|
|
159
|
+
if (snap2) {
|
|
160
|
+
deps.rememberQuota(account.ref, snap2)
|
|
161
|
+
if (snap2.usedPercent != null) {
|
|
162
|
+
const th = typeof deps.alertThresholds === 'function' ? deps.alertThresholds() : deps.alertThresholds
|
|
163
|
+
const wh = typeof deps.webhookUrl === 'function' ? deps.webhookUrl() : deps.webhookUrl
|
|
164
|
+
checkQuotaThresholds({
|
|
165
|
+
ref: account.ref,
|
|
166
|
+
provider,
|
|
167
|
+
usedPercent: snap2.usedPercent,
|
|
168
|
+
thresholds: th,
|
|
169
|
+
webhookUrl: wh,
|
|
170
|
+
fetchImpl: deps.fetchImpl || fetch,
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
}
|
|
143
174
|
}
|
|
144
175
|
}
|
|
145
176
|
} catch { /* ignore quota capture failure */ }
|
package/lib/alerts.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
let pkgVersion = ''
|
|
4
|
+
try {
|
|
5
|
+
pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || ''
|
|
6
|
+
} catch {
|
|
7
|
+
// ignore package read errors
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
// #351: Proactive Quota Alerts & Webhook Dispatcher
|
|
2
11
|
// Manages quota threshold warnings, session expiry notifications,
|
|
3
12
|
// and webhook dispatch (e.g. Telegram/Discord or external monitoring).
|
|
@@ -70,7 +79,7 @@ export async function dispatchWebhookAlert(alert, webhookUrl, fetchImpl = fetch)
|
|
|
70
79
|
method: 'POST',
|
|
71
80
|
headers: {
|
|
72
81
|
'content-type': 'application/json',
|
|
73
|
-
'user-agent': 'dsh-subscriptions
|
|
82
|
+
'user-agent': pkgVersion ? `dsh-subscriptions/${pkgVersion}` : 'dsh-subscriptions',
|
|
74
83
|
},
|
|
75
84
|
body: JSON.stringify(payload),
|
|
76
85
|
})
|
package/lib/client.js
CHANGED
|
@@ -149,7 +149,7 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
'.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
|
|
150
150
|
'.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
|
|
151
151
|
'.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
|
|
152
|
-
'.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-default
|
|
152
|
+
'.dsub-speed-tag{display:inline-flex;align-items:center;gap:4px;font-size:11.5px;color:var(--dsw-alias-brand-default);background:color-mix(in srgb,var(--dsw-alias-brand-default) 8%,transparent);padding:2px 8px;border-radius:6px;font-weight:500;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-default) 20%,transparent)}' +
|
|
153
153
|
'.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
|
|
154
154
|
'.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
|
|
155
155
|
'.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
|
|
@@ -477,6 +477,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
477
477
|
"save": "Save",
|
|
478
478
|
"saved": "Saved",
|
|
479
479
|
"loading": "Loading…",
|
|
480
|
+
"unavailable": "Settings are unavailable",
|
|
480
481
|
"accountLabel": "Account",
|
|
481
482
|
"plan": "Plan",
|
|
482
483
|
"storedAs": "Stored as",
|
|
@@ -674,6 +675,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
674
675
|
"save": "保存",
|
|
675
676
|
"saved": "已保存",
|
|
676
677
|
"loading": "加载中…",
|
|
678
|
+
"unavailable": "设置不可用",
|
|
677
679
|
"accountLabel": "账户",
|
|
678
680
|
"plan": "套餐方案",
|
|
679
681
|
"storedAs": "存储标识",
|
|
@@ -2074,6 +2076,33 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
2074
2076
|
if (props && props.view === 'summary') {
|
|
2075
2077
|
return React.createElement('div', { className: 'dsub-description' }, t('cardIntro'))
|
|
2076
2078
|
}
|
|
2079
|
+
// Check host snapshot status (#408): loading or unavailable
|
|
2080
|
+
if (props && props.status === 'loading') {
|
|
2081
|
+
return React.createElement(page ? 'div' : 'li', {
|
|
2082
|
+
className: page ? 'dsub-page' : 'dsub-card',
|
|
2083
|
+
},
|
|
2084
|
+
React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
|
|
2085
|
+
React.createElement('div', { className: 'dsub-headText' },
|
|
2086
|
+
React.createElement('div', { className: 'dsub-name' }, t('title')),
|
|
2087
|
+
React.createElement('div', { className: 'dsub-description' }, t('loading') || t('settingsLoading') || 'Loading…'),
|
|
2088
|
+
),
|
|
2089
|
+
)
|
|
2090
|
+
)
|
|
2091
|
+
}
|
|
2092
|
+
if (props && props.status === 'unavailable') {
|
|
2093
|
+
return React.createElement(page ? 'div' : 'li', {
|
|
2094
|
+
className: page ? 'dsub-page' : 'dsub-card',
|
|
2095
|
+
},
|
|
2096
|
+
React.createElement('div', { className: 'dsub-header', style: { cursor: 'default' } },
|
|
2097
|
+
React.createElement('div', { className: 'dsub-headText' },
|
|
2098
|
+
React.createElement('div', { className: 'dsub-name' }, t('title')),
|
|
2099
|
+
React.createElement('div', { className: 'dsub-description', style: { color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
2100
|
+
t('unavailable') || t('settingsUnavailable') || 'Settings are unavailable',
|
|
2101
|
+
),
|
|
2102
|
+
),
|
|
2103
|
+
)
|
|
2104
|
+
)
|
|
2105
|
+
}
|
|
2077
2106
|
return React.createElement(page ? 'div' : 'li', {
|
|
2078
2107
|
className: page ? 'dsub-page' : 'dsub-card' + (open ? ' dsub-cardOpen' : ''),
|
|
2079
2108
|
},
|
package/lib/routes.js
CHANGED
|
@@ -24,6 +24,10 @@ export function registerRoutes(ctx, state) {
|
|
|
24
24
|
|
|
25
25
|
ctx.effect(() => registerPluginUpdater(ctx, {
|
|
26
26
|
endpoint: '/dsh-subscriptions/update',
|
|
27
|
+
aliases: [
|
|
28
|
+
'/api/@goodandready/dsh-subscriptions/update',
|
|
29
|
+
'/api/dsh-subscriptions/update',
|
|
30
|
+
],
|
|
27
31
|
packageName: '@goodandready/dsh-subscriptions',
|
|
28
32
|
manifestUrl: new URL('../package.json', import.meta.url),
|
|
29
33
|
}), 'dsh-subscriptions: /update')
|
package/lib/stream-rotate.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { pickAccount, markCooldown, isSwitchableError, isRegionError, modelFamily } from './rotate.js'
|
|
2
2
|
import { putInQuarantine, REASON_RATE_LIMIT, REASON_HARD_LIMIT, REASON_REVOKED } from './quarantine.js'
|
|
3
|
+
import { notifySessionExpired } from './alerts.js'
|
|
3
4
|
import { withRunawayGuard } from './runaway-guard.js'
|
|
4
5
|
|
|
5
6
|
export async function* streamWithRotation({
|
|
@@ -12,6 +13,8 @@ export async function* streamWithRotation({
|
|
|
12
13
|
onCooldown,
|
|
13
14
|
cascadeFallback, // #349: optional cross-vendor cascading fallback generator
|
|
14
15
|
offlineFallback, // #174: optional fallback generator if all accounts exhausted
|
|
16
|
+
webhookUrl,
|
|
17
|
+
fetchImpl = fetch,
|
|
15
18
|
}) {
|
|
16
19
|
const pool = (accounts || []).map((account) => ({ ...account }))
|
|
17
20
|
let lastError = null
|
|
@@ -89,6 +92,19 @@ export async function* streamWithRotation({
|
|
|
89
92
|
account.quarantineUntil = quarantined.quarantineUntil
|
|
90
93
|
account.quarantineReason = quarantined.quarantineReason
|
|
91
94
|
|
|
95
|
+
if (reason === REASON_REVOKED) {
|
|
96
|
+
try {
|
|
97
|
+
notifySessionExpired({
|
|
98
|
+
ref: account.ref,
|
|
99
|
+
provider: options && options.provider,
|
|
100
|
+
message: err && err.message,
|
|
101
|
+
webhookUrl: webhookUrl || (options && options.webhookUrl),
|
|
102
|
+
fetchImpl,
|
|
103
|
+
nowMs: nowMs(),
|
|
104
|
+
})
|
|
105
|
+
} catch { /* fire-and-forget alert */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
if (onCooldown) onCooldown(account)
|
|
93
109
|
// Break inner retry loop to continue to next account in outer while (true) loop
|
|
94
110
|
break
|
package/lib/updater.js
CHANGED
|
@@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
5
|
import { basename, dirname, isAbsolute, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import { writeJson } from './http.js'
|
|
7
|
+
import { isTrustedSettingsRequest, writeJson } from './http.js'
|
|
8
8
|
|
|
9
9
|
const UPDATE_HEADER = 'x-dsh-plugin-update'
|
|
10
10
|
const UPDATE_TIMEOUT_MS = 10 * 60_000
|
|
@@ -25,24 +25,11 @@ export function isLoopback(value) {
|
|
|
25
25
|
|
|
26
26
|
export function isTrustedUpdateRequest(request) {
|
|
27
27
|
const customHeader = header(request, UPDATE_HEADER)
|
|
28
|
-
if (customHeader
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (site === 'cross-site') return false
|
|
28
|
+
if (customHeader === '1') {
|
|
29
|
+
const remote = request.socket?.remoteAddress
|
|
30
|
+
if (remote && isLoopback(remote)) return true
|
|
32
31
|
}
|
|
33
|
-
|
|
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
|
|
32
|
+
return isTrustedSettingsRequest(request)
|
|
46
33
|
}
|
|
47
34
|
|
|
48
35
|
function validProfileName(value) {
|
|
@@ -206,10 +193,11 @@ export async function installExactPackage(target, packageSpec, options) {
|
|
|
206
193
|
|
|
207
194
|
export function registerPluginUpdater(ctx, options) {
|
|
208
195
|
let installing = false
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
196
|
+
const endpoints = Array.isArray(options.endpoint)
|
|
197
|
+
? options.endpoint
|
|
198
|
+
: [options.endpoint, ...(Array.isArray(options.aliases) ? options.aliases : [])]
|
|
199
|
+
|
|
200
|
+
const handler = async (request, response) => {
|
|
213
201
|
try {
|
|
214
202
|
const target = getRuntime()
|
|
215
203
|
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
@@ -255,6 +243,17 @@ export function registerPluginUpdater(ctx, options) {
|
|
|
255
243
|
ctx.logger?.warn?.(`plugin updater failed: ${String(error)}`)
|
|
256
244
|
writeJson(response, 503, { ok: false, error: { code: 'update_failed', message: String(error?.message || error) } })
|
|
257
245
|
}
|
|
258
|
-
|
|
259
|
-
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const unregisters = endpoints.map((ep) => ctx.webServer.register({
|
|
249
|
+
kind: 'exact',
|
|
250
|
+
path: ep,
|
|
251
|
+
handler,
|
|
252
|
+
}))
|
|
253
|
+
|
|
254
|
+
return () => {
|
|
255
|
+
for (const un of unregisters) {
|
|
256
|
+
if (typeof un === 'function') un()
|
|
257
|
+
}
|
|
258
|
+
}
|
|
260
259
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.29",
|
|
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",
|