@goodandready/dsh-subscriptions 0.6.21 → 0.6.23
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/accounts-view.js +84 -0
- package/lib/accounts.js +5 -2
- package/lib/adapter-manager.js +300 -0
- package/lib/adapter.js +7 -7
- package/lib/alerts.js +2 -2
- package/lib/backoff.js +1 -1
- package/lib/client.js +89 -75
- package/lib/code-assist.js +1 -1
- package/lib/config-schema.js +46 -6
- package/lib/diagnostics.js +60 -0
- package/lib/history.js +21 -1
- package/lib/http.js +58 -11
- package/lib/index.js +143 -543
- package/lib/mask.js +1 -1
- package/lib/ollama.js +4 -4
- package/lib/routes/accounts.js +9 -1
- package/lib/routes/proxy.js +1 -1
- package/lib/routes/status.js +58 -7
- package/lib/routes.js +0 -1
- package/lib/session-pin.js +0 -8
- package/lib/subscriptions.js +5 -6
- package/lib/vault.js +2 -2
- package/lib/vendor-factory.js +0 -1
- package/lib/vendors/antigravity.js +1 -1
- package/lib/vendors/codex.js +1 -1
- package/lib/vendors/grok.js +1 -1
- package/lib/vendors/index.js +0 -1
- package/lib/vendors/qwen.js +1 -1
- package/lib/vendors/spark.js +1 -1
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1,96 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
1
2
|
import { bestEffort } from './utils.js'
|
|
2
3
|
export { Config, publicConfig, Slot, defaultSlots } from './config-schema.js'
|
|
3
|
-
import { Config
|
|
4
|
+
import { Config } from './config-schema.js'
|
|
4
5
|
import { registerRoutes } from './routes.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { readFileSync } from 'node:fs'
|
|
8
|
-
import z from '@deepseek-ai/schemastery'
|
|
9
|
-
import { PROVIDERS, oauthRef, parseOauthRef, isProvider, displayName, droppedCredentialRefs } from './refs.js'
|
|
10
|
-
import { createPkce } from './pkce.js'
|
|
11
|
-
import { parseCallbackInput, requestOrigin, webCallbackUri } from './oauth.js'
|
|
12
|
-
import { writeJson, writeHtml, readBody, isTrustedSettingsRequest, queryOf } from './http.js'
|
|
6
|
+
import { oauthRef, parseOauthRef, isProvider, displayName } from './refs.js'
|
|
7
|
+
import { webCallbackUri } from './oauth.js'
|
|
13
8
|
import { getVendor, registerCustomVendor, clearCustomVendors } from './vendors/index.js'
|
|
14
|
-
import { SubscriptionAdapter } from './adapter.js'
|
|
15
9
|
import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
|
|
16
10
|
import { parseBlob } from './blob.js'
|
|
17
11
|
import { createVendorFromProfile, validateProfile } from './vendor-factory.js'
|
|
18
12
|
import { registerCustomProviderIds, registerDisplayName } from './refs.js'
|
|
19
13
|
import { createSubscriptionsService } from './subscriptions.js'
|
|
20
|
-
import { encryptWithPassphrase, decryptWithPassphrase } from './crypto.js'
|
|
21
|
-
import { quotaSnapshot } from './ratelimit.js'
|
|
22
14
|
import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
|
|
23
|
-
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
24
|
-
import { startLoopback } from './loopback.js'
|
|
25
15
|
import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.js'
|
|
26
16
|
import { createResetCreditService } from './reset-credits.js'
|
|
27
|
-
import { maskEmail, maskLabel
|
|
28
|
-
import { proxyFetch
|
|
17
|
+
import { maskEmail, maskLabel } from './mask.js'
|
|
18
|
+
import { proxyFetch } from './proxy.js'
|
|
29
19
|
import { HistoryStore, DEFAULT_HISTORY_DIR } from './history.js'
|
|
20
|
+
import { createDiagnosticsReport } from './diagnostics.js'
|
|
21
|
+
import { createAccountsView } from './accounts-view.js'
|
|
22
|
+
import { createAdapterManager } from './adapter-manager.js'
|
|
23
|
+
|
|
30
24
|
export { observeForecast, estimateForecast } from './forecast.js'
|
|
31
25
|
export { formatRelativeReset } from './relative-time.js'
|
|
32
|
-
import {
|
|
33
|
-
inspectGoogleAccount,
|
|
34
|
-
antigravityMetadata,
|
|
35
|
-
antigravityIdentityHeaders,
|
|
36
|
-
} from './code-assist.js'
|
|
37
|
-
|
|
38
|
-
import { resolveFallbackVendor, mapFallbackModel, DEFAULT_CASCADE_CHAINS } from './cascade.js'
|
|
39
|
-
import { exportVault, importVault } from './vault.js'
|
|
40
|
-
import { recordAlert, getRecentAlerts, checkQuotaThresholds, notifySessionExpired } from './alerts.js'
|
|
41
|
-
import { calculateBurnRatePerHour, computePacingRisk } from './forecast.js'
|
|
42
|
-
import { needsWarmupProbe, enterProbing, resolveWarmupSuccess, resolveWarmupFailure } from './quarantine.js'
|
|
43
26
|
export { resolveFallbackVendor, mapFallbackModel, DEFAULT_CASCADE_CHAINS } from './cascade.js'
|
|
44
27
|
export { exportVault, importVault } from './vault.js'
|
|
45
28
|
export { recordAlert, getRecentAlerts, checkQuotaThresholds, notifySessionExpired } from './alerts.js'
|
|
46
29
|
export { calculateBurnRatePerHour, computePacingRisk } from './forecast.js'
|
|
47
30
|
export { needsWarmupProbe, enterProbing, resolveWarmupSuccess, resolveWarmupFailure } from './quarantine.js'
|
|
31
|
+
|
|
48
32
|
export const name = '@goodandready/dsh-subscriptions'
|
|
49
33
|
export const inject = ['llm', 'credentials', 'webServer', 'settings']
|
|
50
34
|
|
|
51
35
|
const NS = 'dsh-subscriptions'
|
|
52
36
|
|
|
53
37
|
let pkgVersion = ''
|
|
54
|
-
try { pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '' } catch
|
|
38
|
+
try { pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '' } catch { /* ignore package read errors at startup */ }
|
|
55
39
|
const PENDING_TTL_MS = 15 * 60 * 1000
|
|
56
40
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
41
|
+
const OK_HTML = `<!doctype html>
|
|
42
|
+
<html>
|
|
43
|
+
<head>
|
|
44
|
+
<meta charset="utf-8">
|
|
45
|
+
<title>dsh-subscriptions - connected</title>
|
|
46
|
+
<style>
|
|
47
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
48
|
+
background: #0d1117; color: #c9d1d9; display: flex; align-items: center;
|
|
49
|
+
justify-content: center; height: 100vh; margin: 0; }
|
|
50
|
+
.card { background: #161b22; border: 1px solid #30363d; border-radius: 8px;
|
|
51
|
+
padding: 24px 32px; text-align: center; max-width: 360px; }
|
|
52
|
+
h1 { font-size: 18px; margin: 0 0 8px; color: #58a6ff; }
|
|
53
|
+
p { font-size: 13px; line-height: 1.5; margin: 0 0 16px; color: #8b949e; }
|
|
54
|
+
.btn { display: inline-block; background: #238636; color: #fff; padding: 6px 14px;
|
|
55
|
+
border-radius: 6px; text-decoration: none; font-size: 13px; font-weight: 500; }
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<div class="card">
|
|
60
|
+
<h1>Account connected</h1>
|
|
61
|
+
<p>Authentication token was saved. You can close this tab and return to DeepSeek Harness.</p>
|
|
62
|
+
<a class="btn" href="javascript:window.close()">Close window</a>
|
|
63
|
+
</div>
|
|
64
|
+
</body>
|
|
65
|
+
</html>`
|
|
66
|
+
|
|
67
|
+
export function apply(ctx, rawConfig) {
|
|
68
|
+
const logger = ctx.logger ? ctx.logger('subscriptions') : undefined
|
|
67
69
|
let settingsApi
|
|
68
|
-
// #367: maintain a single immutable config snapshot without structuredClone on hot path
|
|
69
|
-
let cachedConfig = Config(config ?? {}) ?? config
|
|
70
|
-
|
|
71
|
-
function syncSnapshot(raw) {
|
|
72
|
-
if (raw !== undefined && raw !== null) {
|
|
73
|
-
try {
|
|
74
|
-
cachedConfig = Config(raw ?? {}) ?? config
|
|
75
|
-
} catch {
|
|
76
|
-
cachedConfig = config
|
|
77
|
-
}
|
|
78
|
-
} else if (settingsApi && typeof settingsApi.get === 'function') {
|
|
79
|
-
try {
|
|
80
|
-
const rawSettings = settingsApi.get()
|
|
81
|
-
cachedConfig = Config(rawSettings ?? {}) ?? config
|
|
82
|
-
} catch {
|
|
83
|
-
cachedConfig = config
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
syncCustomVendors()
|
|
87
|
-
return cachedConfig
|
|
88
|
-
}
|
|
89
70
|
|
|
71
|
+
let cachedConfig = Config(rawConfig || {})
|
|
90
72
|
const live = () => cachedConfig
|
|
91
73
|
|
|
92
|
-
// Registering custom vendors from Config. Runs at startup and on
|
|
93
|
-
// every config change (replace) - the registry is rebuilt from scratch.
|
|
94
74
|
function syncCustomVendors() {
|
|
95
75
|
let profiles
|
|
96
76
|
try {
|
|
@@ -111,60 +91,70 @@ export function apply(ctx, config) {
|
|
|
111
91
|
}
|
|
112
92
|
}
|
|
113
93
|
}
|
|
114
|
-
|
|
115
94
|
syncCustomVendors()
|
|
116
95
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
},
|
|
128
|
-
}
|
|
96
|
+
const syncSnapshot = (next) => {
|
|
97
|
+
try {
|
|
98
|
+
const fresh = next !== undefined ? next : (typeof settingsApi?.get === 'function' ? settingsApi.get() : cachedConfig)
|
|
99
|
+
cachedConfig = Config(fresh || {})
|
|
100
|
+
syncCustomVendors()
|
|
101
|
+
syncAdapter().catch(() => {})
|
|
102
|
+
syncOllama().catch(() => {})
|
|
103
|
+
} catch { /* keep existing snapshot on error */ }
|
|
104
|
+
}
|
|
129
105
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
106
|
+
if (typeof ctx.inject === 'function') {
|
|
107
|
+
ctx.inject(['settings'], (sctx) => {
|
|
108
|
+
const scope = sctx.settings.register(NS, Config, { base: rawConfig })
|
|
109
|
+
settingsApi = {
|
|
110
|
+
...scope,
|
|
111
|
+
get: () => (typeof scope.get === 'function' ? scope.get() : cachedConfig),
|
|
112
|
+
async replace(next) {
|
|
113
|
+
syncSnapshot(next)
|
|
114
|
+
if (typeof scope.replace === 'function') {
|
|
115
|
+
return await scope.replace(next)
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
}
|
|
133
119
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (typeof unwatch === 'function') {
|
|
138
|
-
sctx.effect(() => unwatch)
|
|
139
|
-
}
|
|
140
|
-
} catch { /* ignore watch error */ }
|
|
141
|
-
}
|
|
120
|
+
if (typeof scope.get === 'function') {
|
|
121
|
+
syncSnapshot(scope.get())
|
|
122
|
+
}
|
|
142
123
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
124
|
+
if (typeof scope.watch === 'function') {
|
|
125
|
+
try {
|
|
126
|
+
const unwatch = scope.watch((next) => syncSnapshot(next))
|
|
127
|
+
if (typeof unwatch === 'function') {
|
|
128
|
+
sctx.effect(() => unwatch)
|
|
129
|
+
}
|
|
130
|
+
} catch { /* ignore watch error */ }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Promise.resolve().then(() => {
|
|
134
|
+
reconcileSlots().catch((err) => {
|
|
135
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] reconcileSlots initial error: ' + (err && err.message || err))
|
|
136
|
+
})
|
|
146
137
|
})
|
|
147
|
-
})
|
|
148
138
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
139
|
+
const onDocUpdated = (ns) => {
|
|
140
|
+
if (!ns || ns === NS) {
|
|
141
|
+
syncSnapshot()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (typeof sctx.on === 'function') {
|
|
145
|
+
sctx.on('settings/document-updated', onDocUpdated)
|
|
146
|
+
} else if (typeof ctx.on === 'function') {
|
|
147
|
+
ctx.on('settings/document-updated', onDocUpdated)
|
|
152
148
|
}
|
|
153
|
-
}
|
|
154
|
-
if (typeof sctx.on === 'function') {
|
|
155
|
-
sctx.on('settings/document-updated', onDocUpdated)
|
|
156
|
-
} else if (typeof ctx.on === 'function') {
|
|
157
|
-
ctx.on('settings/document-updated', onDocUpdated)
|
|
158
|
-
}
|
|
159
149
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
150
|
+
sctx.effect(() => () => {
|
|
151
|
+
settingsApi = undefined
|
|
152
|
+
cachedConfig = Config(rawConfig || {})
|
|
153
|
+
syncCustomVendors()
|
|
154
|
+
})
|
|
164
155
|
})
|
|
165
|
-
}
|
|
156
|
+
}
|
|
166
157
|
|
|
167
|
-
// #88: fetch bound to the per-account proxy (slot.proxyUrl), or null for direct.
|
|
168
158
|
const fetchForRef = (ref) => {
|
|
169
159
|
const parsed = parseOauthRef(ref)
|
|
170
160
|
if (!parsed) return null
|
|
@@ -173,6 +163,16 @@ export function apply(ctx, config) {
|
|
|
173
163
|
return proxyFetch(slot.proxyUrl)
|
|
174
164
|
}
|
|
175
165
|
|
|
166
|
+
function redirectFor(ref, provider) {
|
|
167
|
+
if (live().useWebCallback) {
|
|
168
|
+
const origin = (ctx.webServer && typeof ctx.webServer.url === 'string')
|
|
169
|
+
? ctx.webServer.url
|
|
170
|
+
: ''
|
|
171
|
+
return webCallbackUri(origin, ref)
|
|
172
|
+
}
|
|
173
|
+
return getVendor(provider).redirectUri(vendorConfig(provider, live()))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
176
|
const store = createAccountStore({
|
|
177
177
|
credentials: ctx.credentials,
|
|
178
178
|
getConfig: live,
|
|
@@ -186,6 +186,9 @@ export function apply(ctx, config) {
|
|
|
186
186
|
},
|
|
187
187
|
})
|
|
188
188
|
const history = new HistoryStore(DEFAULT_HISTORY_DIR, 7 * 24 * 60 * 60 * 1000, 1000)
|
|
189
|
+
if (typeof ctx?.on === 'function') {
|
|
190
|
+
ctx.on('dispose', () => history.dispose())
|
|
191
|
+
}
|
|
189
192
|
const recordHistory = (entry) => history.add(entry)
|
|
190
193
|
|
|
191
194
|
// #85: host-only reset credit service for Codex accounts.
|
|
@@ -195,8 +198,11 @@ export function apply(ctx, config) {
|
|
|
195
198
|
return slot ? slot.ref : null
|
|
196
199
|
}
|
|
197
200
|
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
function stripLegacySlots(slots) {
|
|
202
|
+
return (slots || []).filter((slot) => isProvider(slot.provider))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Local Ollama adapter and fallback
|
|
200
206
|
const ollamaAdapter = new OllamaAdapter({
|
|
201
207
|
baseUrl: () => ollamaBase(live()),
|
|
202
208
|
fallbackModel: () => live().ollamaFallbackModel || '',
|
|
@@ -212,6 +218,7 @@ export function apply(ctx, config) {
|
|
|
212
218
|
ollamaHandle = undefined
|
|
213
219
|
}
|
|
214
220
|
}
|
|
221
|
+
|
|
215
222
|
async function* ollamaFallbackStream({ options, provider, err }) {
|
|
216
223
|
const cfg = live()
|
|
217
224
|
const models = await ollamaModels(ollamaBase(cfg), fetch).catch(() => [])
|
|
@@ -229,10 +236,27 @@ export function apply(ctx, config) {
|
|
|
229
236
|
status: 200,
|
|
230
237
|
kind: 'fallback',
|
|
231
238
|
})
|
|
232
|
-
} catch
|
|
239
|
+
} catch { /* best-effort */ }
|
|
233
240
|
yield* ollamaAdapter.stream({ ...options, provider: 'ollama', model })
|
|
234
241
|
}
|
|
235
242
|
|
|
243
|
+
// LLM Adapter lifecycle manager
|
|
244
|
+
const adapterManager = createAdapterManager({
|
|
245
|
+
ctx,
|
|
246
|
+
live,
|
|
247
|
+
store,
|
|
248
|
+
logger,
|
|
249
|
+
fetch,
|
|
250
|
+
fetchForRef,
|
|
251
|
+
recordHistory,
|
|
252
|
+
getSettingsApi: () => settingsApi,
|
|
253
|
+
syncCustomVendors,
|
|
254
|
+
syncOllama,
|
|
255
|
+
stripLegacySlots,
|
|
256
|
+
})
|
|
257
|
+
const { syncAdapter, reconcileSlots, refreshModels, cascadeFallbackStream } = adapterManager
|
|
258
|
+
adapterManager.registerAdapterLifecycle()
|
|
259
|
+
|
|
236
260
|
const subscriptions = createSubscriptionsService({
|
|
237
261
|
listAccounts: (provider) => store.listAccounts(provider),
|
|
238
262
|
loadBlob: (ref) => store.loadBlob(ref),
|
|
@@ -259,24 +283,13 @@ export function apply(ctx, config) {
|
|
|
259
283
|
hideDeprecatedModels: () => !!live().hideDeprecatedModels,
|
|
260
284
|
})
|
|
261
285
|
|
|
262
|
-
// Subscription-backed image generation service.
|
|
263
|
-
//
|
|
264
|
-
// An action is exposed, not a token: a network route would hand a live
|
|
265
|
-
// access key to anyone reaching the harness, while the service lives inside
|
|
266
|
-
// the process and is visible only to sibling plugins. Token refresh stays
|
|
267
|
-
// with a single owner - this plugin.
|
|
268
286
|
ctx.effect(() => ctx.provide('subscriptions', subscriptions), 'dsh-subscriptions: subscriptions service')
|
|
269
287
|
ctx.effect(() => ctx.provide('subscriptionImages', {
|
|
270
|
-
/** Providers with an active session right now. */
|
|
271
288
|
async available() {
|
|
272
289
|
const logged = await store.loggedInProviders()
|
|
273
290
|
return ['codex', 'grok'].filter((name) => logged && logged[name])
|
|
274
291
|
},
|
|
275
292
|
sizes: IMAGE_SIZES,
|
|
276
|
-
/**
|
|
277
|
-
* @param request {{provider, prompt, size, quality, signal}}
|
|
278
|
-
* @returns [{ b64_json, revisedPrompt? }]
|
|
279
|
-
*/
|
|
280
293
|
async generate(request) {
|
|
281
294
|
const provider = request && request.provider
|
|
282
295
|
if (provider !== 'codex' && provider !== 'grok') {
|
|
@@ -299,209 +312,12 @@ export function apply(ctx, config) {
|
|
|
299
312
|
})
|
|
300
313
|
},
|
|
301
314
|
}), 'dsh-subscriptions: image generation service')
|
|
302
|
-
const pending = new Map()
|
|
303
|
-
async function* cascadeFallbackStream({ options, provider, err, visited = new Set() }) {
|
|
304
|
-
const cfg = live()
|
|
305
|
-
if (!cfg.cascadingFallback) throw err
|
|
306
|
-
const logged = await store.loggedInProviders()
|
|
307
|
-
const available = Object.keys(logged || {}).filter((p) => logged[p] && p !== provider)
|
|
308
|
-
visited.add(provider)
|
|
309
|
-
const nextVendor = resolveFallbackVendor(provider, available, cfg.cascadingChain, visited)
|
|
310
|
-
if (!nextVendor) throw err
|
|
311
|
-
visited.add(nextVendor)
|
|
312
|
-
const targetModel = mapFallbackModel(provider, options && options.model, nextVendor)
|
|
313
|
-
if (logger && logger.warn) {
|
|
314
|
-
logger.warn('[dsh-subscriptions] ' + provider + ': all accounts exhausted, cascading fallback to ' + nextVendor + '/' + targetModel)
|
|
315
|
-
}
|
|
316
|
-
yield* adapter.stream({ ...options, provider: nextVendor, model: targetModel }, visited)
|
|
317
|
-
try {
|
|
318
|
-
recordHistory({
|
|
319
|
-
provider: nextVendor,
|
|
320
|
-
ref: String(nextVendor).toUpperCase() + '_CASCADE_FALLBACK',
|
|
321
|
-
model: targetModel,
|
|
322
|
-
path: '/v1/chat/completions',
|
|
323
|
-
method: 'POST',
|
|
324
|
-
status: 200,
|
|
325
|
-
kind: 'cascade',
|
|
326
|
-
})
|
|
327
|
-
} catch (e) { /* ignore cascade history recording error */ }
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const adapter = new SubscriptionAdapter({
|
|
331
|
-
listAccounts: (provider) => store.listAccounts(provider),
|
|
332
|
-
loadBlob: (ref) => store.loadBlob(ref),
|
|
333
|
-
ensureFresh: (provider, blob, ref) => store.ensureFresh(provider, blob, ref),
|
|
334
|
-
vendorConfig: (provider) => vendorConfig(provider, live()),
|
|
335
|
-
cooldownMs: () => live().cooldownMs,
|
|
336
|
-
switchAtRemaining: () => live().switchAtRemaining,
|
|
337
|
-
rememberCooldown: (ref, until, families) => store.rememberCooldown(ref, until, families),
|
|
338
|
-
rememberQuarantine: (ref, reason, until) => store.rememberQuarantine(ref, reason, until),
|
|
339
|
-
rememberQuota: (ref, snap) => store.rememberQuota(ref, snap),
|
|
340
|
-
getQuota: (ref) => store.getQuota(ref),
|
|
341
|
-
refreshUsage: (provider) => store.refreshUsage(provider),
|
|
342
|
-
saveBlob: (ref, blob) => store.saveBlob(ref, blob),
|
|
343
|
-
recordHistory,
|
|
344
|
-
fetchImpl: fetch,
|
|
345
|
-
fetchForRef,
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
let handle
|
|
349
|
-
let adapterDisposed = false
|
|
350
|
-
let registeredByUs = new Set()
|
|
351
|
-
function disposeAdapterHandle(h) {
|
|
352
|
-
if (!h) return
|
|
353
|
-
try {
|
|
354
|
-
if (typeof h === 'function') h()
|
|
355
|
-
else if (typeof h.dispose === 'function') h.dispose()
|
|
356
|
-
} catch (err) {
|
|
357
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions] dispose handle error: ' + (err && err.message || err))
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
async function syncAdapter() {
|
|
362
|
-
if (adapterDisposed) return
|
|
363
|
-
const desired = await store.loggedInProviders()
|
|
364
|
-
const globalProviders = ctx.llm && typeof ctx.llm.listProviders === 'function'
|
|
365
|
-
? ctx.llm.listProviders().map((p) => p.id)
|
|
366
|
-
: []
|
|
367
|
-
const globalSet = new Set(globalProviders)
|
|
368
|
-
|
|
369
|
-
const claimable = desired.filter((p) => !globalSet.has(p) || registeredByUs.has(p))
|
|
370
|
-
const skipped = desired.filter((p) => globalSet.has(p) && !registeredByUs.has(p))
|
|
371
315
|
|
|
372
|
-
|
|
373
|
-
logger.info(`[dsh-subscriptions] syncAdapter skipping already-bound providers: ${skipped.join(', ')}`)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (!handle && claimable.length) {
|
|
377
|
-
handle = ctx.llm.registerAdapter(claimable, adapter)
|
|
378
|
-
registeredByUs = new Set(claimable)
|
|
379
|
-
return
|
|
380
|
-
}
|
|
381
|
-
if (!handle) {
|
|
382
|
-
registeredByUs.clear()
|
|
383
|
-
return
|
|
384
|
-
}
|
|
385
|
-
if (!claimable.length) {
|
|
386
|
-
disposeAdapterHandle(handle)
|
|
387
|
-
handle = undefined
|
|
388
|
-
registeredByUs.clear()
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
if (typeof handle.replace === 'function') {
|
|
392
|
-
try {
|
|
393
|
-
handle.replace(claimable)
|
|
394
|
-
registeredByUs = new Set(claimable)
|
|
395
|
-
return
|
|
396
|
-
} catch (err) {
|
|
397
|
-
if (logger && logger.warn) logger.warn('[dsh-subscriptions] handle.replace failed: ' + (err && err.message || err))
|
|
398
|
-
disposeAdapterHandle(handle)
|
|
399
|
-
handle = undefined
|
|
400
|
-
registeredByUs.clear()
|
|
401
|
-
}
|
|
402
|
-
} else {
|
|
403
|
-
disposeAdapterHandle(handle)
|
|
404
|
-
handle = undefined
|
|
405
|
-
registeredByUs.clear()
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const currentGlobal = new Set((ctx.llm && typeof ctx.llm.listProviders === 'function' ? ctx.llm.listProviders() : []).map((p) => p.id))
|
|
409
|
-
const reClaimable = desired.filter((p) => !currentGlobal.has(p))
|
|
410
|
-
if (reClaimable.length) {
|
|
411
|
-
handle = ctx.llm.registerAdapter(reClaimable, adapter)
|
|
412
|
-
registeredByUs = new Set(reClaimable)
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
async function reconcileSlots() {
|
|
417
|
-
const api = settingsApi
|
|
418
|
-
if (!api || typeof api.replace !== 'function') return
|
|
419
|
-
const creds = ctx.credentials
|
|
420
|
-
if (!creds || typeof creds.describe !== 'function') return
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
const existing = normalizeSlots(live().slots)
|
|
424
|
-
const existingRefs = new Set(existing.map((s) => s.ref))
|
|
425
|
-
const candidateProviders = PROVIDERS.filter((p) => isProvider(p))
|
|
426
|
-
const candidates = []
|
|
427
|
-
for (const provider of candidateProviders) {
|
|
428
|
-
for (let index = 1; index <= 10; index++) {
|
|
429
|
-
const ref = oauthRef(provider, index)
|
|
430
|
-
if (!existingRefs.has(ref)) {
|
|
431
|
-
candidates.push({ provider, index, ref })
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const additionResults = await Promise.all(
|
|
437
|
-
candidates.map(async ({ provider, index, ref }) => {
|
|
438
|
-
let d
|
|
439
|
-
try {
|
|
440
|
-
d = await creds.describe(credentialRef(ref))
|
|
441
|
-
} catch (err) {
|
|
442
|
-
if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] describe ${ref} error: ` + (err && err.message || err))
|
|
443
|
-
return null
|
|
444
|
-
}
|
|
445
|
-
if (d && d.configured) {
|
|
446
|
-
let label = d.label || ''
|
|
447
|
-
try {
|
|
448
|
-
const blob = await store.loadBlob(ref).catch(() => null)
|
|
449
|
-
if (blob && (blob.email || blob.label)) {
|
|
450
|
-
label = blob.email || blob.label || label
|
|
451
|
-
}
|
|
452
|
-
} catch (loadErr) {
|
|
453
|
-
if (logger && logger.debug) logger.debug(`[dsh-subscriptions reconcile] loadBlob ${ref} error: ` + (loadErr && loadErr.message || loadErr))
|
|
454
|
-
}
|
|
455
|
-
return { provider, index, label }
|
|
456
|
-
}
|
|
457
|
-
return null
|
|
458
|
-
})
|
|
459
|
-
)
|
|
460
|
-
const additions = additionResults.filter(Boolean)
|
|
461
|
-
|
|
462
|
-
if (!additions.length) return
|
|
463
|
-
if (logger && logger.info) {
|
|
464
|
-
logger.info(`[dsh-subscriptions reconcile] restoring orphaned vault slots: ${additions.map((a) => `${a.provider}#${a.index}`).join(', ')}`)
|
|
465
|
-
}
|
|
466
|
-
const currentSlots = Array.isArray(live().slots) ? live().slots : []
|
|
467
|
-
const newSlots = [...currentSlots, ...additions]
|
|
468
|
-
const parsed = Config({ ...live(), slots: stripLegacySlots(newSlots) })
|
|
469
|
-
await api.replace(parsed)
|
|
470
|
-
syncCustomVendors()
|
|
471
|
-
await syncAdapter()
|
|
472
|
-
refreshModels().catch((err) => {
|
|
473
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions reconcile] refreshModels error: ' + (err && err.message || err))
|
|
474
|
-
})
|
|
475
|
-
} catch (e) {
|
|
476
|
-
if (logger && logger.warn) {
|
|
477
|
-
logger.warn('[dsh-subscriptions reconcile] failed: ' + (e && e.message || e))
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// #46: after a login lands, eagerly refresh the live model catalog so the
|
|
483
|
-
// model picker shows the new provider within ~1 min instead of waiting.
|
|
484
|
-
let lastModelRefresh = 0
|
|
485
|
-
async function refreshModels() {
|
|
486
|
-
if (Date.now() - lastModelRefresh < 60 * 1000) return
|
|
487
|
-
lastModelRefresh = Date.now()
|
|
488
|
-
const providers = await store.loggedInProviders()
|
|
489
|
-
for (const provider of providers) {
|
|
490
|
-
try {
|
|
491
|
-
const slot = normalizeSlots(live().slots).find((x) => x.provider === provider)
|
|
492
|
-
if (!slot) continue
|
|
493
|
-
const blob = await store.loadBlob(slot.ref).catch(() => null)
|
|
494
|
-
if (!blob) continue
|
|
495
|
-
const fresh = await store.ensureFresh(provider, blob, slot.ref)
|
|
496
|
-
const cfg = vendorConfig(provider, live())
|
|
497
|
-
await getVendor(provider).listModels(fresh, cfg, fetch).catch(() => {})
|
|
498
|
-
} catch (err) { /* best-effort */ }
|
|
499
|
-
}
|
|
500
|
-
}
|
|
316
|
+
const pending = new Map()
|
|
501
317
|
|
|
502
318
|
function sweepPending(now) {
|
|
503
|
-
for (const [
|
|
504
|
-
if (now - row.createdAt > PENDING_TTL_MS) pending.delete(
|
|
319
|
+
for (const [stateKey, row] of pending) {
|
|
320
|
+
if (now - row.createdAt > PENDING_TTL_MS) pending.delete(stateKey)
|
|
505
321
|
}
|
|
506
322
|
}
|
|
507
323
|
|
|
@@ -534,226 +350,15 @@ export function apply(ctx, config) {
|
|
|
534
350
|
return { ref, label: pmL(blob.label || blob.email) || displayName(provider) }
|
|
535
351
|
}
|
|
536
352
|
|
|
537
|
-
|
|
538
|
-
if (!info.configured || slot.provider !== 'antigravity') return info
|
|
539
|
-
let blob
|
|
540
|
-
try { blob = await store.loadBlob(slot.ref) } catch { return info }
|
|
541
|
-
if (blob.validationUrl) {
|
|
542
|
-
return {
|
|
543
|
-
...info,
|
|
544
|
-
validationUrl: blob.validationUrl,
|
|
545
|
-
validationMessage: blob.validationMessage || info.validationMessage || '',
|
|
546
|
-
paidTierName: blob.paidTierName || info.paidTierName || '',
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
try {
|
|
550
|
-
const fresh = await store.ensureFresh(slot.provider, blob, slot.ref)
|
|
551
|
-
const probe = await inspectGoogleAccount(fetch, fresh.accessToken, {
|
|
552
|
-
metadata: antigravityMetadata(fresh.projectId || ''),
|
|
553
|
-
extraHeaders: antigravityIdentityHeaders(fresh.projectId || ''),
|
|
554
|
-
projectId: fresh.projectId,
|
|
555
|
-
})
|
|
556
|
-
const next = { ...fresh }
|
|
557
|
-
if (probe.projectId && probe.projectId !== fresh.projectId) next.projectId = probe.projectId
|
|
558
|
-
if (probe.paidTierId) next.paidTierId = probe.paidTierId
|
|
559
|
-
if (probe.paidTierName) next.paidTierName = probe.paidTierName
|
|
560
|
-
if (probe.validation?.validationUrl) {
|
|
561
|
-
next.validationUrl = probe.validation.validationUrl
|
|
562
|
-
next.validationMessage = probe.validation.message || ''
|
|
563
|
-
await store.saveBlob(slot.ref, next)
|
|
564
|
-
return {
|
|
565
|
-
...info,
|
|
566
|
-
validationUrl: next.validationUrl,
|
|
567
|
-
validationMessage: next.validationMessage,
|
|
568
|
-
paidTierName: next.paidTierName || '',
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
if (probe.notice?.message) {
|
|
572
|
-
next.accountNotice = probe.notice.message
|
|
573
|
-
await store.saveBlob(slot.ref, next)
|
|
574
|
-
return { ...info, accountNotice: next.accountNotice, paidTierName: next.paidTierName || '' }
|
|
575
|
-
}
|
|
576
|
-
if (probe.projectId || probe.paidTierId) await store.saveBlob(slot.ref, next)
|
|
577
|
-
return { ...info, paidTierName: next.paidTierName || info.paidTierName || '' }
|
|
578
|
-
} catch { /* keep settings responsive */ }
|
|
579
|
-
return info
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
function stripLegacySlots(slots) {
|
|
583
|
-
return (slots || []).filter((slot) => isProvider(slot.provider))
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// #98: privacy masking. pmE for raw emails, pmL for display labels (only email-looking ones masked).
|
|
353
|
+
// Privacy masking
|
|
587
354
|
const privacyOn = () => !!live().privacyMask
|
|
588
355
|
const pmE = (s) => privacyOn() ? maskEmail(s) : String(s || '')
|
|
589
356
|
const pmL = (s) => privacyOn() ? maskLabel(s) : String(s || '')
|
|
590
357
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
if (typeof v === 'string') return maskText(v)
|
|
594
|
-
if (Array.isArray(v)) return v.map(scrubReport)
|
|
595
|
-
if (v && typeof v === 'object') {
|
|
596
|
-
const o = {}
|
|
597
|
-
for (const k of Object.keys(v)) o[k] = scrubReport(v[k])
|
|
598
|
-
return o
|
|
599
|
-
}
|
|
600
|
-
return v
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
async function diagnosticsReport() {
|
|
604
|
-
const cfg = live()
|
|
605
|
-
const slots = normalizeSlots(cfg.slots)
|
|
606
|
-
const mk = () => ({ loggedIn: false, slots: 0, configured: 0, cooldown: 0, proxy: 0, maxUsagePercent: null })
|
|
607
|
-
const providers = {}
|
|
608
|
-
for (const p of PROVIDERS) providers[p] = mk()
|
|
609
|
-
const logged = await store.loggedInProviders()
|
|
610
|
-
for (const slot of slots) {
|
|
611
|
-
const pv = providers[slot.provider] || (providers[slot.provider] = mk())
|
|
612
|
-
pv.slots++
|
|
613
|
-
if (slot.proxyUrl) pv.proxy++
|
|
614
|
-
try {
|
|
615
|
-
const info = await store.describeRef(slot.ref)
|
|
616
|
-
if (info.configured) pv.configured++
|
|
617
|
-
if (info.cooldownUntil && info.cooldownUntil > Date.now()) pv.cooldown++
|
|
618
|
-
if (info.usagePercent != null) pv.maxUsagePercent = Math.max(pv.maxUsagePercent || 0, Math.round(info.usagePercent))
|
|
619
|
-
} catch (err) { /* best-effort */ }
|
|
620
|
-
}
|
|
621
|
-
for (const p of Object.keys(providers)) providers[p].loggedIn = logged.includes(p)
|
|
622
|
-
const rows = history.all()
|
|
623
|
-
const byStatus = {}
|
|
624
|
-
for (const r of rows) {
|
|
625
|
-
const k = (r.provider || '?') + ':' + (r.status || '?')
|
|
626
|
-
byStatus[k] = (byStatus[k] || 0) + 1
|
|
627
|
-
}
|
|
628
|
-
const lastErrors = rows.filter((r) => r.status && r.status >= 400).slice(0, 10)
|
|
629
|
-
.map((r) => ({ ts: r.ts, provider: r.provider, status: r.status, kind: r.kind || 'request', ms: r.ms || null }))
|
|
630
|
-
return scrubReport({
|
|
631
|
-
generatedAt: new Date().toISOString(),
|
|
632
|
-
plugin: NS + (pkgVersion ? ' v' + pkgVersion : ''),
|
|
633
|
-
runtime: { node: process.version, platform: process.platform, arch: process.arch, uptimeSec: Math.round(process.uptime()) },
|
|
634
|
-
providers,
|
|
635
|
-
requests: { total: rows.length, byStatus, lastErrors },
|
|
636
|
-
settings: {
|
|
637
|
-
cooldownMs: cfg.cooldownMs,
|
|
638
|
-
switchAtRemaining: cfg.switchAtRemaining,
|
|
639
|
-
probeIntervalMin: cfg.probeIntervalMin,
|
|
640
|
-
useWebCallback: !!cfg.useWebCallback,
|
|
641
|
-
autoLoopback: !!cfg.autoLoopback,
|
|
642
|
-
privacyMask: !!cfg.privacyMask,
|
|
643
|
-
customVendors: Array.isArray(cfg.customVendors) ? cfg.customVendors.length : 0,
|
|
644
|
-
proxySlots: slots.filter((s) => s.proxyUrl).length,
|
|
645
|
-
},
|
|
646
|
-
})
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
async function accountsView() {
|
|
650
|
-
const out = []
|
|
651
|
-
for (const slot of normalizeSlots(live().slots)) {
|
|
652
|
-
const info = await enrichAntigravityAccount(slot, await store.describeRef(slot.ref))
|
|
653
|
-
out.push({
|
|
654
|
-
provider: slot.provider,
|
|
655
|
-
index: slot.index,
|
|
656
|
-
ref: slot.ref,
|
|
657
|
-
label: pmL(slot.label || info.label),
|
|
658
|
-
configured: info.configured,
|
|
659
|
-
writable: info.writable,
|
|
660
|
-
cooldownUntil: info.cooldownUntil,
|
|
661
|
-
// #303: expose the cooldown scope and quarantine so the card can
|
|
662
|
-
// explain why only some models are blocked.
|
|
663
|
-
cooldownFamilies: info.cooldownFamilies || null,
|
|
664
|
-
quarantineUntil: info.quarantineUntil || 0,
|
|
665
|
-
quarantineReason: info.quarantineReason || null,
|
|
666
|
-
usagePercent: info.usagePercent,
|
|
667
|
-
quota: info.quota || null,
|
|
668
|
-
usage: info.usage || null,
|
|
669
|
-
requests: store.getRequestCount(slot.ref) || null,
|
|
670
|
-
refreshError: info.refreshError || '',
|
|
671
|
-
validationUrl: info.validationUrl || '',
|
|
672
|
-
validationMessage: info.validationMessage || '',
|
|
673
|
-
accountNotice: info.accountNotice || '',
|
|
674
|
-
paidTierName: info.paidTierName || '',
|
|
675
|
-
})
|
|
676
|
-
}
|
|
677
|
-
return out
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
async function configResponse() {
|
|
681
|
-
return {
|
|
682
|
-
ok: true,
|
|
683
|
-
config: publicConfig(live()),
|
|
684
|
-
accounts: await accountsView(),
|
|
685
|
-
providers: PROVIDERS.map((id) => ({ id, name: displayName(id) })),
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
ctx.effect(() => {
|
|
690
|
-
syncAdapter().catch((err) => {
|
|
691
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncAdapter initial error: ' + (err && err.message || err))
|
|
692
|
-
})
|
|
693
|
-
syncOllama().catch((err) => {
|
|
694
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions] syncOllama initial error: ' + (err && err.message || err))
|
|
695
|
-
})
|
|
696
|
-
reconcileSlots().catch((err) => {
|
|
697
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions] reconcileSlots effect error: ' + (err && err.message || err))
|
|
698
|
-
})
|
|
699
|
-
|
|
700
|
-
let retries = 0
|
|
701
|
-
let retryTimer
|
|
702
|
-
const scheduleRetry = () => {
|
|
703
|
-
if (adapterDisposed || retries >= 30) return
|
|
704
|
-
retryTimer = setTimeout(retrySync, 1000)
|
|
705
|
-
if (retryTimer && typeof retryTimer.unref === 'function') retryTimer.unref()
|
|
706
|
-
}
|
|
707
|
-
const retrySync = async () => {
|
|
708
|
-
if (adapterDisposed) return
|
|
709
|
-
try {
|
|
710
|
-
const loggedIn = await store.loggedInProviders()
|
|
711
|
-
if (!loggedIn.length) return
|
|
712
|
-
const registered = ctx.llm && typeof ctx.llm.listProviders === 'function'
|
|
713
|
-
? ctx.llm.listProviders().map((p) => p.id)
|
|
714
|
-
: []
|
|
715
|
-
const regSet = new Set(registered)
|
|
716
|
-
const pending = loggedIn.filter((p) => !regSet.has(p))
|
|
717
|
-
if (pending.length === 0) return
|
|
718
|
-
retries += 1
|
|
719
|
-
await syncAdapter()
|
|
720
|
-
if (retries < 30) {
|
|
721
|
-
scheduleRetry()
|
|
722
|
-
}
|
|
723
|
-
} catch (err) {
|
|
724
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions] retrySync error: ' + (err && err.message || err))
|
|
725
|
-
if (retries < 30) {
|
|
726
|
-
scheduleRetry()
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
scheduleRetry()
|
|
358
|
+
const diagnosticsReport = () => createDiagnosticsReport({ live, store, history, NS, pkgVersion })
|
|
359
|
+
const accountsView = () => createAccountsView({ live, store, fetch, pmL })
|
|
731
360
|
|
|
732
|
-
|
|
733
|
-
// and the active subscription chip shows 5h/7d/... right away instead of waiting for probeInterval.
|
|
734
|
-
const eager = async () => {
|
|
735
|
-
try {
|
|
736
|
-
for (const slot of normalizeSlots(live().slots)) {
|
|
737
|
-
store.refreshUsage(slot.provider).catch((err) => {
|
|
738
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager usage error: ' + (err && err.message || err))
|
|
739
|
-
})
|
|
740
|
-
}
|
|
741
|
-
} catch (err) {
|
|
742
|
-
if (logger && logger.debug) logger.debug('[dsh-subscriptions] eager refresh error: ' + (err && err.message || err))
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
eager()
|
|
746
|
-
return () => {
|
|
747
|
-
adapterDisposed = true
|
|
748
|
-
if (retryTimer) clearTimeout(retryTimer)
|
|
749
|
-
if (handle) {
|
|
750
|
-
disposeAdapterHandle(handle)
|
|
751
|
-
handle = undefined
|
|
752
|
-
registeredByUs.clear()
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}, 'dsh-subscriptions: llm adapter')
|
|
756
|
-
// ponytail: background refresh ahead of expiry, single timer, per-account lock + retry backoff
|
|
361
|
+
// Ponytail: background refresh ahead of expiry, single timer
|
|
757
362
|
ctx.effect(() => {
|
|
758
363
|
const tick = async () => {
|
|
759
364
|
const cfg = live()
|
|
@@ -774,9 +379,9 @@ export function apply(ctx, config) {
|
|
|
774
379
|
if (blob.expiresAt - now > ahead) continue
|
|
775
380
|
if (typeof store.shouldSkipRefresh === 'function' && store.shouldSkipRefresh(ref, now, retryMs)) continue
|
|
776
381
|
await store.ensureFresh(slot.provider, blob, ref).catch((e) => {
|
|
777
|
-
if (logger && logger.warn) logger.warn(
|
|
382
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] background refresh failed for ' + ref + ': ' + String(e && e.message || e))
|
|
778
383
|
})
|
|
779
|
-
} catch
|
|
384
|
+
} catch { /* best-effort */ }
|
|
780
385
|
}
|
|
781
386
|
}
|
|
782
387
|
tick().catch(() => {})
|
|
@@ -786,15 +391,12 @@ export function apply(ctx, config) {
|
|
|
786
391
|
return () => clear(id)
|
|
787
392
|
}, 'dsh-subscriptions: refresh ahead')
|
|
788
393
|
|
|
789
|
-
// Background health-check
|
|
790
|
-
// connected accounts. Dead ones are flagged via describeRef, no cooldown
|
|
791
|
-
// is set (same as /check).
|
|
394
|
+
// Background health-check probe loop
|
|
792
395
|
ctx.effect(() => {
|
|
793
396
|
const tick = async () => {
|
|
794
397
|
const cfg = live()
|
|
795
398
|
const mins = Number(cfg.probeIntervalMin)
|
|
796
399
|
if (!Number.isFinite(mins) || mins <= 0) return
|
|
797
|
-
const now = Date.now()
|
|
798
400
|
for (const slot of normalizeSlots(cfg.slots)) {
|
|
799
401
|
const ref = slot.ref
|
|
800
402
|
try {
|
|
@@ -811,9 +413,9 @@ export function apply(ctx, config) {
|
|
|
811
413
|
const cfg2 = vendorConfig(slot.provider, live())
|
|
812
414
|
const probeFetch = async (u, i) => ((typeof fetchForRef === 'function' && fetchForRef(ref)) || fetch)(u, i)
|
|
813
415
|
await vendor.check(fresh, cfg2, probeFetch).catch((e) => {
|
|
814
|
-
if (logger && logger.warn) logger.warn(
|
|
416
|
+
if (logger && logger.warn) logger.warn('[dsh-subscriptions] probe ' + ref + ': ' + String(e && e.message || e).slice(0, 200))
|
|
815
417
|
})
|
|
816
|
-
} catch
|
|
418
|
+
} catch { /* best-effort */ }
|
|
817
419
|
}
|
|
818
420
|
}
|
|
819
421
|
let lastProbeAt = 0
|
|
@@ -830,8 +432,6 @@ export function apply(ctx, config) {
|
|
|
830
432
|
return () => clearInterval(timer)
|
|
831
433
|
}, 'dsh-subscriptions: probe loop')
|
|
832
434
|
|
|
833
|
-
|
|
834
|
-
|
|
835
435
|
registerRoutes(ctx, {
|
|
836
436
|
NS,
|
|
837
437
|
live,
|