@goodandready/dsh-subscriptions 0.7.39 → 0.7.41
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 +1 -0
- package/lib/accounts.js +2 -0
- package/lib/client.js +12 -0
- package/lib/config-schema.js +4 -0
- package/lib/feature-helpers.js +107 -0
- package/lib/index.js +6 -1
- package/package.json +1 -1
package/lib/accounts-view.js
CHANGED
|
@@ -168,6 +168,7 @@ export async function createAccountsView({ live, store, fetch, pmL, pmE, concurr
|
|
|
168
168
|
availableModels: info.availableModels || null,
|
|
169
169
|
poolEnabled: slot.poolEnabled !== false,
|
|
170
170
|
isDefault: !!slot.isDefault,
|
|
171
|
+
proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
|
|
171
172
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
172
173
|
credentialError: info.credentialError || null,
|
|
173
174
|
duplicateOf: info.duplicateOf || null,
|
package/lib/accounts.js
CHANGED
|
@@ -74,6 +74,7 @@ export function normalizeSlots(slots) {
|
|
|
74
74
|
...(slot.proxyUrl ? { proxyUrl: String(slot.proxyUrl) } : {}),
|
|
75
75
|
...(slot.expiresAt ? { expiresAt: Number(slot.expiresAt) } : {}),
|
|
76
76
|
...(slot.maxConcurrency !== undefined ? { maxConcurrency: Number(slot.maxConcurrency) } : {}),
|
|
77
|
+
...(Array.isArray(slot.proxyBypass) ? { proxyBypass: slot.proxyBypass.map((host) => String(host).trim()).filter(Boolean) } : {}),
|
|
77
78
|
...(slot.poolEnabled === false ? { poolEnabled: false } : {}),
|
|
78
79
|
...(slot.isDefault ? { isDefault: true } : {}),
|
|
79
80
|
...(Array.isArray(slot.poolModels)
|
|
@@ -427,6 +428,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
427
428
|
isPro: !!slot.isPro,
|
|
428
429
|
poolEnabled: slot.poolEnabled !== false,
|
|
429
430
|
isDefault: !!slot.isDefault,
|
|
431
|
+
proxyBypass: Array.isArray(slot.proxyBypass) ? slot.proxyBypass : [],
|
|
430
432
|
...(Array.isArray(slot.poolModels) ? { poolModels: slot.poolModels } : {}),
|
|
431
433
|
credentialError: info.credentialError || null,
|
|
432
434
|
lastUsedAt: lastUsedAt.get(slot.ref) || 0,
|
package/lib/client.js
CHANGED
|
@@ -264,6 +264,7 @@ window.__ModuleLoader__.load({
|
|
|
264
264
|
"pastePlaceholder": "Paste redirected URL or code",
|
|
265
265
|
"submitCode": "Submit code",
|
|
266
266
|
"proxyPlaceholder": "Per-account proxy (http://, https://, socks5://) - optional",
|
|
267
|
+
"proxyBypassPlaceholder": "Proxy bypass hosts (comma-separated)",
|
|
267
268
|
"proxyCheck": "Check proxy",
|
|
268
269
|
"proxyOk": "proxy ok",
|
|
269
270
|
"proxyFail": "proxy fail",
|
|
@@ -670,6 +671,7 @@ window.__ModuleLoader__.load({
|
|
|
670
671
|
"pastePlaceholder": "粘贴重定向 URL 或授权代码",
|
|
671
672
|
"submitCode": "提交授权代码",
|
|
672
673
|
"proxyPlaceholder": "单账户独立代理 (http://, https://, socks5://) - 可选",
|
|
674
|
+
"proxyBypassPlaceholder": "代理绕过主机(逗号分隔)",
|
|
673
675
|
"proxyCheck": "测试代理",
|
|
674
676
|
"proxyOk": "代理连接成功",
|
|
675
677
|
"proxyFail": "代理连接失败",
|
|
@@ -3222,6 +3224,16 @@ h('div', { className: 'dsub-row' },
|
|
|
3222
3224
|
updateSlot(row.slot.provider, row.slot.index, { proxyUrl: e.target.value })
|
|
3223
3225
|
},
|
|
3224
3226
|
}),
|
|
3227
|
+
h('input', {
|
|
3228
|
+
className: 'dsub-grow',
|
|
3229
|
+
value: (Array.isArray(row.slot.proxyBypass) ? row.slot.proxyBypass : []).join(','),
|
|
3230
|
+
placeholder: t('proxyBypassPlaceholder'),
|
|
3231
|
+
onChange: (e) => {
|
|
3232
|
+
updateSlot(row.slot.provider, row.slot.index, {
|
|
3233
|
+
proxyBypass: e.target.value.split(',').map((host) => host.trim()).filter(Boolean),
|
|
3234
|
+
})
|
|
3235
|
+
},
|
|
3236
|
+
}),
|
|
3225
3237
|
h('button', {
|
|
3226
3238
|
type: 'button', className: 'dsub-mini',
|
|
3227
3239
|
disabled: checking['proxy:' + key],
|
package/lib/config-schema.js
CHANGED
|
@@ -20,6 +20,8 @@ export const Slot = z.object({
|
|
|
20
20
|
.description('#67 Optional subscription expiry timestamp (ms). When set and within expiryNotifyDays, the header chip shows the account and expiry date.'),
|
|
21
21
|
proxyUrl: z.string().default('')
|
|
22
22
|
.description('#88 Per-account proxy URL (http://, https://, socks5://). All requests for this account route through it. Empty = direct connection.'),
|
|
23
|
+
proxyBypass: z.array(z.string()).default([])
|
|
24
|
+
.description('#803 Hosts/domains that bypass the per-account proxy.'),
|
|
23
25
|
maxConcurrency: z.number().default(1)
|
|
24
26
|
.description('#555 Per-slot max concurrent requests before queuing. Defaults to 1 for subscription safety.'),
|
|
25
27
|
monthlyCost: z.number().default(20)
|
|
@@ -157,6 +159,8 @@ export const Config = z.object({
|
|
|
157
159
|
.description('Configurable global priority sequence for cross-vendor fallback.'),
|
|
158
160
|
modelAccountRules: z.array(z.object({ provider: z.string(), pattern: z.string(), account: z.string() })).volatile().default([])
|
|
159
161
|
.description('#788 Per-provider model glob rules. First match wins; account may be a ref, label or index.'),
|
|
162
|
+
tiers: z.array(z.any()).volatile().default([])
|
|
163
|
+
.description('#817 Named picker tiers: { id, name, members: [accountRef] }. No cross-tier fallback.'),
|
|
160
164
|
rotationStrategy: z.string().volatile().default('round-robin')
|
|
161
165
|
.description('Load balancing strategy among available accounts: round-robin (balanced), least-loaded (fair-share quota), quota-aware (urgent quota first), or priority (sequential).'),
|
|
162
166
|
poolEnabled: z.boolean().volatile().default(true)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
function hostOf(value) {
|
|
2
|
+
try { return new URL(String(value)).hostname.toLowerCase() } catch { return '' }
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/** #803: bypass plugin proxy for listed hosts/domains. */
|
|
6
|
+
export function shouldBypassProxy(bypassList, targetUrl) {
|
|
7
|
+
const host = hostOf(targetUrl)
|
|
8
|
+
if (!host) return false
|
|
9
|
+
for (const raw of Array.isArray(bypassList) ? bypassList : []) {
|
|
10
|
+
const entry = String(raw || '').trim().toLowerCase().replace(/^\*\./, '').replace(/^\./, '')
|
|
11
|
+
if (!entry) continue
|
|
12
|
+
if (host === entry || host.endsWith('.' + entry)) return true
|
|
13
|
+
}
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** #803: distinguish proxy transport failure from provider failure. */
|
|
18
|
+
export function classifyConnectivityProbe({ proxyError, providerStatus, latencyMs } = {}) {
|
|
19
|
+
if (proxyError) return { ok: false, kind: 'proxy', error: String(proxyError), latencyMs: null }
|
|
20
|
+
const status = Number(providerStatus) || 0
|
|
21
|
+
return {
|
|
22
|
+
ok: status >= 200 && status < 500,
|
|
23
|
+
kind: status >= 400 ? 'provider' : 'ok',
|
|
24
|
+
status,
|
|
25
|
+
latencyMs: Number.isFinite(Number(latencyMs)) ? Number(latencyMs) : null,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** #817: tiers are independent named pools; no cross-tier fallback. */
|
|
30
|
+
export function validateTiers(tiers) {
|
|
31
|
+
const list = Array.isArray(tiers) ? tiers : []
|
|
32
|
+
const ids = new Set()
|
|
33
|
+
const out = []
|
|
34
|
+
for (const tier of list) {
|
|
35
|
+
if (!tier || typeof tier !== 'object') continue
|
|
36
|
+
const id = String(tier.id || '').trim()
|
|
37
|
+
if (!id || ids.has(id)) continue
|
|
38
|
+
ids.add(id)
|
|
39
|
+
out.push({
|
|
40
|
+
id,
|
|
41
|
+
name: String(tier.name || id),
|
|
42
|
+
members: (Array.isArray(tier.members) ? tier.members : [])
|
|
43
|
+
.map((member) => String(member || '').trim())
|
|
44
|
+
.filter(Boolean),
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
return out
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function selectTierAccount(tier, accounts, nowMs = Date.now()) {
|
|
51
|
+
const members = new Set((tier && tier.members) || [])
|
|
52
|
+
if (!members.size) return null
|
|
53
|
+
return (Array.isArray(accounts) ? accounts : []).find((account) => account
|
|
54
|
+
&& members.has(account.ref)
|
|
55
|
+
&& account.hasToken
|
|
56
|
+
&& (!account.cooldownUntil || Number(account.cooldownUntil) <= nowMs)
|
|
57
|
+
&& (!account.quarantineUntil || Number(account.quarantineUntil) <= nowMs)
|
|
58
|
+
&& !(account.usagePercent != null && Number(account.usagePercent) >= 100)) || null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** #832: session policy snapshots are frozen at session creation. */
|
|
62
|
+
export function snapshotToolPolicy(policy, sessionId, at = Date.now()) {
|
|
63
|
+
return { sessionId: String(sessionId || ''), at: Number(at), policy: JSON.parse(JSON.stringify(policy || {})) }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function toolPolicyForSession(snapshots, sessionId) {
|
|
67
|
+
const found = (Array.isArray(snapshots) ? snapshots : []).find((row) => row && row.sessionId === String(sessionId || ''))
|
|
68
|
+
return found ? found.policy : null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** #834: Claude CLI-synced accounts must not fight standalone OAuth over Keychain. */
|
|
72
|
+
export function claudeAccountMode(slot = {}) {
|
|
73
|
+
const raw = String(slot.accountMode || slot.mode || '').toLowerCase()
|
|
74
|
+
if (raw === 'cli-synced' || raw === 'cli') return 'cli-synced'
|
|
75
|
+
return 'standalone'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function canWriteClaudeKeychain(slot, blob, expectedAccountId) {
|
|
79
|
+
if (claudeAccountMode(slot) !== 'cli-synced') return false
|
|
80
|
+
if (!blob) return false
|
|
81
|
+
const owner = String(blob.accountId || blob.email || '').trim()
|
|
82
|
+
const expected = String(expectedAccountId || slot.accountId || slot.email || '').trim()
|
|
83
|
+
return Boolean(owner && expected && owner === expected)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** #815: normalize Codex web-search results and bound retry behavior. */
|
|
87
|
+
export function normalizeWebSearchResult(payload, maxBytes = 64 * 1024) {
|
|
88
|
+
const body = payload && typeof payload === 'object' ? payload : {}
|
|
89
|
+
const text = String(body.output_text || body.answer || body.text || '').slice(0, maxBytes)
|
|
90
|
+
const citations = (Array.isArray(body.citations) ? body.citations : [])
|
|
91
|
+
.map((citation) => {
|
|
92
|
+
if (!citation) return null
|
|
93
|
+
if (typeof citation === 'string') return { url: citation }
|
|
94
|
+
return { url: String(citation.url || citation.link || ''), title: citation.title ? String(citation.title) : undefined }
|
|
95
|
+
})
|
|
96
|
+
.filter((citation) => citation && citation.url)
|
|
97
|
+
.slice(0, 50)
|
|
98
|
+
return { text, citations }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function webSearchRetryDelay(attempt, status) {
|
|
102
|
+
const n = Number(attempt) || 0
|
|
103
|
+
const code = Number(status) || 0
|
|
104
|
+
if (code === 429) return null
|
|
105
|
+
if (code >= 500 && n < 2) return Math.min(2000, 250 * 2 ** n)
|
|
106
|
+
return null
|
|
107
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { OllamaAdapter, ollamaAlive, ollamaModels, ollamaBase } from './ollama.j
|
|
|
23
23
|
import { createResetCreditService } from './reset-credits.js'
|
|
24
24
|
import { maskEmail, maskLabel } from './mask.js'
|
|
25
25
|
import { proxyFetch } from './proxy.js'
|
|
26
|
+
import { shouldBypassProxy } from './feature-helpers.js'
|
|
26
27
|
import { HistoryStore, resolveHistoryDir } from './history.js'
|
|
27
28
|
import { createDiagnosticsReport } from './diagnostics.js'
|
|
28
29
|
import { createAccountsView } from './accounts-view.js'
|
|
@@ -238,7 +239,11 @@ export function apply(ctx, rawConfig) {
|
|
|
238
239
|
if (!parsed) return null
|
|
239
240
|
const slot = normalizeSlots(live().slots).find((s) => s.provider === parsed.provider && s.index === parsed.index)
|
|
240
241
|
if (!slot || !slot.proxyUrl) return null
|
|
241
|
-
|
|
242
|
+
const proxied = proxyFetch(slot.proxyUrl)
|
|
243
|
+
if (!proxied) return null
|
|
244
|
+
const bypass = Array.isArray(slot.proxyBypass) ? slot.proxyBypass : []
|
|
245
|
+
if (!bypass.length) return proxied
|
|
246
|
+
return (url, init) => shouldBypassProxy(bypass, String(url)) ? fetch(url, init) : proxied(url, init)
|
|
242
247
|
}
|
|
243
248
|
|
|
244
249
|
function redirectFor(provider, ref, reqOrigin) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.41",
|
|
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",
|