@goodandready/dsh-subscriptions 0.5.32 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/README.ru.md +19 -1
- package/README.zh.md +27 -1
- package/lib/accounts.js +1 -1
- package/lib/adapter.js +2 -2
- package/lib/client.js +44 -167
- package/lib/crypto.js +4 -4
- package/lib/google-validation.js +1 -1
- package/lib/health.js +2 -2
- package/lib/history.js +5 -5
- package/lib/http.js +7 -0
- package/lib/images.js +21 -21
- package/lib/index.js +19 -19
- package/lib/loopback.js +10 -10
- package/lib/ratelimit.js +2 -2
- package/lib/refs.js +9 -1
- package/lib/routes/accounts.js +343 -0
- package/lib/routes/oauth.js +248 -0
- package/lib/routes/proxy.js +147 -0
- package/lib/routes/status.js +257 -0
- package/lib/routes.js +12 -916
- package/lib/subscriptions.js +1 -1
- package/lib/ui-styles.js +12 -12
- package/lib/usage.js +1 -1
- package/lib/vendor-factory.js +13 -13
- package/package.json +6 -6
- package/lib/ui-i18n.js +0 -256
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ graph LR
|
|
|
68
68
|
|
|
69
69
|
## ✨ Key Features & Capabilities
|
|
70
70
|
|
|
71
|
-
### 1. 🌐
|
|
71
|
+
### 1. 🌐 16 Supported Built-in Subscription Vendors
|
|
72
72
|
|
|
73
73
|
| Vendor Key | Subscription Tier | Protocol & Features |
|
|
74
74
|
|---|---|---|
|
|
@@ -76,6 +76,18 @@ graph LR
|
|
|
76
76
|
| `claude` | Claude Pro / Max | Native Claude Messages protocol, usage tracking (`/v1/messages`, `/api/oauth/...`) |
|
|
77
77
|
| `grok` | xAI / X Premium | Real-time reasoning responses, billing checks & social search |
|
|
78
78
|
| `antigravity` | Google Cloud Code Assist | Antigravity engine (`/v1/loadCodeAssist`, `/v1/streamGenerateContent`) |
|
|
79
|
+
| `kimi` | Moonshot Kimi | OAuth Device Flow login (`auth.kimi.com`), OpenAI-compatible chat |
|
|
80
|
+
| `glm` | Z.ai GLM Coding Plan | GLM coding endpoint (`api.z.ai`), live quota monitor (`/api/monitor/usage/quota/limit`) |
|
|
81
|
+
| `cursor` | Cursor | Cursor backend (`api2.cursor.sh`), billing-period usage dashboard parsing |
|
|
82
|
+
| `kiro` | AWS Kiro | Kiro desktop OAuth (`app.kiro.dev`), streaming code assistance |
|
|
83
|
+
| `copilot` | GitHub Copilot | GitHub Device Flow login (`github.com/login/device`), Copilot chat completions |
|
|
84
|
+
| `qwen` | Alibaba Qwen (DashScope) | OpenAI-compatible endpoint (`dashscope.aliyuncs.com/compatible-mode/v1`), API key auth |
|
|
85
|
+
| `ernie` | Baidu ERNIE (Qianfan) | OAuth2 token refresh via API Key + Secret Key, Wenxinworkshop chat |
|
|
86
|
+
| `spark` | iFlytek Spark | OpenAI-compatible Spark HTTP API (`spark-api-open.xf-yun.com/v1`) |
|
|
87
|
+
| `jetbrains` | JetBrains AI Assistant | JetBrains AI relay (`api.jetbrains.ai`) |
|
|
88
|
+
| `perplexity` | Perplexity Pro | Sonar model catalog (`api.perplexity.ai`) |
|
|
89
|
+
| `replit` | Replit Core | Replit AI API (`replit.com/api/v1/ai`), connect-token auth |
|
|
90
|
+
| `cody` | Sourcegraph Cody Pro | Sourcegraph API (`sourcegraph.com/.api`), access-token auth |
|
|
79
91
|
|
|
80
92
|
*Custom vendors can also be dynamically registered via the `createVendorFromProfile` factory.*
|
|
81
93
|
|
|
@@ -219,6 +231,12 @@ dsh-subscriptions:
|
|
|
219
231
|
|
|
220
232
|
---
|
|
221
233
|
|
|
234
|
+
## 🌐 Localization
|
|
235
|
+
|
|
236
|
+
The plugin source language is English only. Russian and other translations are provided at runtime by separate language plugins (for example the russification plugin), which translate the registered locale keys - the package itself ships no bundled translations (Changed in 0.6.1).
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
222
240
|
## 📄 License
|
|
223
241
|
|
|
224
242
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.ru.md
CHANGED
|
@@ -68,7 +68,7 @@ graph LR
|
|
|
68
68
|
|
|
69
69
|
## ✨ Ключевые возможности
|
|
70
70
|
|
|
71
|
-
### 1. 🌐
|
|
71
|
+
### 1. 🌐 16 Поддерживаемых встроенных вендоров подписок
|
|
72
72
|
|
|
73
73
|
| Ключ вендора | Тариф подписки | Протокол и возможности |
|
|
74
74
|
|---|---|---|
|
|
@@ -76,6 +76,18 @@ graph LR
|
|
|
76
76
|
| `claude` | Claude Pro / Max | Нативный протокол Claude Messages, трекинг расхода (`/v1/messages`, `/api/oauth/...`) |
|
|
77
77
|
| `grok` | xAI / X Premium | Ответы с рассуждениями, проверка баланса и поиск в соцсети |
|
|
78
78
|
| `antigravity` | Google Cloud Code Assist | Движок Antigravity (`/v1/loadCodeAssist`, `/v1/streamGenerateContent`) |
|
|
79
|
+
| `kimi` | Moonshot Kimi | Вход по OAuth Device Flow (`auth.kimi.com`), OpenAI-совместимый чат |
|
|
80
|
+
| `glm` | Z.ai GLM Coding Plan | Кодинг-эндпоинт GLM (`api.z.ai`), живой монитор квоты (`/api/monitor/usage/quota/limit`) |
|
|
81
|
+
| `cursor` | Cursor | Бэкенд Cursor (`api2.cursor.sh`), разбор дашборда расхода за период |
|
|
82
|
+
| `kiro` | AWS Kiro | Десктопный OAuth Kiro (`app.kiro.dev`), стриминг кода |
|
|
83
|
+
| `copilot` | GitHub Copilot | Вход по GitHub Device Flow (`github.com/login/device`), chat completions Copilot |
|
|
84
|
+
| `qwen` | Alibaba Qwen (DashScope) | OpenAI-совместимый эндпоинт (`dashscope.aliyuncs.com/compatible-mode/v1`), вход по API-ключу |
|
|
85
|
+
| `ernie` | Baidu ERNIE (Qianfan) | Продление OAuth2-токена по связке API Key + Secret Key, чат Wenxinworkshop |
|
|
86
|
+
| `spark` | iFlytek Spark | OpenAI-совместимый HTTP API Spark (`spark-api-open.xf-yun.com/v1`) |
|
|
87
|
+
| `jetbrains` | JetBrains AI Assistant | Релей JetBrains AI (`api.jetbrains.ai`) |
|
|
88
|
+
| `perplexity` | Perplexity Pro | Каталог моделей Sonar (`api.perplexity.ai`) |
|
|
89
|
+
| `replit` | Replit Core | Replit AI API (`replit.com/api/v1/ai`), вход по connect-токену |
|
|
90
|
+
| `cody` | Sourcegraph Cody Pro | Sourcegraph API (`sourcegraph.com/.api`), вход по access-токену |
|
|
79
91
|
|
|
80
92
|
*Также поддерживается регистрация кастомных вендоров через фабрику профилей `createVendorFromProfile`.*
|
|
81
93
|
|
|
@@ -192,6 +204,12 @@ dsh-subscriptions:
|
|
|
192
204
|
|
|
193
205
|
---
|
|
194
206
|
|
|
207
|
+
## 🌐 Локализация
|
|
208
|
+
|
|
209
|
+
Исходный язык плагина - только английский. Русский и другие переводы во время работы предоставляют отдельные языковые плагины (например, плагин русификации), переводящие зарегистрированные ключи локали, - сам пакет не содержит встроенных переводов (Changed in 0.6.1).
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
195
213
|
## 📄 Лицензия
|
|
196
214
|
|
|
197
215
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/README.zh.md
CHANGED
|
@@ -68,13 +68,25 @@ graph LR
|
|
|
68
68
|
|
|
69
69
|
## ✨ 核心功能
|
|
70
70
|
|
|
71
|
-
###
|
|
71
|
+
### 支持的 16 个内置订阅供应商
|
|
72
72
|
| 供应商 | 订阅级别 | 协议 |
|
|
73
73
|
|---|---|---|
|
|
74
74
|
| `codex` | ChatGPT Plus / Pro | 流式响应、工具调用、图像生成 |
|
|
75
75
|
| `claude` | Claude Pro / Max | 原生 Messages 协议、用量跟踪 |
|
|
76
76
|
| `grok` | xAI / X Premium | 实时推理、计费检查 |
|
|
77
77
|
| `antigravity` | Google Cloud Code Assist | 流式生成 |
|
|
78
|
+
| `kimi` | Moonshot Kimi | OAuth Device Flow 登录(`auth.kimi.com`)、OpenAI 兼容聊天 |
|
|
79
|
+
| `glm` | Z.ai GLM Coding Plan | GLM 编码端点(`api.z.ai`)、实时配额监控 |
|
|
80
|
+
| `cursor` | Cursor | Cursor 后端(`api2.cursor.sh`)、用量面板解析 |
|
|
81
|
+
| `kiro` | AWS Kiro | Kiro 桌面 OAuth(`app.kiro.dev`)、流式输出 |
|
|
82
|
+
| `copilot` | GitHub Copilot | GitHub Device Flow 登录、Copilot 聊天补全 |
|
|
83
|
+
| `qwen` | 阿里 Qwen(DashScope) | OpenAI 兼容端点、API Key 认证 |
|
|
84
|
+
| `ernie` | 百度 ERNIE(千帆) | OAuth2 令牌刷新(API Key + Secret Key) |
|
|
85
|
+
| `spark` | 讯飞星火 | OpenAI 兼容 HTTP API |
|
|
86
|
+
| `jetbrains` | JetBrains AI Assistant | JetBrains AI 中继(`api.jetbrains.ai`) |
|
|
87
|
+
| `perplexity` | Perplexity Pro | Sonar 模型目录(`api.perplexity.ai`) |
|
|
88
|
+
| `replit` | Replit Core | Replit AI API、connect-token 认证 |
|
|
89
|
+
| `cody` | Sourcegraph Cody Pro | Sourcegraph API、access-token 认证 |
|
|
78
90
|
|
|
79
91
|
### 多账号轮换与限流保护
|
|
80
92
|
* 每个供应商可挂多个账号(如 `CODEX_OAUTH_1`、`CODEX_OAUTH_2`)。
|
|
@@ -108,6 +120,14 @@ graph LR
|
|
|
108
120
|
* **匿名诊断报告**:设置卡片内一键生成(插件/运行时版本、系统、各供应商健康状态、HTTP 状态聚合、最近错误与耗时、非敏感配置)并自动复制到剪贴板;令牌、邮箱、凭据名与代理地址严格排除。
|
|
109
121
|
* 同区块提供问题追踪器链接,便于提交 issue。
|
|
110
122
|
|
|
123
|
+
### HTTP API 路由(`v0.4.9`)
|
|
124
|
+
| 路由 | 方法 | 用途 |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `/dsh-subscriptions/diagnostics` | GET | 匿名诊断报告(无密钥、无令牌、无代理地址) |
|
|
127
|
+
| `/dsh-subscriptions/proxy-check` | POST | 检测槽位代理到供应商基础地址的延迟 |
|
|
128
|
+
| `/dsh-subscriptions/oauth/device/start` | POST | 发起 Codex 设备码登录(返回用户码与验证地址) |
|
|
129
|
+
| `/dsh-subscriptions/oauth/device/poll` | POST | 轮询设备码授权状态 |
|
|
130
|
+
|
|
111
131
|
---
|
|
112
132
|
|
|
113
133
|
## 📦 安装指南
|
|
@@ -118,6 +138,12 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
|
|
|
118
138
|
|
|
119
139
|
---
|
|
120
140
|
|
|
141
|
+
## 🌐 本地化
|
|
142
|
+
|
|
143
|
+
插件源语言仅为英语。俄语及其他翻译由独立的语言插件(如俄化插件)在运行时对注册的本地化键进行翻译,包本身不内置翻译(Changed in 0.6.1)。
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
121
147
|
## 📄 开源协议
|
|
122
148
|
|
|
123
149
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
package/lib/accounts.js
CHANGED
|
@@ -327,7 +327,7 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
327
327
|
recordExhaust(ref) { health.set(ref, recordExhaust(health.get(ref))) },
|
|
328
328
|
recordBroken(ref) { health.set(ref, recordBroken(health.get(ref))) },
|
|
329
329
|
recordSuccess(ref) {
|
|
330
|
-
//
|
|
330
|
+
// successful request clears the cooldown and puts the account back in rotation
|
|
331
331
|
cooldowns.delete(ref)
|
|
332
332
|
quarantines.delete(ref)
|
|
333
333
|
if (health.has(ref)) health.set(ref, null)
|
package/lib/adapter.js
CHANGED
|
@@ -147,9 +147,9 @@ export class SubscriptionAdapter extends LlmAdapter {
|
|
|
147
147
|
signal: opts.signal,
|
|
148
148
|
saveBlob: (next) => deps.saveBlob(account.ref, next),
|
|
149
149
|
})
|
|
150
|
-
//
|
|
150
|
+
// stream completed successfully: clear cooldown and health penalties
|
|
151
151
|
if (typeof deps.recordSuccess === 'function') deps.recordSuccess(account.ref)
|
|
152
|
-
// #65:
|
|
152
|
+
// #65: record in the request and cost history.
|
|
153
153
|
if (typeof deps.recordHistory === 'function') {
|
|
154
154
|
try {
|
|
155
155
|
deps.recordHistory({
|
package/lib/client.js
CHANGED
|
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
try { return require('react-dom').createPortal } catch { return null }
|
|
9
9
|
})()
|
|
10
10
|
|
|
11
|
-
//
|
|
11
|
+
// Modular translator: shared by component subscriptions and helpers.
|
|
12
12
|
let t = (key) => key
|
|
13
13
|
const setT = (fn) => { t = fn }
|
|
14
14
|
|
|
@@ -87,9 +87,9 @@ window.__ModuleLoader__.load({
|
|
|
87
87
|
'.dsub-pill:active{transform:translateY(0)}' +
|
|
88
88
|
'.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
|
|
89
89
|
'.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
|
|
90
|
-
'.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px
|
|
91
|
-
'.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px
|
|
92
|
-
'.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px
|
|
90
|
+
'.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}' +
|
|
91
|
+
'.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warning-primary) 70%,transparent)}' +
|
|
92
|
+
'.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}' +
|
|
93
93
|
'.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
|
|
94
94
|
'.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:rgba(0,0,0,.65);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
|
|
95
95
|
'.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.07);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
|
|
@@ -108,7 +108,7 @@ window.__ModuleLoader__.load({
|
|
|
108
108
|
'.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
|
|
109
109
|
'.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
|
|
110
110
|
'.dsub-panel-box{margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}' +
|
|
111
|
-
'.dsub-panel-warn{border-color:
|
|
111
|
+
'.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 5%,transparent)}' +
|
|
112
112
|
'.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
|
|
113
113
|
'.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
|
|
114
114
|
'.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
|
|
@@ -117,14 +117,14 @@ window.__ModuleLoader__.load({
|
|
|
117
117
|
'.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}' +
|
|
118
118
|
'.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
|
|
119
119
|
'.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
|
|
120
|
-
'.dsub-tag-ok{border-color:
|
|
121
|
-
'.dsub-tag-warn{border-color:
|
|
122
|
-
'.dsub-tag-bad{border-color:
|
|
120
|
+
'.dsub-tag-ok{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
121
|
+
'.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
122
|
+
'.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
|
|
123
123
|
'.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
|
|
124
124
|
'.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
|
|
125
125
|
'.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
|
|
126
|
-
'.dsub-btn-danger{border-color:
|
|
127
|
-
'.dsub-btn-danger:hover:not(:disabled){background:
|
|
126
|
+
'.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
|
|
127
|
+
'.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
|
|
128
128
|
'.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
|
|
129
129
|
'.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}' +
|
|
130
130
|
'.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
|
|
@@ -142,9 +142,9 @@ window.__ModuleLoader__.load({
|
|
|
142
142
|
'.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
|
|
143
143
|
'.dsub-accName{font-size:13px;font-weight:600}' +
|
|
144
144
|
'.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}' +
|
|
145
|
-
'.dsub-statusOk{background:
|
|
146
|
-
'.dsub-statusWarn{background:
|
|
147
|
-
'.dsub-statusBad{background:
|
|
145
|
+
'.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}' +
|
|
146
|
+
'.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary)}' +
|
|
147
|
+
'.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}' +
|
|
148
148
|
'.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
|
|
149
149
|
'.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}' +
|
|
150
150
|
'.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary,#3b82f6);color:#fff;border:0;cursor:pointer;transition:opacity .15s}' +
|
|
@@ -165,12 +165,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
165
165
|
tag.textContent = CSS
|
|
166
166
|
tag.setAttribute('data-plugin', 'dsh-subscriptions')
|
|
167
167
|
tag.dataset.pluginCss = cssId
|
|
168
|
+
tag.setAttribute('data-dsh-plugin', 'dsh-subscriptions')
|
|
168
169
|
document.head.appendChild(tag)
|
|
169
170
|
}
|
|
170
171
|
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
172
|
+
// Card strings live in the locale registry: a separate package translates
|
|
173
|
+
// them without touching this plugin's code. English is the default,
|
|
174
|
+
// and the fallback when no translation exists.
|
|
174
175
|
const NS = 'dsh-subscriptions'
|
|
175
176
|
|
|
176
177
|
const INSTRUCTIONS = {
|
|
@@ -420,130 +421,6 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
420
421
|
'windowPrimary': 'Primary window',
|
|
421
422
|
'windowSecondary': 'Secondary window',
|
|
422
423
|
}
|
|
423
|
-
const ru = {
|
|
424
|
-
'check': 'Проверить',
|
|
425
|
-
'checking': 'Проверяю…',
|
|
426
|
-
'importToken': 'Сохранить',
|
|
427
|
-
'importTokenPlace': 'Вставить токен или API-ключ',
|
|
428
|
-
'importLocalCli': '📥 Из CLI',
|
|
429
|
-
'importLocalCliTitle': 'Автоматически считать токен из установленного на сервере CLI',
|
|
430
|
-
'importLocalSuccess': 'Токен успешно загружен из локального CLI!',
|
|
431
|
-
'howToGetToken': '❓ Инструкция',
|
|
432
|
-
'howToGetTokenTitle': 'Как получить токен или API-ключ для этого провайдера',
|
|
433
|
-
'instructionsTitle': 'Инструкция по получению токена',
|
|
434
|
-
'close': 'Закрыть',
|
|
435
|
-
'reconnectRequired': 'Нужно переподключить',
|
|
436
|
-
'manualTitle': 'Если браузер не вернулся сам',
|
|
437
|
-
'windowPrimary': 'Основное окно',
|
|
438
|
-
'windowSecondary': 'Дополнительное окно',
|
|
439
|
-
'notConnected': 'Не подключено',
|
|
440
|
-
'verifyAccount': 'Требуется проверка',
|
|
441
|
-
'coolingDown': 'Пауза после лимита',
|
|
442
|
-
'usageFull': 'Лимит исчерпан',
|
|
443
|
-
'connected': 'Подключено',
|
|
444
|
-
'title': 'Подписки',
|
|
445
|
-
'cardIntro': 'Аккаунты подписок, ротация и лимиты.',
|
|
446
|
-
'show': 'Показать',
|
|
447
|
-
'hide': 'Скрыть',
|
|
448
|
-
'intro': 'Вход по обычной пользовательской подписке. Токены остаются в хранилище учётных данных харнесса, браузер их обратно не читает. Если после «Подключить» провайдер увёл на localhost или на свою страницу, вставьте сюда адрес перехода или код.',
|
|
449
|
-
'useOrigin': 'Использовать адрес этого веб-интерфейса как redirect_uri',
|
|
450
|
-
'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
|
|
451
|
-
'privacyMask': 'Маскировать email и учётные записи',
|
|
452
|
-
'privacyMaskHint': 'Для демонстрации экрана: email отображается как j***n@example.com.',
|
|
453
|
-
'resetCredits': 'Карты сброса',
|
|
454
|
-
'resetCreditsHint': 'Карты сброса квоты ChatGPT: показывает, сколько доступно. Списание одной карты — осознанное действие с 5-секундным подтверждением.',
|
|
455
|
-
'resetAvailable': 'Доступно попыток сброса',
|
|
456
|
-
'resetExpires': 'ближайшая истекает',
|
|
457
|
-
'resetAck': 'Я понимаю, что будет списана 1 попытка',
|
|
458
|
-
'resetWait': 'кнопка активна через',
|
|
459
|
-
'resetReady': 'готово',
|
|
460
|
-
'resetGo': 'Сбросить',
|
|
461
|
-
'resetBusy': 'сбрасываю…',
|
|
462
|
-
'resetDone': 'квота сброшена',
|
|
463
|
-
'resetNothing': 'сервер сообщает: сброс не нужен (попытка не списана)',
|
|
464
|
-
'resetNoCredit': 'нет пригодной карты (не списано)',
|
|
465
|
-
'resetRedeemed': 'эта карта уже использована ранее',
|
|
466
|
-
'diagGenerate': 'Сгенерировать диагностический отчёт',
|
|
467
|
-
'diagCopy': 'Скопировать отчёт',
|
|
468
|
-
'diagIssues': 'Открыть трекер задач',
|
|
469
|
-
'diagHint': 'В отчёте нет токенов, email, адресов прокси и персональных данных.',
|
|
470
|
-
'subsPill': 'SUBS',
|
|
471
|
-
'subsModalTitle': 'Консоль подписок',
|
|
472
|
-
'subsActiveHero': 'Активная подписка',
|
|
473
|
-
'subsPoolTitle': 'Пул аккаунтов',
|
|
474
|
-
'subsRefresh': 'Обновить',
|
|
475
|
-
'subsOpenSettings': 'Все настройки →',
|
|
476
|
-
'subsFastMode': 'Режим Fast 1.5x',
|
|
477
|
-
'subsHealthy': 'Исправен',
|
|
478
|
-
'subsFree': 'свободно',
|
|
479
|
-
'subsUsed': 'израсходовано',
|
|
480
|
-
'subsModalClose': 'Закрыть',
|
|
481
|
-
'subsLogged': 'подключен',
|
|
482
|
-
'subsNotLogged': 'не подключен',
|
|
483
|
-
'subsCooldown': 'кулдаун',
|
|
484
|
-
'subsOpenSettings': 'Открыть настройки',
|
|
485
|
-
'hudTitle': 'Остаток подписки',
|
|
486
|
-
'settingsLoading': 'Загрузка настроек…',
|
|
487
|
-
'settingsUnavailable': 'Настройки недоступны',
|
|
488
|
-
'settingsRetry': 'Повторить',
|
|
489
|
-
'hudHint': 'перетащи · прилипает к краям · клик — обновить',
|
|
490
|
-
'fcCalibrating': 'калибровка…',
|
|
491
|
-
'fcIdle': 'нет расхода',
|
|
492
|
-
'fcHours': 'ч',
|
|
493
|
-
'fcMinutes': 'мин',
|
|
494
|
-
'reconnect': 'Переподключить',
|
|
495
|
-
'connect': 'Подключить',
|
|
496
|
-
'disconnect': 'Отключить',
|
|
497
|
-
'removeSlot': 'Убрать место',
|
|
498
|
-
'pastePlaceholder': 'Адрес перехода или код',
|
|
499
|
-
'submitCode': 'Отправить код',
|
|
500
|
-
'proxyPlaceholder': 'Прокси аккаунта (http://, https://, socks5://) - необязательно',
|
|
501
|
-
'proxyCheck': 'Проверить прокси',
|
|
502
|
-
'proxyOk': 'прокси ок',
|
|
503
|
-
'proxyFail': 'прокси недоступен',
|
|
504
|
-
'deviceLogin': 'Вход по коду',
|
|
505
|
-
'deviceHint': 'Headless: откройте ссылку, введите код, держите вкладку открытой.',
|
|
506
|
-
'deviceCopy': 'Скопировать код',
|
|
507
|
-
'devicePending': 'Ожидание подтверждения',
|
|
508
|
-
'deviceAuthorized': 'Вход выполнен',
|
|
509
|
-
'deviceExpired': 'Истёк - начните заново',
|
|
510
|
-
'verifyPrefix': 'Google требует однократной проверки аккаунта. ',
|
|
511
|
-
'verifyLink': 'Открыть страницу проверки',
|
|
512
|
-
'verifySuffix': ' затем подключитесь заново.',
|
|
513
|
-
'addAccount': '+ Добавить аккаунт',
|
|
514
|
-
'save': 'Сохранить',
|
|
515
|
-
'saved': 'Сохранено',
|
|
516
|
-
'loading': 'Загрузка…',
|
|
517
|
-
'accountLabel': 'Аккаунт',
|
|
518
|
-
'plan': 'Тариф',
|
|
519
|
-
'storedAs': 'Хранится как',
|
|
520
|
-
'switchLabel': 'Провайдер',
|
|
521
|
-
'chipActive': 'Подписки',
|
|
522
|
-
'expiryLabel': 'окончание',
|
|
523
|
-
'slashLogin': 'Вход в провайдера подписки',
|
|
524
|
-
'slashLogout': 'Выйти из провайдера подписки',
|
|
525
|
-
'slashStatus': 'Статус подписок',
|
|
526
|
-
'none': 'нет',
|
|
527
|
-
'forecast': '≈',
|
|
528
|
-
'resetCredits': 'Сброс лимитов ChatGPT',
|
|
529
|
-
'resetCreditsHint': 'Экстренный сброс 5-часового окна ChatGPT Plus/Pro. Требует 5 секунд подтверждения перед списанием.',
|
|
530
|
-
'resetCreditsAction': 'Сбросить лимит ChatGPT',
|
|
531
|
-
'resetAvailable': 'Доступно сбросов',
|
|
532
|
-
'resetExpires': 'срок действия',
|
|
533
|
-
'resetAck': 'Я понимаю, что одна попытка сброса будет списана',
|
|
534
|
-
'resetWait': 'подтверждение через',
|
|
535
|
-
'resetReady': 'готово к сбросу',
|
|
536
|
-
'resetGo': 'Сбросить лимит сейчас',
|
|
537
|
-
'resetBusy': 'Сброс…',
|
|
538
|
-
'resetDone': 'Лимит успешно сброшен',
|
|
539
|
-
'resetNothing': 'Лимит не требует сброса',
|
|
540
|
-
'resetNoCredit': 'Нет доступных попыток сброса',
|
|
541
|
-
'resetRedeemed': 'Попытка уже использована',
|
|
542
|
-
'windowPrimary': 'Окно 5 часов',
|
|
543
|
-
'windowSecondary': 'Окно 7 дней',
|
|
544
|
-
'usageLimitsTitle': 'Лимиты использования',
|
|
545
|
-
'slotDetailsTitle': 'Параметры слота',
|
|
546
|
-
}
|
|
547
424
|
|
|
548
425
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
549
426
|
function cleanErrorMessage(raw) {
|
|
@@ -632,9 +509,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
632
509
|
return t('connected')
|
|
633
510
|
}
|
|
634
511
|
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
//
|
|
512
|
+
// The card in the Plugins tab draws its own header and collapse toggle:
|
|
513
|
+
// the core only provides the list frame.
|
|
514
|
+
// Core chevron icon. Without a guarded require, a missing IconChevronDownOutline14 could take down the whole client half.
|
|
638
515
|
let ChevronIcon = null
|
|
639
516
|
try {
|
|
640
517
|
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
@@ -652,7 +529,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
652
529
|
|
|
653
530
|
|
|
654
531
|
function SubsSection(props) {
|
|
655
|
-
//
|
|
532
|
+
// The translator comes from the slot because its registration declares locale.
|
|
656
533
|
const t = (props && props.t) || ((key) => key)
|
|
657
534
|
const [draft, setDraft] = React.useState(null)
|
|
658
535
|
const [accounts, setAccounts] = React.useState([])
|
|
@@ -851,7 +728,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
851
728
|
setPaste((p) => Object.assign({}, p, { [key]: '' }))
|
|
852
729
|
}
|
|
853
730
|
|
|
854
|
-
// #88:
|
|
731
|
+
// #88: probe the account proxy with a real request and measure latency.
|
|
855
732
|
const doProxyCheck = async (provider, index) => {
|
|
856
733
|
const key = provider + ':' + index
|
|
857
734
|
setChecking((c) => Object.assign({}, c, { ['proxy:' + key]: true }))
|
|
@@ -1330,8 +1207,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1330
1207
|
)
|
|
1331
1208
|
}
|
|
1332
1209
|
|
|
1333
|
-
//
|
|
1334
|
-
//
|
|
1210
|
+
// The card in the Plugins tab follows the bash/agent-loop/web-search cards:
|
|
1211
|
+
// titled header with description and chevron, collapsible, bordered.
|
|
1335
1212
|
function PluginCard(props) {
|
|
1336
1213
|
const t = (props && props.t) || ((key) => key)
|
|
1337
1214
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1357,14 +1234,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1357
1234
|
}
|
|
1358
1235
|
|
|
1359
1236
|
function registerSettings(ctx) {
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
// load plugins
|
|
1237
|
+
// Languages can come from more than this plugin: dictionary packages declare
|
|
1238
|
+
// Russian for foreign namespaces. The core throws on re-declaring the same
|
|
1239
|
+
// namespace+language pair, and an unguarded call used to take down the
|
|
1240
|
+
// whole plugin - in the UI it surfaced as Failed to
|
|
1241
|
+
// load plugins listing perfectly innocent neighbours.
|
|
1365
1242
|
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1243
|
+
// So each language is declared separately and politely: if someone got
|
|
1244
|
+
// there first we yield, while our own English still lands.
|
|
1368
1245
|
const addLocale = (locale, dictionary) => {
|
|
1369
1246
|
try {
|
|
1370
1247
|
return ctx.locale.register(NS, locale, dictionary)
|
|
@@ -1373,14 +1250,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1373
1250
|
}
|
|
1374
1251
|
}
|
|
1375
1252
|
ctx.effect(() => {
|
|
1376
|
-
const undo = [addLocale('en', en)
|
|
1253
|
+
const undo = [addLocale('en', en)]
|
|
1377
1254
|
return () => { for (const off of undo) off() }
|
|
1378
1255
|
}, 'dsh-subscriptions: словари')
|
|
1379
|
-
|
|
1256
|
+
// Labels outside a component take the translator bound to the namespace.
|
|
1380
1257
|
setT(ctx.locale.bind(NS))
|
|
1381
|
-
//
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1258
|
+
// The canonical place is the Plugins tab: a card with its own header
|
|
1259
|
+
// and collapse instead of a row in the side list. The registration key must
|
|
1260
|
+
// equal the settings namespace, otherwise the tab silently hides the slot.
|
|
1384
1261
|
let moved = false
|
|
1385
1262
|
try {
|
|
1386
1263
|
moved = !!ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
@@ -1416,8 +1293,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1416
1293
|
}
|
|
1417
1294
|
}
|
|
1418
1295
|
|
|
1419
|
-
//
|
|
1420
|
-
//
|
|
1296
|
+
// Hot provider switcher in the composer row: a click cycles
|
|
1297
|
+
// the active subscription provider for the next request.
|
|
1421
1298
|
let activeIdx = 0
|
|
1422
1299
|
let labelEl = null
|
|
1423
1300
|
const ORDER = ['codex', 'claude', 'grok', 'antigravity']
|
|
@@ -1442,8 +1319,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1442
1319
|
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null } }
|
|
1443
1320
|
}
|
|
1444
1321
|
|
|
1445
|
-
// #84:
|
|
1446
|
-
//
|
|
1322
|
+
// #84: compact quota indicator of the active subscription in the input area.
|
|
1323
|
+
// The runway forecast uses a rolling window of /status samples.
|
|
1447
1324
|
const fcSamples = { windows: {} }
|
|
1448
1325
|
|
|
1449
1326
|
function fcObserve(key, remaining, now) {
|
|
@@ -1527,8 +1404,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1527
1404
|
return React.createElement('span', { className: 'dsub-cq', title: t('forecast') + ' · ' + state.active.provider }, value)
|
|
1528
1405
|
}
|
|
1529
1406
|
|
|
1530
|
-
// #83:
|
|
1531
|
-
//
|
|
1407
|
+
// #83: SUBS(N) pill in the session header with a pool health LED and
|
|
1408
|
+
// a modal accounts console (close: X button, Escape, outside click).
|
|
1532
1409
|
function brandBadge(prov) {
|
|
1533
1410
|
const p = String(prov || '').toLowerCase()
|
|
1534
1411
|
if (p.includes('codex') || p.includes('chatgpt') || p.includes('openai')) return { label: 'OpenAI', cls: 'dsub-brandCodex', icon: '⚡' }
|
|
@@ -1616,7 +1493,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1616
1493
|
React.createElement('div', { className: 'dsub-heroTitle' },
|
|
1617
1494
|
React.createElement('span', { className: 'dsub-brandBadge ' + b.cls, style: { width: 24, height: 24, fontSize: 11 } }, b.icon),
|
|
1618
1495
|
React.createElement('span', null, (a.provider ? a.provider.toUpperCase() : 'LLM') + (a.index ? ' #' + a.index : '')),
|
|
1619
|
-
a.fastMode ? React.createElement('span', { className: 'dsub-pillTag', style: { background: '
|
|
1496
|
+
a.fastMode ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent)', color: 'var(--dsw-alias-state-warning-primary)' } }, '⚡ FAST 1.5x') : null,
|
|
1620
1497
|
),
|
|
1621
1498
|
React.createElement('span', { className: 'dsub-heroModel' }, a.model || 'active model'),
|
|
1622
1499
|
),
|
|
@@ -1680,7 +1557,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
|
|
|
1680
1557
|
React.createElement('div', { className: 'dsub-accNameRow' },
|
|
1681
1558
|
React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
|
|
1682
1559
|
React.createElement('span', { className: 'dsub-accName' }, a.provider.toUpperCase() + (a.index ? ' #' + a.index : '')),
|
|
1683
|
-
isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: '
|
|
1560
|
+
isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent)', color: 'var(--dsw-alias-state-success-primary)' } }, 'ACTIVE') : null,
|
|
1684
1561
|
a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
|
|
1685
1562
|
),
|
|
1686
1563
|
React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
|
package/lib/crypto.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { scryptSync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
|
|
2
2
|
|
|
3
|
-
// AES-256-GCM
|
|
4
|
-
//
|
|
3
|
+
// AES-256-GCM with a passphrase-derived key (scrypt, no separately stored salt -
|
|
4
|
+
// the salt is stored at the head of the block).
|
|
5
5
|
export function encryptWithPassphrase(plainText, passphrase) {
|
|
6
6
|
const salt = randomBytes(16)
|
|
7
7
|
const key = scryptSync(String(passphrase), salt, 32)
|
|
@@ -9,13 +9,13 @@ export function encryptWithPassphrase(plainText, passphrase) {
|
|
|
9
9
|
const cipher = createCipheriv("aes-256-gcm", key, iv)
|
|
10
10
|
const encrypted = Buffer.concat([cipher.update(String(plainText), "utf8"), cipher.final()])
|
|
11
11
|
const tag = cipher.getAuthTag()
|
|
12
|
-
//
|
|
12
|
+
// format: magic + salt + iv + tag + data
|
|
13
13
|
return "DSHE1:" + Buffer.concat([salt, iv, tag, encrypted]).toString("base64")
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function decryptWithPassphrase(payload, passphrase) {
|
|
17
17
|
const raw = String(payload || "")
|
|
18
|
-
if (!raw.startsWith("DSHE1:")) throw new Error("
|
|
18
|
+
if (!raw.startsWith("DSHE1:")) throw new Error("unknown export format")
|
|
19
19
|
const buf = Buffer.from(raw.slice(6), "base64")
|
|
20
20
|
const salt = buf.subarray(0, 16)
|
|
21
21
|
const iv = buf.subarray(16, 28)
|
package/lib/google-validation.js
CHANGED
|
@@ -121,7 +121,7 @@ export function googleLicenseMessage(status, bodyText) {
|
|
|
121
121
|
const api = parseGoogleApiError(bodyText)
|
|
122
122
|
const msg = String(api?.message || bodyText || '')
|
|
123
123
|
if (/do not have a valid license|#3501|PERMISSION_DENIED/i.test(msg)) {
|
|
124
|
-
return '
|
|
124
|
+
return 'Google account has no active Gemini Code Assist / Antigravity license (#3501), or no Google Cloud project is attached. Check the subscription at https://one.google.com or connect a different Google account.'
|
|
125
125
|
}
|
|
126
126
|
return ''
|
|
127
127
|
}
|
package/lib/health.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Per-account health score for smart rotation.
|
|
2
|
+
// Numbers mirror dsh-key-rotation pool.js: switch 5, exhaustion 10, bad key 15.
|
|
3
3
|
const PENALTY = { switch: 5, exhaust: 10, broken: 15 }
|
|
4
4
|
|
|
5
5
|
export function emptyHealth() {
|
package/lib/history.js
CHANGED
|
@@ -2,9 +2,9 @@ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { homedir } from 'node:os'
|
|
4
4
|
|
|
5
|
-
// #65:
|
|
6
|
-
// ~/.dsh/storages/dsh-subscriptions/history.json (JSON
|
|
7
|
-
//
|
|
5
|
+
// #65: request and cost history. Stored in
|
|
6
|
+
// ~/.dsh/storages/dsh-subscriptions/history.json (JSON array, newest first).
|
|
7
|
+
// All IO is best-effort and synchronous: a write failure must never crash the harness.
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_HISTORY_DIR = join(homedir(), '.dsh', 'storages', 'dsh-subscriptions')
|
|
10
10
|
|
|
@@ -42,7 +42,7 @@ export class HistoryStore {
|
|
|
42
42
|
} catch { /* best-effort */ }
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/** Add one entry. Returns the history length. */
|
|
46
46
|
add(entry) {
|
|
47
47
|
if (!entry || typeof entry !== 'object') return this.rows.length
|
|
48
48
|
this.rows.unshift({ ts: Date.now(), ...entry })
|
|
@@ -51,7 +51,7 @@ export class HistoryStore {
|
|
|
51
51
|
return this.rows.length
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/** Last N entries (newest first). */
|
|
55
55
|
recent(n) {
|
|
56
56
|
return this.rows.slice(0, n)
|
|
57
57
|
}
|