@goodandready/dsh-subscriptions 0.2.5 → 0.2.7
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.js +18 -1
- package/lib/blob.js +4 -0
- package/lib/client.js +64 -11
- package/lib/code-assist.js +4 -2
- package/lib/index.js +1 -0
- package/lib/usage.js +53 -1
- package/lib/vendors/claude.js +4 -2
- package/lib/vendors/codex.js +4 -2
- package/lib/vendors/grok.js +4 -2
- package/package.json +1 -1
package/lib/accounts.js
CHANGED
|
@@ -54,6 +54,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
|
|
|
54
54
|
const refreshLocks = new Map()
|
|
55
55
|
const refreshFailures = new Map()
|
|
56
56
|
const usage = new Map()
|
|
57
|
+
const windows = new Map()
|
|
57
58
|
const usageFetched = new Map()
|
|
58
59
|
const doFetch = fetchImpl || fetch
|
|
59
60
|
|
|
@@ -107,6 +108,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
|
|
|
107
108
|
const blob = parseBlob(await resolveRaw(ref))
|
|
108
109
|
base.label = blob.label || blob.email || ''
|
|
109
110
|
base.email = blob.email || ''
|
|
111
|
+
if (Array.isArray(blob.usage)) { base.usage = blob.usage; base.usageAt = blob.usageAt || 0 }
|
|
110
112
|
if (blob.validationUrl) base.validationUrl = blob.validationUrl
|
|
111
113
|
if (blob.validationMessage) base.validationMessage = blob.validationMessage
|
|
112
114
|
if (blob.accountNotice) base.accountNotice = blob.accountNotice
|
|
@@ -117,6 +119,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
|
|
|
117
119
|
...base,
|
|
118
120
|
cooldownUntil: cooldowns.get(ref) || 0,
|
|
119
121
|
quota: quotas.get(ref) || null,
|
|
122
|
+
usage: windows.get(ref) || base.usage || null,
|
|
120
123
|
usagePercent: usage.has(ref) ? usage.get(ref) : null,
|
|
121
124
|
refreshError: refreshFailures.get(ref)?.error || '',
|
|
122
125
|
validationUrl: base.validationUrl || '',
|
|
@@ -194,9 +197,23 @@ export function createAccountStore({ credentials, getConfig, fetchImpl }) {
|
|
|
194
197
|
const blob = await ensureFresh(provider, parseBlob(raw), slot.ref)
|
|
195
198
|
const snap = await vendor.usage(blob, cfg, doFetch)
|
|
196
199
|
usageFetched.set(slot.ref, Date.now())
|
|
197
|
-
if (snap
|
|
200
|
+
if (!snap) continue
|
|
201
|
+
if (Number.isFinite(Number(snap.usedPercent))) {
|
|
198
202
|
usage.set(slot.ref, Number(snap.usedPercent))
|
|
199
203
|
}
|
|
204
|
+
const list = Array.isArray(snap.windows) ? snap.windows : null
|
|
205
|
+
if (list) {
|
|
206
|
+
windows.set(slot.ref, list)
|
|
207
|
+
// persist last known windows so the card survives restarts
|
|
208
|
+
try {
|
|
209
|
+
const prev = Array.isArray(blob.usage) ? JSON.stringify(blob.usage) : ''
|
|
210
|
+
if (prev !== JSON.stringify(list)) {
|
|
211
|
+
await saveBlob(slot.ref, { ...blob, usage: list, usageAt: Date.now() })
|
|
212
|
+
} else if (!blob.usageAt) {
|
|
213
|
+
await saveBlob(slot.ref, { ...blob, usageAt: Date.now() })
|
|
214
|
+
}
|
|
215
|
+
} catch { /* persistence best-effort */ }
|
|
216
|
+
}
|
|
200
217
|
} catch {
|
|
201
218
|
usageFetched.set(slot.ref, Date.now())
|
|
202
219
|
}
|
package/lib/blob.js
CHANGED
|
@@ -16,6 +16,8 @@ export function serializeBlob(obj) {
|
|
|
16
16
|
email: asString(obj && obj.email),
|
|
17
17
|
accountId: asString(obj && obj.accountId),
|
|
18
18
|
projectId: asString(obj && obj.projectId),
|
|
19
|
+
...(Array.isArray(obj && obj.usage) ? { usage: obj.usage } : {}),
|
|
20
|
+
...((obj && obj.usageAt) ? { usageAt: Number(obj.usageAt) } : {}),
|
|
19
21
|
})
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -30,6 +32,8 @@ export function parseBlob(text) {
|
|
|
30
32
|
email: asString(obj.email),
|
|
31
33
|
accountId: asString(obj.accountId),
|
|
32
34
|
projectId: asString(obj.projectId),
|
|
35
|
+
...(Array.isArray(obj.usage) ? { usage: obj.usage } : {}),
|
|
36
|
+
...(obj.usageAt ? { usageAt: Number(obj.usageAt) } : {}),
|
|
33
37
|
}
|
|
34
38
|
}
|
|
35
39
|
|
package/lib/client.js
CHANGED
|
@@ -46,6 +46,9 @@ window.__ModuleLoader__.load({
|
|
|
46
46
|
'usageFull': 'Usage 100%',
|
|
47
47
|
'connected': 'Connected',
|
|
48
48
|
'title': 'Subscriptions',
|
|
49
|
+
'cardIntro': 'Subscription accounts, rotation and limits.',
|
|
50
|
+
'show': 'Show',
|
|
51
|
+
'hide': 'Hide',
|
|
49
52
|
'intro': 'Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.',
|
|
50
53
|
'useOrigin': 'Use this Web UI origin as OAuth redirect_uri',
|
|
51
54
|
'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
|
|
@@ -73,6 +76,9 @@ window.__ModuleLoader__.load({
|
|
|
73
76
|
'usageFull': 'Лимит исчерпан',
|
|
74
77
|
'connected': 'Подключено',
|
|
75
78
|
'title': 'Подписки',
|
|
79
|
+
'cardIntro': 'Аккаунты подписок, ротация и лимиты.',
|
|
80
|
+
'show': 'Показать',
|
|
81
|
+
'hide': 'Скрыть',
|
|
76
82
|
'intro': 'Вход по обычной пользовательской подписке. Токены остаются в хранилище учётных данных харнесса, браузер их обратно не читает. Если после «Подключить» провайдер увёл на localhost или на свою страницу, вставьте сюда адрес перехода или код.',
|
|
77
83
|
'useOrigin': 'Использовать адрес этого веб-интерфейса как redirect_uri',
|
|
78
84
|
'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
|
|
@@ -102,6 +108,26 @@ window.__ModuleLoader__.load({
|
|
|
102
108
|
return t('connected')
|
|
103
109
|
}
|
|
104
110
|
|
|
111
|
+
// Карточка во вкладке «Плагины» рисует свой заголовок и сворачивание:
|
|
112
|
+
// ядро даёт только рамку списка.
|
|
113
|
+
function PluginCard(props) {
|
|
114
|
+
const t = (props && props.t) || ((key) => key)
|
|
115
|
+
const [open, setOpen] = React.useState(false)
|
|
116
|
+
return React.createElement('div', { className: 'dsub-block' },
|
|
117
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
118
|
+
React.createElement('span', { className: 'dsub-h' }, t('title')),
|
|
119
|
+
React.createElement('span', { className: 'dsub-sub' }, t('cardIntro')),
|
|
120
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
121
|
+
React.createElement('button', {
|
|
122
|
+
type: 'button',
|
|
123
|
+
className: 'dsub-mini',
|
|
124
|
+
onClick: () => setOpen(!open),
|
|
125
|
+
}, open ? t('hide') : t('show')),
|
|
126
|
+
),
|
|
127
|
+
open ? React.createElement(SubsSection, props) : null,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
105
131
|
function SubsSection(props) {
|
|
106
132
|
// Переводчик приходит от слота, потому что в его записи указан locale.
|
|
107
133
|
const t = (props && props.t) || ((key) => key)
|
|
@@ -301,17 +327,27 @@ window.__ModuleLoader__.load({
|
|
|
301
327
|
t('verifySuffix'),
|
|
302
328
|
) : null,
|
|
303
329
|
(function(){
|
|
304
|
-
var
|
|
330
|
+
var parts=[]
|
|
331
|
+
var wins=account.usage
|
|
332
|
+
if(Array.isArray(wins)&&wins.length){
|
|
333
|
+
wins.forEach(function(w){
|
|
334
|
+
if(w&&w.usedPercent!=null)parts.push(t('limit')+' '+(w.ru||w.id)+': '+Math.round(w.usedPercent)+'/100%')
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
var q=account.quota
|
|
305
338
|
if(q){
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if(q.
|
|
309
|
-
if(q.
|
|
310
|
-
if(q.
|
|
339
|
+
var qa=[]
|
|
340
|
+
if(q.remaining!=null&&q.limit!=null)qa.push(q.remaining+'/'+q.limit)
|
|
341
|
+
else if(q.remaining!=null)qa.push(String(q.remaining))
|
|
342
|
+
if(q.usedPercent!=null)qa.push(Math.round(q.usedPercent)+'%')
|
|
343
|
+
if(q.resetAt){var d=new Date(q.resetAt);qa.push(d.toLocaleTimeString())}
|
|
344
|
+
if(qa.length)parts.push(t('quota')+': '+qa.join(' / '))
|
|
311
345
|
}
|
|
312
|
-
if(
|
|
313
|
-
|
|
314
|
-
|
|
346
|
+
if(!wins&&!q&&account.usagePercent!=null)parts.push(Math.round(account.usagePercent)+'/100%')
|
|
347
|
+
var at=(q&&q.measuredAt)||(account.usageAt||0)
|
|
348
|
+
if(at){var m=Math.round((Date.now()-at)/60000);if(m>=1)parts.push(m+'m')}
|
|
349
|
+
if(!parts.length)return null
|
|
350
|
+
return React.createElement('span',{className:'dsub-sub'},parts.join(' \u00b7 '))
|
|
315
351
|
})(),
|
|
316
352
|
|
|
317
353
|
account.paidTierName ? React.createElement('span', { className: 'dsub-sub' }, t('plan') + ': ' + account.paidTierName) : null,
|
|
@@ -337,9 +373,26 @@ window.__ModuleLoader__.load({
|
|
|
337
373
|
|
|
338
374
|
function registerSettings(ctx) {
|
|
339
375
|
ctx.effect(() => ctx.locale.register(NS, { en, ru }), 'dsh-subscriptions: словари')
|
|
340
|
-
//
|
|
341
|
-
// туда не доходит, поэтому берём переводчик, привязанный к namespace.
|
|
376
|
+
// Подписи вне компонента берут переводчик, привязанный к namespace.
|
|
342
377
|
const t = ctx.locale.bind(NS)
|
|
378
|
+
// Штатное место — вкладка «Плагины»: карточка со своим заголовком и
|
|
379
|
+
// сворачиванием вместо строки в боковом списке. Ключ регистрации обязан
|
|
380
|
+
// равняться пространству настроек, иначе вкладка молча не покажет слот.
|
|
381
|
+
let moved = false
|
|
382
|
+
try {
|
|
383
|
+
moved = !!ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
384
|
+
{
|
|
385
|
+
name: 'settings.plugin.item',
|
|
386
|
+
key: NS,
|
|
387
|
+
locale: NS,
|
|
388
|
+
inject: () => ({ ctx: ctx }),
|
|
389
|
+
},
|
|
390
|
+
PluginCard,
|
|
391
|
+
))
|
|
392
|
+
} catch { moved = false }
|
|
393
|
+
if (moved) return
|
|
394
|
+
// Запасной путь для сборок без settings.plugin.item: прежний раздел,
|
|
395
|
+
// чтобы настройки не пропали.
|
|
343
396
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
344
397
|
{
|
|
345
398
|
name: 'settings.section',
|
package/lib/code-assist.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto'
|
|
2
2
|
import { httpError, readJson } from './wire.js'
|
|
3
3
|
import { validationFromLoadCodeAssist, validationFromHttpError, noticeFromLoadCodeAssist, validationRequiredError } from './google-validation.js'
|
|
4
|
-
import { asUsageSnapshot, deepestUsedPercent } from './usage.js'
|
|
4
|
+
import { asUsageSnapshot, usageWindows, deepestUsedPercent } from './usage.js'
|
|
5
5
|
|
|
6
6
|
export const CODE_ASSIST_PROD = 'https://cloudcode-pa.googleapis.com/v1internal'
|
|
7
7
|
export const CODE_ASSIST_STREAM = 'https://daily-cloudcode-pa.googleapis.com/v1internal'
|
|
@@ -188,7 +188,9 @@ export async function fetchAvailableModels(fetchImpl, token, extraHeaders, fallb
|
|
|
188
188
|
export async function retrieveQuotaPercent(fetchImpl, token, extraHeaders) {
|
|
189
189
|
try {
|
|
190
190
|
const json = await postAssist(fetchImpl, 'retrieveUserQuota', token, {}, extraHeaders)
|
|
191
|
-
|
|
191
|
+
const snap = asUsageSnapshot(deepestUsedPercent(json))
|
|
192
|
+
if (snap) snap.windows = usageWindows(json)
|
|
193
|
+
return snap
|
|
192
194
|
} catch {
|
|
193
195
|
return null
|
|
194
196
|
}
|
package/lib/index.js
CHANGED
|
@@ -273,6 +273,7 @@ export function apply(ctx, config) {
|
|
|
273
273
|
cooldownUntil: info.cooldownUntil,
|
|
274
274
|
usagePercent: info.usagePercent,
|
|
275
275
|
quota: info.quota || null,
|
|
276
|
+
usage: info.usage || null,
|
|
276
277
|
refreshError: info.refreshError || '',
|
|
277
278
|
validationUrl: info.validationUrl || '',
|
|
278
279
|
validationMessage: info.validationMessage || '',
|
package/lib/usage.js
CHANGED
|
@@ -48,4 +48,56 @@ export function asUsageSnapshot(percent) {
|
|
|
48
48
|
const usedPercent = numberOrNull(percent)
|
|
49
49
|
if (usedPercent == null) return null
|
|
50
50
|
return { usedPercent }
|
|
51
|
-
}
|
|
51
|
+
}
|
|
52
|
+
// Named limit windows reported by vendors (five_hour, seven_day, primary...).
|
|
53
|
+
// Label map is a data table: extend it when a vendor adds a window name.
|
|
54
|
+
const WINDOW_LABELS = {
|
|
55
|
+
five_hour: { ru: "5ч", en: "5h" },
|
|
56
|
+
seven_day: { ru: "7д", en: "7d" },
|
|
57
|
+
seven_day_oauth_apps: { ru: "7д прил.", en: "7d apps" },
|
|
58
|
+
weekly_limit_7_days: { ru: "7д", en: "7d" },
|
|
59
|
+
primary: { ru: "осн.", en: "primary" },
|
|
60
|
+
secondary: { ru: "втор.", en: "secondary" },
|
|
61
|
+
monthly: { ru: "месяц", en: "month" },
|
|
62
|
+
credits: { ru: "кредиты", en: "credits" },
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function windowLabel(id) {
|
|
66
|
+
const known = WINDOW_LABELS[id]
|
|
67
|
+
return known || { ru: id, en: id }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Walk vendor usage JSON; every node carrying a utilization-style number
|
|
71
|
+
// becomes a named window keyed by its JSON key.
|
|
72
|
+
export function usageWindows(obj) {
|
|
73
|
+
const out = []
|
|
74
|
+
const seen = new Set()
|
|
75
|
+
function walk(value, key, depth) {
|
|
76
|
+
if (!value || typeof value !== "object" || depth > 6) return
|
|
77
|
+
const pct = numberOrNull(
|
|
78
|
+
value.utilization ?? value.used_percent ?? value.usedPercent ?? value.creditUsagePercent ?? value.used_percentage,
|
|
79
|
+
)
|
|
80
|
+
if (pct != null && key && !seen.has(key)) {
|
|
81
|
+
seen.add(key)
|
|
82
|
+
const label = windowLabel(key)
|
|
83
|
+
out.push({ id: key, ru: label.ru, en: label.en, usedPercent: pct })
|
|
84
|
+
}
|
|
85
|
+
const frac = numberOrNull(value.remainingFraction)
|
|
86
|
+
if (frac != null && key && !seen.has(key)) {
|
|
87
|
+
seen.add(key)
|
|
88
|
+
const label = windowLabel(key)
|
|
89
|
+
out.push({ id: key, ru: label.ru, en: label.en, usedPercent: (1 - frac) * 100 })
|
|
90
|
+
}
|
|
91
|
+
const ufrac = numberOrNull(value.usedFraction)
|
|
92
|
+
if (ufrac != null && key && !seen.has(key)) {
|
|
93
|
+
seen.add(key)
|
|
94
|
+
const label = windowLabel(key)
|
|
95
|
+
out.push({ id: key, ru: label.ru, en: label.en, usedPercent: ufrac * 100 })
|
|
96
|
+
}
|
|
97
|
+
for (const [k, child] of Object.entries(value)) {
|
|
98
|
+
if (child && typeof child === "object") walk(child, k, depth + 1)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
walk(obj, "", 0)
|
|
102
|
+
return out.length ? out : null
|
|
103
|
+
}
|
package/lib/vendors/claude.js
CHANGED
|
@@ -2,7 +2,7 @@ import { buildAuthorizeUrl } from '../oauth.js'
|
|
|
2
2
|
import { anthropicPayload, modelCatalog } from '../messages.js'
|
|
3
3
|
import { jsonTokenRequest, anthropicStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
4
4
|
import { emailFromToken } from '../jwt.js'
|
|
5
|
-
import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
|
|
5
|
+
import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
|
|
6
6
|
|
|
7
7
|
export const id = 'claude'
|
|
8
8
|
|
|
@@ -104,7 +104,9 @@ export async function usage(blob, _cfg, fetchImpl) {
|
|
|
104
104
|
try {
|
|
105
105
|
const res = await (fetchImpl || fetch)(USAGE, { headers: oauthHeaders(blob.accessToken) })
|
|
106
106
|
const json = await readJson(res)
|
|
107
|
-
|
|
107
|
+
const snap = asUsageSnapshot(deepestUsedPercent(json))
|
|
108
|
+
if (snap) snap.windows = usageWindows(json)
|
|
109
|
+
return snap
|
|
108
110
|
} catch {
|
|
109
111
|
return null
|
|
110
112
|
}
|
package/lib/vendors/codex.js
CHANGED
|
@@ -3,7 +3,7 @@ import { buildAuthorizeUrl } from '../oauth.js'
|
|
|
3
3
|
import { codexResponsesBody, modelCatalog } from '../messages.js'
|
|
4
4
|
import { chatgptAccountId, emailFromToken } from '../jwt.js'
|
|
5
5
|
import { formTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError } from '../wire.js'
|
|
6
|
-
import { asUsageSnapshot, deepestUsedPercent } from '../usage.js'
|
|
6
|
+
import { asUsageSnapshot, usageWindows, deepestUsedPercent } from '../usage.js'
|
|
7
7
|
|
|
8
8
|
export const id = 'codex'
|
|
9
9
|
|
|
@@ -150,7 +150,9 @@ export async function usage(blob, cfg, fetchImpl) {
|
|
|
150
150
|
headers: identityHeaders(blob, cfg, { Accept: 'application/json' }),
|
|
151
151
|
})
|
|
152
152
|
const json = await readJson(res)
|
|
153
|
-
|
|
153
|
+
const snap = asUsageSnapshot(deepestUsedPercent(json))
|
|
154
|
+
if (snap) snap.windows = usageWindows(json)
|
|
155
|
+
return snap
|
|
154
156
|
} catch {
|
|
155
157
|
return null
|
|
156
158
|
}
|
package/lib/vendors/grok.js
CHANGED
|
@@ -2,7 +2,7 @@ import { buildAuthorizeUrl } from '../oauth.js'
|
|
|
2
2
|
import { modelCatalog, codexResponsesBody } from '../messages.js'
|
|
3
3
|
import { formTokenRequest, codexResponsesStream, httpError, tokenBlobFromOAuth, readJson } from '../wire.js'
|
|
4
4
|
import { emailFromToken } from '../jwt.js'
|
|
5
|
-
import { asUsageSnapshot, grokBillingPercent } from '../usage.js'
|
|
5
|
+
import { asUsageSnapshot, usageWindows, grokBillingPercent } from '../usage.js'
|
|
6
6
|
|
|
7
7
|
export const id = 'grok'
|
|
8
8
|
|
|
@@ -201,7 +201,9 @@ export async function usage(blob, cfg, fetchImpl) {
|
|
|
201
201
|
headers: { ...identityHeaders(blob, cfg), Accept: 'application/json' },
|
|
202
202
|
})
|
|
203
203
|
const json = await readJson(res)
|
|
204
|
-
|
|
204
|
+
const snap = asUsageSnapshot(grokBillingPercent(json))
|
|
205
|
+
if (snap) snap.windows = usageWindows(json)
|
|
206
|
+
return snap
|
|
205
207
|
} catch {
|
|
206
208
|
return null
|
|
207
209
|
}
|
package/package.json
CHANGED