@goodandready/dsh-subscriptions 0.7.35 → 0.7.37
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 +89 -1
- package/lib/config-schema.js +2 -0
- package/lib/media-policy.js +48 -0
- package/lib/provider-icon.js +22 -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": "剩余配额",
|
|
@@ -1443,6 +1457,8 @@ window.__ModuleLoader__.load({
|
|
|
1443
1457
|
}
|
|
1444
1458
|
}
|
|
1445
1459
|
const [checking, setChecking] = React.useState({})
|
|
1460
|
+
const [batch, setBatch] = React.useState(null)
|
|
1461
|
+
const batchCancelRef = React.useRef(false)
|
|
1446
1462
|
const [saved, setSaved] = React.useState(false)
|
|
1447
1463
|
const [savedSnapshot, setSavedSnapshot] = React.useState(null)
|
|
1448
1464
|
const [tokenDraft, setTokenDraft] = React.useState({})
|
|
@@ -1555,6 +1571,21 @@ window.__ModuleLoader__.load({
|
|
|
1555
1571
|
const updateSlot = (provider, index, patch) => setSlots((prev) => prev.map((s) => (
|
|
1556
1572
|
s && s.provider === provider && Number(s.index) === Number(index) ? Object.assign({}, s, patch) : s
|
|
1557
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
|
+
})
|
|
1558
1589
|
const accountOf = (provider, index) => accounts.find((a) => a.provider === provider && a.index === index) || {}
|
|
1559
1590
|
|
|
1560
1591
|
const save = async () => {
|
|
@@ -1773,6 +1804,34 @@ window.__ModuleLoader__.load({
|
|
|
1773
1804
|
later(() => setInfo(''), 3000)
|
|
1774
1805
|
}
|
|
1775
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
|
+
|
|
1776
1835
|
const importToken = async (provider, index) => {
|
|
1777
1836
|
setErr('')
|
|
1778
1837
|
const tok = (tokenDraft[provider + ':' + index] || '').trim()
|
|
@@ -2860,10 +2919,22 @@ h('div', { className: 'dsub-row' },
|
|
|
2860
2919
|
.map((slot, i) => ({ slot: slot, i: i }))
|
|
2861
2920
|
.filter((row) => row.slot.provider === prov.id)
|
|
2862
2921
|
return h('div', { className: 'dsub-block', key: prov.id },
|
|
2863
|
-
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
|
+
),
|
|
2864
2934
|
rows.map((row) => {
|
|
2865
2935
|
const account = accountOf(row.slot.provider, row.slot.index)
|
|
2866
2936
|
const key = row.slot.provider + ':' + row.slot.index
|
|
2937
|
+
const rowPosition = rows.findIndex((candidate) => candidate.i === row.i)
|
|
2867
2938
|
const on = !!account.configured
|
|
2868
2939
|
const poolEnabled = row.slot.poolEnabled !== false
|
|
2869
2940
|
const poolModels = Array.isArray(row.slot.poolModels) ? row.slot.poolModels : null
|
|
@@ -2914,6 +2985,9 @@ h('div', { className: 'dsub-row' },
|
|
|
2914
2985
|
},
|
|
2915
2986
|
}),
|
|
2916
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,
|
|
2917
2991
|
normalizePlanBadge(row.slot.provider, account.plan || account.paidTierName || (account.quota && account.quota.plan)),
|
|
2918
2992
|
h('button', {
|
|
2919
2993
|
type: 'button', className: 'dsub-mini',
|
|
@@ -2979,6 +3053,20 @@ h('div', { className: 'dsub-row' },
|
|
|
2979
3053
|
type: 'button', className: 'dsub-mini', title: t('quarantineParkTitle'),
|
|
2980
3054
|
onClick: () => doParkQuarantine(row.slot.provider, row.slot.index, 3600000),
|
|
2981
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
|
+
}, '📋'),
|
|
2982
3070
|
h('button', {
|
|
2983
3071
|
type: 'button', className: 'dsub-mini', title: t('removeSlot'),
|
|
2984
3072
|
onClick: () => {
|
package/lib/config-schema.js
CHANGED
|
@@ -253,6 +253,8 @@ export const Config = z.object({
|
|
|
253
253
|
.description('#694 Data-only vendor packs registered through the provider SDK.'),
|
|
254
254
|
nativeTools: z.boolean().volatile().default(false)
|
|
255
255
|
.description('#18 Opt-in subscription-native image/x/web/video tools. Off by default because these names shadow builtin tools; enable only when the host does not provide them.'),
|
|
256
|
+
toolPolicies: z.any().volatile().default({})
|
|
257
|
+
.description('#809 Per-provider tool policy map: { provider: { web, image, video, search, tools } }.'),
|
|
256
258
|
})
|
|
257
259
|
|
|
258
260
|
export function publicConfig(cfg) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
function statusOf(err) {
|
|
2
|
+
return Number(err && (err.status || err.statusCode)) || 0
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** #822: do not retry ambiguous image/video failures. */
|
|
6
|
+
export function classifyMediaFailure(err) {
|
|
7
|
+
const status = statusOf(err)
|
|
8
|
+
const code = String(err && err.code || '').toUpperCase()
|
|
9
|
+
if (code === 'ABORT_ERR' || code === 'CANCELED' || code === 'CANCELLED') return 'cancelled'
|
|
10
|
+
if (code === 'AMBIGUOUS' || code === 'TIMEOUT_AFTER_SUBMIT') return 'ambiguous'
|
|
11
|
+
if (status === 401 || status === 403 || status === 400 || status === 422) return 'fatal'
|
|
12
|
+
if (status === 429 || status === 408 || status >= 500) return 'retryable'
|
|
13
|
+
if (code === 'TIMEOUT' || code === 'EMPTY_RESPONSE' || code === 'NETWORK') return 'retryable'
|
|
14
|
+
return 'fatal'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function shouldRetryMedia(err, attempt, maxRetries = 0) {
|
|
18
|
+
if (Number(attempt) >= Number(maxRetries)) return false
|
|
19
|
+
return classifyMediaFailure(err) === 'retryable'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** #800: conservative media-pool selection. */
|
|
23
|
+
export function selectMediaAccount(accounts, { nowMs = Date.now(), defaultRef = null, exclude = [] } = {}) {
|
|
24
|
+
const skip = new Set(exclude)
|
|
25
|
+
const usable = (Array.isArray(accounts) ? accounts : []).filter((account) => {
|
|
26
|
+
if (!account || !account.hasToken || skip.has(account.ref)) return false
|
|
27
|
+
if (account.poolEnabled === false) return false
|
|
28
|
+
if (account.cooldownUntil && Number(account.cooldownUntil) > nowMs) return false
|
|
29
|
+
if (account.quarantineUntil && Number(account.quarantineUntil) > nowMs) return false
|
|
30
|
+
if (account.usagePercent != null && Number(account.usagePercent) >= 100) return false
|
|
31
|
+
return true
|
|
32
|
+
})
|
|
33
|
+
if (!usable.length) return null
|
|
34
|
+
return usable.find((account) => account.ref === defaultRef) || usable[0]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** #809: per-provider tool policy. */
|
|
38
|
+
export function resolveToolPolicy(config, provider) {
|
|
39
|
+
const defaults = { web: true, image: true, video: true, search: true, tools: true }
|
|
40
|
+
const all = config && config.toolPolicies
|
|
41
|
+
const row = all && typeof all === 'object' ? all[provider] : null
|
|
42
|
+
if (!row || typeof row !== 'object') return defaults
|
|
43
|
+
return Object.fromEntries(Object.entries(defaults).map(([key, value]) => [key, row[key] === undefined ? value : Boolean(row[key])]))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function toolAllowed(config, provider, kind) {
|
|
47
|
+
return resolveToolPolicy(config, provider)[kind] !== false
|
|
48
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** #835/#836: safe provider icon candidate with deterministic fallback. */
|
|
2
|
+
export function providerInitials(provider) {
|
|
3
|
+
const text = String(provider || '').trim()
|
|
4
|
+
if (!text) return '?'
|
|
5
|
+
return text.slice(0, 2).toUpperCase()
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function safeProviderIconUrl(raw) {
|
|
9
|
+
const value = String(raw || '').trim()
|
|
10
|
+
if (!value) return ''
|
|
11
|
+
try {
|
|
12
|
+
const url = new URL(value)
|
|
13
|
+
if (url.protocol === 'http:' || url.protocol === 'https:') return url.href
|
|
14
|
+
if (url.protocol === 'data:' && /^data:image\/(?:png|jpe?g|gif|webp|svg\+xml)[;,]/i.test(value)) return value
|
|
15
|
+
} catch { /* fall through */ }
|
|
16
|
+
return ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveProviderIcon({ provider, iconUrl, hostIconUrl } = {}) {
|
|
20
|
+
const safe = safeProviderIconUrl(iconUrl) || safeProviderIconUrl(hostIconUrl)
|
|
21
|
+
return { url: safe, fallback: providerInitials(provider), source: safe ? 'url' : 'initials' }
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.37",
|
|
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",
|