@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 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' }, prov.name),
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: () => {
@@ -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.35",
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",