@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 +21 -0
- package/README.ru.md +21 -0
- package/README.zh.md +21 -0
- package/lib/accounts.js +4 -2
- package/lib/adapter.js +6 -5
- package/lib/client.js +36 -30
- package/lib/config-schema.js +2 -0
- package/lib/index.js +18 -16
- package/lib/mask.js +1 -1
- package/lib/oauth.js +3 -0
- package/lib/relative-time.js +0 -8
- package/lib/routes/oauth.js +14 -1
- package/lib/routes/proxy.js +1 -1
- package/lib/routes/status.js +5 -5
- package/lib/subscriptions.js +5 -5
- package/lib/usage.js +14 -14
- package/lib/utils.js +25 -0
- package/lib/vendor-factory.js +3 -2
- package/lib/vendors/antigravity.js +3 -0
- package/lib/vendors/glm.js +1 -1
- package/lib/vendors/grok.js +1 -1
- package/lib/wire.js +3 -1
- package/package.json +5 -2
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
|
|
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
|
|
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)}' +
|
|
@@ -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://
|
|
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 ? '
|
|
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 ? '
|
|
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
|
),
|
package/lib/config-schema.js
CHANGED
|
@@ -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
|
-
|
|
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
|
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)
|
package/lib/relative-time.js
CHANGED
package/lib/routes/oauth.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/lib/routes/proxy.js
CHANGED
|
@@ -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 })
|
package/lib/routes/status.js
CHANGED
|
@@ -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 {
|
package/lib/subscriptions.js
CHANGED
|
@@ -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小时",
|
|
56
|
-
seven_day: { zh: "7天",
|
|
57
|
-
seven_day_oauth_apps: { zh: "7天应用",
|
|
58
|
-
weekly_limit_7_days: { zh: "7天",
|
|
59
|
-
primary: { zh: "主配额",
|
|
60
|
-
secondary: { zh: "次配额",
|
|
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小时",
|
|
63
|
-
secondary_window: { zh: "7天",
|
|
64
|
-
monthly: { zh: "月度",
|
|
65
|
-
credits: { zh: "点数",
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
+
}
|
package/lib/vendor-factory.js
CHANGED
|
@@ -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
|
|
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,
|
package/lib/vendors/glm.js
CHANGED
package/lib/vendors/grok.js
CHANGED
|
@@ -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.
|
|
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": {
|