@goodandready/dsh-subscriptions 0.4.16 → 0.4.18
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/README.md +47 -0
- package/README.ru.md +23 -0
- package/README.zh.md +14 -0
- package/lib/accounts.js +14 -2
- package/lib/adapter.js +25 -6
- package/lib/client.js +616 -13
- package/lib/forecast.js +71 -0
- package/lib/index.js +142 -3
- package/lib/messages.js +8 -1
- package/lib/ollama.js +70 -0
- package/lib/reset-credits.js +193 -0
- package/lib/rotate.js +28 -3
- package/lib/stream-rotate.js +4 -3
- package/lib/subscriptions.js +4 -4
- package/lib/vendors/codex.js +1 -1
- package/lib/vendors/grok.js +2 -0
- package/package.json +4 -1
package/lib/client.js
CHANGED
|
@@ -4,6 +4,9 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} }
|
|
5
5
|
var exports = module.exports
|
|
6
6
|
const React = require('react')
|
|
7
|
+
const createPortal = (function () {
|
|
8
|
+
try { return require('react-dom').createPortal } catch { return null }
|
|
9
|
+
})()
|
|
7
10
|
|
|
8
11
|
// Модульный переводчик: доступен всем подпискам компонентов и хелперам.
|
|
9
12
|
let t = (key) => key
|
|
@@ -60,7 +63,48 @@ window.__ModuleLoader__.load({
|
|
|
60
63
|
'.dsub-barRow{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--dsw-alias-label-secondary)}' +
|
|
61
64
|
'.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
62
65
|
'.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
|
|
63
|
-
'.dsub-verify a{color:var(--dsw-alias-brand-primary)}'
|
|
66
|
+
'.dsub-verify a{color:var(--dsw-alias-brand-primary)}' +
|
|
67
|
+
'.dsub-hud{pointer-events:auto;position:fixed;z-index:10010;width:64px;height:64px;transition:left .2s,top .2s}' +
|
|
68
|
+
'.dsub-hud[data-dragging=true]{transition:none}' +
|
|
69
|
+
'.dsub-hudBtn{position:relative;width:64px;height:64px;padding:0;border:0;border-radius:50%;cursor:grab;display:grid;place-items:center;user-select:none;touch-action:none;' +
|
|
70
|
+
'background:var(--dsw-alias-bg-layer-3);box-shadow:0 0 0 1px var(--dsw-alias-border-l2),0 8px 24px rgba(0,0,0,.25);backdrop-filter:blur(18px)}' +
|
|
71
|
+
'.dsub-hudBtn:hover{transform:scale(1.05)}' +
|
|
72
|
+
'.dsub-hudBtn:active{cursor:grabbing}' +
|
|
73
|
+
'.dsub-hudRing{position:absolute;inset:4px;width:56px;height:56px;transform:rotate(-90deg)}' +
|
|
74
|
+
'.dsub-hudTrack{fill:none;stroke:var(--dsw-alias-border-l2);stroke-width:4}' +
|
|
75
|
+
'.dsub-hudVal{fill:none;stroke:var(--dsub-tone,var(--dsw-alias-state-success-primary));stroke-width:4;stroke-linecap:round;transition:stroke-dashoffset .4s}' +
|
|
76
|
+
'.dsub-hudCore{position:relative;z-index:2;font-size:14px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}' +
|
|
77
|
+
'.dsub-hud[data-collapsed=true] .dsub-hudBtn{opacity:.45}' +
|
|
78
|
+
'.dsub-hud[data-collapsed=true]:hover .dsub-hudBtn{opacity:1}' +
|
|
79
|
+
'.dsub-hudPanel{pointer-events:none;position:absolute;width:280px;padding:12px;border-radius:14px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);box-shadow:0 18px 48px rgba(0,0,0,.35);backdrop-filter:blur(28px);opacity:0;transform:translateY(6px);transition:opacity .16s,transform .16s}' +
|
|
80
|
+
'.dsub-hud:hover .dsub-hudPanel{opacity:1;transform:none}' +
|
|
81
|
+
'.dsub-hud[data-dragging=true] .dsub-hudPanel{opacity:0}' +
|
|
82
|
+
'.dsub-hudRow{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:11.5px}' +
|
|
83
|
+
'.dsub-hudRow .dsub-bar{min-width:70px}' +
|
|
84
|
+
'.dsub-hudName{font-weight:600;min-width:52px}' +
|
|
85
|
+
'.dsub-hudPct{margin-left:auto;font-variant-numeric:tabular-nums}' +
|
|
86
|
+
'.dsub-hudHead{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
87
|
+
'.dsub-hudHint{margin-top:10px;font-size:10.5px;color:var(--dsw-alias-label-tertiary)}' +
|
|
88
|
+
'.dsub-pill{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:11.5px;font-weight:600;cursor:pointer;letter-spacing:.06em}' +
|
|
89
|
+
'.dsub-pill:hover{border-color:var(--dsw-alias-label-dimmed)}' +
|
|
90
|
+
'.dsub-led{width:7px;height:7px;border-radius:50%;flex:none}' +
|
|
91
|
+
'.dsub-ledOk{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 6px var(--dsw-alias-state-success-primary)}' +
|
|
92
|
+
'.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 6px var(--dsw-alias-state-warning-primary)}' +
|
|
93
|
+
'.dsub-ledBad{background:var(--dsw-alias-state-error-primary);box-shadow:0 0 6px var(--dsw-alias-state-error-primary)}' +
|
|
94
|
+
'.dsub-ledOff{background:var(--dsw-alias-label-tertiary)}' +
|
|
95
|
+
'.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:color-mix(in srgb,#000 45%,transparent)}' +
|
|
96
|
+
'.dsub-modal{width:min(560px,92vw);max-height:min(70vh,640px);overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.4);padding:16px}' +
|
|
97
|
+
'.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}' +
|
|
98
|
+
'.dsub-modalRow{display:flex;align-items:center;gap:10px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
|
|
99
|
+
'.dsub-modalName{font-weight:600;font-size:13px}' +
|
|
100
|
+
'.dsub-modalSub{font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
|
|
101
|
+
'.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}' +
|
|
102
|
+
'.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
|
|
103
|
+
'.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}' +
|
|
104
|
+
'.dsub-cqBarWarn .dsub-cqBarFill{background:var(--dsw-alias-state-warning-primary)}' +
|
|
105
|
+
'.dsub-cqBarFull .dsub-cqBarFill{background:var(--dsw-alias-state-error-primary)}' +
|
|
106
|
+
'.dsub-cqB{font-variant-numeric:tabular-nums;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
107
|
+
'.dsub-diag{font:11px/1.5 ui-monospace,SFMono-Regular,monospace;max-height:260px;overflow:auto;white-space:pre-wrap;word-break:break-word;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary)}'
|
|
64
108
|
|
|
65
109
|
const cssId = 'dsh-subscriptions/settings.module.css'
|
|
66
110
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="' + cssId + '"]')) {
|
|
@@ -90,6 +134,39 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
90
134
|
'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.',
|
|
91
135
|
'privacyMask': 'Mask emails and account identifiers',
|
|
92
136
|
'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
|
|
137
|
+
'resetCredits': 'Reset credits',
|
|
138
|
+
'resetCreditsHint': 'ChatGPT reset cards: shows how many are available. Consuming one is a deliberate action with a 5s confirmation.',
|
|
139
|
+
'resetAvailable': 'Reset attempts available',
|
|
140
|
+
'resetExpires': 'earliest expires',
|
|
141
|
+
'resetAck': 'I understand one attempt will be consumed',
|
|
142
|
+
'resetWait': 'confirm enabled in',
|
|
143
|
+
'resetReady': 'ready',
|
|
144
|
+
'resetGo': 'Reset now',
|
|
145
|
+
'resetBusy': 'resetting…',
|
|
146
|
+
'resetDone': 'quota reset',
|
|
147
|
+
'resetNothing': 'server says nothing needs a reset (no attempt consumed)',
|
|
148
|
+
'resetNoCredit': 'no usable credit (not consumed)',
|
|
149
|
+
'resetRedeemed': 'already redeemed earlier',
|
|
150
|
+
'diagGenerate': 'Generate diagnostics report',
|
|
151
|
+
'diagCopy': 'Copy report',
|
|
152
|
+
'diagIssues': 'Open issue tracker',
|
|
153
|
+
'diagHint': 'The report has no tokens, emails, proxy addresses or personal data.',
|
|
154
|
+
'subsPill': 'SUBS',
|
|
155
|
+
'subsModalTitle': 'Subscription console',
|
|
156
|
+
'subsModalClose': 'Close',
|
|
157
|
+
'subsLogged': 'connected',
|
|
158
|
+
'subsNotLogged': 'not connected',
|
|
159
|
+
'subsCooldown': 'cooldown',
|
|
160
|
+
'subsOpenSettings': 'Open settings',
|
|
161
|
+
'hudTitle': 'Subscription balance',
|
|
162
|
+
'settingsLoading': 'Loading settings…',
|
|
163
|
+
'settingsUnavailable': 'Settings are unavailable',
|
|
164
|
+
'settingsRetry': 'Retry',
|
|
165
|
+
'hudHint': 'drag · edge docks · click refresh',
|
|
166
|
+
'fcCalibrating': 'calibrating…',
|
|
167
|
+
'fcIdle': 'no usage',
|
|
168
|
+
'fcHours': 'h',
|
|
169
|
+
'fcMinutes': 'm',
|
|
93
170
|
'reconnect': 'Reconnect',
|
|
94
171
|
'connect': 'Connect',
|
|
95
172
|
'disconnect': 'Disconnect',
|
|
@@ -157,6 +234,39 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
157
234
|
'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
|
|
158
235
|
'privacyMask': 'Маскировать email и учётные записи',
|
|
159
236
|
'privacyMaskHint': 'Для демонстрации экрана: email отображается как j***n@example.com.',
|
|
237
|
+
'resetCredits': 'Карты сброса',
|
|
238
|
+
'resetCreditsHint': 'Карты сброса квоты ChatGPT: показывает, сколько доступно. Списание одной карты — осознанное действие с 5-секундным подтверждением.',
|
|
239
|
+
'resetAvailable': 'Доступно попыток сброса',
|
|
240
|
+
'resetExpires': 'ближайшая истекает',
|
|
241
|
+
'resetAck': 'Я понимаю, что будет списана 1 попытка',
|
|
242
|
+
'resetWait': 'кнопка активна через',
|
|
243
|
+
'resetReady': 'готово',
|
|
244
|
+
'resetGo': 'Сбросить',
|
|
245
|
+
'resetBusy': 'сбрасываю…',
|
|
246
|
+
'resetDone': 'квота сброшена',
|
|
247
|
+
'resetNothing': 'сервер сообщает: сброс не нужен (попытка не списана)',
|
|
248
|
+
'resetNoCredit': 'нет пригодной карты (не списано)',
|
|
249
|
+
'resetRedeemed': 'эта карта уже использована ранее',
|
|
250
|
+
'diagGenerate': 'Сгенерировать диагностический отчёт',
|
|
251
|
+
'diagCopy': 'Скопировать отчёт',
|
|
252
|
+
'diagIssues': 'Открыть трекер задач',
|
|
253
|
+
'diagHint': 'В отчёте нет токенов, email, адресов прокси и персональных данных.',
|
|
254
|
+
'subsPill': 'SUBS',
|
|
255
|
+
'subsModalTitle': 'Консоль подписок',
|
|
256
|
+
'subsModalClose': 'Закрыть',
|
|
257
|
+
'subsLogged': 'подключен',
|
|
258
|
+
'subsNotLogged': 'не подключен',
|
|
259
|
+
'subsCooldown': 'кулдаун',
|
|
260
|
+
'subsOpenSettings': 'Открыть настройки',
|
|
261
|
+
'hudTitle': 'Остаток подписки',
|
|
262
|
+
'settingsLoading': 'Загрузка настроек…',
|
|
263
|
+
'settingsUnavailable': 'Настройки недоступны',
|
|
264
|
+
'settingsRetry': 'Повторить',
|
|
265
|
+
'hudHint': 'перетащи · прилипает к краям · клик — обновить',
|
|
266
|
+
'fcCalibrating': 'калибровка…',
|
|
267
|
+
'fcIdle': 'нет расхода',
|
|
268
|
+
'fcHours': 'ч',
|
|
269
|
+
'fcMinutes': 'мин',
|
|
160
270
|
'reconnect': 'Переподключить',
|
|
161
271
|
'connect': 'Подключить',
|
|
162
272
|
'disconnect': 'Отключить',
|
|
@@ -263,6 +373,69 @@ function PluginCard(props) {
|
|
|
263
373
|
const [paste, setPaste] = React.useState({})
|
|
264
374
|
const [device, setDevice] = React.useState({}) // #90 key -> {state,userCode,authUrl,intervalMs,status}
|
|
265
375
|
const [proxyRes, setProxyRes] = React.useState({}) // #88 key -> {ok,latencyMs,error}
|
|
376
|
+
const [diag, setDiag] = React.useState('')
|
|
377
|
+
const [reset, setReset] = React.useState({}) // #85 key -> challenge state
|
|
378
|
+
const [, setResetTick] = React.useState(0)
|
|
379
|
+
React.useEffect(() => {
|
|
380
|
+
const anyPending = Object.values(reset).some((r) => r && r.phase === 'confirm' && !r.result)
|
|
381
|
+
if (!anyPending) return undefined
|
|
382
|
+
const id = setInterval(() => setResetTick((n) => n + 1), 1000)
|
|
383
|
+
return () => clearInterval(id)
|
|
384
|
+
}, [reset])
|
|
385
|
+
|
|
386
|
+
// #85: reset credits challenge flow (prepare -> 5s cooldown + ack -> consume).
|
|
387
|
+
const resetPrepare = async (provider, index, key) => {
|
|
388
|
+
setReset((m) => Object.assign({}, m, { [key]: { phase: 'loading' } }))
|
|
389
|
+
try {
|
|
390
|
+
const res = await fetch('/dsh-subscriptions/reset-credits/prepare', {
|
|
391
|
+
method: 'POST',
|
|
392
|
+
headers: { 'Content-Type': 'application/json' },
|
|
393
|
+
body: JSON.stringify({ provider, index }),
|
|
394
|
+
})
|
|
395
|
+
const d = await res.json()
|
|
396
|
+
if (!d || !d.ok) throw new Error((d && d.error && d.error.message) || 'prepare failed')
|
|
397
|
+
setReset((m) => Object.assign({}, m, { [key]: {
|
|
398
|
+
phase: 'confirm',
|
|
399
|
+
challengeId: d.challengeId,
|
|
400
|
+
availableCount: d.availableCount,
|
|
401
|
+
readyAt: d.readyAt,
|
|
402
|
+
expiresAt: d.expiresAt,
|
|
403
|
+
creditExpiresAt: d.creditExpiresAt || null,
|
|
404
|
+
} }))
|
|
405
|
+
} catch (e) {
|
|
406
|
+
setReset((m) => Object.assign({}, m, { [key]: { phase: 'idle', error: String(e && e.message || e) } }))
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const resetConsume = async (key) => {
|
|
410
|
+
const st = reset[key]
|
|
411
|
+
if (!st || st.phase !== 'confirm' || !st.ack || Date.now() < st.readyAt || st.busy) return
|
|
412
|
+
setReset((m) => Object.assign({}, m, { [key]: Object.assign({}, st, { busy: true }) }))
|
|
413
|
+
try {
|
|
414
|
+
const res = await fetch('/dsh-subscriptions/reset-credits/consume', {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { 'Content-Type': 'application/json' },
|
|
417
|
+
body: JSON.stringify({ challengeId: st.challengeId, acknowledged: true }),
|
|
418
|
+
})
|
|
419
|
+
const d = await res.json()
|
|
420
|
+
if (!d || !d.ok) throw new Error((d && d.error && d.error.message) || 'consume failed')
|
|
421
|
+
const code = d.result && d.result.code
|
|
422
|
+
setReset((m) => Object.assign({}, m, { [key]: Object.assign({}, st, { busy: false, result: code, windowsReset: (d.result && d.result.windowsReset) || [] }) }))
|
|
423
|
+
reload().catch(() => {})
|
|
424
|
+
} catch (e) {
|
|
425
|
+
setReset((m) => Object.assign({}, m, { [key]: Object.assign({}, st, { busy: false, error: String(e && e.message || e) }) }))
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// #99: one click - fetch the anonymized report and copy it to the clipboard.
|
|
429
|
+
const genDiag = () => {
|
|
430
|
+
fetch('/dsh-subscriptions/diagnostics', { cache: 'no-store' })
|
|
431
|
+
.then((r) => r.json())
|
|
432
|
+
.then((d) => {
|
|
433
|
+
const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
|
|
434
|
+
setDiag(txt)
|
|
435
|
+
try { navigator.clipboard.writeText(txt) } catch {}
|
|
436
|
+
})
|
|
437
|
+
.catch((e) => setDiag('error: ' + String(e && e.message || e)))
|
|
438
|
+
}
|
|
266
439
|
const [checkRes, setCheckRes] = React.useState({})
|
|
267
440
|
const [checking, setChecking] = React.useState({})
|
|
268
441
|
const [saved, setSaved] = React.useState(false)
|
|
@@ -295,7 +468,20 @@ function PluginCard(props) {
|
|
|
295
468
|
return () => timers.forEach(clearInterval)
|
|
296
469
|
})
|
|
297
470
|
|
|
298
|
-
|
|
471
|
+
// #81: settings snapshot status - never render phantom inputs before the
|
|
472
|
+
// config snapshot arrives, and offer a retry when the store is unavailable.
|
|
473
|
+
if (!draft) {
|
|
474
|
+
return React.createElement('div', { className: 'dsub-wrap' },
|
|
475
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
476
|
+
React.createElement('span', { className: 'dsub-sub' }, err ? t('settingsUnavailable') : t('settingsLoading')),
|
|
477
|
+
err ? React.createElement('button', {
|
|
478
|
+
type: 'button', className: 'dsub-mini',
|
|
479
|
+
onClick: () => { setErr(''); reload().catch((e) => setErr(String(e && e.message || e))) },
|
|
480
|
+
}, t('settingsRetry')) : null,
|
|
481
|
+
),
|
|
482
|
+
err ? React.createElement('div', { className: 'dsub-bad' }, err) : null,
|
|
483
|
+
)
|
|
484
|
+
}
|
|
299
485
|
|
|
300
486
|
const slots = Array.isArray(draft.slots) ? draft.slots : []
|
|
301
487
|
const setSlots = (next) => setDraft((d) => Object.assign({}, d, { slots: next }))
|
|
@@ -480,6 +666,20 @@ function PluginCard(props) {
|
|
|
480
666
|
React.createElement('div', { className: 'dsub-sub' },
|
|
481
667
|
t('privacyMaskHint')),
|
|
482
668
|
),
|
|
669
|
+
React.createElement('div', { className: 'dsub-block' },
|
|
670
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
671
|
+
React.createElement('button', { type: 'button', className: 'dsub-mini', onClick: genDiag }, t('diagGenerate')),
|
|
672
|
+
React.createElement('a', { href: 'https://github.com/GooDAnDReaDY/dsh-subscriptions/issues', target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('diagIssues')),
|
|
673
|
+
),
|
|
674
|
+
React.createElement('div', { className: 'dsub-sub' }, t('diagHint')),
|
|
675
|
+
diag ? React.createElement('div', { className: 'dsub-row' },
|
|
676
|
+
React.createElement('button', {
|
|
677
|
+
type: 'button', className: 'dsub-mini',
|
|
678
|
+
onClick: () => { try { navigator.clipboard.writeText(diag) } catch {} },
|
|
679
|
+
}, t('diagCopy')),
|
|
680
|
+
React.createElement('pre', { className: 'dsub-diag' }, diag),
|
|
681
|
+
) : null,
|
|
682
|
+
),
|
|
483
683
|
names.map((prov) => {
|
|
484
684
|
const rows = slots
|
|
485
685
|
.map((slot, i) => ({ slot: slot, i: i }))
|
|
@@ -601,6 +801,53 @@ function PluginCard(props) {
|
|
|
601
801
|
) : null,
|
|
602
802
|
)
|
|
603
803
|
})(),
|
|
804
|
+
(row.slot.provider === 'codex' && on ? (function resetPanel(){
|
|
805
|
+
var st = reset[key] || { phase: 'idle' }
|
|
806
|
+
var now = Date.now()
|
|
807
|
+
function resultText(code) {
|
|
808
|
+
if (code === 'reset') return t('resetDone')
|
|
809
|
+
if (code === 'nothing_to_reset') return t('resetNothing')
|
|
810
|
+
if (code === 'no_credit') return t('resetNoCredit')
|
|
811
|
+
if (code === 'already_redeemed') return t('resetRedeemed')
|
|
812
|
+
return code
|
|
813
|
+
}
|
|
814
|
+
if (st.phase !== 'confirm') {
|
|
815
|
+
return React.createElement('div', { className: 'dsub-row' },
|
|
816
|
+
React.createElement('button', {
|
|
817
|
+
type: 'button', className: 'dsub-mini',
|
|
818
|
+
onClick: () => resetPrepare(row.slot.provider, row.slot.index, key).catch((e) => setErr(String(e && e.message || e))),
|
|
819
|
+
}, st.phase === 'loading' ? '\u2026' : t('resetCredits')),
|
|
820
|
+
React.createElement('div', { className: 'dsub-sub' }, t('resetCreditsHint')),
|
|
821
|
+
)
|
|
822
|
+
}
|
|
823
|
+
var ready = now >= st.readyAt
|
|
824
|
+
var secs = Math.max(0, Math.ceil((st.readyAt - now) / 1000))
|
|
825
|
+
return React.createElement('div', { className: 'dsub-verify' },
|
|
826
|
+
React.createElement('div', null,
|
|
827
|
+
t('resetAvailable') + ': ' + st.availableCount +
|
|
828
|
+
(st.creditExpiresAt ? (' · ' + t('resetExpires') + ' ' + new Date(st.creditExpiresAt).toLocaleString()) : '')
|
|
829
|
+
),
|
|
830
|
+
React.createElement('label', { className: 'dsub-row' },
|
|
831
|
+
React.createElement('input', {
|
|
832
|
+
type: 'checkbox',
|
|
833
|
+
checked: !!st.ack,
|
|
834
|
+
onChange: (e) => setReset((m) => Object.assign({}, m, { [key]: Object.assign({}, st, { ack: e.target.checked }) })),
|
|
835
|
+
}),
|
|
836
|
+
React.createElement('span', null, t('resetAck')),
|
|
837
|
+
),
|
|
838
|
+
React.createElement('div', { className: 'dsub-row' },
|
|
839
|
+
React.createElement('button', {
|
|
840
|
+
type: 'button', className: 'dsub-mini',
|
|
841
|
+
disabled: !st.ack || !ready || !!st.busy,
|
|
842
|
+
onClick: () => resetConsume(key).catch((e) => setErr(String(e && e.message || e))),
|
|
843
|
+
}, st.busy ? t('resetBusy') : (ready ? t('resetGo') : (t('resetWait') + ' ' + secs + 's'))),
|
|
844
|
+
!ready && !st.busy ? React.createElement('span', { className: 'dsub-sub' }, t('resetWait') + ' ' + secs + 's') : null,
|
|
845
|
+
st.ack && ready ? React.createElement('span', { className: 'dsub-ok' }, t('resetReady')) : null,
|
|
846
|
+
),
|
|
847
|
+
st.result ? React.createElement('div', { className: 'dsub-ok' }, resultText(st.result)) : null,
|
|
848
|
+
st.error ? React.createElement('div', { className: 'dsub-bad' }, st.error) : null,
|
|
849
|
+
)
|
|
850
|
+
})() : null),
|
|
604
851
|
account.accountNotice ? React.createElement('div', { className: 'dsub-verify' }, account.accountNotice) : null,
|
|
605
852
|
account.refreshError ? React.createElement('div', { className: 'dsub-bad' }, t('reconnectRequired') + ': ' + account.refreshError) : null,
|
|
606
853
|
(function(){ var r=checkRes[key]; if(!r) return null; var txt=r.ok ? ('ok ' + (r.email||'')) : ('fail ' + (r.error && r.error.message || '')); var cls=r.ok ? 'dsub-ok' : 'dsub-bad'; var q=r.quota; if(q && q.remaining!=null) txt += ' quota:'+q.remaining+(q.limit!=null?'/'+q.limit:''); return React.createElement('div', {className: cls}, txt) })(),
|
|
@@ -696,15 +943,7 @@ function PluginCard(props) {
|
|
|
696
943
|
React.createElement('div', { className: 'dsub-name' }, t('title')),
|
|
697
944
|
React.createElement('div', { className: 'dsub-description' }, t('cardIntro')),
|
|
698
945
|
),
|
|
699
|
-
React.createElement('
|
|
700
|
-
className: 'dsub-chev' + (open ? ' dsub-chevOpen' : ''),
|
|
701
|
-
width: 16, height: 16, viewBox: '0 0 16 16', fill: 'none',
|
|
702
|
-
},
|
|
703
|
-
React.createElement('path', {
|
|
704
|
-
d: 'M4 6l4 4 4-4', stroke: 'currentColor', 'stroke-width': 1.5,
|
|
705
|
-
'stroke-linecap': 'round', 'stroke-linejoin': 'round',
|
|
706
|
-
}),
|
|
707
|
-
),
|
|
946
|
+
React.createElement(Chevron, { className: 'dsub-chev' + (open ? ' dsub-chevOpen' : '') }),
|
|
708
947
|
),
|
|
709
948
|
open ? React.createElement('div', { className: 'dsub-body' },
|
|
710
949
|
React.createElement(SubsSection, props),
|
|
@@ -786,9 +1025,370 @@ function PluginCard(props) {
|
|
|
786
1025
|
expiresAt: (data && data.expiresAt) || {},
|
|
787
1026
|
labels: (data && data.labels) || {},
|
|
788
1027
|
expiryNotifyDays: (data && data.expiryNotifyDays) || 7,
|
|
1028
|
+
composerQuota: (data && data.composerQuota) || 'off',
|
|
789
1029
|
active: (data && data.active) || null,
|
|
790
1030
|
}
|
|
791
|
-
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, active: null } }
|
|
1031
|
+
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null } }
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// #84: компактный индикатор квоты активной подписки в области ввода.
|
|
1035
|
+
// Прогноз (runway) считается по скользящему окну сэмплов /status.
|
|
1036
|
+
const fcSamples = { windows: {} }
|
|
1037
|
+
|
|
1038
|
+
function fcObserve(key, remaining, now) {
|
|
1039
|
+
const windows = fcSamples.windows
|
|
1040
|
+
const prev = windows[key]
|
|
1041
|
+
const samples = (prev && prev.samples) || []
|
|
1042
|
+
const last = samples[samples.length - 1]
|
|
1043
|
+
if (!last || (now > last.at && (Math.abs(remaining - last.pct) >= 0.1 || now - last.at >= 15 * 60 * 1000))) {
|
|
1044
|
+
samples.push({ at: now, pct: remaining })
|
|
1045
|
+
}
|
|
1046
|
+
windows[key] = { samples: samples.filter((x) => x.at >= now - 24 * 60 * 60 * 1000).slice(-192) }
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function fcEstimate(key, remaining, now) {
|
|
1050
|
+
const rec = fcSamples.windows[key]
|
|
1051
|
+
if (!rec) return { status: 'calibrating' }
|
|
1052
|
+
const samples = rec.samples
|
|
1053
|
+
if (samples.length < 3) return { status: 'calibrating' }
|
|
1054
|
+
const first = samples[0]
|
|
1055
|
+
const last = samples[samples.length - 1]
|
|
1056
|
+
const span = last.at - first.at
|
|
1057
|
+
const consumed = first.pct - last.pct
|
|
1058
|
+
if (span < 30 * 60 * 1000 || consumed < 1) return { status: 'calibrating' }
|
|
1059
|
+
const t0 = first.at
|
|
1060
|
+
let sw = 0, sx = 0, sy = 0, sxx = 0, sxy = 0
|
|
1061
|
+
for (const x of samples) {
|
|
1062
|
+
const wx = (x.at - t0) / 3600000
|
|
1063
|
+
const wy = first.pct - x.pct
|
|
1064
|
+
const w = Math.exp((x.at - last.at) / (6 * 3600000))
|
|
1065
|
+
sw += w; sx += w * wx; sy += w * wy; sxx += w * wx * wx; sxy += w * wx * wy
|
|
1066
|
+
}
|
|
1067
|
+
const den = sw * sxx - sx * sx
|
|
1068
|
+
const pace = den > 0 ? (sw * sxy - sx * sy) / den : 0
|
|
1069
|
+
if (!Number.isFinite(pace) || pace < 0.02) return { status: 'idle' }
|
|
1070
|
+
return { status: 'ready', runwaySeconds: Math.round((remaining / pace) * 3600) }
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function ComposerQuota(props) {
|
|
1074
|
+
const t = (props && props.t) || ((k) => k)
|
|
1075
|
+
const [state, setState] = React.useState({ mode: 'off', active: null })
|
|
1076
|
+
React.useEffect(() => {
|
|
1077
|
+
let alive = true
|
|
1078
|
+
const pull = () => {
|
|
1079
|
+
refreshLoggedIn().then((r) => {
|
|
1080
|
+
if (!alive) return
|
|
1081
|
+
const mode = r.composerQuota || 'off'
|
|
1082
|
+
const a = r.active
|
|
1083
|
+
if (mode !== 'off' && a && a.provider && a.provider !== 'ollama' && a.usagePercent != null) {
|
|
1084
|
+
const remaining = Math.max(0, 100 - a.usagePercent)
|
|
1085
|
+
const key = 'subs:' + a.provider
|
|
1086
|
+
fcObserve(key, remaining, Date.now())
|
|
1087
|
+
const est = fcEstimate(key, remaining, Date.now())
|
|
1088
|
+
setState({ mode, active: a, remaining, est })
|
|
1089
|
+
} else {
|
|
1090
|
+
setState({ mode, active: a, remaining: null, est: null })
|
|
1091
|
+
}
|
|
1092
|
+
}).catch(() => {})
|
|
1093
|
+
}
|
|
1094
|
+
pull()
|
|
1095
|
+
const id = setInterval(pull, 60 * 1000)
|
|
1096
|
+
return () => { alive = false; clearInterval(id) }
|
|
1097
|
+
}, [])
|
|
1098
|
+
if (state.mode === 'off' || !state.active || state.remaining == null) return null
|
|
1099
|
+
const cls = state.remaining <= 10 ? ' dsub-cqBarFull' : (state.remaining <= 30 ? ' dsub-cqBarWarn' : '')
|
|
1100
|
+
var value = null
|
|
1101
|
+
if (state.mode === 'percent') {
|
|
1102
|
+
value = React.createElement('span', { className: 'dsub-cqB' }, Math.round(state.remaining) + '%')
|
|
1103
|
+
} else if (state.mode === 'bar') {
|
|
1104
|
+
value = React.createElement('span', { className: 'dsub-cqBar' + cls },
|
|
1105
|
+
React.createElement('span', { className: 'dsub-cqBarFill', style: { width: Math.min(100, Math.max(0, state.remaining)) + '%' } }))
|
|
1106
|
+
} else if (state.mode === 'forecast') {
|
|
1107
|
+
var est = state.est
|
|
1108
|
+
if (!est || est.status === 'calibrating') value = React.createElement('span', { className: 'dsub-cqB' }, t('fcCalibrating'))
|
|
1109
|
+
else if (est.status === 'idle') value = React.createElement('span', { className: 'dsub-cqB' }, t('fcIdle'))
|
|
1110
|
+
else {
|
|
1111
|
+
var secs = est.runwaySeconds || 0
|
|
1112
|
+
var txt = secs >= 3600 ? ('~' + (Math.round((secs / 3600) * 10) / 10) + t('fcHours')) : ('~' + Math.max(1, Math.round(secs / 60)) + t('fcMinutes'))
|
|
1113
|
+
value = React.createElement('span', { className: 'dsub-cqB' }, txt)
|
|
1114
|
+
}
|
|
1115
|
+
} else return null
|
|
1116
|
+
return React.createElement('span', { className: 'dsub-cq', title: t('forecast') + ' · ' + state.active.provider }, value)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// #83: пилл SUBS(N) в шапке сессии со светодиодом здоровья пула и
|
|
1120
|
+
// модальной консолью аккаунтов (закрытие: крестик, Escape, клик мимо).
|
|
1121
|
+
function SubsPill(props) {
|
|
1122
|
+
const t = (props && props.t) || ((k) => k)
|
|
1123
|
+
const [state, setState] = React.useState({ logged: [], usage: 0, accounts: [], open: false })
|
|
1124
|
+
React.useEffect(() => {
|
|
1125
|
+
let alive = true
|
|
1126
|
+
const pull = () => {
|
|
1127
|
+
refreshLoggedIn().then((r) => {
|
|
1128
|
+
if (!alive) return
|
|
1129
|
+
const logged = r.logged || []
|
|
1130
|
+
fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
1131
|
+
.then((res) => res.json())
|
|
1132
|
+
.then((cfg) => {
|
|
1133
|
+
if (!alive) return
|
|
1134
|
+
setState({ logged, usage: r.usage, accounts: (cfg && cfg.accounts) || [], open: false })
|
|
1135
|
+
})
|
|
1136
|
+
.catch(() => { if (alive) setState({ logged, usage: r.usage, accounts: [], open: false }) })
|
|
1137
|
+
}).catch(() => {})
|
|
1138
|
+
}
|
|
1139
|
+
pull()
|
|
1140
|
+
const id = setInterval(pull, 60 * 1000)
|
|
1141
|
+
return () => { alive = false; clearInterval(id) }
|
|
1142
|
+
}, [])
|
|
1143
|
+
React.useEffect(() => {
|
|
1144
|
+
if (!state.open) return undefined
|
|
1145
|
+
const onKey = (e) => { if (e.key === 'Escape') setState((s) => Object.assign({}, s, { open: false })) }
|
|
1146
|
+
document.addEventListener('keydown', onKey)
|
|
1147
|
+
return () => document.removeEventListener('keydown', onKey)
|
|
1148
|
+
}, [state.open])
|
|
1149
|
+
const n = state.logged.length
|
|
1150
|
+
const led = n === 0 ? 'dsub-ledOff' : (state.usage >= 90 ? 'dsub-ledBad' : (state.usage >= 50 ? 'dsub-ledWarn' : 'dsub-ledOk'))
|
|
1151
|
+
const now = Date.now()
|
|
1152
|
+
const rows = state.accounts.map((a, i) => {
|
|
1153
|
+
const isLogged = state.logged.includes(a.provider)
|
|
1154
|
+
const cooled = a.cooldownUntil && a.cooldownUntil > now
|
|
1155
|
+
const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
|
|
1156
|
+
return React.createElement('div', { className: 'dsub-modalRow', key: i },
|
|
1157
|
+
React.createElement('span', { className: 'dsub-led ' + (pct == null ? 'dsub-ledOff' : (pct >= 90 ? 'dsub-ledBad' : (pct >= 50 ? 'dsub-ledWarn' : 'dsub-ledOk'))) }),
|
|
1158
|
+
React.createElement('span', { className: 'dsub-modalName' }, a.provider + (a.index ? ' #' + a.index : '')),
|
|
1159
|
+
React.createElement('span', { style: { flex: 1 } }),
|
|
1160
|
+
React.createElement('span', { className: 'dsub-modalSub' },
|
|
1161
|
+
(isLogged ? t('subsLogged') : t('subsNotLogged')) +
|
|
1162
|
+
(cooled ? ' · ' + t('subsCooldown') : '') +
|
|
1163
|
+
(pct != null ? ' · ' + pct + '%' : '')
|
|
1164
|
+
),
|
|
1165
|
+
)
|
|
1166
|
+
})
|
|
1167
|
+
return React.createElement(React.Fragment, null,
|
|
1168
|
+
React.createElement('button', {
|
|
1169
|
+
type: 'button', className: 'dsub-pill', 'aria-expanded': state.open ? 'true' : 'false',
|
|
1170
|
+
onClick: () => setState((s) => Object.assign({}, s, { open: !s.open })),
|
|
1171
|
+
},
|
|
1172
|
+
React.createElement('span', { className: 'dsub-led ' + led }),
|
|
1173
|
+
t('subsPill') + ' (' + n + ')',
|
|
1174
|
+
),
|
|
1175
|
+
state.open ? React.createElement('div', {
|
|
1176
|
+
className: 'dsub-modalWrap',
|
|
1177
|
+
onClick: (e) => { if (e.target === e.currentTarget) setState((s) => Object.assign({}, s, { open: false })) },
|
|
1178
|
+
},
|
|
1179
|
+
React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-label': t('subsModalTitle') },
|
|
1180
|
+
React.createElement('div', { className: 'dsub-modalHead' },
|
|
1181
|
+
React.createElement('span', { className: 'dsub-h' }, t('subsModalTitle')),
|
|
1182
|
+
React.createElement('button', {
|
|
1183
|
+
type: 'button', className: 'dsub-mini',
|
|
1184
|
+
onClick: () => setState((s) => Object.assign({}, s, { open: false })),
|
|
1185
|
+
}, t('subsModalClose') + ' \u00d7'),
|
|
1186
|
+
),
|
|
1187
|
+
rows.length ? rows : React.createElement('div', { className: 'dsub-sub' }, t('subsNotLogged')),
|
|
1188
|
+
React.createElement('div', { className: 'dsub-row', style: { marginTop: 12 } },
|
|
1189
|
+
React.createElement('a', {
|
|
1190
|
+
className: 'dsub-mini', href: '#', onClick: (e) => {
|
|
1191
|
+
e.preventDefault()
|
|
1192
|
+
setState((s) => Object.assign({}, s, { open: false }))
|
|
1193
|
+
try { document.querySelector('[data-slot-key=\"settings.plugin.item\"],[data-dsub-open]') } catch {}
|
|
1194
|
+
try {
|
|
1195
|
+
const btn = Array.from(document.querySelectorAll('button')).find((b) => (b.textContent || '').includes(t('title')))
|
|
1196
|
+
if (btn) btn.click()
|
|
1197
|
+
} catch {}
|
|
1198
|
+
},
|
|
1199
|
+
}, t('subsOpenSettings')),
|
|
1200
|
+
),
|
|
1201
|
+
),
|
|
1202
|
+
) : null,
|
|
1203
|
+
)
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// #82: перетаскиваемый HUD-бабл (shell.overlay) с кольцевой шкалой
|
|
1207
|
+
// остатка активной подписки, прилипанием к краям и frosted-панелью.
|
|
1208
|
+
const HUD_KEY = 'dsh-subscriptions.hud'
|
|
1209
|
+
const HUD_SIZE = 64
|
|
1210
|
+
|
|
1211
|
+
function hudClamp(pos) {
|
|
1212
|
+
const w = window.innerWidth, h = window.innerHeight
|
|
1213
|
+
return {
|
|
1214
|
+
left: Math.min(Math.max(8, pos.left), Math.max(8, w - HUD_SIZE - 8)),
|
|
1215
|
+
top: Math.min(Math.max(8, pos.top), Math.max(8, h - HUD_SIZE - 8)),
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function hudLoad() {
|
|
1220
|
+
try {
|
|
1221
|
+
const raw = JSON.parse(localStorage.getItem(HUD_KEY) || 'null')
|
|
1222
|
+
if (raw && typeof raw.left === 'number' && typeof raw.top === 'number') return hudClamp(raw)
|
|
1223
|
+
} catch {}
|
|
1224
|
+
return hudClamp({ left: window.innerWidth - HUD_SIZE - 24, top: window.innerHeight - HUD_SIZE - 120 })
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function hudDock(pos) {
|
|
1228
|
+
const w = window.innerWidth, h = window.innerHeight
|
|
1229
|
+
const dist = { left: pos.left, right: w - (pos.left + HUD_SIZE), top: pos.top, bottom: h - (pos.top + HUD_SIZE) }
|
|
1230
|
+
const TH = 24
|
|
1231
|
+
let side = null, best = Infinity
|
|
1232
|
+
for (const k of ['left', 'right', 'top', 'bottom']) {
|
|
1233
|
+
if (dist[k] <= TH && dist[k] < best) { best = dist[k]; side = k }
|
|
1234
|
+
}
|
|
1235
|
+
if (!side) return pos
|
|
1236
|
+
const next = { left: pos.left, top: pos.top }
|
|
1237
|
+
if (side === 'left') next.left = 0
|
|
1238
|
+
if (side === 'right') next.left = Math.max(0, w - HUD_SIZE)
|
|
1239
|
+
if (side === 'top') next.top = 0
|
|
1240
|
+
if (side === 'bottom') next.top = Math.max(0, h - HUD_SIZE)
|
|
1241
|
+
return next
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function UsageBubble(props) {
|
|
1245
|
+
const t = (props && props.t) || ((k) => k)
|
|
1246
|
+
const [pos, setPos] = React.useState(() => (typeof window === 'undefined' ? { left: 0, top: 0 } : hudLoad()))
|
|
1247
|
+
const [accounts, setAccounts] = React.useState([])
|
|
1248
|
+
const [active, setActive] = React.useState(null)
|
|
1249
|
+
const [dragging, setDragging] = React.useState(false)
|
|
1250
|
+
const [busy, setBusy] = React.useState(false)
|
|
1251
|
+
const drag = React.useRef(null)
|
|
1252
|
+
React.useEffect(() => {
|
|
1253
|
+
let alive = true
|
|
1254
|
+
const pull = () => {
|
|
1255
|
+
refreshLoggedIn().then((r) => {
|
|
1256
|
+
if (!alive) return
|
|
1257
|
+
setActive(r.active)
|
|
1258
|
+
fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
1259
|
+
.then((res) => res.json())
|
|
1260
|
+
.then((cfg) => { if (alive) setAccounts((cfg && cfg.accounts) || []) })
|
|
1261
|
+
.catch(() => {})
|
|
1262
|
+
}).catch(() => {})
|
|
1263
|
+
}
|
|
1264
|
+
pull()
|
|
1265
|
+
const id = setInterval(pull, 60 * 1000)
|
|
1266
|
+
return () => { alive = false; clearInterval(id) }
|
|
1267
|
+
}, [])
|
|
1268
|
+
const onPointerDown = (e) => {
|
|
1269
|
+
if (e.button !== 0) return
|
|
1270
|
+
e.preventDefault()
|
|
1271
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
1272
|
+
drag.current = { id: e.pointerId, sx: e.clientX, sy: e.clientY, left: pos.left, top: pos.top, moved: false }
|
|
1273
|
+
}
|
|
1274
|
+
const onPointerMove = (e) => {
|
|
1275
|
+
const d = drag.current
|
|
1276
|
+
if (!d || e.pointerId !== d.id) return
|
|
1277
|
+
if (Math.abs(e.clientX - d.sx) > 3 || Math.abs(e.clientY - d.sy) > 3) { d.moved = true; setDragging(true) }
|
|
1278
|
+
if (!d.moved) return
|
|
1279
|
+
setPos(hudClamp({ left: d.left + (e.clientX - d.sx), top: d.top + (e.clientY - d.sy) }))
|
|
1280
|
+
}
|
|
1281
|
+
const onPointerUp = (e) => {
|
|
1282
|
+
const d = drag.current
|
|
1283
|
+
if (!d || e.pointerId !== d.id) return
|
|
1284
|
+
drag.current = null
|
|
1285
|
+
setDragging(false)
|
|
1286
|
+
setPos((cur) => {
|
|
1287
|
+
const next = d.moved ? hudDock(hudClamp(cur)) : cur
|
|
1288
|
+
try { localStorage.setItem(HUD_KEY, JSON.stringify(next)) } catch {}
|
|
1289
|
+
return next
|
|
1290
|
+
})
|
|
1291
|
+
if (!d.moved) {
|
|
1292
|
+
setBusy(true)
|
|
1293
|
+
refreshLoggedIn().then((r) => {
|
|
1294
|
+
setActive(r.active)
|
|
1295
|
+
return fetch('/dsh-subscriptions/config', { cache: 'no-store' }).then((res) => res.json())
|
|
1296
|
+
}).then((cfg) => setAccounts((cfg && cfg.accounts) || [])).catch(() => {}).finally(() => setBusy(false))
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
// Остаток активной подписки: 100 - usagePercent.
|
|
1300
|
+
const remaining = active && active.usagePercent != null ? Math.max(0, 100 - active.usagePercent) : null
|
|
1301
|
+
const tone = remaining == null ? 'var(--dsw-alias-label-tertiary)'
|
|
1302
|
+
: (remaining < 10 ? 'var(--dsw-alias-state-error-primary)'
|
|
1303
|
+
: (remaining < 30 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)'))
|
|
1304
|
+
const R = 26
|
|
1305
|
+
const CIRC = 2 * Math.PI * R
|
|
1306
|
+
const offset = CIRC * (1 - (remaining == null ? 0 : Math.min(100, remaining)) / 100)
|
|
1307
|
+
const rows = accounts.map((a, i) => {
|
|
1308
|
+
const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
|
|
1309
|
+
return React.createElement('div', { className: 'dsub-hudRow', key: i },
|
|
1310
|
+
React.createElement('span', { className: 'dsub-hudName' }, a.provider + (a.index ? ' #' + a.index : '')),
|
|
1311
|
+
React.createElement('span', { className: 'dsub-bar' },
|
|
1312
|
+
React.createElement('span', { className: 'dsub-barFill', style: { width: (pct == null ? 0 : Math.min(100, pct)) + '%' } })),
|
|
1313
|
+
React.createElement('span', { className: 'dsub-hudPct' }, pct == null ? '—' : pct + '%'),
|
|
1314
|
+
)
|
|
1315
|
+
})
|
|
1316
|
+
if (!createPortal) return null
|
|
1317
|
+
return createPortal(React.createElement('div', {
|
|
1318
|
+
className: 'dsub-hud',
|
|
1319
|
+
'data-dragging': dragging ? 'true' : undefined,
|
|
1320
|
+
style: { left: pos.left + 'px', top: pos.top + 'px' },
|
|
1321
|
+
},
|
|
1322
|
+
React.createElement('button', {
|
|
1323
|
+
type: 'button', className: 'dsub-hudBtn', 'aria-label': t('hudTitle'),
|
|
1324
|
+
style: { '--dsub-tone': tone },
|
|
1325
|
+
onPointerDown, onPointerMove, onPointerUp, onPointerCancel: onPointerUp,
|
|
1326
|
+
},
|
|
1327
|
+
React.createElement('svg', { className: 'dsub-hudRing', viewBox: '0 0 56 56', 'aria-hidden': 'true' },
|
|
1328
|
+
React.createElement('circle', { className: 'dsub-hudTrack', cx: 28, cy: 28, r: R }),
|
|
1329
|
+
React.createElement('circle', {
|
|
1330
|
+
className: 'dsub-hudVal', cx: 28, cy: 28, r: R,
|
|
1331
|
+
strokeDasharray: String(CIRC), strokeDashoffset: String(offset),
|
|
1332
|
+
}),
|
|
1333
|
+
),
|
|
1334
|
+
React.createElement('span', { className: 'dsub-hudCore' },
|
|
1335
|
+
remaining == null ? '—' : (busy ? '\u2026' : Math.round(remaining) + '%'),
|
|
1336
|
+
),
|
|
1337
|
+
),
|
|
1338
|
+
React.createElement('div', { className: 'dsub-hudPanel' },
|
|
1339
|
+
React.createElement('div', { className: 'dsub-hudHead' }, t('hudTitle')),
|
|
1340
|
+
rows.length ? rows : React.createElement('div', { className: 'dsub-sub' }, '\u2014'),
|
|
1341
|
+
React.createElement('div', { className: 'dsub-hudHint' }, t('hudHint')),
|
|
1342
|
+
),
|
|
1343
|
+
), document.body)
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function registerUsageBubble(ctx) {
|
|
1347
|
+
ctx.effect(() => {
|
|
1348
|
+
ctx.slots.inject('shell.overlay', () =>
|
|
1349
|
+
ctx.slots.register(
|
|
1350
|
+
{
|
|
1351
|
+
name: 'shell.overlay',
|
|
1352
|
+
id: 'dsh-subscriptions-usage-bubble',
|
|
1353
|
+
order: 40,
|
|
1354
|
+
locale: NS,
|
|
1355
|
+
},
|
|
1356
|
+
(props) => React.createElement(UsageBubble, { t }),
|
|
1357
|
+
),
|
|
1358
|
+
)
|
|
1359
|
+
}, 'dsh-subscriptions: usage bubble')
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function registerSubsPill(ctx) {
|
|
1363
|
+
ctx.effect(() => {
|
|
1364
|
+
ctx.slots.inject('conversation.session.header.actions', () =>
|
|
1365
|
+
ctx.slots.register(
|
|
1366
|
+
{
|
|
1367
|
+
name: 'conversation.session.header.actions',
|
|
1368
|
+
id: 'dsh-subscriptions-subs-pill',
|
|
1369
|
+
order: 15,
|
|
1370
|
+
locale: NS,
|
|
1371
|
+
},
|
|
1372
|
+
(props) => React.createElement(SubsPill, { t }),
|
|
1373
|
+
),
|
|
1374
|
+
)
|
|
1375
|
+
}, 'dsh-subscriptions: subs pill')
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function registerComposerQuota(ctx) {
|
|
1379
|
+
ctx.effect(() => {
|
|
1380
|
+
ctx.slots.inject('conversation.input.right', () =>
|
|
1381
|
+
ctx.slots.register(
|
|
1382
|
+
{
|
|
1383
|
+
name: 'conversation.input.right',
|
|
1384
|
+
id: 'dsh-subscriptions-composer-quota',
|
|
1385
|
+
order: 5,
|
|
1386
|
+
locale: NS,
|
|
1387
|
+
},
|
|
1388
|
+
(props) => React.createElement(ComposerQuota, { t }),
|
|
1389
|
+
),
|
|
1390
|
+
)
|
|
1391
|
+
}, 'dsh-subscriptions: composer quota')
|
|
792
1392
|
}
|
|
793
1393
|
|
|
794
1394
|
// #52: компактный чип в шапке сессии со статусом подключённых подписок.
|
|
@@ -833,7 +1433,7 @@ function PluginCard(props) {
|
|
|
833
1433
|
const idx = a.index != null ? ('#' + a.index) : ''
|
|
834
1434
|
const plan = a.plan ? (' ' + a.plan) : ''
|
|
835
1435
|
// #77: без provider-префикса
|
|
836
|
-
const label = idx + plan
|
|
1436
|
+
const label = (a.provider === 'codex' && state.fastMode ? '\u26a1' : '') + idx + plan
|
|
837
1437
|
// windows: "5h 12% 7d 8%"
|
|
838
1438
|
const winLabels = wins.map((w) => (w.label || w.id) + ' ' + Math.round(w.usedPercent) + '%').join(' ')
|
|
839
1439
|
return React.createElement('span', {
|
|
@@ -995,6 +1595,9 @@ function PluginCard(props) {
|
|
|
995
1595
|
exports.apply = function apply(ctx) {
|
|
996
1596
|
setT(ctx.locale.bind(NS))
|
|
997
1597
|
registerSettings(ctx)
|
|
1598
|
+
registerUsageBubble(ctx)
|
|
1599
|
+
registerSubsPill(ctx)
|
|
1600
|
+
registerComposerQuota(ctx)
|
|
998
1601
|
registerHeaderChip(ctx)
|
|
999
1602
|
registerComposerSwitch(ctx)
|
|
1000
1603
|
registerSlashCommands(ctx)
|