@goodandready/dsh-subscriptions 0.6.0 → 0.6.1

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 CHANGED
@@ -231,6 +231,12 @@ dsh-subscriptions:
231
231
 
232
232
  ---
233
233
 
234
+ ## 🌐 Localization
235
+
236
+ The plugin source language is English only. Russian and other translations are provided at runtime by separate language plugins (for example the russification plugin), which translate the registered locale keys - the package itself ships no bundled translations (Changed in 0.6.1).
237
+
238
+ ---
239
+
234
240
  ## 📄 License
235
241
 
236
242
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/README.ru.md CHANGED
@@ -204,6 +204,12 @@ dsh-subscriptions:
204
204
 
205
205
  ---
206
206
 
207
+ ## 🌐 Локализация
208
+
209
+ Исходный язык плагина - только английский. Русский и другие переводы во время работы предоставляют отдельные языковые плагины (например, плагин русификации), переводящие зарегистрированные ключи локали, - сам пакет не содержит встроенных переводов (Changed in 0.6.1).
210
+
211
+ ---
212
+
207
213
  ## 📄 Лицензия
208
214
 
209
215
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/README.zh.md CHANGED
@@ -138,6 +138,12 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
138
138
 
139
139
  ---
140
140
 
141
+ ## 🌐 本地化
142
+
143
+ 插件源语言仅为英语。俄语及其他翻译由独立的语言插件(如俄化插件)在运行时对注册的本地化键进行翻译,包本身不内置翻译(Changed in 0.6.1)。
144
+
145
+ ---
146
+
141
147
  ## 📄 开源协议
142
148
 
143
149
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/lib/accounts.js CHANGED
@@ -327,7 +327,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
327
327
  recordExhaust(ref) { health.set(ref, recordExhaust(health.get(ref))) },
328
328
  recordBroken(ref) { health.set(ref, recordBroken(health.get(ref))) },
329
329
  recordSuccess(ref) {
330
- // успешный запрос сбрасывает кулдаун и возвращает аккаунт в строй
330
+ // successful request clears the cooldown and puts the account back in rotation
331
331
  cooldowns.delete(ref)
332
332
  quarantines.delete(ref)
333
333
  if (health.has(ref)) health.set(ref, null)
package/lib/adapter.js CHANGED
@@ -147,9 +147,9 @@ export class SubscriptionAdapter extends LlmAdapter {
147
147
  signal: opts.signal,
148
148
  saveBlob: (next) => deps.saveBlob(account.ref, next),
149
149
  })
150
- // поток завершился успешно: снять кулдаун и здоровье-штрафы
150
+ // stream completed successfully: clear cooldown and health penalties
151
151
  if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
152
- // #65: записать в историю запросов и стоимости.
152
+ // #65: record in the request and cost history.
153
153
  if (typeof deps.recordHistory === 'function') {
154
154
  try {
155
155
  deps.recordHistory({
package/lib/client.js CHANGED
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
8
8
  try { return require('react-dom').createPortal } catch { return null }
9
9
  })()
10
10
 
11
- // Модульный переводчик: доступен всем подпискам компонентов и хелперам.
11
+ // Modular translator: shared by component subscriptions and helpers.
12
12
  let t = (key) => key
13
13
  const setT = (fn) => { t = fn }
14
14
 
@@ -87,9 +87,9 @@ window.__ModuleLoader__.load({
87
87
  '.dsub-pill:active{transform:translateY(0)}' +
88
88
  '.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
89
89
  '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
90
- '.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px rgba(16,185,129,.7)}' +
91
- '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px rgba(245,158,11,.7)}' +
92
- '.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px rgba(239,68,68,.7)}' +
90
+ '.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}' +
91
+ '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warning-primary) 70%,transparent)}' +
92
+ '.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}' +
93
93
  '.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
94
94
  '.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:rgba(0,0,0,.65);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
95
95
  '.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.07);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
@@ -108,7 +108,7 @@ window.__ModuleLoader__.load({
108
108
  '.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
109
109
  '.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
110
110
  '.dsub-panel-box{margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}' +
111
- '.dsub-panel-warn{border-color:rgba(245,158,11,0.4);background:rgba(245,158,11,0.05)}' +
111
+ '.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 5%,transparent)}' +
112
112
  '.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
113
113
  '.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
114
114
  '.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
@@ -117,14 +117,14 @@ window.__ModuleLoader__.load({
117
117
  '.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}' +
118
118
  '.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
119
119
  '.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
120
- '.dsub-tag-ok{border-color:rgba(16,185,129,0.3);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
121
- '.dsub-tag-warn{border-color:rgba(245,158,11,0.4);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
122
- '.dsub-tag-bad{border-color:rgba(239,68,68,0.4);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
120
+ '.dsub-tag-ok{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
121
+ '.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
122
+ '.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
123
123
  '.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
124
124
  '.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
125
125
  '.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
126
- '.dsub-btn-danger{border-color:rgba(239,68,68,0.4)!important;color:var(--dsw-alias-state-error-primary)!important}' +
127
- '.dsub-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important}' +
126
+ '.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
127
+ '.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
128
128
  '.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
129
129
  '.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}' +
130
130
  '.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
@@ -142,9 +142,9 @@ window.__ModuleLoader__.load({
142
142
  '.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
143
143
  '.dsub-accName{font-size:13px;font-weight:600}' +
144
144
  '.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}' +
145
- '.dsub-statusOk{background:rgba(16,185,129,.15);color:#34d399}' +
146
- '.dsub-statusWarn{background:rgba(245,158,11,.15);color:#fbbf24}' +
147
- '.dsub-statusBad{background:rgba(239,68,68,.15);color:#f87171}' +
145
+ '.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}' +
146
+ '.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary)}' +
147
+ '.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}' +
148
148
  '.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
149
149
  '.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}' +
150
150
  '.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary,#3b82f6);color:#fff;border:0;cursor:pointer;transition:opacity .15s}' +
@@ -169,9 +169,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
169
169
  document.head.appendChild(tag)
170
170
  }
171
171
 
172
- // Строки карточки живут в реестре локалей: так их переводит отдельный
173
- // пакет, не трогая код этого плагина. Английский язык по умолчанию,
174
- // на него же приходится откат, если перевода нет.
172
+ // Card strings live in the locale registry: a separate package translates
173
+ // them without touching this plugin's code. English is the default,
174
+ // and the fallback when no translation exists.
175
175
  const NS = 'dsh-subscriptions'
176
176
 
177
177
  const INSTRUCTIONS = {
@@ -421,130 +421,6 @@ const cssId = 'dsh-subscriptions/settings.module.css'
421
421
  'windowPrimary': 'Primary window',
422
422
  'windowSecondary': 'Secondary window',
423
423
  }
424
- const ru = {
425
- 'check': 'Проверить',
426
- 'checking': 'Проверяю…',
427
- 'importToken': 'Сохранить',
428
- 'importTokenPlace': 'Вставить токен или API-ключ',
429
- 'importLocalCli': '📥 Из CLI',
430
- 'importLocalCliTitle': 'Автоматически считать токен из установленного на сервере CLI',
431
- 'importLocalSuccess': 'Токен успешно загружен из локального CLI!',
432
- 'howToGetToken': '❓ Инструкция',
433
- 'howToGetTokenTitle': 'Как получить токен или API-ключ для этого провайдера',
434
- 'instructionsTitle': 'Инструкция по получению токена',
435
- 'close': 'Закрыть',
436
- 'reconnectRequired': 'Нужно переподключить',
437
- 'manualTitle': 'Если браузер не вернулся сам',
438
- 'windowPrimary': 'Основное окно',
439
- 'windowSecondary': 'Дополнительное окно',
440
- 'notConnected': 'Не подключено',
441
- 'verifyAccount': 'Требуется проверка',
442
- 'coolingDown': 'Пауза после лимита',
443
- 'usageFull': 'Лимит исчерпан',
444
- 'connected': 'Подключено',
445
- 'title': 'Подписки',
446
- 'cardIntro': 'Аккаунты подписок, ротация и лимиты.',
447
- 'show': 'Показать',
448
- 'hide': 'Скрыть',
449
- 'intro': 'Вход по обычной пользовательской подписке. Токены остаются в хранилище учётных данных харнесса, браузер их обратно не читает. Если после «Подключить» провайдер увёл на localhost или на свою страницу, вставьте сюда адрес перехода или код.',
450
- 'useOrigin': 'Использовать адрес этого веб-интерфейса как redirect_uri',
451
- 'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
452
- 'privacyMask': 'Маскировать email и учётные записи',
453
- 'privacyMaskHint': 'Для демонстрации экрана: email отображается как j***n@example.com.',
454
- 'resetCredits': 'Карты сброса',
455
- 'resetCreditsHint': 'Карты сброса квоты ChatGPT: показывает, сколько доступно. Списание одной карты — осознанное действие с 5-секундным подтверждением.',
456
- 'resetAvailable': 'Доступно попыток сброса',
457
- 'resetExpires': 'ближайшая истекает',
458
- 'resetAck': 'Я понимаю, что будет списана 1 попытка',
459
- 'resetWait': 'кнопка активна через',
460
- 'resetReady': 'готово',
461
- 'resetGo': 'Сбросить',
462
- 'resetBusy': 'сбрасываю…',
463
- 'resetDone': 'квота сброшена',
464
- 'resetNothing': 'сервер сообщает: сброс не нужен (попытка не списана)',
465
- 'resetNoCredit': 'нет пригодной карты (не списано)',
466
- 'resetRedeemed': 'эта карта уже использована ранее',
467
- 'diagGenerate': 'Сгенерировать диагностический отчёт',
468
- 'diagCopy': 'Скопировать отчёт',
469
- 'diagIssues': 'Открыть трекер задач',
470
- 'diagHint': 'В отчёте нет токенов, email, адресов прокси и персональных данных.',
471
- 'subsPill': 'SUBS',
472
- 'subsModalTitle': 'Консоль подписок',
473
- 'subsActiveHero': 'Активная подписка',
474
- 'subsPoolTitle': 'Пул аккаунтов',
475
- 'subsRefresh': 'Обновить',
476
- 'subsOpenSettings': 'Все настройки →',
477
- 'subsFastMode': 'Режим Fast 1.5x',
478
- 'subsHealthy': 'Исправен',
479
- 'subsFree': 'свободно',
480
- 'subsUsed': 'израсходовано',
481
- 'subsModalClose': 'Закрыть',
482
- 'subsLogged': 'подключен',
483
- 'subsNotLogged': 'не подключен',
484
- 'subsCooldown': 'кулдаун',
485
- 'subsOpenSettings': 'Открыть настройки',
486
- 'hudTitle': 'Остаток подписки',
487
- 'settingsLoading': 'Загрузка настроек…',
488
- 'settingsUnavailable': 'Настройки недоступны',
489
- 'settingsRetry': 'Повторить',
490
- 'hudHint': 'перетащи · прилипает к краям · клик — обновить',
491
- 'fcCalibrating': 'калибровка…',
492
- 'fcIdle': 'нет расхода',
493
- 'fcHours': 'ч',
494
- 'fcMinutes': 'мин',
495
- 'reconnect': 'Переподключить',
496
- 'connect': 'Подключить',
497
- 'disconnect': 'Отключить',
498
- 'removeSlot': 'Убрать место',
499
- 'pastePlaceholder': 'Адрес перехода или код',
500
- 'submitCode': 'Отправить код',
501
- 'proxyPlaceholder': 'Прокси аккаунта (http://, https://, socks5://) - необязательно',
502
- 'proxyCheck': 'Проверить прокси',
503
- 'proxyOk': 'прокси ок',
504
- 'proxyFail': 'прокси недоступен',
505
- 'deviceLogin': 'Вход по коду',
506
- 'deviceHint': 'Headless: откройте ссылку, введите код, держите вкладку открытой.',
507
- 'deviceCopy': 'Скопировать код',
508
- 'devicePending': 'Ожидание подтверждения',
509
- 'deviceAuthorized': 'Вход выполнен',
510
- 'deviceExpired': 'Истёк - начните заново',
511
- 'verifyPrefix': 'Google требует однократной проверки аккаунта. ',
512
- 'verifyLink': 'Открыть страницу проверки',
513
- 'verifySuffix': ' затем подключитесь заново.',
514
- 'addAccount': '+ Добавить аккаунт',
515
- 'save': 'Сохранить',
516
- 'saved': 'Сохранено',
517
- 'loading': 'Загрузка…',
518
- 'accountLabel': 'Аккаунт',
519
- 'plan': 'Тариф',
520
- 'storedAs': 'Хранится как',
521
- 'switchLabel': 'Провайдер',
522
- 'chipActive': 'Подписки',
523
- 'expiryLabel': 'окончание',
524
- 'slashLogin': 'Вход в провайдера подписки',
525
- 'slashLogout': 'Выйти из провайдера подписки',
526
- 'slashStatus': 'Статус подписок',
527
- 'none': 'нет',
528
- 'forecast': '≈',
529
- 'resetCredits': 'Сброс лимитов ChatGPT',
530
- 'resetCreditsHint': 'Экстренный сброс 5-часового окна ChatGPT Plus/Pro. Требует 5 секунд подтверждения перед списанием.',
531
- 'resetCreditsAction': 'Сбросить лимит ChatGPT',
532
- 'resetAvailable': 'Доступно сбросов',
533
- 'resetExpires': 'срок действия',
534
- 'resetAck': 'Я понимаю, что одна попытка сброса будет списана',
535
- 'resetWait': 'подтверждение через',
536
- 'resetReady': 'готово к сбросу',
537
- 'resetGo': 'Сбросить лимит сейчас',
538
- 'resetBusy': 'Сброс…',
539
- 'resetDone': 'Лимит успешно сброшен',
540
- 'resetNothing': 'Лимит не требует сброса',
541
- 'resetNoCredit': 'Нет доступных попыток сброса',
542
- 'resetRedeemed': 'Попытка уже использована',
543
- 'windowPrimary': 'Окно 5 часов',
544
- 'windowSecondary': 'Окно 7 дней',
545
- 'usageLimitsTitle': 'Лимиты использования',
546
- 'slotDetailsTitle': 'Параметры слота',
547
- }
548
424
 
549
425
  // #51: live countdown to the quota window reset (account.quota.resetAt).
550
426
  function cleanErrorMessage(raw) {
@@ -633,9 +509,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
633
509
  return t('connected')
634
510
  }
635
511
 
636
- // Карточка во вкладке «Плагины» рисует свой заголовок и сворачивание:
637
- // ядро даёт только рамку списка.
638
- // Ядровый значок раскрытия. Без защищённого require вместо IconChevronDownOutline14 может упасть вся клиентская половина.
512
+ // The card in the Plugins tab draws its own header and collapse toggle:
513
+ // the core only provides the list frame.
514
+ // Core chevron icon. Without a guarded require, a missing IconChevronDownOutline14 could take down the whole client half.
639
515
  let ChevronIcon = null
640
516
  try {
641
517
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
@@ -653,7 +529,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
653
529
 
654
530
 
655
531
  function SubsSection(props) {
656
- // Переводчик приходит от слота, потому что в его записи указан locale.
532
+ // The translator comes from the slot because its registration declares locale.
657
533
  const t = (props && props.t) || ((key) => key)
658
534
  const [draft, setDraft] = React.useState(null)
659
535
  const [accounts, setAccounts] = React.useState([])
@@ -852,7 +728,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
852
728
  setPaste((p) => Object.assign({}, p, { [key]: '' }))
853
729
  }
854
730
 
855
- // #88: проверить прокси аккаунта реальным запросом с замером задержки.
731
+ // #88: probe the account proxy with a real request and measure latency.
856
732
  const doProxyCheck = async (provider, index) => {
857
733
  const key = provider + ':' + index
858
734
  setChecking((c) => Object.assign({}, c, { ['proxy:' + key]: true }))
@@ -1331,8 +1207,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1331
1207
  )
1332
1208
  }
1333
1209
 
1334
- // Карточка во вкладке «Плагины» как карточки bash/agent-loop/web-search:
1335
- // заголовок с именем, описанием и шевроном, сворачивание, каёмка.
1210
+ // The card in the Plugins tab follows the bash/agent-loop/web-search cards:
1211
+ // titled header with description and chevron, collapsible, bordered.
1336
1212
  function PluginCard(props) {
1337
1213
  const t = (props && props.t) || ((key) => key)
1338
1214
  const [open, setOpen] = React.useState(false)
@@ -1358,14 +1234,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1358
1234
  }
1359
1235
 
1360
1236
  function registerSettings(ctx) {
1361
- // Язык может принести не только плагин: словарные пакеты объявляют
1362
- // русский для чужих пространств. Ядро на повторное объявление той же
1363
- // пары «пространство + язык» бросает исключение, и незащищённый вызов
1364
- // уносил с собой весь плагин в интерфейсе это выглядело как «Failed to
1365
- // load plugins» с перечнем ни в чём не повинных соседей.
1237
+ // Languages can come from more than this plugin: dictionary packages declare
1238
+ // Russian for foreign namespaces. The core throws on re-declaring the same
1239
+ // namespace+language pair, and an unguarded call used to take down the
1240
+ // whole plugin - in the UI it surfaced as Failed to
1241
+ // load plugins listing perfectly innocent neighbours.
1366
1242
  //
1367
- // Поэтому каждый язык объявляется отдельно и по-хорошему: заняли до нас
1368
- // уступаем, свой английский при этом всё равно встаёт на место.
1243
+ // So each language is declared separately and politely: if someone got
1244
+ // there first we yield, while our own English still lands.
1369
1245
  const addLocale = (locale, dictionary) => {
1370
1246
  try {
1371
1247
  return ctx.locale.register(NS, locale, dictionary)
@@ -1374,14 +1250,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1374
1250
  }
1375
1251
  }
1376
1252
  ctx.effect(() => {
1377
- const undo = [addLocale('en', en), addLocale('ru', ru)]
1253
+ const undo = [addLocale('en', en)]
1378
1254
  return () => { for (const off of undo) off() }
1379
1255
  }, 'dsh-subscriptions: словари')
1380
- // Подписи вне компонента берут переводчик, привязанный к namespace.
1256
+ // Labels outside a component take the translator bound to the namespace.
1381
1257
  setT(ctx.locale.bind(NS))
1382
- // Штатное место вкладка «Плагины»: карточка со своим заголовком и
1383
- // сворачиванием вместо строки в боковом списке. Ключ регистрации обязан
1384
- // равняться пространству настроек, иначе вкладка молча не покажет слот.
1258
+ // The canonical place is the Plugins tab: a card with its own header
1259
+ // and collapse instead of a row in the side list. The registration key must
1260
+ // equal the settings namespace, otherwise the tab silently hides the slot.
1385
1261
  let moved = false
1386
1262
  try {
1387
1263
  moved = !!ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
@@ -1417,8 +1293,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1417
1293
  }
1418
1294
  }
1419
1295
 
1420
- // Горячий переключатель провайдера в строке композера: клик циклично
1421
- // меняет активного провайдера подписки для следующего запроса.
1296
+ // Hot provider switcher in the composer row: a click cycles
1297
+ // the active subscription provider for the next request.
1422
1298
  let activeIdx = 0
1423
1299
  let labelEl = null
1424
1300
  const ORDER = ['codex', 'claude', 'grok', 'antigravity']
@@ -1443,8 +1319,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1443
1319
  } catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null } }
1444
1320
  }
1445
1321
 
1446
- // #84: компактный индикатор квоты активной подписки в области ввода.
1447
- // Прогноз (runway) считается по скользящему окну сэмплов /status.
1322
+ // #84: compact quota indicator of the active subscription in the input area.
1323
+ // The runway forecast uses a rolling window of /status samples.
1448
1324
  const fcSamples = { windows: {} }
1449
1325
 
1450
1326
  function fcObserve(key, remaining, now) {
@@ -1528,8 +1404,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1528
1404
  return React.createElement('span', { className: 'dsub-cq', title: t('forecast') + ' · ' + state.active.provider }, value)
1529
1405
  }
1530
1406
 
1531
- // #83: пилл SUBS(N) в шапке сессии со светодиодом здоровья пула и
1532
- // модальной консолью аккаунтов (закрытие: крестик, Escape, клик мимо).
1407
+ // #83: SUBS(N) pill in the session header with a pool health LED and
1408
+ // a modal accounts console (close: X button, Escape, outside click).
1533
1409
  function brandBadge(prov) {
1534
1410
  const p = String(prov || '').toLowerCase()
1535
1411
  if (p.includes('codex') || p.includes('chatgpt') || p.includes('openai')) return { label: 'OpenAI', cls: 'dsub-brandCodex', icon: '⚡' }
@@ -1617,7 +1493,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1617
1493
  React.createElement('div', { className: 'dsub-heroTitle' },
1618
1494
  React.createElement('span', { className: 'dsub-brandBadge ' + b.cls, style: { width: 24, height: 24, fontSize: 11 } }, b.icon),
1619
1495
  React.createElement('span', null, (a.provider ? a.provider.toUpperCase() : 'LLM') + (a.index ? ' #' + a.index : '')),
1620
- a.fastMode ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'rgba(245,158,11,.15)', color: '#fbbf24' } }, '⚡ FAST 1.5x') : null,
1496
+ a.fastMode ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent)', color: 'var(--dsw-alias-state-warning-primary)' } }, '⚡ FAST 1.5x') : null,
1621
1497
  ),
1622
1498
  React.createElement('span', { className: 'dsub-heroModel' }, a.model || 'active model'),
1623
1499
  ),
@@ -1681,7 +1557,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1681
1557
  React.createElement('div', { className: 'dsub-accNameRow' },
1682
1558
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1683
1559
  React.createElement('span', { className: 'dsub-accName' }, a.provider.toUpperCase() + (a.index ? ' #' + a.index : '')),
1684
- isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'rgba(16,185,129,.15)', color: '#34d399' } }, 'ACTIVE') : null,
1560
+ isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent)', color: 'var(--dsw-alias-state-success-primary)' } }, 'ACTIVE') : null,
1685
1561
  a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
1686
1562
  ),
1687
1563
  React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
package/lib/crypto.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { scryptSync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
2
2
 
3
- // AES-256-GCM с ключом из passphrase (scrypt, без хранения соли отдельно
4
- // соль в начале блока).
3
+ // AES-256-GCM with a passphrase-derived key (scrypt, no separately stored salt -
4
+ // the salt is stored at the head of the block).
5
5
  export function encryptWithPassphrase(plainText, passphrase) {
6
6
  const salt = randomBytes(16)
7
7
  const key = scryptSync(String(passphrase), salt, 32)
@@ -9,13 +9,13 @@ export function encryptWithPassphrase(plainText, passphrase) {
9
9
  const cipher = createCipheriv("aes-256-gcm", key, iv)
10
10
  const encrypted = Buffer.concat([cipher.update(String(plainText), "utf8"), cipher.final()])
11
11
  const tag = cipher.getAuthTag()
12
- // формат: magic + salt + iv + tag + data
12
+ // format: magic + salt + iv + tag + data
13
13
  return "DSHE1:" + Buffer.concat([salt, iv, tag, encrypted]).toString("base64")
14
14
  }
15
15
 
16
16
  export function decryptWithPassphrase(payload, passphrase) {
17
17
  const raw = String(payload || "")
18
- if (!raw.startsWith("DSHE1:")) throw new Error("неизвестный формат экспорта")
18
+ if (!raw.startsWith("DSHE1:")) throw new Error("unknown export format")
19
19
  const buf = Buffer.from(raw.slice(6), "base64")
20
20
  const salt = buf.subarray(0, 16)
21
21
  const iv = buf.subarray(16, 28)
@@ -121,7 +121,7 @@ export function googleLicenseMessage(status, bodyText) {
121
121
  const api = parseGoogleApiError(bodyText)
122
122
  const msg = String(api?.message || bodyText || '')
123
123
  if (/do not have a valid license|#3501|PERMISSION_DENIED/i.test(msg)) {
124
- return 'У аккаунта Google нет активной лицензии Gemini Code Assist / Antigravity (#3501), либо не привязан проект Google Cloud. Проверьте подписку на https://one.google.com или подключите другой Google аккаунт.'
124
+ return 'Google account has no active Gemini Code Assist / Antigravity license (#3501), or no Google Cloud project is attached. Check the subscription at https://one.google.com or connect a different Google account.'
125
125
  }
126
126
  return ''
127
127
  }
package/lib/health.js CHANGED
@@ -1,5 +1,5 @@
1
- // Health-счёт аккаунта для умной ротации.
2
- // Числа как в dsh-key-rotation pool.js: свитч 5, исчерпание 10, битый ключ 15.
1
+ // Per-account health score for smart rotation.
2
+ // Numbers mirror dsh-key-rotation pool.js: switch 5, exhaustion 10, bad key 15.
3
3
  const PENALTY = { switch: 5, exhaust: 10, broken: 15 }
4
4
 
5
5
  export function emptyHealth() {
package/lib/history.js CHANGED
@@ -2,9 +2,9 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { homedir } from 'node:os'
4
4
 
5
- // #65: история запросов и стоимости. Хранится в
6
- // ~/.dsh/storages/dsh-subscriptions/history.json (JSON-массив, новые сверху).
7
- // Все IO best-effort и синхронные: сбой записи не должен ронять харнесс.
5
+ // #65: request and cost history. Stored in
6
+ // ~/.dsh/storages/dsh-subscriptions/history.json (JSON array, newest first).
7
+ // All IO is best-effort and synchronous: a write failure must never crash the harness.
8
8
 
9
9
  export const DEFAULT_HISTORY_DIR = join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
10
10
 
@@ -42,7 +42,7 @@ export class HistoryStore {
42
42
  } catch { /* best-effort */ }
43
43
  }
44
44
 
45
- /** Добавить одну запись. Возвращает длину истории. */
45
+ /** Add one entry. Returns the history length. */
46
46
  add(entry) {
47
47
  if (!entry || typeof entry !== 'object') return this.rows.length
48
48
  this.rows.unshift({ ts: Date.now(), ...entry })
@@ -51,7 +51,7 @@ export class HistoryStore {
51
51
  return this.rows.length
52
52
  }
53
53
 
54
- /** Последние N записей (новые сверху). */
54
+ /** Last N entries (newest first). */
55
55
  recent(n) {
56
56
  return this.rows.slice(0, n)
57
57
  }
package/lib/http.js CHANGED
@@ -80,3 +80,10 @@ export function safeJsonHandler(fn) {
80
80
  }
81
81
  }
82
82
  }
83
+
84
+ export function escapeHtml(text) {
85
+ return String(text)
86
+ .replace(/&/g, '&')
87
+ .replace(/</g, '&lt;')
88
+ .replace(/>/g, '&gt;')
89
+ }
package/lib/images.js CHANGED
@@ -1,26 +1,26 @@
1
- // Генерация картинок на подписке.
1
+ // Image generation on a subscription.
2
2
  //
3
- // Здесь только протокол: куда идти, что послать и как прочитать ответ. Всё
4
- // остальное сохранение файла, вложение в разговор, карточка дело плагина
5
- // генерации; этот плагин лишь одалживает свой аккаунт.
3
+ // This module only implements the protocol: where to go, what to send and how to read the response. Everything
4
+ // else - saving the file, attaching it to the conversation, the card - is the job of the generation
5
+ // plugin; this plugin only lends its account.
6
6
  //
7
- // Токен наружу не отдаётся: плагин объявляет службу внутри процесса, а не
8
- // маршрут в сети. Харнесс на этой машине доступен без пароля, и ручка,
9
- // раздающая живой токен подписки, была бы дырой пошире тех, что мы закрывали.
7
+ // The token never leaves the process: the plugin exposes an in-process service, not
8
+ // a network route. The harness on this machine is reachable without a password, and an endpoint
9
+ // handing out a live subscription token would be a wider hole than the ones we already closed.
10
10
 
11
- /** Куда уходит запрос у подписки ChatGPT. */
11
+ /** Where the ChatGPT subscription request goes. */
12
12
  export const CODEX_URL = 'https://chatgpt.com/backend-api/codex/images/generations'
13
- /** Модель, которую отдаёт этот адрес. */
13
+ /** The model served by this endpoint. */
14
14
  export const CODEX_MODEL = 'gpt-image-2'
15
- /** Куда уходит запрос у подписки Grok. */
15
+ /** Where the Grok subscription request goes. */
16
16
  export const GROK_URL = 'https://api.x.ai/v1/images/generations'
17
- /** Модель, которую отдаёт этот адрес. */
17
+ /** The model served by this endpoint. */
18
18
  export const GROK_MODEL = 'grok-imagine-image-2.0'
19
19
 
20
- /** Размеры, которые понимает ChatGPT. */
20
+ /** Sizes understood by ChatGPT. */
21
21
  export const SIZES = ['1024x1024', '1024x1536', '1536x1024', 'auto']
22
22
 
23
- /** Grok мыслит не размерами, а соотношением сторон. */
23
+ /** Grok thinks in aspect ratios, not sizes. */
24
24
  const GROK_ASPECT = {
25
25
  '1024x1024': '1:1',
26
26
  '1024x1536': '2:3',
@@ -30,7 +30,7 @@ const GROK_ASPECT = {
30
30
 
31
31
  export function codexBody({ prompt, size, quality }) {
32
32
  const text = String(prompt || '').trim()
33
- if (!text) throw new Error('нужен непустой запрос')
33
+ if (!text) throw new Error('prompt must be a non-empty string')
34
34
  return {
35
35
  prompt: text,
36
36
  model: CODEX_MODEL,
@@ -41,8 +41,8 @@ export function codexBody({ prompt, size, quality }) {
41
41
 
42
42
  export function grokBody({ prompt, size, quality }) {
43
43
  const text = String(prompt || '').trim()
44
- if (!text) throw new Error('нужен непустой запрос')
45
- // У Grok качество всего двух ступеней: высокое складывается со средним.
44
+ if (!text) throw new Error('prompt must be a non-empty string')
45
+ // Grok has only two quality tiers: high is composed from medium.
46
46
  const level = quality === 'low' ? 'low'
47
47
  : (quality === 'medium' || quality === 'high') ? 'medium'
48
48
  : undefined
@@ -55,7 +55,7 @@ export function grokBody({ prompt, size, quality }) {
55
55
  }
56
56
  }
57
57
 
58
- /** Разбор ответа: обе стороны отвечают одинаково. */
58
+ /** Response parsing: both sides reply in the same shape. */
59
59
  export function parseImages(payload) {
60
60
  const body = payload && typeof payload === 'object' ? payload : {}
61
61
  const rows = Array.isArray(body.data) ? body.data : []
@@ -70,15 +70,15 @@ export function parseImages(payload) {
70
70
  : {}),
71
71
  })
72
72
  }
73
- if (!images.length) throw new Error('в ответе нет картинок')
73
+ if (!images.length) throw new Error('response contains no images')
74
74
  return images
75
75
  }
76
76
 
77
77
  /**
78
- * Один запрос к нужному адресу с заголовками этого провайдера.
78
+ * One request to the right endpoint with this provider's headers.
79
79
  *
80
80
  * @param options {{provider, prompt, size, quality, session, fetchImpl, signal}}
81
- * session уже освежённый блок токенов: accessToken и, для ChatGPT, accountId.
81
+ * session - already refreshed token block: accessToken and, for ChatGPT, accountId.
82
82
  */
83
83
  export async function generateOnce(options) {
84
84
  const { provider, session, fetchImpl, signal } = options
@@ -88,7 +88,7 @@ export async function generateOnce(options) {
88
88
  const headers = isCodex
89
89
  ? {
90
90
  authorization: `Bearer ${session.accessToken}`,
91
- // ChatGPT различает аккаунты отдельным заголовком, без него отвечает отказом.
91
+ // ChatGPT distinguishes accounts with a separate header; without it the request is rejected.
92
92
  'chatgpt-account-id': session.accountId || '',
93
93
  originator: 'codex_cli_rs',
94
94
  'content-type': 'application/json',