@goodandready/dsh-subscriptions 0.7.34 → 0.7.36
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/client.js +100 -176
- package/lib/client.panels.js +187 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -313,6 +313,13 @@ window.__ModuleLoader__.load({
|
|
|
313
313
|
"save": "Save",
|
|
314
314
|
"saved": "Saved",
|
|
315
315
|
"cancel": "Cancel",
|
|
316
|
+
"checkAllAccounts": "⚡ Check all",
|
|
317
|
+
"refreshAllQuotas": "🔄 Refresh quotas",
|
|
318
|
+
"importAllCli": "📥 Import all CLI",
|
|
319
|
+
"batchProgress": "{done}/{total} done",
|
|
320
|
+
"moveUp": "Move up",
|
|
321
|
+
"moveDown": "Move down",
|
|
322
|
+
"copyRef": "Copy account ref",
|
|
316
323
|
"unsavedChanges": "Unsaved changes",
|
|
317
324
|
"accountLabel": "Account",
|
|
318
325
|
"usageRemaining": "Remaining quota",
|
|
@@ -712,6 +719,13 @@ window.__ModuleLoader__.load({
|
|
|
712
719
|
"save": "保存",
|
|
713
720
|
"saved": "已保存",
|
|
714
721
|
"cancel": "取消",
|
|
722
|
+
"checkAllAccounts": "⚡ 全部检查",
|
|
723
|
+
"refreshAllQuotas": "🔄 刷新配额",
|
|
724
|
+
"importAllCli": "📥 导入全部 CLI",
|
|
725
|
+
"batchProgress": "已完成 {done}/{total}",
|
|
726
|
+
"moveUp": "上移",
|
|
727
|
+
"moveDown": "下移",
|
|
728
|
+
"copyRef": "复制账户引用",
|
|
715
729
|
"unsavedChanges": "有未保存的更改",
|
|
716
730
|
"accountLabel": "账户",
|
|
717
731
|
"usageRemaining": "剩余配额",
|
|
@@ -1018,6 +1032,17 @@ window.__ModuleLoader__.load({
|
|
|
1018
1032
|
}
|
|
1019
1033
|
const Chevron = ChevronIcon || FallbackChevron
|
|
1020
1034
|
|
|
1035
|
+
function lazyPanel(name) {
|
|
1036
|
+
const Component = React.lazy(() => require.async('./client.panels.js').then((mod) => ({ default: mod[name] })))
|
|
1037
|
+
return function LazyPanel(props) {
|
|
1038
|
+
return h(React.Suspense, { fallback: null }, h(Component, props))
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
const WebhookTestButton = lazyPanel('WebhookTestButton')
|
|
1042
|
+
const VaultSnapshotsPanel = lazyPanel('VaultSnapshotsPanel')
|
|
1043
|
+
const ProviderPacksPanel = lazyPanel('ProviderPacksPanel')
|
|
1044
|
+
const SourcesPanel = lazyPanel('SourcesPanel')
|
|
1045
|
+
|
|
1021
1046
|
// #839 model-list helpers
|
|
1022
1047
|
function modelListSearchText(model) {
|
|
1023
1048
|
const parts = [model && model.id, model && model.name, model && model.alias, model && model.provider, model && model.providerName]
|
|
@@ -1432,6 +1457,8 @@ window.__ModuleLoader__.load({
|
|
|
1432
1457
|
}
|
|
1433
1458
|
}
|
|
1434
1459
|
const [checking, setChecking] = React.useState({})
|
|
1460
|
+
const [batch, setBatch] = React.useState(null)
|
|
1461
|
+
const batchCancelRef = React.useRef(false)
|
|
1435
1462
|
const [saved, setSaved] = React.useState(false)
|
|
1436
1463
|
const [savedSnapshot, setSavedSnapshot] = React.useState(null)
|
|
1437
1464
|
const [tokenDraft, setTokenDraft] = React.useState({})
|
|
@@ -1544,6 +1571,21 @@ window.__ModuleLoader__.load({
|
|
|
1544
1571
|
const updateSlot = (provider, index, patch) => setSlots((prev) => prev.map((s) => (
|
|
1545
1572
|
s && s.provider === provider && Number(s.index) === Number(index) ? Object.assign({}, s, patch) : s
|
|
1546
1573
|
)))
|
|
1574
|
+
const moveSlot = (provider, index, delta) => setSlots((prev) => {
|
|
1575
|
+
const positions = prev
|
|
1576
|
+
.map((slot, position) => ({ slot, position }))
|
|
1577
|
+
.filter((row) => row.slot && row.slot.provider === provider)
|
|
1578
|
+
const from = positions.findIndex((row) => Number(row.slot.index) === Number(index))
|
|
1579
|
+
const to = from + delta
|
|
1580
|
+
if (from < 0 || to < 0 || to >= positions.length) return prev
|
|
1581
|
+
const next = prev.slice()
|
|
1582
|
+
const a = positions[from].position
|
|
1583
|
+
const b = positions[to].position
|
|
1584
|
+
const tmp = next[a]
|
|
1585
|
+
next[a] = next[b]
|
|
1586
|
+
next[b] = tmp
|
|
1587
|
+
return next
|
|
1588
|
+
})
|
|
1547
1589
|
const accountOf = (provider, index) => accounts.find((a) => a.provider === provider && a.index === index) || {}
|
|
1548
1590
|
|
|
1549
1591
|
const save = async () => {
|
|
@@ -1762,6 +1804,34 @@ window.__ModuleLoader__.load({
|
|
|
1762
1804
|
later(() => setInfo(''), 3000)
|
|
1763
1805
|
}
|
|
1764
1806
|
|
|
1807
|
+
const runBatch = async (kind, provider) => {
|
|
1808
|
+
if (batch) return
|
|
1809
|
+
const rows = slots.filter((slot) => slot && slot.provider === provider)
|
|
1810
|
+
if (!rows.length) return
|
|
1811
|
+
batchCancelRef.current = false
|
|
1812
|
+
setBatch({ kind, provider, done: 0, total: rows.length })
|
|
1813
|
+
const queue = rows.slice()
|
|
1814
|
+
const worker = async () => {
|
|
1815
|
+
while (queue.length && !batchCancelRef.current) {
|
|
1816
|
+
const slot = queue.shift()
|
|
1817
|
+
try {
|
|
1818
|
+
if (kind === 'import') await importLocalCli(slot.provider, slot.index)
|
|
1819
|
+
else await doCheck(slot.provider, slot.index)
|
|
1820
|
+
} catch (e) {
|
|
1821
|
+
setErr(cleanErrorMessage(e.message || e))
|
|
1822
|
+
} finally {
|
|
1823
|
+
setBatch((b) => b ? Object.assign({}, b, { done: b.done + 1 }) : b)
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
await Promise.all([worker(), worker()])
|
|
1828
|
+
setBatch(null)
|
|
1829
|
+
}
|
|
1830
|
+
const cancelBatch = () => {
|
|
1831
|
+
batchCancelRef.current = true
|
|
1832
|
+
setBatch((b) => b ? Object.assign({}, b, { cancelled: true }) : b)
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1765
1835
|
const importToken = async (provider, index) => {
|
|
1766
1836
|
setErr('')
|
|
1767
1837
|
const tok = (tokenDraft[provider + ':' + index] || '').trim()
|
|
@@ -2849,10 +2919,22 @@ h('div', { className: 'dsub-row' },
|
|
|
2849
2919
|
.map((slot, i) => ({ slot: slot, i: i }))
|
|
2850
2920
|
.filter((row) => row.slot.provider === prov.id)
|
|
2851
2921
|
return h('div', { className: 'dsub-block', key: prov.id },
|
|
2852
|
-
h('div', { className: 'dsub-h'
|
|
2922
|
+
h('div', { className: 'dsub-h', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 6, flexWrap: 'wrap' } },
|
|
2923
|
+
h('span', null, prov.name),
|
|
2924
|
+
h('div', { className: 'dsub-row', style: { marginTop: 0, gap: 4, flexWrap: 'wrap' } },
|
|
2925
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: Boolean(batch), onClick: () => runBatch('check', prov.id).catch((e) => setErr(cleanErrorMessage(e.message || e))) }, t('checkAllAccounts')),
|
|
2926
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: Boolean(batch), onClick: () => runBatch('check', prov.id).catch((e) => setErr(cleanErrorMessage(e.message || e))) }, t('refreshAllQuotas')),
|
|
2927
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: Boolean(batch), onClick: () => runBatch('import', prov.id).catch((e) => setErr(cleanErrorMessage(e.message || e))) }, t('importAllCli')),
|
|
2928
|
+
batch && batch.provider === prov.id ? h(React.Fragment, null,
|
|
2929
|
+
h('span', { className: 'dsub-sub' }, t('batchProgress').replace('{done}', String(batch.done)).replace('{total}', String(batch.total))),
|
|
2930
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: cancelBatch }, t('cancel'))
|
|
2931
|
+
) : null,
|
|
2932
|
+
),
|
|
2933
|
+
),
|
|
2853
2934
|
rows.map((row) => {
|
|
2854
2935
|
const account = accountOf(row.slot.provider, row.slot.index)
|
|
2855
2936
|
const key = row.slot.provider + ':' + row.slot.index
|
|
2937
|
+
const rowPosition = rows.findIndex((candidate) => candidate.i === row.i)
|
|
2856
2938
|
const on = !!account.configured
|
|
2857
2939
|
const poolEnabled = row.slot.poolEnabled !== false
|
|
2858
2940
|
const poolModels = Array.isArray(row.slot.poolModels) ? row.slot.poolModels : null
|
|
@@ -2903,6 +2985,9 @@ h('div', { className: 'dsub-row' },
|
|
|
2903
2985
|
},
|
|
2904
2986
|
}),
|
|
2905
2987
|
h('span', { className: 'dsub-badge' + (account.isRevoked ? ' dsub-badge-bad' : (account.validationUrl ? ' dsub-badge-warn' : (on ? ' dsub-badge-on' : ''))) }, badgeFor(account, t)),
|
|
2988
|
+
account.capabilities && account.capabilities.vision ? h('span', { className: 'dsub-model-tag', title: 'Vision' }, '👁') : null,
|
|
2989
|
+
account.capabilities && account.capabilities.tools ? h('span', { className: 'dsub-model-tag', title: 'Tools' }, '🔧') : null,
|
|
2990
|
+
account.capabilities && account.capabilities.thinking ? h('span', { className: 'dsub-model-tag', title: 'Thinking' }, '🧠') : null,
|
|
2906
2991
|
normalizePlanBadge(row.slot.provider, account.plan || account.paidTierName || (account.quota && account.quota.plan)),
|
|
2907
2992
|
h('button', {
|
|
2908
2993
|
type: 'button', className: 'dsub-mini',
|
|
@@ -2968,6 +3053,20 @@ h('div', { className: 'dsub-row' },
|
|
|
2968
3053
|
type: 'button', className: 'dsub-mini', title: t('quarantineParkTitle'),
|
|
2969
3054
|
onClick: () => doParkQuarantine(row.slot.provider, row.slot.index, 3600000),
|
|
2970
3055
|
}, ('⏸ ' + t('quarantinePark'))) : null)),
|
|
3056
|
+
h('button', {
|
|
3057
|
+
type: 'button', className: 'dsub-mini', title: t('moveUp'),
|
|
3058
|
+
disabled: rowPosition <= 0,
|
|
3059
|
+
onClick: () => moveSlot(row.slot.provider, row.slot.index, -1),
|
|
3060
|
+
}, '↑'),
|
|
3061
|
+
h('button', {
|
|
3062
|
+
type: 'button', className: 'dsub-mini', title: t('moveDown'),
|
|
3063
|
+
disabled: rowPosition < 0 || rowPosition >= rows.length - 1,
|
|
3064
|
+
onClick: () => moveSlot(row.slot.provider, row.slot.index, 1),
|
|
3065
|
+
}, '↓'),
|
|
3066
|
+
h('button', {
|
|
3067
|
+
type: 'button', className: 'dsub-mini', title: t('copyRef'),
|
|
3068
|
+
onClick: () => copyText(row.slot.ref || key),
|
|
3069
|
+
}, '📋'),
|
|
2971
3070
|
h('button', {
|
|
2972
3071
|
type: 'button', className: 'dsub-mini', title: t('removeSlot'),
|
|
2973
3072
|
onClick: () => {
|
|
@@ -4260,181 +4359,6 @@ h('div', { className: 'dsub-row' },
|
|
|
4260
4359
|
)
|
|
4261
4360
|
}
|
|
4262
4361
|
|
|
4263
|
-
function WebhookTestButton(props) {
|
|
4264
|
-
const t = (props && props.t) || ((k) => k)
|
|
4265
|
-
const [state, setState] = React.useState({ busy: false, message: '', error: null })
|
|
4266
|
-
const send = () => {
|
|
4267
|
-
setState({ busy: true, message: '', error: null })
|
|
4268
|
-
fetch('/dsh-subscriptions/webhook/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
4269
|
-
.then(async (res) => {
|
|
4270
|
-
const data = await res.json().catch(() => ({}))
|
|
4271
|
-
if (!res.ok || !data.delivered) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
4272
|
-
setState({ busy: false, message: t('delivered'), error: null })
|
|
4273
|
-
})
|
|
4274
|
-
.catch((err) => setState({ busy: false, message: '', error: String(err && err.message || err) }))
|
|
4275
|
-
}
|
|
4276
|
-
return h('div', { className: 'dsub-row' },
|
|
4277
|
-
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: send }, state.busy ? '…' : t('sendTest')),
|
|
4278
|
-
state.message ? h('span', { className: 'dsub-ok' }, state.message) : null,
|
|
4279
|
-
state.error ? h('span', { className: 'dsub-bad' }, state.error) : null,
|
|
4280
|
-
)
|
|
4281
|
-
}
|
|
4282
|
-
|
|
4283
|
-
// #736: vault snapshot list with create/restore. Payload stays server-side.
|
|
4284
|
-
function VaultSnapshotsPanel(props) {
|
|
4285
|
-
const t = (props && props.t) || ((k) => k)
|
|
4286
|
-
const [state, setState] = React.useState({ snapshots: [], stats: null, message: '', error: null, busy: false })
|
|
4287
|
-
const pull = () => {
|
|
4288
|
-
fetch('/dsh-subscriptions/vault/snapshots', { cache: 'no-store' })
|
|
4289
|
-
.then((res) => res.json())
|
|
4290
|
-
.then((data) => setState((st) => Object.assign({}, st, { snapshots: data.snapshots || [], stats: data.stats || null, error: null })))
|
|
4291
|
-
.catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
|
|
4292
|
-
}
|
|
4293
|
-
React.useEffect(() => { pull() }, [])
|
|
4294
|
-
const call = (url, body) => fetch(url, {
|
|
4295
|
-
method: 'POST',
|
|
4296
|
-
headers: { 'content-type': 'application/json' },
|
|
4297
|
-
body: JSON.stringify(body || {}),
|
|
4298
|
-
}).then(async (res) => {
|
|
4299
|
-
const data = await res.json().catch(() => ({}))
|
|
4300
|
-
if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
4301
|
-
return data
|
|
4302
|
-
})
|
|
4303
|
-
const create = () => {
|
|
4304
|
-
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
4305
|
-
call('/dsh-subscriptions/vault/snapshots', { label: 'manual' })
|
|
4306
|
-
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: t('created') + ' ' + data.snapshot.id })); pull() })
|
|
4307
|
-
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
4308
|
-
}
|
|
4309
|
-
const restore = (id) => {
|
|
4310
|
-
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
4311
|
-
call('/dsh-subscriptions/vault/snapshots/restore', { id })
|
|
4312
|
-
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: t('restoredSlots') + ' ' + data.slotCount })); pull() })
|
|
4313
|
-
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
4314
|
-
}
|
|
4315
|
-
return h('div', { className: 'dsub-section-card' },
|
|
4316
|
-
h('div', { className: 'dsub-section-title' }, t('vaultSnapshots'), h('button', { type: 'button', className: 'dsub-mini', onClick: create, disabled: state.busy }, state.busy ? '…' : t('createSnapshot'))),
|
|
4317
|
-
h('div', { className: 'dsub-section-desc' }, t('vaultSnapshotsHint')),
|
|
4318
|
-
state.stats ? h('div', { className: 'dsub-dim' }, state.stats.count + ' snapshots · ' + state.stats.bytes + ' bytes · ' + state.stats.path) : null,
|
|
4319
|
-
state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
|
|
4320
|
-
h('span', { className: 'dsub-h' }, snap.id),
|
|
4321
|
-
h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
|
|
4322
|
-
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, t('restoreSnapshot')),
|
|
4323
|
-
)) : h('div', { className: 'dsub-dim' }, t('noSnapshots')),
|
|
4324
|
-
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
4325
|
-
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
4326
|
-
)
|
|
4327
|
-
}
|
|
4328
|
-
|
|
4329
|
-
// #724: provider pack manager. Displays ids/models only; headers are masked.
|
|
4330
|
-
function ProviderPacksPanel(props) {
|
|
4331
|
-
const t = (props && props.t) || ((k) => k)
|
|
4332
|
-
const [state, setState] = React.useState({ packs: [], text: '', message: '', error: null, busy: false })
|
|
4333
|
-
const publicPack = (pack) => {
|
|
4334
|
-
const copy = Object.assign({}, pack)
|
|
4335
|
-
if (copy.clientSecret) copy.clientSecret = '••••'
|
|
4336
|
-
if (copy.headers && typeof copy.headers === 'object') {
|
|
4337
|
-
copy.headers = Object.fromEntries(Object.keys(copy.headers).map((key) => [key, '••••']))
|
|
4338
|
-
}
|
|
4339
|
-
return copy
|
|
4340
|
-
}
|
|
4341
|
-
const pull = () => {
|
|
4342
|
-
fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
4343
|
-
.then((res) => res.json())
|
|
4344
|
-
.then((cfg) => setState((st) => Object.assign({}, st, { packs: (cfg.config && cfg.config.providerPacks) || [] })))
|
|
4345
|
-
.catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
|
|
4346
|
-
}
|
|
4347
|
-
React.useEffect(() => { pull() }, [])
|
|
4348
|
-
const parse = () => {
|
|
4349
|
-
const raw = JSON.parse(state.text || '{}')
|
|
4350
|
-
return raw && raw.pack ? raw.pack : raw
|
|
4351
|
-
}
|
|
4352
|
-
const validate = (pack) => fetch('/dsh-subscriptions/provider-packs/validate', {
|
|
4353
|
-
method: 'POST',
|
|
4354
|
-
headers: { 'content-type': 'application/json' },
|
|
4355
|
-
body: JSON.stringify({ pack }),
|
|
4356
|
-
}).then(async (res) => {
|
|
4357
|
-
const data = await res.json()
|
|
4358
|
-
if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
4359
|
-
return data.pack
|
|
4360
|
-
})
|
|
4361
|
-
const save = () => {
|
|
4362
|
-
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
4363
|
-
Promise.resolve().then(parse)
|
|
4364
|
-
.then(async (pack) => {
|
|
4365
|
-
const checked = await validate(pack)
|
|
4366
|
-
const cfg = await fetch('/dsh-subscriptions/config', { cache: 'no-store' }).then((res) => res.json())
|
|
4367
|
-
const packs = ((cfg.config && cfg.config.providerPacks) || []).filter((p) => p && p.id !== pack.id)
|
|
4368
|
-
packs.push(pack)
|
|
4369
|
-
const res = await fetch('/dsh-subscriptions/config', {
|
|
4370
|
-
method: 'PUT',
|
|
4371
|
-
headers: { 'content-type': 'application/json' },
|
|
4372
|
-
body: JSON.stringify({ config: Object.assign({}, cfg.config, { providerPacks: packs }), revision: cfg.revision }),
|
|
4373
|
-
})
|
|
4374
|
-
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
4375
|
-
setState((st) => Object.assign({}, st, { packs, busy: false, message: t('saved') + ' ' + checked.id, text: '' }))
|
|
4376
|
-
})
|
|
4377
|
-
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
4378
|
-
}
|
|
4379
|
-
const exportPacks = () => setState((st) => Object.assign({}, st, { text: JSON.stringify(st.packs.map(publicPack), null, 2), message: t('exported') }))
|
|
4380
|
-
return h('div', { className: 'dsub-section-card' },
|
|
4381
|
-
h('div', { className: 'dsub-section-title' }, t('providerPacks'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, '↻')),
|
|
4382
|
-
h('div', { className: 'dsub-section-desc' }, t('providerPacksHint')),
|
|
4383
|
-
state.packs.length ? state.packs.map((pack) => h('div', { className: 'dsub-row', key: pack.id },
|
|
4384
|
-
h('span', { className: 'dsub-h' }, pack.id),
|
|
4385
|
-
h('span', { className: 'dsub-dim' }, (pack.displayName || '') + ' · ' + ((pack.models || []).length) + ' models'),
|
|
4386
|
-
)) : h('div', { className: 'dsub-dim' }, t('noPacks')),
|
|
4387
|
-
h('textarea', {
|
|
4388
|
-
className: 'dsub-inp',
|
|
4389
|
-
style: { width: '100%', minHeight: 120, fontFamily: 'ui-monospace,monospace', padding: 8 },
|
|
4390
|
-
value: state.text,
|
|
4391
|
-
placeholder: '{"id":"acme","displayName":"Acme AI","authUrl":"https://…","tokenUrl":"https://…","baseUrl":"https://…","clientId":"…","models":[{"id":"acme-1"}]}',
|
|
4392
|
-
onChange: (e) => setState((st) => Object.assign({}, st, { text: e.target.value, message: '', error: null })),
|
|
4393
|
-
}),
|
|
4394
|
-
h('div', { className: 'dsub-row' },
|
|
4395
|
-
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: save }, state.busy ? '…' : t('validateAndSave')),
|
|
4396
|
-
h('button', { type: 'button', className: 'dsub-mini', onClick: exportPacks }, t('exportPacks')),
|
|
4397
|
-
),
|
|
4398
|
-
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
4399
|
-
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
4400
|
-
)
|
|
4401
|
-
}
|
|
4402
|
-
|
|
4403
|
-
// #713: read-only source chain panel. Never renders key values.
|
|
4404
|
-
function SourcesPanel(props) {
|
|
4405
|
-
const t = (props && props.t) || ((k) => k)
|
|
4406
|
-
const [state, setState] = React.useState({ loading: true, data: null, error: null })
|
|
4407
|
-
const pull = () => {
|
|
4408
|
-
fetch('/dsh-subscriptions/sources', { cache: 'no-store' })
|
|
4409
|
-
.then((res) => res.json())
|
|
4410
|
-
.then((data) => setState({ loading: false, data, error: null }))
|
|
4411
|
-
.catch((err) => setState({ loading: false, data: null, error: String(err && err.message || err) }))
|
|
4412
|
-
}
|
|
4413
|
-
React.useEffect(() => {
|
|
4414
|
-
pull()
|
|
4415
|
-
const id = setInterval(pull, 30 * 1000)
|
|
4416
|
-
return () => clearInterval(id)
|
|
4417
|
-
}, [])
|
|
4418
|
-
const sourceLabel = (source) => source === 'api-key' ? t('apiKeySource') : (source === 'oauth' ? t('oauthSource') : (source === 'ollama' ? t('ollamaSource') : t('sourceNone')))
|
|
4419
|
-
return h('div', { className: 'dsub-section-card' },
|
|
4420
|
-
h('div', { className: 'dsub-section-title' }, t('sources'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, state.loading ? '…' : '↻')),
|
|
4421
|
-
h('div', { className: 'dsub-section-desc' }, t('sourcesHint')),
|
|
4422
|
-
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
4423
|
-
state.data && state.data.keyRotation && !state.data.keyRotation.available
|
|
4424
|
-
? h('div', { className: 'dsub-dim' }, t('keyRotationOffline') + (state.data.keyRotation.reason ? ': ' + state.data.keyRotation.reason : ''))
|
|
4425
|
-
: null,
|
|
4426
|
-
state.data && state.data.keyRotation && state.data.keyRotation.available
|
|
4427
|
-
? h('div', { className: 'dsub-dim' }, t('keyRotationHealth') + ': ' + state.data.keyRotation.healthyKeys + '/' + state.data.keyRotation.totalKeys + ' healthy keys')
|
|
4428
|
-
: null,
|
|
4429
|
-
(state.data && state.data.providers ? state.data.providers : []).map((row) => h('div', { className: 'dsub-row', key: row.provider },
|
|
4430
|
-
h('span', { className: 'dsub-h' }, row.provider),
|
|
4431
|
-
h('span', { className: 'dsub-pillTag' }, sourceLabel(row.selected)),
|
|
4432
|
-
row.candidates.map((c, i) => h('span', { key: i, className: 'dsub-dim' },
|
|
4433
|
-
sourceLabel(c.source) + (c.available ? ' ✓' : ' ✕'))),
|
|
4434
|
-
)),
|
|
4435
|
-
)
|
|
4436
|
-
}
|
|
4437
|
-
|
|
4438
4362
|
// #688: floating/docked quota capsule. Draggable unless docked; opens settings.
|
|
4439
4363
|
function QuotaCapsule(props) {
|
|
4440
4364
|
const t = (props && props.t) || ((k) => k)
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/* global window */
|
|
2
|
+
window.__ModuleLoader__.load({
|
|
3
|
+
id: '@goodandready/dsh-subscriptions',
|
|
4
|
+
chunk: 'client.panels.js',
|
|
5
|
+
factory: (require) => {
|
|
6
|
+
const React = require('react')
|
|
7
|
+
const h = React.createElement
|
|
8
|
+
|
|
9
|
+
function WebhookTestButton(props) {
|
|
10
|
+
const t = (props && props.t) || ((k) => k)
|
|
11
|
+
const [state, setState] = React.useState({ busy: false, message: '', error: null })
|
|
12
|
+
const send = () => {
|
|
13
|
+
setState({ busy: true, message: '', error: null })
|
|
14
|
+
fetch('/dsh-subscriptions/webhook/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' })
|
|
15
|
+
.then(async (res) => {
|
|
16
|
+
const data = await res.json().catch(() => ({}))
|
|
17
|
+
if (!res.ok || !data.delivered) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
18
|
+
setState({ busy: false, message: t('delivered'), error: null })
|
|
19
|
+
})
|
|
20
|
+
.catch((err) => setState({ busy: false, message: '', error: String(err && err.message || err) }))
|
|
21
|
+
}
|
|
22
|
+
return h('div', { className: 'dsub-row' },
|
|
23
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: send }, state.busy ? '…' : t('sendTest')),
|
|
24
|
+
state.message ? h('span', { className: 'dsub-ok' }, state.message) : null,
|
|
25
|
+
state.error ? h('span', { className: 'dsub-bad' }, state.error) : null,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// #736: vault snapshot list with create/restore. Payload stays server-side.
|
|
30
|
+
function VaultSnapshotsPanel(props) {
|
|
31
|
+
const t = (props && props.t) || ((k) => k)
|
|
32
|
+
const [state, setState] = React.useState({ snapshots: [], stats: null, message: '', error: null, busy: false })
|
|
33
|
+
const pull = () => {
|
|
34
|
+
fetch('/dsh-subscriptions/vault/snapshots', { cache: 'no-store' })
|
|
35
|
+
.then((res) => res.json())
|
|
36
|
+
.then((data) => setState((st) => Object.assign({}, st, { snapshots: data.snapshots || [], stats: data.stats || null, error: null })))
|
|
37
|
+
.catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
|
|
38
|
+
}
|
|
39
|
+
React.useEffect(() => { pull() }, [])
|
|
40
|
+
const call = (url, body) => fetch(url, {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'content-type': 'application/json' },
|
|
43
|
+
body: JSON.stringify(body || {}),
|
|
44
|
+
}).then(async (res) => {
|
|
45
|
+
const data = await res.json().catch(() => ({}))
|
|
46
|
+
if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
47
|
+
return data
|
|
48
|
+
})
|
|
49
|
+
const create = () => {
|
|
50
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
51
|
+
call('/dsh-subscriptions/vault/snapshots', { label: 'manual' })
|
|
52
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: t('created') + ' ' + data.snapshot.id })); pull() })
|
|
53
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
54
|
+
}
|
|
55
|
+
const restore = (id) => {
|
|
56
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
57
|
+
call('/dsh-subscriptions/vault/snapshots/restore', { id })
|
|
58
|
+
.then((data) => { setState((st) => Object.assign({}, st, { busy: false, message: t('restoredSlots') + ' ' + data.slotCount })); pull() })
|
|
59
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
60
|
+
}
|
|
61
|
+
return h('div', { className: 'dsub-section-card' },
|
|
62
|
+
h('div', { className: 'dsub-section-title' }, t('vaultSnapshots'), h('button', { type: 'button', className: 'dsub-mini', onClick: create, disabled: state.busy }, state.busy ? '…' : t('createSnapshot'))),
|
|
63
|
+
h('div', { className: 'dsub-section-desc' }, t('vaultSnapshotsHint')),
|
|
64
|
+
state.stats ? h('div', { className: 'dsub-dim' }, state.stats.count + ' snapshots · ' + state.stats.bytes + ' bytes · ' + state.stats.path) : null,
|
|
65
|
+
state.snapshots.length ? state.snapshots.map((snap) => h('div', { className: 'dsub-row', key: snap.id },
|
|
66
|
+
h('span', { className: 'dsub-h' }, snap.id),
|
|
67
|
+
h('span', { className: 'dsub-dim' }, (snap.label || '-') + ' · ' + snap.slotCount + ' slots · ' + snap.createdAt),
|
|
68
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: () => restore(snap.id) }, t('restoreSnapshot')),
|
|
69
|
+
)) : h('div', { className: 'dsub-dim' }, t('noSnapshots')),
|
|
70
|
+
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
71
|
+
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// #724: provider pack manager. Displays ids/models only; headers are masked.
|
|
76
|
+
function ProviderPacksPanel(props) {
|
|
77
|
+
const t = (props && props.t) || ((k) => k)
|
|
78
|
+
const [state, setState] = React.useState({ packs: [], text: '', message: '', error: null, busy: false })
|
|
79
|
+
const publicPack = (pack) => {
|
|
80
|
+
const copy = Object.assign({}, pack)
|
|
81
|
+
if (copy.clientSecret) copy.clientSecret = '••••'
|
|
82
|
+
if (copy.headers && typeof copy.headers === 'object') {
|
|
83
|
+
copy.headers = Object.fromEntries(Object.keys(copy.headers).map((key) => [key, '••••']))
|
|
84
|
+
}
|
|
85
|
+
return copy
|
|
86
|
+
}
|
|
87
|
+
const pull = () => {
|
|
88
|
+
fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
89
|
+
.then((res) => res.json())
|
|
90
|
+
.then((cfg) => setState((st) => Object.assign({}, st, { packs: (cfg.config && cfg.config.providerPacks) || [] })))
|
|
91
|
+
.catch((err) => setState((st) => Object.assign({}, st, { error: String(err && err.message || err) })))
|
|
92
|
+
}
|
|
93
|
+
React.useEffect(() => { pull() }, [])
|
|
94
|
+
const parse = () => {
|
|
95
|
+
const raw = JSON.parse(state.text || '{}')
|
|
96
|
+
return raw && raw.pack ? raw.pack : raw
|
|
97
|
+
}
|
|
98
|
+
const validate = (pack) => fetch('/dsh-subscriptions/provider-packs/validate', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'content-type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ pack }),
|
|
102
|
+
}).then(async (res) => {
|
|
103
|
+
const data = await res.json()
|
|
104
|
+
if (!res.ok || !data.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
105
|
+
return data.pack
|
|
106
|
+
})
|
|
107
|
+
const save = () => {
|
|
108
|
+
setState((st) => Object.assign({}, st, { busy: true, message: '', error: null }))
|
|
109
|
+
Promise.resolve().then(parse)
|
|
110
|
+
.then(async (pack) => {
|
|
111
|
+
const checked = await validate(pack)
|
|
112
|
+
const cfg = await fetch('/dsh-subscriptions/config', { cache: 'no-store' }).then((res) => res.json())
|
|
113
|
+
const packs = ((cfg.config && cfg.config.providerPacks) || []).filter((p) => p && p.id !== pack.id)
|
|
114
|
+
packs.push(pack)
|
|
115
|
+
const res = await fetch('/dsh-subscriptions/config', {
|
|
116
|
+
method: 'PUT',
|
|
117
|
+
headers: { 'content-type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ config: Object.assign({}, cfg.config, { providerPacks: packs }), revision: cfg.revision }),
|
|
119
|
+
})
|
|
120
|
+
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
121
|
+
setState((st) => Object.assign({}, st, { packs, busy: false, message: t('saved') + ' ' + checked.id, text: '' }))
|
|
122
|
+
})
|
|
123
|
+
.catch((err) => setState((st) => Object.assign({}, st, { busy: false, error: String(err && err.message || err) })))
|
|
124
|
+
}
|
|
125
|
+
const exportPacks = () => setState((st) => Object.assign({}, st, { text: JSON.stringify(st.packs.map(publicPack), null, 2), message: t('exported') }))
|
|
126
|
+
return h('div', { className: 'dsub-section-card' },
|
|
127
|
+
h('div', { className: 'dsub-section-title' }, t('providerPacks'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, '↻')),
|
|
128
|
+
h('div', { className: 'dsub-section-desc' }, t('providerPacksHint')),
|
|
129
|
+
state.packs.length ? state.packs.map((pack) => h('div', { className: 'dsub-row', key: pack.id },
|
|
130
|
+
h('span', { className: 'dsub-h' }, pack.id),
|
|
131
|
+
h('span', { className: 'dsub-dim' }, (pack.displayName || '') + ' · ' + ((pack.models || []).length) + ' models'),
|
|
132
|
+
)) : h('div', { className: 'dsub-dim' }, t('noPacks')),
|
|
133
|
+
h('textarea', {
|
|
134
|
+
className: 'dsub-inp',
|
|
135
|
+
style: { width: '100%', minHeight: 120, fontFamily: 'ui-monospace,monospace', padding: 8 },
|
|
136
|
+
value: state.text,
|
|
137
|
+
placeholder: '{"id":"acme","displayName":"Acme AI","authUrl":"https://…","tokenUrl":"https://…","baseUrl":"https://…","clientId":"…","models":[{"id":"acme-1"}]}',
|
|
138
|
+
onChange: (e) => setState((st) => Object.assign({}, st, { text: e.target.value, message: '', error: null })),
|
|
139
|
+
}),
|
|
140
|
+
h('div', { className: 'dsub-row' },
|
|
141
|
+
h('button', { type: 'button', className: 'dsub-mini', disabled: state.busy, onClick: save }, state.busy ? '…' : t('validateAndSave')),
|
|
142
|
+
h('button', { type: 'button', className: 'dsub-mini', onClick: exportPacks }, t('exportPacks')),
|
|
143
|
+
),
|
|
144
|
+
state.message ? h('div', { className: 'dsub-ok' }, state.message) : null,
|
|
145
|
+
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// #713: read-only source chain panel. Never renders key values.
|
|
150
|
+
function SourcesPanel(props) {
|
|
151
|
+
const t = (props && props.t) || ((k) => k)
|
|
152
|
+
const [state, setState] = React.useState({ loading: true, data: null, error: null })
|
|
153
|
+
const pull = () => {
|
|
154
|
+
fetch('/dsh-subscriptions/sources', { cache: 'no-store' })
|
|
155
|
+
.then((res) => res.json())
|
|
156
|
+
.then((data) => setState({ loading: false, data, error: null }))
|
|
157
|
+
.catch((err) => setState({ loading: false, data: null, error: String(err && err.message || err) }))
|
|
158
|
+
}
|
|
159
|
+
React.useEffect(() => {
|
|
160
|
+
pull()
|
|
161
|
+
const id = setInterval(pull, 30 * 1000)
|
|
162
|
+
return () => clearInterval(id)
|
|
163
|
+
}, [])
|
|
164
|
+
const sourceLabel = (source) => source === 'api-key' ? t('apiKeySource') : (source === 'oauth' ? t('oauthSource') : (source === 'ollama' ? t('ollamaSource') : t('sourceNone')))
|
|
165
|
+
return h('div', { className: 'dsub-section-card' },
|
|
166
|
+
h('div', { className: 'dsub-section-title' }, t('sources'), h('button', { type: 'button', className: 'dsub-mini', onClick: pull }, state.loading ? '…' : '↻')),
|
|
167
|
+
h('div', { className: 'dsub-section-desc' }, t('sourcesHint')),
|
|
168
|
+
state.error ? h('div', { className: 'dsub-bad' }, state.error) : null,
|
|
169
|
+
state.data && state.data.keyRotation && !state.data.keyRotation.available
|
|
170
|
+
? h('div', { className: 'dsub-dim' }, t('keyRotationOffline') + (state.data.keyRotation.reason ? ': ' + state.data.keyRotation.reason : ''))
|
|
171
|
+
: null,
|
|
172
|
+
state.data && state.data.keyRotation && state.data.keyRotation.available
|
|
173
|
+
? h('div', { className: 'dsub-dim' }, t('keyRotationHealth') + ': ' + state.data.keyRotation.healthyKeys + '/' + state.data.keyRotation.totalKeys + ' healthy keys')
|
|
174
|
+
: null,
|
|
175
|
+
(state.data && state.data.providers ? state.data.providers : []).map((row) => h('div', { className: 'dsub-row', key: row.provider },
|
|
176
|
+
h('span', { className: 'dsub-h' }, row.provider),
|
|
177
|
+
h('span', { className: 'dsub-pillTag' }, sourceLabel(row.selected)),
|
|
178
|
+
row.candidates.map((c, i) => h('span', { key: i, className: 'dsub-dim' },
|
|
179
|
+
sourceLabel(c.source) + (c.available ? ' ✓' : ' ✕'))),
|
|
180
|
+
)),
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
return { WebhookTestButton, VaultSnapshotsPanel, ProviderPacksPanel, SourcesPanel }
|
|
186
|
+
}
|
|
187
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.36",
|
|
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",
|