@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 +23 -0
- package/README.ru.md +23 -0
- package/README.zh.md +23 -0
- package/lib/accounts.js +4 -19
- package/lib/adapter.js +6 -5
- package/lib/client.js +113 -37
- package/lib/http.js +7 -0
- package/lib/index.js +18 -16
- package/lib/mask.js +1 -1
- package/lib/relative-time.js +0 -8
- package/lib/routes/proxy.js +1 -1
- package/lib/routes/status.js +22 -17
- package/lib/routes.js +9 -2
- package/lib/subscriptions.js +5 -5
- package/lib/updater.js +260 -0
- package/lib/usage.js +14 -14
- package/lib/utils.js +25 -0
- package/lib/vendor-factory.js +3 -2
- package/lib/vendors/grok.js +1 -1
- package/lib/wire.js +3 -1
- package/package.json +10 -7
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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:
|
|
117
|
-
'.dsub-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:
|
|
118
|
-
'.dsub-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:
|
|
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:
|
|
128
|
-
'.dsub-alert-bad{padding:8px 12px;border-radius:8px;background:
|
|
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:
|
|
162
|
-
'.dsub-brandClaude{background:
|
|
163
|
-
'.dsub-brandGrok{background:
|
|
164
|
-
'.dsub-brandAgy{background:
|
|
165
|
-
'.dsub-brandOllama{background:
|
|
166
|
-
'.dsub-brandKimi{background:
|
|
167
|
-
'.dsub-brandGlm{background:
|
|
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:
|
|
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
|
|
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 ? '
|
|
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 ? '
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|