@goodandready/dsh-subscriptions 0.6.91 → 0.7.3
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/bin/dsh-subscriptions.mjs +65 -0
- package/lib/adapter-manager.js +3 -0
- package/lib/adapter.js +10 -2
- package/lib/alerts.js +54 -22
- package/lib/client.js +283 -9
- package/lib/coexistence.js +45 -0
- package/lib/config-schema.js +22 -0
- package/lib/index.js +24 -1
- package/lib/messages.js +1 -1
- package/lib/model-settings.js +49 -0
- package/lib/native-tools.js +220 -0
- package/lib/reset-clock.js +16 -6
- package/lib/route-report.js +28 -0
- package/lib/routes/status.js +88 -2
- package/lib/speed-mode.js +17 -0
- package/lib/stream-rotate.js +10 -2
- package/lib/subscriptions.js +1 -1
- package/lib/usage-export.js +52 -0
- package/lib/vault-sync.js +53 -15
- package/lib/vendor-pack.js +36 -0
- package/overlay.example.yml +8 -0
- package/package.json +33 -2
package/lib/vault-sync.js
CHANGED
|
@@ -244,6 +244,40 @@ export async function pullFromS3({ endpoint, bucket, key = 'dsh-subscriptions-va
|
|
|
244
244
|
return res.text()
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// #691: v2 sync primitives — deterministic slot merge, encrypted snapshots and
|
|
248
|
+
// provider-level selection. Remote wins on conflicts, but conflicting rows are
|
|
249
|
+
// reported so the caller can surface them instead of silently dropping local state.
|
|
250
|
+
export function mergeVaultSlots(localSlots, remoteSlots) {
|
|
251
|
+
const out = (Array.isArray(localSlots) ? localSlots : []).map((slot) => ({ ...slot }))
|
|
252
|
+
const keyOf = (slot) => slot && (slot.ref || (slot.provider + ':' + (slot.index == null ? 1 : slot.index)))
|
|
253
|
+
const indexByKey = new Map(out.map((slot, idx) => [keyOf(slot), idx]))
|
|
254
|
+
const conflicts = []
|
|
255
|
+
for (const remote of (Array.isArray(remoteSlots) ? remoteSlots : [])) {
|
|
256
|
+
if (!remote) continue
|
|
257
|
+
const key = keyOf(remote)
|
|
258
|
+
const idx = indexByKey.get(key)
|
|
259
|
+
if (idx == null) {
|
|
260
|
+
out.push({ ...remote })
|
|
261
|
+
indexByKey.set(key, out.length - 1)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
if (JSON.stringify(out[idx]) !== JSON.stringify(remote)) conflicts.push({ key, local: out[idx], remote })
|
|
265
|
+
out[idx] = { ...out[idx], ...remote }
|
|
266
|
+
}
|
|
267
|
+
return { slots: out, conflicts }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createVaultSnapshot({ slots, blobs = {} }, passphrase, label = '') {
|
|
271
|
+
if (!passphrase) throw new Error('passphrase is required for a vault snapshot')
|
|
272
|
+
const { vault, slotCount, blobCount } = exportVault({ slots: slots || [], blobs }, passphrase)
|
|
273
|
+
return { version: 2, label: String(label || ''), createdAt: new Date().toISOString(), vault, slotCount, blobCount }
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function restoreVaultSnapshot(snapshot, passphrase) {
|
|
277
|
+
if (!snapshot || typeof snapshot.vault !== 'string' || !snapshot.vault) throw new Error('snapshot is missing its encrypted vault')
|
|
278
|
+
return importVault(snapshot.vault, passphrase)
|
|
279
|
+
}
|
|
280
|
+
|
|
247
281
|
// --- Sync Coordination: Push & Pull ---
|
|
248
282
|
|
|
249
283
|
export async function syncVaultPush({ live, store, fetchFn = fetch }) {
|
|
@@ -258,7 +292,9 @@ export async function syncVaultPush({ live, store, fetchFn = fetch }) {
|
|
|
258
292
|
syncStatus.lastPushStatus = 'running'
|
|
259
293
|
|
|
260
294
|
try {
|
|
261
|
-
const
|
|
295
|
+
const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
|
|
296
|
+
const allSlots = cfg.slots || []
|
|
297
|
+
const slots = providerFilter.size ? allSlots.filter((slot) => providerFilter.has(String(slot && slot.provider))) : allSlots
|
|
262
298
|
const blobs = {}
|
|
263
299
|
for (const slot of normalizeSlots(slots)) {
|
|
264
300
|
try {
|
|
@@ -366,26 +402,27 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
366
402
|
}
|
|
367
403
|
|
|
368
404
|
const result = importVault(encryptedData, passphrase)
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
405
|
+
const providerFilter = new Set((cfg.vaultSyncProviders || []).map((p) => String(p)))
|
|
406
|
+
const remoteSlots = providerFilter.size
|
|
407
|
+
? result.slots.filter((slot) => providerFilter.has(String(slot && slot.provider)))
|
|
408
|
+
: result.slots
|
|
409
|
+
const allowedRefs = new Set(remoteSlots.map((slot) => slot && slot.ref))
|
|
410
|
+
const remoteBlobs = Object.fromEntries(Object.entries(result.blobs || {}).filter(([ref]) =>
|
|
411
|
+
!providerFilter.size || allowedRefs.has(ref)))
|
|
412
|
+
|
|
413
|
+
// Merge slots into live configuration; keep local-only rows and report conflicts.
|
|
414
|
+
let conflicts = []
|
|
415
|
+
if (remoteSlots.length && settingsApi) {
|
|
372
416
|
const revision = revisionOf(settingsApi)
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
if (idx >= 0) {
|
|
377
|
-
curSlots[idx] = { ...curSlots[idx], ...s }
|
|
378
|
-
} else {
|
|
379
|
-
curSlots.push(s)
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
const parsed = plainConfig(Config({ ...cfg, slots: curSlots }))
|
|
417
|
+
const merged = mergeVaultSlots(Array.isArray(cfg.slots) ? cfg.slots.slice() : [], remoteSlots)
|
|
418
|
+
conflicts = merged.conflicts
|
|
419
|
+
const parsed = plainConfig(Config({ ...cfg, slots: merged.slots }))
|
|
383
420
|
await settingsApi.replace(parsed, revision)
|
|
384
421
|
}
|
|
385
422
|
|
|
386
423
|
// Save blobs into store
|
|
387
424
|
let blobCount = 0
|
|
388
|
-
for (const [ref, blob] of Object.entries(
|
|
425
|
+
for (const [ref, blob] of Object.entries(remoteBlobs)) {
|
|
389
426
|
await store.saveBlob(ref, validateCredentialBlob(blob))
|
|
390
427
|
blobCount++
|
|
391
428
|
}
|
|
@@ -403,6 +440,7 @@ export async function syncVaultPull({ live, store, settingsApi, syncAdapter, ref
|
|
|
403
440
|
backend,
|
|
404
441
|
slotCount: result.slots.length,
|
|
405
442
|
blobCount,
|
|
443
|
+
conflicts: conflicts.length,
|
|
406
444
|
exportedAt: result.exportedAt,
|
|
407
445
|
timestamp: syncStatus.lastPullAt,
|
|
408
446
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { validateProfile, createVendorFromProfile } from './vendor-factory.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A vendor pack is a data-only provider declaration. It reuses the declarative
|
|
5
|
+
* profile contract but also carries the built vendor object, so a pack can be
|
|
6
|
+
* registered by another plugin without touching providers/vendors/cascade.
|
|
7
|
+
*/
|
|
8
|
+
export function normalizeVendorPack(raw) {
|
|
9
|
+
const pack = raw && typeof raw === 'object' ? raw : {}
|
|
10
|
+
const profile = validateProfile({
|
|
11
|
+
id: pack.id,
|
|
12
|
+
displayName: pack.displayName,
|
|
13
|
+
authUrl: pack.authUrl,
|
|
14
|
+
tokenUrl: pack.tokenUrl,
|
|
15
|
+
baseUrl: pack.baseUrl,
|
|
16
|
+
scope: pack.scope,
|
|
17
|
+
tokenStyle: pack.tokenStyle,
|
|
18
|
+
clientId: pack.clientId,
|
|
19
|
+
redirectUri: pack.redirectUri,
|
|
20
|
+
modelsPath: pack.modelsPath,
|
|
21
|
+
models: pack.models,
|
|
22
|
+
headers: pack.headers,
|
|
23
|
+
})
|
|
24
|
+
return { ...profile, vendor: createVendorFromProfile(profile) }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function registerVendorPack(raw, registry = {}) {
|
|
28
|
+
const pack = normalizeVendorPack(raw)
|
|
29
|
+
if (typeof registry.registerVendor !== 'function') throw new Error('vendor pack registry is unavailable')
|
|
30
|
+
registry.registerVendor(pack.vendor)
|
|
31
|
+
if (typeof registry.registerProviderId === 'function') registry.registerProviderId([pack.id])
|
|
32
|
+
if (typeof registry.registerDisplayName === 'function' && pack.displayName) {
|
|
33
|
+
registry.registerDisplayName(pack.id, pack.displayName)
|
|
34
|
+
}
|
|
35
|
+
return pack
|
|
36
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Headless/TUI overlay for a profile that has no web UI.
|
|
2
|
+
# Usage: dsh --profile headless --patch /absolute/path/overlay.example.yml "your task"
|
|
3
|
+
- id: dsh-subscriptions
|
|
4
|
+
name: '@goodandready/dsh-subscriptions'
|
|
5
|
+
config:
|
|
6
|
+
composerQuota: percent
|
|
7
|
+
composerQuotaDisplay: current
|
|
8
|
+
rotationStrategy: least-loaded
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.3",
|
|
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",
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
"README.md",
|
|
18
18
|
"README.ru.md",
|
|
19
19
|
"README.zh.md",
|
|
20
|
-
"LICENSE"
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"bin/",
|
|
22
|
+
"overlay.example.yml"
|
|
21
23
|
],
|
|
22
24
|
"keywords": [
|
|
23
25
|
"dsh",
|
|
@@ -50,6 +52,32 @@
|
|
|
50
52
|
"@deepseek-ai/dsh-client-locale",
|
|
51
53
|
"@deepseek-ai/dsh-client-ui-slots"
|
|
52
54
|
]
|
|
55
|
+
},
|
|
56
|
+
"market": {
|
|
57
|
+
"name": {
|
|
58
|
+
"en": "Subscriptions",
|
|
59
|
+
"zh": "订阅管理"
|
|
60
|
+
},
|
|
61
|
+
"description": {
|
|
62
|
+
"en": "Use ChatGPT, Claude, Grok, Antigravity and 12 more subscriptions as DSH providers with account pools, quota tools and OAuth.",
|
|
63
|
+
"zh": "将 ChatGPT、Claude、Grok、Antigravity 等 16 种订阅接入 DSH,支持账户池、额度工具和 OAuth。"
|
|
64
|
+
},
|
|
65
|
+
"category": "providers",
|
|
66
|
+
"icon": "subscriptions",
|
|
67
|
+
"tags": [
|
|
68
|
+
"subscriptions",
|
|
69
|
+
"oauth",
|
|
70
|
+
"rotation",
|
|
71
|
+
"quota",
|
|
72
|
+
"providers",
|
|
73
|
+
"media"
|
|
74
|
+
],
|
|
75
|
+
"defaultProviders": [
|
|
76
|
+
"codex",
|
|
77
|
+
"claude",
|
|
78
|
+
"grok",
|
|
79
|
+
"antigravity"
|
|
80
|
+
]
|
|
53
81
|
}
|
|
54
82
|
},
|
|
55
83
|
"peerDependencies": {
|
|
@@ -77,5 +105,8 @@
|
|
|
77
105
|
"@types/node": "^22.20.2",
|
|
78
106
|
"eslint": "^10.4.0",
|
|
79
107
|
"typescript": "^7.0.2"
|
|
108
|
+
},
|
|
109
|
+
"bin": {
|
|
110
|
+
"dsh-subscriptions": "./bin/dsh-subscriptions.mjs"
|
|
80
111
|
}
|
|
81
112
|
}
|