@goodandready/dsh-subscriptions 0.6.8 → 0.6.10

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
@@ -260,6 +260,29 @@ Supports Anthropic's adaptive thinking (`thinking: { type: "adaptive" }`) and re
260
260
 
261
261
  ---
262
262
 
263
+ ## 🚀 One-Click Plugin Updater & Stability Hardening (Added in v0.6.9)
264
+
265
+ - **Host One-Click Updater**: Automatic in-place updater mounted at `/dsh-subscriptions/update`, checking the npm registry for newer releases and executing single-flight installation via the host DSH CLI (`dsh plugin add --config.minimumReleaseAge=0`).
266
+ - **Security & Origin Protection**: Updater endpoints enforce strict loopback address checks (supporting IPv4 `127.0.0.1`, IPv6 `::1`, `localhost`), `x-dsh-plugin-update` header verification, and same-origin validation to prevent unauthorized updates.
267
+ - **Header Badge & UI Action**: The settings header bar displays current plugin version, an update warning badge when a new release is detected, and an instant "Update" button with live restart countdown.
268
+ - **Network Timeout Hardening**: Quota balance and smoke test probes now enforce guaranteed 15-second abort timeouts (`AbortSignal.timeout(15_000)`), preventing hanging sockets during vendor outages.
269
+ - **Parameter Normalization & Deduplication**: Unified support for `provider || vendor` and `index || accountIndex` aliases in check endpoints, and deduplicated threshold notification loops.
270
+
271
+ ---
272
+
273
+ ---
274
+
275
+ ## 🛡️ Reliability & Quality Hardening (Added in v0.6.10)
276
+
277
+ - **Structured Error Handling**: Eliminated all empty catch blocks across the plugin runtime via safe `bestEffort` helper logging debug diagnostics.
278
+ - **Native Context Logger**: Migrated all plugin logging to the canonical Cordis `ctx.logger('subscriptions')` subsystem.
279
+ - **Network Timeout Protection**: Hardened external vendor API calls (`listModels`, `fetchFor`, `quotaFetch`, `jsonTokenRequest`) with explicit `fetchWithTimeout` and `AbortSignal.timeout` safeguards.
280
+ - **Design System Theming**: Replaced hardcoded styling values with native DeepSeek Harness CSS theme tokens (`--dsw-alias-*`, `color-mix`), achieving 100% theme token coverage and zero standalone `rgba` values.
281
+ - **Client Dependency Injections**: Explicitly declared required client platform dependencies (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`) in package manifest.
282
+ - **Strict Repository Hygiene**: Removed internal development instructions and planning artifacts from git tracking, enforcing clean public releases.
283
+
284
+ ---
285
+
263
286
  ## 🌐 Localization
264
287
 
265
288
  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).
package/README.ru.md CHANGED
@@ -233,6 +233,29 @@ dsh-subscriptions:
233
233
 
234
234
  ---
235
235
 
236
+ ## 🚀 Обновление в один клик и повышение стабильности (Добавлено в v0.6.9)
237
+
238
+ - **Встроенный апдейтер в один клик**: Фоновая проверка новых версий в npm registry и обновление на месте по маршруту `/dsh-subscriptions/update` через хостовый CLI DSH (`dsh plugin add --config.minimumReleaseAge=0`).
239
+ - **Безопасность и защита эндпоинта**: Строгая верификация loopback-адресов (поддержка IPv4 `127.0.0.1`, IPv6 `::1`, `localhost`), валидация заголовка `x-dsh-plugin-update` и защита от CSRF/cross-origin запросов.
240
+ - **Индикатор в шапке и кнопка обновления**: В строке состояния панели отображается текущая версия, бейдж наличия обновления и кнопка быстрого обновления («Update → vX.Y.Z») с уведомлением о перезапуске службы.
241
+ - **Сетевые таймауты и устойчивость**: Ограничение времени ожидания запросов баланса и smoke-тестов до 15 секунд (`AbortSignal.timeout(15_000)`), предотвращающее зависание сокетов при недоступности внешних API.
242
+ - **Нормализация параметров и дедупликация**: Поддержка алиасов параметров `provider || vendor` и `index || accountIndex` в статусных маршрутах, а также устранение дублирующего цикла нотификаций в `lib/accounts.js`.
243
+
244
+ ---
245
+
246
+ ---
247
+
248
+ ## 🛡️ Надежность и стандарты качества (Добавлено в v0.6.10)
249
+
250
+ - **Структурированная обработка ошибок**: Устранены все пустые блоки catch в рантайме плагина с переходом на безопасный хелпер `bestEffort` с диагностическим логированием.
251
+ - **Нативный логгер контекста**: Все вызовы логирования переведены на официальную подсистему Cordis `ctx.logger('subscriptions')`.
252
+ - **Защита внешних вызовов таймаутами**: Все сетевые интеграции к API вендоров (`listModels`, `fetchFor`, `quotaFetch`, `jsonTokenRequest`) защищены таймаутами через `fetchWithTimeout` и `AbortSignal.timeout`.
253
+ - **Семантическая UI-темизация**: Статические цвета и rgba-значения в `client.js` заменены на нативные CSS-переменные дизайн-системы DeepSeek Harness (`--dsw-alias-*`, `color-mix`), достигнут 0 остаточных `rgba`.
254
+ - **Декларация клиентских зависимостей**: Задекларированы платформенные инжекты (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`) в манифесте `package.json`.
255
+ - **Чистота публичного репозитория**: Внутренние инструкции разработки и артефакты планирования исключены из git-отслеживания и добавлены в `.gitignore`.
256
+
257
+ ---
258
+
236
259
  ## 🌐 Локализация
237
260
 
238
261
  Исходный язык плагина - только английский. Русский и другие переводы во время работы предоставляют отдельные языковые плагины (например, плагин русификации), переводящие зарегистрированные ключи локали, - сам пакет не содержит встроенных переводов (Changed in 0.6.1).
package/README.zh.md CHANGED
@@ -155,6 +155,29 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
155
155
 
156
156
  ---
157
157
 
158
+ ## 🚀 一键插件自更新与稳定性强化 (v0.6.9 新增)
159
+
160
+ - **宿主端一键更新**: 挂载于 `/dsh-subscriptions/update` 的就地更新机制,自动对比 npm registry 最新版本并通过宿主 DSH CLI 执行单飞安装 (`dsh plugin add --config.minimumReleaseAge=0`)。
161
+ - **安全与来源防护**: 更新接口严格校验环回地址(支持 IPv4 `127.0.0.1`、IPv6 `::1`、`localhost`)、`x-dsh-plugin-update` 请求头及同源策略,阻断未授权跨站调用。
162
+ - **状态栏徽章与更新按钮**: 设置面板顶部状态栏显示当前插件版本,有新版本时提示告警徽章并提供一键更新按钮与重启提示。
163
+ - **网络超时熔断**: 配额检测与冒烟测试增加 15 秒超时信号保护 (`AbortSignal.timeout(15_000)`),防止外部服务商接口故障导致请求挂起。
164
+ - **参数容错与逻辑去重**: 状态接口兼容 `provider || vendor` 与 `index || accountIndex` 参数别名,并消除 `lib/accounts.js` 中的重复配额通知循环。
165
+
166
+ ---
167
+
168
+ ---
169
+
170
+ ## 🛡️ 稳定性与代码质量强化 (v0.6.10 新增)
171
+
172
+ - **结构化异常降级**: 全面移除运行时空 catch 块,统一采用带诊断日志的 `bestEffort` 安全兜底机制。
173
+ - **原生上下文日志**: 所有日志统一接入 Cordis 规范子系统 `ctx.logger('subscriptions')`。
174
+ - **外部网络超时保护**: 为所有模型商 API 网络调用(`listModels`、`fetchFor`、`quotaFetch`、`jsonTokenRequest`)建立 `fetchWithTimeout` 与 `AbortSignal.timeout` 超时熔断保护。
175
+ - **设计系统主题对齐**: 将 `client.js` 中的静态样式硬编码替换为 DeepSeek Harness 原生 CSS 主题令牌(`--dsw-alias-*`、`color-mix`),彻底消除独立的 `rgba` 颜色。
176
+ - **客户端依赖注入显式化**: 在 `package.json` 清单中显式声明前端所需平台注入项(`@deepseek-ai/dsh-client-locale` 与 `@deepseek-ai/dsh-client-ui-slots`)。
177
+ - **公开仓库卫生规范**: 将内部研发指令与计划文件移出 Git 跟踪并加入 `.gitignore`,确保公开版本干净无私有残余。
178
+
179
+ ---
180
+
158
181
  ## 🌐 本地化
159
182
 
160
183
  插件源语言仅为英语。俄语及其他翻译由独立的语言插件(如俄化插件)在运行时对注册的本地化键进行翻译,包本身不内置翻译(Changed in 0.6.1)。
package/lib/accounts.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { fetchWithTimeout } from "./http.js"
1
2
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
2
3
  import { oauthRef, isProvider } from './refs.js'
3
4
  import { parseBlob, serializeBlob } from './blob.js'
@@ -70,7 +71,8 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
70
71
  const usageFetched = new Map()
71
72
  const doFetch = fetchImpl || fetch
72
73
  // #88: per-account proxy fetch for token refresh / usage (falls back to doFetch).
73
- const fetchFor = (ref) => (typeof fetchForRef === 'function' && fetchForRef(ref)) || doFetch
74
+ const rawFetchFor = (ref) => (typeof fetchForRef === 'function' && fetchForRef(ref)) || doFetch
75
+ const fetchFor = (ref) => (url, init, opts) => fetchWithTimeout(rawFetchFor(ref), url, init, { timeoutMs: 25000, ...(opts || {}) })
74
76
 
75
77
  async function resolveRaw(ref) {
76
78
  try {
@@ -262,24 +264,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
262
264
  for (const th of THRESHOLDS) {
263
265
  if (pct >= th && prev < th) {
264
266
  notifyThresholds.set(key, th)
265
- try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch {}
266
- break
267
- }
268
- }
269
- }
270
- }
271
- // Notification thresholds: 70/90/100% — fire once per window until reset.
272
- if (Array.isArray(snap.windows)) {
273
- for (const win of snap.windows) {
274
- if (!win || !Number.isFinite(Number(win.usedPercent))) continue
275
- const pct = Number(win.usedPercent)
276
- const key = slot.ref + ':' + win.id
277
- const prev = notifyThresholds.get(key) || 0
278
- const THRESHOLDS = [70, 90, 100]
279
- for (const th of THRESHOLDS) {
280
- if (pct >= th && prev < th) {
281
- notifyThresholds.set(key, th)
282
- try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch {}
267
+ try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch (err) { /* ignore notification failure */ }
283
268
  break
284
269
  }
285
270
  }
package/lib/adapter.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { fetchWithTimeout } from "./http.js"
1
2
  import { LlmAdapter, LlmError, attributionHeaders } from '@deepseek-ai/dsh-llm'
2
3
  import { displayName } from './refs.js'
3
4
  import { getVendor } from './vendors/index.js'
@@ -116,15 +117,15 @@ export class SubscriptionAdapter extends LlmAdapter {
116
117
  // ponytail: capture x-ratelimit headers without touching vendors
117
118
  const baseFetch = pickFetch(deps, account.ref)
118
119
  const quotaFetch = async (url, init) => {
119
- const res = await baseFetch(url, init)
120
+ const res = await fetchWithTimeout(baseFetch, url, init, { timeoutMs: 120000 })
120
121
  try {
121
122
  const snap = quotaSnapshot(provider, res.headers, null, Date.now())
122
123
  if (snap && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap)
123
- } catch {}
124
+ } catch (err) { /* ignore quota capture failure */ }
124
125
  if (!res.ok && res.clone) {
125
126
  try {
126
127
  const txt = await res.clone().text()
127
- let j=null; try{ j=JSON.parse(txt)}catch{}
128
+ let j=null; try { j = JSON.parse(txt) } catch (err) { j = null }
128
129
  if (j) {
129
130
  const b = parseBody(provider, j, Date.now())
130
131
  if (b) {
@@ -132,7 +133,7 @@ export class SubscriptionAdapter extends LlmAdapter {
132
133
  if (snap2) deps.rememberQuota(account.ref, snap2)
133
134
  }
134
135
  }
135
- } catch {}
136
+ } catch (err) { /* ignore quota capture failure */ }
136
137
  }
137
138
  return res
138
139
  }
@@ -162,7 +163,7 @@ export class SubscriptionAdapter extends LlmAdapter {
162
163
  ms: Date.now() - t0,
163
164
  kind: 'stream',
164
165
  })
165
- } catch {}
166
+ } catch (err) { /* ignore quota capture failure */ }
166
167
  }
167
168
  } catch (err) {
168
169
  if (err && err.code === 'VALIDATION_REQUIRED' && err.validationUrl) {
package/lib/client.js CHANGED
@@ -21,7 +21,7 @@ window.__ModuleLoader__.load({
21
21
  return { hasError: true, error }
22
22
  }
23
23
  componentDidCatch(error, info) {
24
- try { console.error("[dsh-subscriptions UI error]", error, info) } catch {}
24
+ try { console.error("[dsh-subscriptions UI error]", error, info) } catch (err) { /* fallback if console fails */ }
25
25
  }
26
26
  render() {
27
27
  if (this.state.hasError) {
@@ -63,7 +63,7 @@ window.__ModuleLoader__.load({
63
63
  '.dsub-helpBox{margin-top:8px;padding:12px 14px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:1.6}' +
64
64
  '.dsub-helpHead{display:flex;align-items:center;justify-content:space-between;font-weight:600;margin-bottom:8px;color:var(--dsw-alias-brand-primary)}' +
65
65
  '.dsub-helpStep{margin-bottom:6px;color:var(--dsw-alias-label-primary)}' +
66
- '.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:#fff;border-color:var(--dsw-alias-brand-primary)}' +
66
+ '.dsub-btnActive{background:var(--dsw-alias-brand-primary);color:var(--dsw-alias-text-contrast,#fff);border-color:var(--dsw-alias-brand-primary)}' +
67
67
  '.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
68
68
  '.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
69
69
  '.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}' +
@@ -77,7 +77,7 @@ window.__ModuleLoader__.load({
77
77
  '.dsub-inp{height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 8px;font-size:12px;min-width:180px}' +
78
78
  '.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}' +
79
79
  '.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
80
- '.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:#000;font-size:11px;font-weight:600}' +
80
+ '.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-bg-layer-1,#000);font-size:11px;font-weight:600}' +
81
81
  '.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
82
82
  '.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}' +
83
83
  '.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warning-primary)}' +
@@ -86,8 +86,8 @@ window.__ModuleLoader__.load({
86
86
  '.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}' +
87
87
  '.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
88
88
  '.dsub-verify a{color:var(--dsw-alias-brand-primary)}' +
89
- '.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;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:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.15),inset 0 1px 0 rgba(255,255,255,.06);transition:all .16s ease;user-select:none}' +
90
- '.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px rgba(0,0,0,.25),inset 0 1px 0 rgba(255,255,255,.1)}' +
89
+ '.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;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:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px color-mix(in srgb, black 15%, transparent),inset 0 1px 0 color-mix(in srgb, white 6%, transparent);transition:all .16s ease;user-select:none}' +
90
+ '.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px color-mix(in srgb, black 25%, transparent),inset 0 1px 0 color-mix(in srgb, white 10%, transparent)}' +
91
91
  '.dsub-pill:active{transform:translateY(0)}' +
92
92
  '.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)}' +
93
93
  '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
@@ -95,15 +95,15 @@ window.__ModuleLoader__.load({
95
95
  '.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)}' +
96
96
  '.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)}' +
97
97
  '.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
98
- '.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}' +
99
- '.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)}' +
98
+ '.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:color-mix(in srgb, black 65%, transparent);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
99
+ '.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 color-mix(in srgb, black 60%, transparent),0 0 0 1px color-mix(in srgb, white 7%, transparent);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
100
100
  '@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}' +
101
101
  '@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}' +
102
102
  '.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
103
103
  '.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}' +
104
104
  '.dsub-modalIcon{width:30px;height:30px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);display:grid;place-items:center;color:var(--dsw-alias-brand-primary);font-size:14px}' +
105
105
  '.dsub-modalBadge{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
106
- '.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 rgba(255,255,255,.05)}' +
106
+ '.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 color-mix(in srgb, white 5%, transparent)}' +
107
107
  '.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}' +
108
108
  '.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}' +
109
109
  '.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
@@ -113,9 +113,9 @@ window.__ModuleLoader__.load({
113
113
  '.dsub-summary:hover{color:var(--dsw-alias-label-primary)}' +
114
114
  '.dsub-header-bar{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:8px 0 12px;margin-bottom:8px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
115
115
  '.dsub-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500}' +
116
- '.dsub-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}' +
117
- '.dsub-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}' +
118
- '.dsub-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}' +
116
+ '.dsub-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent)}' +
117
+ '.dsub-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 10%,transparent)}' +
118
+ '.dsub-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}' +
119
119
  '.dsub-badge-neutral{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2)}' +
120
120
  '.dsub-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:14px 16px;display:flex;flex-direction:column;gap:10px;margin-top:12px}' +
121
121
  '.dsub-section-title{font-size:14.5px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}' +
@@ -124,8 +124,8 @@ window.__ModuleLoader__.load({
124
124
  '.dsub-stat-box{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:3px}' +
125
125
  '.dsub-stat-val{font-size:16.5px;font-weight:700;color:var(--dsw-alias-label-primary)}' +
126
126
  '.dsub-stat-lbl{font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
127
- '.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:rgba(16,185,129,0.09);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid rgba(16,185,129,0.25)}' +
128
- '.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:rgba(239,68,68,0.09);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid rgba(239,68,68,0.25)}' +
127
+ '.dsub-alert-ok{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 10%,transparent);color:var(--dsw-alias-state-success-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 25%,transparent)}' +
128
+ '.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12.5px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 25%,transparent)}' +
129
129
  '.dsub-btn-primary{background:var(--dsw-alias-label-primary) !important;color:var(--dsw-alias-bg-layer-3) !important;border-color:transparent !important;font-weight:600}' +
130
130
  '.dsub-btn-primary:hover:not(:disabled){opacity:0.9}' +
131
131
  '.dsub-model-grid{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}' +
@@ -158,14 +158,14 @@ window.__ModuleLoader__.load({
158
158
  '.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}' +
159
159
  '.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
160
160
  '.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}' +
161
- '.dsub-brandCodex{background:rgba(16,185,129,.15);color:#10b981;border:1px solid rgba(16,185,129,.3)}' +
162
- '.dsub-brandClaude{background:rgba(245,158,11,.15);color:#f59e0b;border:1px solid rgba(245,158,11,.3)}' +
163
- '.dsub-brandGrok{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3)}' +
164
- '.dsub-brandAgy{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3)}' +
165
- '.dsub-brandOllama{background:rgba(107,114,128,.15);color:#9ca3af;border:1px solid rgba(107,114,128,.3)}' +
166
- '.dsub-brandKimi{background:rgba(236,72,153,.15);color:#f472b6;border:1px solid rgba(236,72,153,.3)}' +
167
- '.dsub-brandGlm{background:rgba(20,184,166,.15);color:#2dd4bf;border:1px solid rgba(20,184,166,.3)}' +
168
- '.dsub-planBadge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;background:rgba(99,102,241,.15);color:#818cf8;border:1px solid rgba(99,102,241,.3);margin-left:6px}' +
161
+ '.dsub-brandCodex{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent)}' +
162
+ '.dsub-brandClaude{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-state-warning-primary) 30%,transparent)}' +
163
+ '.dsub-brandGrok{background:color-mix(in srgb,#a855f7 15%,transparent);color:#c084fc;border:1px solid color-mix(in srgb,#a855f7 30%,transparent)}' +
164
+ '.dsub-brandAgy{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent)}' +
165
+ '.dsub-brandOllama{background:color-mix(in srgb,var(--dsw-alias-label-tertiary) 15%,transparent);color:var(--dsw-alias-label-secondary);border:1px solid color-mix(in srgb,var(--dsw-alias-label-tertiary) 30%,transparent)}' +
166
+ '.dsub-brandKimi{background:color-mix(in srgb,#ec4899 15%,transparent);color:#f472b6;border:1px solid color-mix(in srgb,#ec4899 30%,transparent)}' +
167
+ '.dsub-brandGlm{background:color-mix(in srgb,#14b8a6 15%,transparent);color:#2dd4bf;border:1px solid color-mix(in srgb,#14b8a6 30%,transparent)}' +
168
+ '.dsub-planBadge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:4px;font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:0.5px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 15%,transparent);color:var(--dsw-alias-brand-primary);border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,transparent);margin-left:6px}' +
169
169
 
170
170
  '.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}' +
171
171
  '.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
@@ -176,7 +176,7 @@ window.__ModuleLoader__.load({
176
176
  '.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}' +
177
177
  '.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
178
178
  '.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)}' +
179
- '.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}' +
179
+ '.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:var(--dsw-alias-text-contrast,#fff);border:0;cursor:pointer;transition:opacity .15s}' +
180
180
  '.dsub-btnPrimary:hover{opacity:.9}' +
181
181
  '.dsub-btnSec{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;font-size:11.5px;font-weight:500;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;text-decoration:none;transition:all .15s}' +
182
182
  '.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}' +
@@ -481,7 +481,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
481
481
  "instructionsTitle": "How to obtain token",
482
482
  "close": "Close",
483
483
  "reconnectRequired": "Reconnect required",
484
- "manualTitle": "If the browser did not come back"
484
+ "manualTitle": "If the browser did not come back",
485
+ "updateChecking": "Checking updates…",
486
+ "updateUpToDate": "Up to date",
487
+ "updateAvailable": "Update available",
488
+ "updateNow": "Update",
489
+ "updating": "Updating…",
490
+ "updateSuccessRestart": "Update complete. Restarting service…",
491
+ "updateFailed": "Update failed"
485
492
  };
486
493
 
487
494
  const zh = {
@@ -643,7 +650,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
643
650
  "instructionsTitle": "获取令牌指引",
644
651
  "close": "关闭",
645
652
  "reconnectRequired": "需要重新连接",
646
- "manualTitle": "若浏览器未能自动跳转回调"
653
+ "manualTitle": "若浏览器未能自动跳转回调",
654
+ "updateChecking": "检查更新中…",
655
+ "updateUpToDate": "已是最新版本",
656
+ "updateAvailable": "有新版本可用",
657
+ "updateNow": "立即更新",
658
+ "updating": "正在更新…",
659
+ "updateSuccessRestart": "更新成功,服务重启中…",
660
+ "updateFailed": "更新失败"
647
661
  };
648
662
  // #51: live countdown to the quota window reset (account.quota.resetAt).
649
663
  function cleanErrorMessage(raw) {
@@ -656,7 +670,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
656
670
  if (parsed.error && parsed.error.message) text = parsed.error.message
657
671
  else if (parsed.message) text = parsed.message
658
672
  else if (parsed.code) text = parsed.code
659
- } catch (e) {}
673
+ } catch (e) { /* ignore parse error */ }
660
674
  }
661
675
  var firstLine = text.split('\n')[0].trim()
662
676
  if (firstLine.length > 160) return firstLine.slice(0, 157) + '...'
@@ -823,7 +837,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
823
837
  .then((d) => {
824
838
  const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
825
839
  setDiag(txt)
826
- try { navigator.clipboard.writeText(txt) } catch {}
840
+ try { navigator.clipboard.writeText(txt) } catch (err) { /* clipboard write disallowed */ }
827
841
  })
828
842
  .catch((e) => setDiag('error: ' + String(e && e.message || e)))
829
843
  }
@@ -831,6 +845,43 @@ const cssId = 'dsh-subscriptions/settings.module.css'
831
845
  const [hostPing, setHostPing] = React.useState({ ok: true, latencyMs: null })
832
846
  const [telemetry, setTelemetry] = React.useState(null)
833
847
  const [smoke, setSmoke] = React.useState({ testing: false, result: null })
848
+ const [updater, setUpdater] = React.useState({ checking: false, updating: false, status: null, message: '' })
849
+
850
+ const checkUpdater = () => {
851
+ setUpdater((u) => Object.assign({}, u, { checking: true, message: '' }))
852
+ return fetch('/dsh-subscriptions/update', { cache: 'no-store' })
853
+ .then((res) => res.json())
854
+ .then((data) => {
855
+ setUpdater((u) => Object.assign({}, u, { checking: false, status: data }))
856
+ return data
857
+ })
858
+ .catch((e) => {
859
+ setUpdater((u) => Object.assign({}, u, { checking: false, message: cleanErrorMessage(e.message || e) }))
860
+ })
861
+ }
862
+
863
+ const performUpdate = async () => {
864
+ setUpdater((u) => Object.assign({}, u, { updating: true, message: '' }))
865
+ try {
866
+ const res = await fetch('/dsh-subscriptions/update', {
867
+ method: 'POST',
868
+ headers: {
869
+ 'Content-Type': 'application/json',
870
+ 'x-dsh-plugin-update': '1',
871
+ },
872
+ })
873
+ const data = await res.json().catch(() => ({}))
874
+ if (!res.ok || !data.ok) {
875
+ throw new Error((data && data.error && (data.error.message || data.error.code)) || ('HTTP ' + res.status))
876
+ }
877
+ setUpdater((u) => Object.assign({}, u, { updating: false, message: t('updateSuccessRestart') }))
878
+ setTimeout(() => {
879
+ window.location.reload()
880
+ }, 3500)
881
+ } catch (e) {
882
+ setUpdater((u) => Object.assign({}, u, { updating: false, message: (t('updateFailed') + ': ' + cleanErrorMessage(e.message || e)) }))
883
+ }
884
+ }
834
885
 
835
886
  const reloadTelemetry = () => {
836
887
  fetch('/dsh-subscriptions/telemetry', { cache: 'no-store' })
@@ -886,6 +937,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
886
937
  React.useEffect(() => {
887
938
  let alive = true
888
939
  reload().catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
940
+ checkUpdater().catch(() => {})
889
941
  return () => { alive = false }
890
942
  }, [])
891
943
 
@@ -1107,14 +1159,38 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1107
1159
  React.createElement('span', {
1108
1160
  className: 'dsub-badge dsub-badge-neutral'
1109
1161
  }, 'Pool: ' + accounts.length + ' accounts'),
1162
+ updater.status && updater.status.updateAvailable
1163
+ ? React.createElement('span', { className: 'dsub-badge dsub-badge-warn' },
1164
+ '⬆ ' + t('updateAvailable') + ' (v' + (updater.status.latestVersion || '') + ')'
1165
+ )
1166
+ : (updater.status && updater.status.currentVersion
1167
+ ? React.createElement('span', { className: 'dsub-badge dsub-badge-neutral' },
1168
+ 'v' + updater.status.currentVersion
1169
+ )
1170
+ : null
1171
+ ),
1172
+ ),
1173
+ React.createElement('div', { className: 'dsub-row', style: { marginTop: 0, gap: '8px' } },
1174
+ updater.status && updater.status.updateAvailable && updater.status.canAutoUpdate
1175
+ ? React.createElement('button', {
1176
+ type: 'button',
1177
+ className: 'dsub-mini dsub-btn-primary',
1178
+ disabled: updater.updating,
1179
+ onClick: () => performUpdate(),
1180
+ }, updater.updating ? t('updating') : (t('updateNow') + ' → v' + updater.status.latestVersion))
1181
+ : null,
1182
+ React.createElement('button', {
1183
+ type: 'button',
1184
+ className: 'dsub-mini',
1185
+ disabled: smoke.testing,
1186
+ onClick: () => runSmokeTest(),
1187
+ }, smoke.testing ? t('smokeTesting') : t('runSmoke')),
1110
1188
  ),
1111
- React.createElement('button', {
1112
- type: 'button',
1113
- className: 'dsub-mini dsub-btn-primary',
1114
- disabled: smoke.testing,
1115
- onClick: () => runSmokeTest(),
1116
- }, smoke.testing ? t('smokeTesting') : t('runSmoke')),
1117
1189
  ),
1190
+ updater.message ? React.createElement('div', {
1191
+ className: updater.message.includes(t('updateSuccessRestart')) ? 'dsub-alert-ok' : 'dsub-alert-bad',
1192
+ style: { marginBottom: '12px' }
1193
+ }, updater.message) : null,
1118
1194
  smoke.result ? React.createElement('div', {
1119
1195
  className: smoke.result.ok ? 'dsub-alert-ok' : 'dsub-alert-bad',
1120
1196
  style: { marginBottom: '12px' }
@@ -1343,7 +1419,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1343
1419
  diag ? React.createElement('div', { className: 'dsub-row' },
1344
1420
  React.createElement('button', {
1345
1421
  type: 'button', className: 'dsub-mini',
1346
- onClick: () => { try { navigator.clipboard.writeText(diag) } catch {} },
1422
+ onClick: () => { try { navigator.clipboard.writeText(diag) } catch (err) { /* clipboard write error */ } },
1347
1423
  }, t('diagCopy')),
1348
1424
  React.createElement('pre', { className: 'dsub-diag' }, diag),
1349
1425
  ) : null,
@@ -1512,7 +1588,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1512
1588
  React.createElement('div', { className: 'dsub-row' },
1513
1589
  React.createElement('button', {
1514
1590
  type: 'button', className: 'dsub-mini',
1515
- onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch {} },
1591
+ onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch (err) { /* clipboard write error */ } },
1516
1592
  }, t('deviceCopy')),
1517
1593
  React.createElement('a', { href: d.authUrl, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
1518
1594
  ),
@@ -1989,7 +2065,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1989
2065
  className: 'dsub-barFillGrad',
1990
2066
  style: {
1991
2067
  width: Math.min(100, Math.max(0, secPct)) + '%',
1992
- background: secPct >= 90 ? '#ef4444' : (secPct >= 50 ? '#f59e0b' : '#3b82f6'),
2068
+ background: secPct >= 90 ? 'var(--dsw-alias-state-error-primary)' : (secPct >= 50 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-brand-primary)'),
1993
2069
  },
1994
2070
  }),
1995
2071
  ),
@@ -2038,7 +2114,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2038
2114
  className: 'dsub-barFillGrad',
2039
2115
  style: {
2040
2116
  width: Math.min(100, Math.max(0, pct)) + '%',
2041
- background: pct >= 90 ? '#ef4444' : (pct >= 50 ? '#f59e0b' : '#10b981'),
2117
+ background: pct >= 90 ? 'var(--dsw-alias-state-error-primary)' : (pct >= 50 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)'),
2042
2118
  },
2043
2119
  }),
2044
2120
  ) : null,
@@ -2101,7 +2177,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2101
2177
  try {
2102
2178
  const btn = Array.from(document.querySelectorAll('button')).find((b) => (b.textContent || '').includes(t('title')))
2103
2179
  if (btn) btn.click()
2104
- } catch {}
2180
+ } catch (err) { /* best-effort */ }
2105
2181
  },
2106
2182
  }, t('subsOpenSettings')),
2107
2183
  ),
package/lib/http.js CHANGED
@@ -30,6 +30,13 @@ export function readBody(req, maxBytes = 64 * 1024) {
30
30
  })
31
31
  }
32
32
 
33
+ export function isLoopback(value) {
34
+ const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
35
+ return address === 'localhost' || address === 'localhost.' || address === '::1'
36
+ || address?.startsWith('127.') === true
37
+ || address?.startsWith('::ffff:127.') === true
38
+ }
39
+
33
40
  /**
34
41
  * @param {import('node:http').IncomingMessage} request
35
42
  * @returns {boolean}
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { bestEffort } from './utils.js'
1
2
  export { Config, publicConfig, Slot, defaultSlots } from './config-schema.js'
2
3
  import { Config, publicConfig } from './config-schema.js'
3
4
  import { registerRoutes } from './routes.js'
@@ -37,7 +38,7 @@ export const inject = ['llm', 'credentials', 'webServer', 'settings']
37
38
  const NS = 'dsh-subscriptions'
38
39
 
39
40
  let pkgVersion = ''
40
- try { pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '' } catch {}
41
+ try { pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '' } catch (err) { /* ignore package read errors at startup */ }
41
42
  const PENDING_TTL_MS = 15 * 60 * 1000
42
43
 
43
44
  function redirectFor(provider, cfg, origin) {
@@ -49,6 +50,7 @@ function redirectFor(provider, cfg, origin) {
49
50
  const OK_HTML = '<!doctype html><meta charset="utf-8"><title>Subscriptions</title><p>Signed in. You can close this tab and return to Settings.</p>'
50
51
 
51
52
  export function apply(ctx, config) {
53
+ const logger = ctx.logger ? ctx.logger('subscriptions') : console
52
54
  let getConfig = () => config
53
55
  let settingsApi
54
56
  ctx.inject(['settings'], (sctx) => {
@@ -68,7 +70,7 @@ export function apply(ctx, config) {
68
70
  try {
69
71
  profiles = (live().customVendors || []).map(validateProfile)
70
72
  } catch (e) {
71
- try { ctx.log && ctx.log.warn && ctx.log.warn('[dsh-subscriptions] customVendors: ' + String(e && e.message || e)) } catch {}
73
+ if (logger && logger.warn) logger.warn('[dsh-subscriptions] customVendors: ' + String(e && e.message || e))
72
74
  return
73
75
  }
74
76
  clearCustomVendors()
@@ -79,7 +81,7 @@ export function apply(ctx, config) {
79
81
  registerCustomProviderIds([profile.id])
80
82
  if (profile.displayName) registerDisplayName(profile.id, profile.displayName)
81
83
  } catch (e) {
82
- try { ctx.log && ctx.log.warn && ctx.log.warn('[dsh-subscriptions] customVendors[' + profile.id + ']: ' + String(e && e.message || e)) } catch {}
84
+ if (logger && logger.warn) logger.warn('[dsh-subscriptions] customVendors[' + profile.id + ']: ' + String(e && e.message || e))
83
85
  }
84
86
  }
85
87
  }
@@ -103,9 +105,9 @@ export function apply(ctx, config) {
103
105
  fetchForRef,
104
106
  onLimitNotice: (provider, ref, win, threshold) => {
105
107
  if (!live().notifyLimits) return
106
- const label = win.ru || win.en || win.id
107
- try { ctx.log && ctx.log.warn && ctx.log.warn(`[dsh-subscriptions] ${provider} ${ref}: limit ${label} at ${threshold}%`) } catch {}
108
- try { ctx.emit && ctx.emit('subscriptions.limit-notice', { provider, ref, window: win.id, usedPercent: win.usedPercent, threshold }) } catch {}
108
+ const label = win.en || win.ru || win.id
109
+ if (logger && logger.warn) logger.warn(`[dsh-subscriptions] ${provider} ${ref}: limit ${label} at ${threshold}%`)
110
+ bestEffort('emit limit-notice', () => ctx.emit && ctx.emit('subscriptions.limit-notice', { provider, ref, window: win.id, usedPercent: win.usedPercent, threshold }), undefined, logger)
109
111
  },
110
112
  })
111
113
  const history = new HistoryStore()
@@ -140,8 +142,8 @@ export function apply(ctx, config) {
140
142
  const models = await ollamaModels(ollamaBase(cfg), fetch).catch(() => [])
141
143
  if (!cfg.ollamaFallback || !models.length) throw err
142
144
  const model = cfg.ollamaFallbackModel || models[0].id
143
- try { ctx.emit && ctx.emit('subscriptions.ollama-fallback', { provider, model, reason: err && err.code || 'EXHAUSTED' }) } catch {}
144
- try { ctx.log && ctx.log.warn && ctx.log.warn(`[dsh-subscriptions] ${provider}: all accounts exhausted (${err && err.code || 'EXHAUSTED'}), falling back to ollama/${model}`) } catch {}
145
+ bestEffort('emit ollama-fallback', () => ctx.emit && ctx.emit('subscriptions.ollama-fallback', { provider, model, reason: err && err.code || 'EXHAUSTED' }), undefined, logger)
146
+ if (logger && logger.warn) logger.warn(`[dsh-subscriptions] ${provider}: all accounts exhausted (${err && err.code || 'EXHAUSTED'}), falling back to ollama/${model}`)
145
147
  try {
146
148
  recordHistory({
147
149
  provider: 'ollama',
@@ -152,7 +154,7 @@ export function apply(ctx, config) {
152
154
  status: 200,
153
155
  kind: 'fallback',
154
156
  })
155
- } catch {}
157
+ } catch (err) { if (logger && logger.debug) logger.debug('[history] fallback record failed: ' + (err && err.message || err)) }
156
158
  yield* ollamaAdapter.stream({ ...options, provider: 'ollama', model })
157
159
  }
158
160
 
@@ -277,7 +279,7 @@ export function apply(ctx, config) {
277
279
  const fresh = await store.ensureFresh(provider, blob, slot.ref)
278
280
  const cfg = vendorConfig(provider, live())
279
281
  await getVendor(provider).listModels(fresh, cfg, fetch).catch(() => {})
280
- } catch {}
282
+ } catch (err) { /* best-effort */ }
281
283
  }
282
284
  }
283
285
 
@@ -398,7 +400,7 @@ export function apply(ctx, config) {
398
400
  if (info.configured) pv.configured++
399
401
  if (info.cooldownUntil && info.cooldownUntil > Date.now()) pv.cooldown++
400
402
  if (info.usagePercent != null) pv.maxUsagePercent = Math.max(pv.maxUsagePercent || 0, Math.round(info.usagePercent))
401
- } catch {}
403
+ } catch (err) { /* best-effort */ }
402
404
  }
403
405
  for (const p of Object.keys(providers)) providers[p].loggedIn = logged.includes(p)
404
406
  const rows = history.all()
@@ -478,7 +480,7 @@ export function apply(ctx, config) {
478
480
  for (const slot of normalizeSlots(live().slots)) {
479
481
  store.refreshUsage(slot.provider).catch(() => {})
480
482
  }
481
- } catch {}
483
+ } catch (err) { /* best-effort */ }
482
484
  }
483
485
  eager()
484
486
  return () => {
@@ -509,9 +511,9 @@ export function apply(ctx, config) {
509
511
  if (blob.expiresAt - now > ahead) continue
510
512
  if (typeof store.shouldSkipRefresh === 'function' && store.shouldSkipRefresh(ref, now, retryMs)) continue
511
513
  await store.ensureFresh(slot.provider, blob, ref).catch((e) => {
512
- try { ctx.log && ctx.log.warn && ctx.log.warn("[dsh-subscriptions] background refresh failed for " + ref + ": " + String(e && e.message || e)) } catch {}
514
+ if (logger && logger.warn) logger.warn("[dsh-subscriptions] background refresh failed for " + ref + ": " + String(e && e.message || e))
513
515
  })
514
- } catch {}
516
+ } catch (err) { /* best-effort */ }
515
517
  }
516
518
  }
517
519
  tick().catch(() => {})
@@ -546,9 +548,9 @@ export function apply(ctx, config) {
546
548
  const cfg2 = vendorConfig(slot.provider, live())
547
549
  const probeFetch = async (u, i) => fetch(u, i)
548
550
  await vendor.check(fresh, cfg2, probeFetch).catch((e) => {
549
- try { ctx.log && ctx.log.warn && ctx.log.warn("[dsh-subscriptions] probe " + ref + ": " + String(e && e.message || e).slice(0, 200)) } catch {}
551
+ if (logger && logger.warn) logger.warn("[dsh-subscriptions] probe " + ref + ": " + String(e && e.message || e).slice(0, 200))
550
552
  })
551
- } catch {}
553
+ } catch (err) { /* best-effort */ }
552
554
  }
553
555
  }
554
556
  let lastProbeAt = 0