@goodandready/dsh-subscriptions 0.6.9 → 0.6.11

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
@@ -270,6 +270,27 @@ Supports Anthropic's adaptive thinking (`thinking: { type: "adaptive" }`) and re
270
270
 
271
271
  ---
272
272
 
273
+ ---
274
+
275
+ ## 🔒 OAuth Client Validation & Route Updates (Added in v0.6.11)
276
+
277
+ - **OAuth Client ID Guard (GitHub #2)**: uildAuthorizeUrl and ntigravity.authorizeUrl now strictly require a non-empty clientId. If unconfigured, the endpoint returns an explicit HTTP 400 (missing_client_id) and the UI prompts the user to configure ntigravityClientId in plugin settings or use "From CLI" instead of directing the browser to a failing Google OAuth page.
278
+ - **Config Schema Secret Key**: Added ntigravityClientSecret to the plugin configuration schema, allowing convenient entry of private client secrets alongside ntigravityClientId with automatic masking in public endpoints.
279
+ - **Zhipu GLM Console Route Migration (GitHub #3)**: Updated the Zhipu GLM authorization and API key center link to the active console route (https://bigmodel.cn/usercenter/proj-mgmt/apikeys), eliminating 404 navigation errors caused by vendor console restructuring.
280
+
281
+ ---
282
+
283
+ ## 🛡️ Reliability & Quality Hardening (Added in v0.6.10)
284
+
285
+ - **Structured Error Handling**: Eliminated all empty catch blocks across the plugin runtime via safe `bestEffort` helper logging debug diagnostics.
286
+ - **Native Context Logger**: Migrated all plugin logging to the canonical Cordis `ctx.logger('subscriptions')` subsystem.
287
+ - **Network Timeout Protection**: Hardened external vendor API calls (`listModels`, `fetchFor`, `quotaFetch`, `jsonTokenRequest`) with explicit `fetchWithTimeout` and `AbortSignal.timeout` safeguards.
288
+ - **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.
289
+ - **Client Dependency Injections**: Explicitly declared required client platform dependencies (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`) in package manifest.
290
+ - **Strict Repository Hygiene**: Removed internal development instructions and planning artifacts from git tracking, enforcing clean public releases.
291
+
292
+ ---
293
+
273
294
  ## 🌐 Localization
274
295
 
275
296
  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
@@ -243,6 +243,27 @@ dsh-subscriptions:
243
243
 
244
244
  ---
245
245
 
246
+ ---
247
+
248
+ ## 🔒 Защита OAuth Client ID и обновление ссылок (Добавлено в v0.6.11)
249
+
250
+ - **Защита OAuth Client ID (GitHub #2)**: uildAuthorizeUrl и ntigravity.authorizeUrl теперь строго требуют непустой clientId. Если параметр не задан, сервер возвращает явный код ошибки HTTP 400 (missing_client_id), а интерфейс выводит подсказку с предложением указать ntigravityClientId в настройках или нажать «📥 Из CLI», предотвращая отправку битых запросов в Google OAuth.
251
+ - **Секретный ключ в схеме настроек**: В схему Config добавлено поле ntigravityClientSecret для удобного ввода Client Secret рядом с ntigravityClientId с автоматическим маскированием в публичном API.
252
+ - **Актуализация маршрута Zhipu GLM (GitHub #3)**: Ссылка на консоль API-ключей Zhipu GLM обновлена на актуальный адрес (https://bigmodel.cn/usercenter/proj-mgmt/apikeys), устраняя ошибку 404 («页面跑丢啦!!»), вызванную миграцией маршрутов вендора.
253
+
254
+ ---
255
+
256
+ ## 🛡️ Надежность и стандарты качества (Добавлено в v0.6.10)
257
+
258
+ - **Структурированная обработка ошибок**: Устранены все пустые блоки catch в рантайме плагина с переходом на безопасный хелпер `bestEffort` с диагностическим логированием.
259
+ - **Нативный логгер контекста**: Все вызовы логирования переведены на официальную подсистему Cordis `ctx.logger('subscriptions')`.
260
+ - **Защита внешних вызовов таймаутами**: Все сетевые интеграции к API вендоров (`listModels`, `fetchFor`, `quotaFetch`, `jsonTokenRequest`) защищены таймаутами через `fetchWithTimeout` и `AbortSignal.timeout`.
261
+ - **Семантическая UI-темизация**: Статические цвета и rgba-значения в `client.js` заменены на нативные CSS-переменные дизайн-системы DeepSeek Harness (`--dsw-alias-*`, `color-mix`), достигнут 0 остаточных `rgba`.
262
+ - **Декларация клиентских зависимостей**: Задекларированы платформенные инжекты (`@deepseek-ai/dsh-client-locale`, `@deepseek-ai/dsh-client-ui-slots`) в манифесте `package.json`.
263
+ - **Чистота публичного репозитория**: Внутренние инструкции разработки и артефакты планирования исключены из git-отслеживания и добавлены в `.gitignore`.
264
+
265
+ ---
266
+
246
267
  ## 🌐 Локализация
247
268
 
248
269
  Исходный язык плагина - только английский. Русский и другие переводы во время работы предоставляют отдельные языковые плагины (например, плагин русификации), переводящие зарегистрированные ключи локали, - сам пакет не содержит встроенных переводов (Changed in 0.6.1).
package/README.zh.md CHANGED
@@ -165,6 +165,27 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
165
165
 
166
166
  ---
167
167
 
168
+ ---
169
+
170
+ ## 🔒 OAuth Client ID 校验保护与控制台地址更新 (v0.6.11 新增)
171
+
172
+ - **Google OAuth Client ID 校验保护 (GitHub #2)**:uildAuthorizeUrl 和 ntigravity.authorizeUrl 严格要求非空 clientId。未配置时服务端返回明确的 HTTP 400 (missing_client_id),前端界面提示用户在插件设置中填写 ntigravityClientId 或使用“📥 从 CLI 导入”,避免浏览器跳转至 Google OAuth 400 报错页面。
173
+ - **配置架构新增密钥项**:在插件 Config 架构中新增 ntigravityClientSecret,方便用户在设置中配置客户端密钥,并在公共接口中自动脱敏脱密。
174
+ - **智谱 GLM 控制台地址迁移 (GitHub #3)**:将智谱 GLM 授权与 API Key 获取链接更新为最新控制台有效路由 (https://bigmodel.cn/usercenter/proj-mgmt/apikeys),彻底解决因服务商控制台路由调整导致的 404 错误。
175
+
176
+ ---
177
+
178
+ ## 🛡️ 稳定性与代码质量强化 (v0.6.10 新增)
179
+
180
+ - **结构化异常降级**: 全面移除运行时空 catch 块,统一采用带诊断日志的 `bestEffort` 安全兜底机制。
181
+ - **原生上下文日志**: 所有日志统一接入 Cordis 规范子系统 `ctx.logger('subscriptions')`。
182
+ - **外部网络超时保护**: 为所有模型商 API 网络调用(`listModels`、`fetchFor`、`quotaFetch`、`jsonTokenRequest`)建立 `fetchWithTimeout` 与 `AbortSignal.timeout` 超时熔断保护。
183
+ - **设计系统主题对齐**: 将 `client.js` 中的静态样式硬编码替换为 DeepSeek Harness 原生 CSS 主题令牌(`--dsw-alias-*`、`color-mix`),彻底消除独立的 `rgba` 颜色。
184
+ - **客户端依赖注入显式化**: 在 `package.json` 清单中显式声明前端所需平台注入项(`@deepseek-ai/dsh-client-locale` 与 `@deepseek-ai/dsh-client-ui-slots`)。
185
+ - **公开仓库卫生规范**: 将内部研发指令与计划文件移出 Git 跟踪并加入 `.gitignore`,确保公开版本干净无私有残余。
186
+
187
+ ---
188
+
168
189
  ## 🌐 本地化
169
190
 
170
191
  插件源语言仅为英语。俄语及其他翻译由独立的语言插件(如俄化插件)在运行时对注册的本地化键进行翻译,包本身不内置翻译(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,7 +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 {}
267
+ try { onLimitNotice && onLimitNotice(slot.provider, slot.ref, win, th) } catch (err) { /* ignore notification failure */ }
266
268
  break
267
269
  }
268
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)}' +
@@ -292,7 +292,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
292
292
  "3. 粘贴到输入框中并点击 \"保存令牌\"。",
293
293
  "4. 若已安装 ZCode CLI (~/.zcode),点击 \"📥 从 CLI 导入\" 即可自动导入。",
294
294
  ],
295
- link: "https://open.bigmodel.cn/usercenter/apikeys",
295
+ link: "https://bigmodel.cn/usercenter/proj-mgmt/apikeys",
296
296
  },
297
297
  grok: {
298
298
  title: "xAI Grok",
@@ -434,6 +434,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
434
434
  "fcMinutes": "m",
435
435
  "reconnect": "Reconnect",
436
436
  "connect": "Connect",
437
+ "missingClientIdAntigravity": "Google OAuth requires a custom Client ID. Please configure antigravityClientId in plugin settings below, or click '📥 From CLI' to import active credentials.",
437
438
  "disconnect": "Disconnect",
438
439
  "removeSlot": "Remove slot",
439
440
  "pastePlaceholder": "Paste redirected URL or code",
@@ -603,6 +604,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
603
604
  "fcMinutes": "分",
604
605
  "reconnect": "重新连接",
605
606
  "connect": "连接",
607
+ "missingClientIdAntigravity": "Google OAuth 需要自定义 Client ID。请在下方设置中配置 antigravityClientId,或点击‘📥 从 CLI 导入’直接导入本地会话。",
606
608
  "disconnect": "断开连接",
607
609
  "removeSlot": "移除槽位",
608
610
  "pastePlaceholder": "粘贴重定向 URL 或授权代码",
@@ -670,7 +672,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
670
672
  if (parsed.error && parsed.error.message) text = parsed.error.message
671
673
  else if (parsed.message) text = parsed.message
672
674
  else if (parsed.code) text = parsed.code
673
- } catch (e) {}
675
+ } catch (e) { /* ignore parse error */ }
674
676
  }
675
677
  var firstLine = text.split('\n')[0].trim()
676
678
  if (firstLine.length > 160) return firstLine.slice(0, 157) + '...'
@@ -837,7 +839,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
837
839
  .then((d) => {
838
840
  const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
839
841
  setDiag(txt)
840
- try { navigator.clipboard.writeText(txt) } catch {}
842
+ try { navigator.clipboard.writeText(txt) } catch (err) { /* clipboard write disallowed */ }
841
843
  })
842
844
  .catch((e) => setDiag('error: ' + String(e && e.message || e)))
843
845
  }
@@ -985,6 +987,10 @@ const cssId = 'dsh-subscriptions/settings.module.css'
985
987
 
986
988
  const connect = async (provider, index) => {
987
989
  setErr('')
990
+ if (provider === 'antigravity' && (!draft || !draft.antigravityClientId || !String(draft.antigravityClientId).trim())) {
991
+ setErr(t('missingClientIdAntigravity'))
992
+ return
993
+ }
988
994
  const res = await fetch('/dsh-subscriptions/oauth/start?provider=' + encodeURIComponent(provider) + '&index=' + encodeURIComponent(index), { cache: 'no-store' })
989
995
  const data = await res.json().catch(() => ({}))
990
996
  if (!res.ok) throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
@@ -1419,7 +1425,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1419
1425
  diag ? React.createElement('div', { className: 'dsub-row' },
1420
1426
  React.createElement('button', {
1421
1427
  type: 'button', className: 'dsub-mini',
1422
- onClick: () => { try { navigator.clipboard.writeText(diag) } catch {} },
1428
+ onClick: () => { try { navigator.clipboard.writeText(diag) } catch (err) { /* clipboard write error */ } },
1423
1429
  }, t('diagCopy')),
1424
1430
  React.createElement('pre', { className: 'dsub-diag' }, diag),
1425
1431
  ) : null,
@@ -1588,7 +1594,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1588
1594
  React.createElement('div', { className: 'dsub-row' },
1589
1595
  React.createElement('button', {
1590
1596
  type: 'button', className: 'dsub-mini',
1591
- onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch {} },
1597
+ onClick: () => { try { navigator.clipboard.writeText(d.userCode) } catch (err) { /* clipboard write error */ } },
1592
1598
  }, t('deviceCopy')),
1593
1599
  React.createElement('a', { href: d.authUrl, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
1594
1600
  ),
@@ -2065,7 +2071,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2065
2071
  className: 'dsub-barFillGrad',
2066
2072
  style: {
2067
2073
  width: Math.min(100, Math.max(0, secPct)) + '%',
2068
- background: secPct >= 90 ? '#ef4444' : (secPct >= 50 ? '#f59e0b' : '#3b82f6'),
2074
+ background: secPct >= 90 ? 'var(--dsw-alias-state-error-primary)' : (secPct >= 50 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-brand-primary)'),
2069
2075
  },
2070
2076
  }),
2071
2077
  ),
@@ -2114,7 +2120,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2114
2120
  className: 'dsub-barFillGrad',
2115
2121
  style: {
2116
2122
  width: Math.min(100, Math.max(0, pct)) + '%',
2117
- background: pct >= 90 ? '#ef4444' : (pct >= 50 ? '#f59e0b' : '#10b981'),
2123
+ background: pct >= 90 ? 'var(--dsw-alias-state-error-primary)' : (pct >= 50 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)'),
2118
2124
  },
2119
2125
  }),
2120
2126
  ) : null,
@@ -2177,7 +2183,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
2177
2183
  try {
2178
2184
  const btn = Array.from(document.querySelectorAll('button')).find((b) => (b.textContent || '').includes(t('title')))
2179
2185
  if (btn) btn.click()
2180
- } catch {}
2186
+ } catch (err) { /* best-effort */ }
2181
2187
  },
2182
2188
  }, t('subsOpenSettings')),
2183
2189
  ),
@@ -64,6 +64,8 @@ export const Config = z.object({
64
64
  grokClientVersion: z.string().default('')
65
65
  .description('Grok CLI identity version header. Empty uses the built-in default.'),
66
66
  antigravityClientId: z.string().default(''),
67
+ antigravityClientSecret: z.string().default('')
68
+ .description('Google Cloud OAuth Client Secret for Antigravity.'),
67
69
  antigravityRedirectUri: z.string().default(''),
68
70
  customVendors: z.array(z.any()).default([])
69
71
  .description('Declarative OpenAI-Responses-compatible providers. See README.'),
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
package/lib/mask.js CHANGED
@@ -30,7 +30,7 @@ export function cleanErrorMessage(raw) {
30
30
  if (parsed.error && parsed.error.message) text = parsed.error.message
31
31
  else if (parsed.message) text = parsed.message
32
32
  else if (parsed.code) text = parsed.code
33
- } catch {}
33
+ } catch (err) { /* ignore JSON parse failure */ }
34
34
  }
35
35
  const firstLine = text.split('\n')[0].trim()
36
36
  if (firstLine.length > 160) {
package/lib/oauth.js CHANGED
@@ -7,6 +7,9 @@ export function buildAuthorizeUrl({
7
7
  extra,
8
8
  scope,
9
9
  }) {
10
+ if (!clientId || typeof clientId !== 'string' || !clientId.trim()) {
11
+ throw new Error('Missing required parameter: clientId')
12
+ }
10
13
  const url = new URL(authUrl)
11
14
  url.searchParams.set('response_type', 'code')
12
15
  url.searchParams.set('client_id', clientId)
@@ -20,14 +20,6 @@ export const RELATIVE_UNITS = {
20
20
  hour: '{n}小时',
21
21
  day: '{n}天',
22
22
  },
23
- ru: {
24
- soon: 'только что',
25
- prefix: 'через ',
26
- suffix: '',
27
- minute: '{n} мин',
28
- hour: '{n} ч',
29
- day: '{n} дн',
30
- },
31
23
  }
32
24
 
33
25
  function fill(template, n) {
@@ -51,7 +51,20 @@ export function registerOauthRoutes(ctx, state) {
51
51
  createdAt: Date.now(),
52
52
  })
53
53
  const cfg = { ...vendorConfig(provider, live()), redirectUri }
54
- const url = getVendor(provider).authorizeUrl(cfg, pkce)
54
+ let url
55
+ try {
56
+ url = getVendor(provider).authorizeUrl(cfg, pkce)
57
+ } catch (err) {
58
+ pending.delete(pkce.state)
59
+ writeJson(res, 400, {
60
+ ok: false,
61
+ error: {
62
+ code: 'missing_client_id',
63
+ message: err.message || 'OAuth authorization configuration error',
64
+ },
65
+ })
66
+ return
67
+ }
55
68
  // #89: if redirect_uri is loopback - start a temporary catch server.
56
69
  let autoCatch = false
57
70
  if (live().autoLoopback) {
@@ -21,7 +21,7 @@ export function registerProxyRoutes(ctx, state) {
21
21
  }
22
22
  const raw = await readBody(req, 128 * 1024).catch(() => Buffer.alloc(0))
23
23
  let body = {}
24
- try { body = JSON.parse(raw.toString('utf8') || '{}') } catch {}
24
+ try { body = JSON.parse(raw.toString('utf8') || '{}') } catch (err) { body = {} }
25
25
  const events = Array.isArray(body && body.events) ? body.events : []
26
26
  const analysis = analyzeSessionEvents(events)
27
27
  writeJson(res, 200, { ok: true, analysis })
@@ -95,7 +95,7 @@ export function registerStatusRoutes(ctx, state) {
95
95
  expires[slot.provider] = Math.max(expires[slot.provider] || 0, slot.expiresAt)
96
96
  labels[slot.provider] = pmL(slot.label || info.label || slot.provider)
97
97
  }
98
- } catch {}
98
+ } catch (err) { /* ignore status inspection error */ }
99
99
  }
100
100
  // #69/#72: active subscription = last successful request (newest first).
101
101
  let active = null
@@ -118,7 +118,7 @@ export function registerStatusRoutes(ctx, state) {
118
118
  .filter((w) => w && typeof w.usedPercent === 'number')
119
119
  .map((w) => ({ id: w.id || w.en || w.ru, label: w.en || w.ru || w.id, usedPercent: w.usedPercent }))
120
120
  }
121
- } catch {}
121
+ } catch (err) { /* ignore status inspection error */ }
122
122
  active = {
123
123
  provider: lastRow.provider,
124
124
  index,
@@ -217,7 +217,7 @@ export function registerStatusRoutes(ctx, state) {
217
217
  try {
218
218
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
219
219
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
220
- } catch {}
220
+ } catch (err) { /* ignore status inspection error */ }
221
221
  return res2
222
222
  }
223
223
  try {
@@ -283,7 +283,7 @@ export function registerStatusRoutes(ctx, state) {
283
283
  return
284
284
  }
285
285
  let payload = {}
286
- try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch {}
286
+ try { payload = JSON.parse((await readBody(req, 64 * 1024)).toString('utf8') || '{}') } catch (err) { /* ignore status inspection error */ }
287
287
 
288
288
  let provider = payload.provider
289
289
  let index = payload.index || 1
@@ -331,7 +331,7 @@ export function registerStatusRoutes(ctx, state) {
331
331
  try {
332
332
  const snap = quotaSnapshot(provider, res2.headers, null, Date.now())
333
333
  if (snap) { capturedQuota = snap; store.rememberQuota(ref, snap) }
334
- } catch {}
334
+ } catch (err) { /* ignore status inspection error */ }
335
335
  return res2
336
336
  }
337
337
  try {
@@ -20,7 +20,7 @@ export function isAllowed(provider, path) {
20
20
  p = p.split("?")[0].split("#")[0]
21
21
  try {
22
22
  if (p.startsWith("http://") || p.startsWith("https://")) p = new URL(p).pathname
23
- } catch {}
23
+ } catch (err) { /* ignore subscriptions error */ }
24
24
  if (!p.startsWith("/")) p = "/" + p
25
25
  for (const allowed of list) {
26
26
  const a = allowed.split("?")[0]
@@ -140,17 +140,17 @@ export function createSubscriptionsService(deps) {
140
140
  try {
141
141
  const snap = quotaSnapshot(provider, res.headers, null, Date.now())
142
142
  if (snap && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap)
143
- } catch {}
143
+ } catch (err) { /* ignore subscriptions error */ }
144
144
  if (!res.ok && res.clone) {
145
145
  try {
146
146
  const txt = await res.clone().text()
147
147
  let j = null
148
- try { j = JSON.parse(txt) } catch {}
148
+ try { j = JSON.parse(txt) } catch (err) { /* ignore subscriptions error */ }
149
149
  if (j) {
150
150
  const snap2 = quotaSnapshot(provider, res.headers, j, Date.now())
151
151
  if (snap2 && typeof deps.rememberQuota === "function") deps.rememberQuota(account.ref, snap2)
152
152
  }
153
- } catch {}
153
+ } catch (err) { /* ignore subscriptions error */ }
154
154
  }
155
155
  if (!res.ok) {
156
156
  const txt = await res.text().catch(() => "")
@@ -174,7 +174,7 @@ export function createSubscriptionsService(deps) {
174
174
  ms: Date.now() - t0,
175
175
  kind: "request",
176
176
  })
177
- } catch {}
177
+ } catch (err) { /* ignore subscriptions error */ }
178
178
  }
179
179
  return res
180
180
  } catch (err) {
package/lib/usage.js CHANGED
@@ -52,22 +52,22 @@ export function asUsageSnapshot(percent) {
52
52
  // Named limit windows reported by vendors (five_hour, seven_day, primary...).
53
53
  // Label map is a data table: extend it when a vendor adds a window name.
54
54
  const WINDOW_LABELS = {
55
- five_hour: { zh: "5小时", ru: "5ч", en: "5h" },
56
- seven_day: { zh: "7天", ru: "7д", en: "7d" },
57
- seven_day_oauth_apps: { zh: "7天应用", ru: "7д прил.", en: "7d apps" },
58
- weekly_limit_7_days: { zh: "7天", ru: "7д", en: "7d" },
59
- primary: { zh: "主配额", ru: "осн.", en: "primary" },
60
- secondary: { zh: "次配额", ru: "втор.", en: "secondary" },
55
+ five_hour: { zh: "5小时", en: "5h" },
56
+ seven_day: { zh: "7天", en: "7d" },
57
+ seven_day_oauth_apps: { zh: "7天应用", en: "7d apps" },
58
+ weekly_limit_7_days: { zh: "7天", en: "7d" },
59
+ primary: { zh: "主配额", en: "primary" },
60
+ secondary: { zh: "次配额", en: "secondary" },
61
61
  // codex: the vendor API returns primary_window/secondary_window - surfaced as 5h/7d in the UI
62
- primary_window: { zh: "5小时", ru: "5ч", en: "5h" },
63
- secondary_window: { zh: "7天", ru: "7д", en: "7d" },
64
- monthly: { zh: "月度", ru: "месяц", en: "month" },
65
- credits: { zh: "点数", ru: "кредиты", en: "credits" },
62
+ primary_window: { zh: "5小时", en: "5h" },
63
+ secondary_window: { zh: "7天", en: "7d" },
64
+ monthly: { zh: "月度", en: "month" },
65
+ credits: { zh: "点数", en: "credits" },
66
66
  }
67
67
 
68
68
  export function windowLabel(id) {
69
69
  const known = WINDOW_LABELS[id]
70
- return known || { zh: id, ru: id, en: id }
70
+ return known || { zh: id, en: id }
71
71
  }
72
72
 
73
73
  // Walk vendor usage JSON; every node carrying a utilization-style number
@@ -83,19 +83,19 @@ export function usageWindows(obj) {
83
83
  if (pct != null && key && !seen.has(key)) {
84
84
  seen.add(key)
85
85
  const label = windowLabel(key)
86
- out.push({ id: key, ru: label.ru, en: label.en, usedPercent: pct })
86
+ out.push({ id: key, zh: label.zh, en: label.en, usedPercent: pct })
87
87
  }
88
88
  const frac = numberOrNull(value.remainingFraction)
89
89
  if (frac != null && key && !seen.has(key)) {
90
90
  seen.add(key)
91
91
  const label = windowLabel(key)
92
- out.push({ id: key, ru: label.ru, en: label.en, usedPercent: (1 - frac) * 100 })
92
+ out.push({ id: key, zh: label.zh, en: label.en, usedPercent: (1 - frac) * 100 })
93
93
  }
94
94
  const ufrac = numberOrNull(value.usedFraction)
95
95
  if (ufrac != null && key && !seen.has(key)) {
96
96
  seen.add(key)
97
97
  const label = windowLabel(key)
98
- out.push({ id: key, ru: label.ru, en: label.en, usedPercent: ufrac * 100 })
98
+ out.push({ id: key, zh: label.zh, en: label.en, usedPercent: ufrac * 100 })
99
99
  }
100
100
  for (const [k, child] of Object.entries(value)) {
101
101
  if (child && typeof child === "object") walk(child, k, depth + 1)
package/lib/utils.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Executes a function safely, capturing any error and logging it at debug level
3
+ * if a logger is provided, then returning the fallback value.
4
+ *
5
+ * Used across the plugin to eliminate empty catch blocks per dsh-plugin-preflight.
6
+ */
7
+ export function bestEffort(label, fn, fallback = undefined, logger = null) {
8
+ try {
9
+ const res = fn()
10
+ if (res && typeof res.then === 'function') {
11
+ return res.catch((err) => {
12
+ if (logger && typeof logger.debug === 'function') {
13
+ logger.debug(`[bestEffort] ${label}: ${err && err.message || err}`)
14
+ }
15
+ return fallback
16
+ })
17
+ }
18
+ return res
19
+ } catch (err) {
20
+ if (logger && typeof logger.debug === 'function') {
21
+ logger.debug(`[bestEffort] ${label}: ${err && err.message || err}`)
22
+ }
23
+ return fallback
24
+ }
25
+ }
@@ -1,3 +1,4 @@
1
+ import { fetchWithTimeout } from "./http.js"
1
2
  // Vendor factory from a declarative profile.
2
3
  //
3
4
  // Covers the OpenAI-Responses-compatible subscription family: PKCE auth,
@@ -87,9 +88,9 @@ export function createVendorFromProfile(profile) {
87
88
  const catalog = defaultModels(cfg.models || profile.models || [])
88
89
  if (!profile.modelsPath) return catalog
89
90
  try {
90
- const res = await (fetchImpl || fetch)(`${base}${profile.modelsPath}`, {
91
+ const res = await fetchWithTimeout(fetchImpl || fetch, `${base}${profile.modelsPath}`, {
91
92
  headers: identityHeaders(blob, { Accept: "application/json" }),
92
- })
93
+ }, { timeoutMs: 15000 })
93
94
  const json = await readJson(res)
94
95
  const rows = []
95
96
  for (const entry of json.data || json.models || []) {
@@ -52,6 +52,9 @@ export function defaults() {
52
52
  }
53
53
 
54
54
  export function authorizeUrl(cfg, pkce) {
55
+ if (!cfg || !cfg.clientId || !String(cfg.clientId).trim()) {
56
+ throw new Error('Google OAuth requires a custom Client ID. Please configure antigravityClientId in plugin settings or use "From CLI" to import active credentials.')
57
+ }
55
58
  return buildAuthorizeUrl({
56
59
  authUrl: AUTH,
57
60
  clientId: cfg.clientId,
@@ -47,7 +47,7 @@ export function defaults() {
47
47
  }
48
48
 
49
49
  export function authorizeUrl() {
50
- return 'https://open.bigmodel.cn/usercenter/apikeys'
50
+ return 'https://bigmodel.cn/usercenter/proj-mgmt/apikeys'
51
51
  }
52
52
 
53
53
  export async function listModels(_blob, cfg) {
@@ -220,7 +220,7 @@ export async function usage(blob, cfg, fetchImpl) {
220
220
  const decoded = decodeGrokCreditsFrame(Buffer.from(buf))
221
221
  if (decoded != null) pct = decoded
222
222
  }
223
- } catch {}
223
+ } catch (err) { /* ignore grok usage capture error */ }
224
224
  }
225
225
  const snap = asUsageSnapshot(pct)
226
226
  if (snap) snap.windows = usageWindows(json)
package/lib/wire.js CHANGED
@@ -199,12 +199,14 @@ export async function formTokenRequest(url, params, fetchImpl, headers, { timeou
199
199
  return readJson(res)
200
200
  }
201
201
 
202
- export async function jsonTokenRequest(url, body, fetchImpl, headers) {
202
+ export async function jsonTokenRequest(url, body, fetchImpl, headers, { timeoutMs = 25000 } = {}) {
203
203
  const impl = fetchImpl || fetch
204
+ const timeoutSignal = AbortSignal.timeout(timeoutMs)
204
205
  const res = await impl(url, {
205
206
  method: 'POST',
206
207
  headers: { 'Content-Type': 'application/json', Accept: 'application/json', ...(headers || {}) },
207
208
  body: JSON.stringify(body),
209
+ signal: timeoutSignal,
208
210
  })
209
211
  return readJson(res)
210
212
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.6.9",
3
+ "version": "0.6.11",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, Antigravity, Kimi, GLM, Cursor, Kiro, Copilot, Qwen, ERNIE, Spark, JetBrains, Perplexity, Replit, and Cody subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,7 +44,10 @@
44
44
  },
45
45
  "client": {
46
46
  "platform": "web",
47
- "inject": []
47
+ "inject": [
48
+ "@deepseek-ai/dsh-client-locale",
49
+ "@deepseek-ai/dsh-client-ui-slots"
50
+ ]
48
51
  }
49
52
  },
50
53
  "peerDependencies": {