@goodandready/dsh-subscriptions 0.6.86 → 0.6.88
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 +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/accounts.js +4 -0
- package/lib/client.js +119 -165
- package/lib/config-schema.js +8 -0
- package/lib/http.js +16 -1
- package/lib/index.js +2 -8
- package/lib/routes/accounts.js +1 -108
- package/lib/routes/proxy.js +1 -1
- package/lib/updater.js +2 -47
- package/lib/vendor-factory.js +16 -9
- package/lib/vendors/codex.js +5 -5
- package/lib/vendors/cody.js +1 -4
- package/lib/vendors/copilot.js +4 -0
- package/lib/vendors/ernie.js +0 -1
- package/lib/vendors/perplexity.js +3 -2
- package/lib/vendors/replit.js +1 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -311,6 +311,7 @@ dsh-subscriptions:
|
|
|
311
311
|
- Grok `baseUrl` overrides must point to `api.x.ai`, `grok.com` / `*.grok.com`, or a loopback host; other hosts are rejected with `GROK_UNTRUSTED_HOST` so bearer tokens never leave trusted xAI infrastructure.
|
|
312
312
|
- Antigravity CLI imports must configure `antigravityClientSecret`; the embedded default was removed.
|
|
313
313
|
- `copilotCliPath` must point to a `copilot`, `github-copilot-cli` or `copilot-cli` binary; rename custom wrappers.
|
|
314
|
+
- Credential backup uses `/dsh-subscriptions/vault/export` and `/vault/import`; the legacy `/export`, `/import`, `/export-backup` and `/import-backup` routes were removed.
|
|
314
315
|
- The error buffer and diagnostics reports redact bearer tokens, API keys, JWTs and `key=value` credentials before storing or returning them.
|
|
315
316
|
|
|
316
317
|
|
package/README.ru.md
CHANGED
|
@@ -284,6 +284,7 @@ dsh-subscriptions:
|
|
|
284
284
|
- Переопределение Grok `baseUrl` должно указывать на `api.x.ai`, `grok.com` / `*.grok.com` или loopback-хост; остальные хосты отклоняются с `GROK_UNTRUSTED_HOST`, чтобы bearer-токен не покидал доверенную инфраструктуру xAI.
|
|
285
285
|
- Импорт Antigravity CLI требует явного `antigravityClientSecret`; встроенный default удалён.
|
|
286
286
|
- `copilotCliPath` должен указывать на бинарь `copilot`, `github-copilot-cli` или `copilot-cli`; кастомные обёртки переименуйте.
|
|
287
|
+
- Бэкап credentials использует `/dsh-subscriptions/vault/export` и `/vault/import`; legacy-маршруты `/export`, `/import`, `/export-backup` и `/import-backup` удалены.
|
|
287
288
|
- Error buffer и диагностические отчёты редактируют bearer-токены, API-ключи, JWT и `key=value`-credentials до сохранения и выдачи.
|
|
288
289
|
|
|
289
290
|
|
package/README.zh.md
CHANGED
|
@@ -190,6 +190,7 @@ dsh plugin --profile web add @goodandready/dsh-subscriptions
|
|
|
190
190
|
- Grok `baseUrl` 覆盖只能指向 `api.x.ai`、`grok.com` / `*.grok.com` 或回环主机;其他主机会以 `GROK_UNTRUSTED_HOST` 拒绝,避免 bearer 令牌离开可信的 xAI 基础设施。
|
|
191
191
|
- Antigravity CLI 导入必须配置 `antigravityClientSecret`;内置默认值已移除。
|
|
192
192
|
- `copilotCliPath` 必须指向 `copilot`、`github-copilot-cli` 或 `copilot-cli` 可执行文件;自定义包装器请重命名。
|
|
193
|
+
- 凭据备份使用 `/dsh-subscriptions/vault/export` 和 `/vault/import`;旧路由 `/export`、`/import`、`/export-backup`、`/import-backup` 已移除。
|
|
193
194
|
- 错误缓冲区和诊断报告会在存储或返回前脱敏 bearer 令牌、API 密钥、JWT 和 `key=value` 凭据。
|
|
194
195
|
|
|
195
196
|
|
package/lib/accounts.js
CHANGED
|
@@ -380,6 +380,10 @@ export function createAccountStore({ credentials, getConfig, fetchImpl, fetchFor
|
|
|
380
380
|
pacePerHour: info.pacePerHour || 0,
|
|
381
381
|
maxConcurrency: slot.maxConcurrency !== undefined ? slot.maxConcurrency : (curConfig.accountMaxConcurrency || 1),
|
|
382
382
|
maxQueueSize: slot.maxQueueSize > 0 ? Number(slot.maxQueueSize) : (curConfig.maxQueueSize || 20),
|
|
383
|
+
vipOnly: !!slot.vipOnly,
|
|
384
|
+
tags: Array.isArray(slot.tags) ? slot.tags : [],
|
|
385
|
+
weight: Number(slot.weight) || 0,
|
|
386
|
+
isPro: !!slot.isPro,
|
|
383
387
|
})
|
|
384
388
|
}
|
|
385
389
|
return out
|
package/lib/client.js
CHANGED
|
@@ -68,23 +68,28 @@ window.__ModuleLoader__.load({
|
|
|
68
68
|
} catch { /* invalid url */ }
|
|
69
69
|
return ''
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
async function copyText(text) {
|
|
73
|
+
try {
|
|
74
|
+
if (typeof navigator === 'undefined' || !navigator.clipboard || typeof navigator.clipboard.writeText !== 'function') return false
|
|
75
|
+
await navigator.clipboard.writeText(String(text == null ? '' : text))
|
|
76
|
+
return true
|
|
77
|
+
} catch {
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
71
81
|
|
|
72
82
|
const en = {
|
|
73
83
|
"syncModels": "Sync Models",
|
|
74
84
|
"syncModelsTitle": "Discover and synchronize available models from vendor API",
|
|
75
85
|
"modelsSynced": "Models synchronized",
|
|
76
|
-
"doctorDiagnosis": "Doctor Verdict",
|
|
77
86
|
"recommendation": "Recommendation",
|
|
78
|
-
"pingTitle": "Measure network latency to provider API",
|
|
79
87
|
"notConnected": "Not connected",
|
|
80
88
|
"verifyAccount": "Verify account",
|
|
81
89
|
"coolingDown": "Cooling down",
|
|
82
90
|
"usageFull": "Usage 100%",
|
|
83
91
|
"connected": "Connected",
|
|
84
92
|
"title": "Subscriptions",
|
|
85
|
-
"cardIntro": "Subscription accounts, rotation and limits.",
|
|
86
|
-
"show": "Show",
|
|
87
|
-
"hide": "Hide",
|
|
88
93
|
"intro": "Log in with a consumer subscription. Tokens stay on the host credentials store. The browser never reads them back. After Connect, if the provider lands on localhost or a vendor page, paste the redirected URL or the code here.",
|
|
89
94
|
"useOrigin": "Use this Web UI origin as OAuth redirect_uri",
|
|
90
95
|
"useOriginHint": "Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.",
|
|
@@ -125,8 +130,6 @@ window.__ModuleLoader__.load({
|
|
|
125
130
|
"copilotCliPathHint": "Optional path to copilot or github-copilot-cli binary.",
|
|
126
131
|
"ollamaFallback": "Ollama local fallback",
|
|
127
132
|
"ollamaFallbackHint": "Automatically route queries to a local Ollama instance when cloud quota is exhausted.",
|
|
128
|
-
"ollamaBaseUrl": "Ollama Base URL",
|
|
129
|
-
"ollamaFallbackModel": "Ollama Fallback Model",
|
|
130
133
|
"advancedSettings": "Advanced Settings & Monitoring",
|
|
131
134
|
"cooldownMs": "Failure cooldown (minutes)",
|
|
132
135
|
"cooldownMsHint": "How long to pause a failing or rate-limited account before retrying.",
|
|
@@ -136,12 +139,10 @@ window.__ModuleLoader__.load({
|
|
|
136
139
|
"notifyLimitsHint": "Show notification alerts when account usage reaches quota ceiling.",
|
|
137
140
|
"hostOnline": "Host online",
|
|
138
141
|
"hostOffline": "Host unreachable",
|
|
139
|
-
"statsTitle": "Session Telemetry",
|
|
140
142
|
"statsRequests": "Successful / Total Requests",
|
|
141
143
|
"statsLatency": "Avg Response Latency",
|
|
142
144
|
"statsSuccessRate": "Session Success Rate",
|
|
143
145
|
"statsLastActivity": "Last Activity",
|
|
144
|
-
"smokeTitle": "Connection Smoke Test (Ping)",
|
|
145
146
|
"smokeDesc": "Test live upstream connectivity and measure round-trip latency to the subscription provider.",
|
|
146
147
|
"runSmoke": "⚡ Run Smoke Test (Ping)",
|
|
147
148
|
"smokeTesting": "Pinging…",
|
|
@@ -179,10 +180,7 @@ window.__ModuleLoader__.load({
|
|
|
179
180
|
"capabilitiesHint": "Subscription capabilities, context window and model features",
|
|
180
181
|
"orPasteVaultPayload": "or paste vault payload below",
|
|
181
182
|
"workspaceTitle": "Organization / Workspace",
|
|
182
|
-
"workspaceSelect": "Select active organization/workspace",
|
|
183
|
-
"personalWorkspace": "Personal Default",
|
|
184
183
|
"windowResetsAt": "Window resets at",
|
|
185
|
-
"quotaRecovery": "Quota recovery",
|
|
186
184
|
"toastCooldownRecovered": "is now available (cooldown expired)",
|
|
187
185
|
"modelsCatalogTitle": "Curated Subscription Models",
|
|
188
186
|
"modelsCatalogDesc": "Available model catalog across active subscriptions in DeepSeek Harness.",
|
|
@@ -201,7 +199,6 @@ window.__ModuleLoader__.load({
|
|
|
201
199
|
"resetCreditsHint": "ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.",
|
|
202
200
|
"resetCreditsAction": "Reset ChatGPT Limit",
|
|
203
201
|
"usageLimitsTitle": "Usage Limits",
|
|
204
|
-
"slotDetailsTitle": "Slot Details",
|
|
205
202
|
"resetAvailable": "Reset attempts available",
|
|
206
203
|
"resetExpires": "earliest expires",
|
|
207
204
|
"resetAck": "I understand one attempt will be consumed",
|
|
@@ -222,23 +219,17 @@ window.__ModuleLoader__.load({
|
|
|
222
219
|
"tokensSaved": "Saved Tokens",
|
|
223
220
|
"subsPill": "SUBS",
|
|
224
221
|
"subsModalTitle": "Subscription Hub",
|
|
225
|
-
"subsActiveHero": "Active Subscription",
|
|
226
222
|
"subsPoolTitle": "Provider Accounts",
|
|
227
223
|
"subsRefresh": "Refresh",
|
|
228
224
|
"subsOpenSettings": "All Settings →",
|
|
229
|
-
"subsFastMode": "Fast Mode 1.5x",
|
|
230
|
-
"subsHealthy": "Healthy",
|
|
231
225
|
"subsFree": "free",
|
|
232
226
|
"subsUsed": "used",
|
|
233
|
-
"subsModalClose": "Close",
|
|
234
227
|
"subsLogged": "connected",
|
|
235
228
|
"subsNotLogged": "not connected",
|
|
236
229
|
"subsCooldown": "cooldown",
|
|
237
|
-
"hudTitle": "Subscription balance",
|
|
238
230
|
"settingsLoading": "Loading settings…",
|
|
239
231
|
"settingsUnavailable": "Settings are unavailable",
|
|
240
232
|
"settingsRetry": "Retry",
|
|
241
|
-
"hudHint": "drag · edge docks · click refresh",
|
|
242
233
|
"fcCalibrating": "calibrating…",
|
|
243
234
|
"fcIdle": "no usage",
|
|
244
235
|
"fcHours": "h",
|
|
@@ -266,23 +257,14 @@ window.__ModuleLoader__.load({
|
|
|
266
257
|
"addAccount": "+ Add account",
|
|
267
258
|
"save": "Save",
|
|
268
259
|
"saved": "Saved",
|
|
269
|
-
"loading": "Loading…",
|
|
270
|
-
"unavailable": "Settings are unavailable",
|
|
271
260
|
"accountLabel": "Account",
|
|
272
261
|
"plan": "Plan",
|
|
273
262
|
"storedAs": "Stored as",
|
|
274
|
-
"switchLabel": "Provider",
|
|
275
|
-
"chipActive": "Subscriptions",
|
|
276
|
-
"expiryLabel": "expires",
|
|
277
263
|
"slashLogin": "Login to a subscription provider",
|
|
278
264
|
"slashLogout": "Log out a subscription provider",
|
|
279
|
-
"slashStatus": "Subscription status",
|
|
280
|
-
"none": "none",
|
|
281
265
|
"forecast": "≈",
|
|
282
266
|
"windowPrimary": "Primary window",
|
|
283
267
|
"windowSecondary": "Secondary window",
|
|
284
|
-
"resetLabel": "reset",
|
|
285
|
-
"check": "Check",
|
|
286
268
|
"checking": "Checking…",
|
|
287
269
|
"importToken": "Save token",
|
|
288
270
|
"importTokenPlace": "Paste token or API key",
|
|
@@ -292,7 +274,15 @@ window.__ModuleLoader__.load({
|
|
|
292
274
|
"howToGetToken": "❓ Instructions",
|
|
293
275
|
"howToGetTokenTitle": "How to get token / API key for this provider",
|
|
294
276
|
"instructionsTitle": "How to obtain token",
|
|
295
|
-
"
|
|
277
|
+
"lang": "en",
|
|
278
|
+
"cardIntro": "Subscription accounts, rotation and limits.",
|
|
279
|
+
"loading": "Loading…",
|
|
280
|
+
"unavailable": "Settings are unavailable",
|
|
281
|
+
"error": "Error",
|
|
282
|
+
"settingsError": "Settings error",
|
|
283
|
+
"cancel": "Cancel",
|
|
284
|
+
"quota": "Quota",
|
|
285
|
+
"subs5hWindow": "5h window",
|
|
296
286
|
"reconnectRequired": "Reconnect required",
|
|
297
287
|
"reconnectNow": "Re-authenticate now",
|
|
298
288
|
"scopeWarning": "OAuth scope warning",
|
|
@@ -300,8 +290,6 @@ window.__ModuleLoader__.load({
|
|
|
300
290
|
"sessionRevokedBadge": "Revoked / Expired",
|
|
301
291
|
"sessionRevokedWarning": "Session expired / revoked — re-authorization required",
|
|
302
292
|
"manualTitle": "If the browser did not come back",
|
|
303
|
-
"updateChecking": "Checking updates…",
|
|
304
|
-
"updateUpToDate": "Up to date",
|
|
305
293
|
"updateAvailable": "Update available",
|
|
306
294
|
"updateNow": "Update",
|
|
307
295
|
"updating": "Updating…",
|
|
@@ -326,7 +314,6 @@ window.__ModuleLoader__.load({
|
|
|
326
314
|
"backupImportAction": "Import",
|
|
327
315
|
"backupPassphrase": "Vault Passphrase (min 12 chars)",
|
|
328
316
|
"backupExportHint": "Encrypt connected subscription accounts and slots with AES-256-GCM. ⚠️ Contains sensitive OAuth refresh tokens; use a strong password (min 12 chars).",
|
|
329
|
-
"vaultTokenWarning": "⚠️ Contains sensitive OAuth refresh tokens",
|
|
330
317
|
"backupImportHint": "Restore encrypted subscription accounts and credentials from a vault payload.",
|
|
331
318
|
"backupPayloadPlaceholder": "Paste DSHE... encrypted string here",
|
|
332
319
|
"backupExportOk": "Backup exported successfully",
|
|
@@ -341,10 +328,7 @@ window.__ModuleLoader__.load({
|
|
|
341
328
|
"resetsIn": "Resets in",
|
|
342
329
|
"toastQuarantined": "Account placed in quarantine",
|
|
343
330
|
"toastReleased": "Account active and healthy",
|
|
344
|
-
"toastQuotaWarning": "Quota threshold exceeded",
|
|
345
|
-
"enableToasts": "In-app toast alerts",
|
|
346
331
|
"headlessTip": "Headless / Remote Server?",
|
|
347
|
-
"headlessSshCommand": "SSH Port Forward Command:",
|
|
348
332
|
"headlessCopySsh": "Copy SSH Command",
|
|
349
333
|
"headlessCopied": "Copied!",
|
|
350
334
|
"headlessScanQr": "Scan with Mobile to Authenticate:",
|
|
@@ -382,23 +366,14 @@ window.__ModuleLoader__.load({
|
|
|
382
366
|
"wizardAll": "Account Wizard",
|
|
383
367
|
"wizardIncludeNetwork": "Include active network checks (proxy ping & model catalog discovery)",
|
|
384
368
|
"wizardRunning": "Running diagnostic checklist...",
|
|
385
|
-
"wizardStepCreds": "Credentials & Expiry",
|
|
386
|
-
"wizardStepProxy": "Proxy & Gateway",
|
|
387
|
-
"wizardStepToken": "Token Validation",
|
|
388
|
-
"wizardStepCatalog": "Model Catalog Reachability",
|
|
389
369
|
"wizardHealthy": "Healthy",
|
|
390
370
|
"wizardDegraded": "Degraded / Warnings",
|
|
391
371
|
"wizardFailing": "Issues Detected",
|
|
392
372
|
"wizardRecommendations": "Recommendations & Next Steps",
|
|
393
373
|
"wizardCopy": "Copy Masked Diagnostic Report",
|
|
394
374
|
"cachePinnedBadge": "Cache Pinned",
|
|
395
|
-
"field.enableToasts": "Show popup toast notifications on quarantine or quota events",
|
|
396
375
|
"reasoningEffortTitle": "Reasoning Effort Control",
|
|
397
376
|
"reasoningEffortDesc": "Configure reasoning depth for models with extended thinking capabilities.",
|
|
398
|
-
"reasoningLevelLow": "Low",
|
|
399
|
-
"reasoningLevelMed": "Medium",
|
|
400
|
-
"reasoningLevelHigh": "High",
|
|
401
|
-
"reasoningLevelMax": "Max",
|
|
402
377
|
"reasoningLevelOff": "Off"
|
|
403
378
|
};
|
|
404
379
|
|
|
@@ -406,18 +381,13 @@ window.__ModuleLoader__.load({
|
|
|
406
381
|
"syncModels": "同步模型",
|
|
407
382
|
"syncModelsTitle": "从服务商 API 实时发现并同步可用模型列表",
|
|
408
383
|
"modelsSynced": "模型已同步",
|
|
409
|
-
"doctorDiagnosis": "诊断结论",
|
|
410
384
|
"recommendation": "修复建议",
|
|
411
|
-
"pingTitle": "测试到服务商 API 的网络往返延迟",
|
|
412
385
|
"notConnected": "未连接",
|
|
413
386
|
"verifyAccount": "需要验证账户",
|
|
414
387
|
"coolingDown": "冷却等待中",
|
|
415
388
|
"usageFull": "用量已达 100%",
|
|
416
389
|
"connected": "已连接",
|
|
417
390
|
"title": "个人订阅管理",
|
|
418
|
-
"cardIntro": "AI 服务订阅账户、自动轮换与速率限制管理。",
|
|
419
|
-
"show": "展开",
|
|
420
|
-
"hide": "收起",
|
|
421
391
|
"intro": "通过个人订阅登录。凭据安全保存在服务器端凭据库中,浏览器不回读敏感令牌。点击“连接”后,若页面跳转至 localhost 或服务商页面,请将重定向网址或授权代码粘贴到下方。",
|
|
422
392
|
"useOrigin": "将当前 Web UI 地址作为 OAuth 重定向地址 (redirect_uri)",
|
|
423
393
|
"useOriginHint": "除非您为当前域名申请了专属 OAuth 客户端,否则请保持关闭。官方 CLI 客户端通常需使用其公开重定向地址并配合粘贴代码流程。",
|
|
@@ -458,8 +428,6 @@ window.__ModuleLoader__.load({
|
|
|
458
428
|
"copilotCliPathHint": "可选的 copilot 或 github-copilot-cli 二进制文件路径。",
|
|
459
429
|
"ollamaFallback": "Ollama 本地模型自动兜底",
|
|
460
430
|
"ollamaFallbackHint": "当云端配额耗尽或服务商故障时,自动将请求转发给本地 Ollama 实例。",
|
|
461
|
-
"ollamaBaseUrl": "Ollama 服务基础 URL",
|
|
462
|
-
"ollamaFallbackModel": "Ollama 兜底模型名称",
|
|
463
431
|
"advancedSettings": "高级配置与监控",
|
|
464
432
|
"cooldownMs": "故障与限流冷却时间(分钟)",
|
|
465
433
|
"cooldownMsHint": "当账户遇到 429 或故障时暂停该账户的分钟数。",
|
|
@@ -469,12 +437,10 @@ window.__ModuleLoader__.load({
|
|
|
469
437
|
"notifyLimitsHint": "当账户使用量达到上限时触发系统通知提醒。",
|
|
470
438
|
"hostOnline": "主机在线",
|
|
471
439
|
"hostOffline": "主机无法连接",
|
|
472
|
-
"statsTitle": "会话遥测统计",
|
|
473
440
|
"statsRequests": "成功 / 总请求数",
|
|
474
441
|
"statsLatency": "平均响应延迟",
|
|
475
442
|
"statsSuccessRate": "会话成功率",
|
|
476
443
|
"statsLastActivity": "最近活跃时间",
|
|
477
|
-
"smokeTitle": "链路连通性冒烟测试 (Ping)",
|
|
478
444
|
"smokeDesc": "向当前订阅服务商发起实时探活并测量双向网络往返延迟 (RTT)。",
|
|
479
445
|
"runSmoke": "⚡ 执行冒烟探活 (Ping)",
|
|
480
446
|
"smokeTesting": "正在探活…",
|
|
@@ -512,10 +478,7 @@ window.__ModuleLoader__.load({
|
|
|
512
478
|
"capabilitiesHint": "订阅账号模型上下文窗口与特权特性",
|
|
513
479
|
"orPasteVaultPayload": "或在下方粘贴保险库文本",
|
|
514
480
|
"workspaceTitle": "工作区与组织",
|
|
515
|
-
"workspaceSelect": "选择生效的工作区或组织",
|
|
516
|
-
"personalWorkspace": "个人默认",
|
|
517
481
|
"windowResetsAt": "额度重置于",
|
|
518
|
-
"quotaRecovery": "额度恢复",
|
|
519
482
|
"toastCooldownRecovered": "已解除冷却恢复可用",
|
|
520
483
|
"modelsCatalogTitle": "精选订阅模型",
|
|
521
484
|
"modelsCatalogDesc": "当前 DeepSeek Harness 活跃订阅下可用模型一览。",
|
|
@@ -534,7 +497,6 @@ window.__ModuleLoader__.load({
|
|
|
534
497
|
"resetCreditsHint": "ChatGPT 重置卡:紧急重置 5 小时额度窗口。扣除前需等待 5 秒倒计时确认。",
|
|
535
498
|
"resetCreditsAction": "重置 ChatGPT 限制",
|
|
536
499
|
"usageLimitsTitle": "用量限制",
|
|
537
|
-
"slotDetailsTitle": "槽位详情",
|
|
538
500
|
"resetAvailable": "可用重置次数",
|
|
539
501
|
"resetExpires": "最早到期时间",
|
|
540
502
|
"resetAck": "我确认将消耗一次重置机会",
|
|
@@ -555,23 +517,17 @@ window.__ModuleLoader__.load({
|
|
|
555
517
|
"tokensSaved": "已节省 Token",
|
|
556
518
|
"subsPill": "订阅",
|
|
557
519
|
"subsModalTitle": "订阅管理中心",
|
|
558
|
-
"subsActiveHero": "当前主用订阅",
|
|
559
520
|
"subsPoolTitle": "服务商账户池",
|
|
560
521
|
"subsRefresh": "刷新",
|
|
561
522
|
"subsOpenSettings": "全部设置 →",
|
|
562
|
-
"subsFastMode": "加速模式 1.5x",
|
|
563
|
-
"subsHealthy": "健康运行中",
|
|
564
523
|
"subsFree": "空闲可用",
|
|
565
524
|
"subsUsed": "已用",
|
|
566
|
-
"subsModalClose": "关闭",
|
|
567
525
|
"subsLogged": "已连接",
|
|
568
526
|
"subsNotLogged": "未连接",
|
|
569
527
|
"subsCooldown": "冷却中",
|
|
570
|
-
"hudTitle": "订阅额度余量",
|
|
571
528
|
"settingsLoading": "正在加载设置…",
|
|
572
529
|
"settingsUnavailable": "设置加载失败",
|
|
573
530
|
"settingsRetry": "重试",
|
|
574
|
-
"hudHint": "可拖拽 · 边缘停靠 · 点击刷新",
|
|
575
531
|
"fcCalibrating": "校准中…",
|
|
576
532
|
"fcIdle": "暂无消耗",
|
|
577
533
|
"fcHours": "小时",
|
|
@@ -599,23 +555,14 @@ window.__ModuleLoader__.load({
|
|
|
599
555
|
"addAccount": "+ 添加账户",
|
|
600
556
|
"save": "保存",
|
|
601
557
|
"saved": "已保存",
|
|
602
|
-
"loading": "加载中…",
|
|
603
|
-
"unavailable": "设置不可用",
|
|
604
558
|
"accountLabel": "账户",
|
|
605
559
|
"plan": "套餐方案",
|
|
606
560
|
"storedAs": "存储标识",
|
|
607
|
-
"switchLabel": "切换服务商",
|
|
608
|
-
"chipActive": "已启用订阅",
|
|
609
|
-
"expiryLabel": "到期时间",
|
|
610
561
|
"slashLogin": "登录订阅服务商",
|
|
611
562
|
"slashLogout": "登出订阅服务商",
|
|
612
|
-
"slashStatus": "查看订阅状态",
|
|
613
|
-
"none": "无",
|
|
614
563
|
"forecast": "≈",
|
|
615
564
|
"windowPrimary": "主配额窗口",
|
|
616
565
|
"windowSecondary": "次配额窗口",
|
|
617
|
-
"resetLabel": "重置倒计时",
|
|
618
|
-
"check": "检测",
|
|
619
566
|
"checking": "检测中…",
|
|
620
567
|
"importToken": "保存令牌",
|
|
621
568
|
"importTokenPlace": "粘贴令牌或 API Key",
|
|
@@ -625,7 +572,15 @@ window.__ModuleLoader__.load({
|
|
|
625
572
|
"howToGetToken": "❓ 获取指引",
|
|
626
573
|
"howToGetTokenTitle": "如何获取此服务商的令牌或 API 密钥",
|
|
627
574
|
"instructionsTitle": "获取令牌指引",
|
|
628
|
-
"
|
|
575
|
+
"lang": "zh",
|
|
576
|
+
"cardIntro": "AI 服务订阅账户、自动轮换与速率限制管理。",
|
|
577
|
+
"loading": "加载中…",
|
|
578
|
+
"unavailable": "设置不可用",
|
|
579
|
+
"error": "错误",
|
|
580
|
+
"settingsError": "设置错误",
|
|
581
|
+
"cancel": "取消",
|
|
582
|
+
"quota": "配额",
|
|
583
|
+
"subs5hWindow": "5小时窗口",
|
|
629
584
|
"reconnectRequired": "需要重新连接",
|
|
630
585
|
"reconnectNow": "立即重新认证",
|
|
631
586
|
"scopeWarning": "OAuth权限范围警告",
|
|
@@ -633,8 +588,6 @@ window.__ModuleLoader__.load({
|
|
|
633
588
|
"sessionRevokedBadge": "已失效 / 过期",
|
|
634
589
|
"sessionRevokedWarning": "会话已过期或失效 — 需要重新授权",
|
|
635
590
|
"manualTitle": "若浏览器未能自动跳转回调",
|
|
636
|
-
"updateChecking": "检查更新中…",
|
|
637
|
-
"updateUpToDate": "已是最新版本",
|
|
638
591
|
"updateAvailable": "有新版本可用",
|
|
639
592
|
"updateNow": "立即更新",
|
|
640
593
|
"updating": "正在更新…",
|
|
@@ -659,7 +612,6 @@ window.__ModuleLoader__.load({
|
|
|
659
612
|
"backupImportAction": "导入",
|
|
660
613
|
"backupPassphrase": "凭据库密码(至少 12 位)",
|
|
661
614
|
"backupExportHint": "使用 AES-256-GCM 安全加密导出所有订阅账户与配置。⚠️ 包含敏感的 OAuth 刷新令牌,请使用至少 12 位的强密码保护。",
|
|
662
|
-
"vaultTokenWarning": "⚠️ 包含敏感的 OAuth 刷新令牌",
|
|
663
615
|
"backupImportHint": "从已加密的备份内容中导入并恢复订阅账户与凭据。",
|
|
664
616
|
"backupPayloadPlaceholder": "在此粘贴 DSHE... 加密内容",
|
|
665
617
|
"backupExportOk": "备份导出成功",
|
|
@@ -674,7 +626,6 @@ window.__ModuleLoader__.load({
|
|
|
674
626
|
"resetsIn": "重置倒计时",
|
|
675
627
|
"toastQuarantined": "账户进入隔离期",
|
|
676
628
|
"headlessTip": "无头环境 / 远程服务器?",
|
|
677
|
-
"headlessSshCommand": "SSH 端口转发命令:",
|
|
678
629
|
"headlessCopySsh": "复制 SSH 命令",
|
|
679
630
|
"headlessCopied": "已复制!",
|
|
680
631
|
"headlessScanQr": "使用手机扫描以登录:",
|
|
@@ -687,8 +638,6 @@ window.__ModuleLoader__.load({
|
|
|
687
638
|
"vaultSyncLastPush": "上次推送:",
|
|
688
639
|
"vaultSyncLastPull": "上次拉取:",
|
|
689
640
|
"toastReleased": "账户已解除隔离恢复使用",
|
|
690
|
-
"toastQuotaWarning": "额度预警",
|
|
691
|
-
"enableToasts": "应用内弹窗提醒",
|
|
692
641
|
"groupQueue": "并发限制与请求队列",
|
|
693
642
|
"enableQueue": "启用并发请求排队",
|
|
694
643
|
"enableQueueHint": "当账户达到并发上限时将多余请求排队等待,避免直接触发 429 报错",
|
|
@@ -715,23 +664,14 @@ window.__ModuleLoader__.load({
|
|
|
715
664
|
"wizardAll": "诊断向导",
|
|
716
665
|
"wizardIncludeNetwork": "包含活跃网络探测(代理连通性与模型目录探测)",
|
|
717
666
|
"wizardRunning": "正在执行诊断检查清单...",
|
|
718
|
-
"wizardStepCreds": "凭据完整性与有效期",
|
|
719
|
-
"wizardStepProxy": "网络代理与网关",
|
|
720
|
-
"wizardStepToken": "令牌有效性验证",
|
|
721
|
-
"wizardStepCatalog": "模型目录可达性",
|
|
722
667
|
"wizardHealthy": "健康正常",
|
|
723
668
|
"wizardDegraded": "降级预警",
|
|
724
669
|
"wizardFailing": "发现故障",
|
|
725
670
|
"wizardRecommendations": "排查建议与修复步骤",
|
|
726
671
|
"wizardCopy": "复制脱敏诊断报告",
|
|
727
672
|
"cachePinnedBadge": "缓存粘性",
|
|
728
|
-
"field.enableToasts": "当账户发生隔离或额度达到警戒线时显示即时弹窗提醒",
|
|
729
673
|
"reasoningEffortTitle": "推理强度控制",
|
|
730
674
|
"reasoningEffortDesc": "为具备扩展思考能力的模型配置推理深度与思考预算。",
|
|
731
|
-
"reasoningLevelLow": "低",
|
|
732
|
-
"reasoningLevelMed": "中",
|
|
733
|
-
"reasoningLevelHigh": "高",
|
|
734
|
-
"reasoningLevelMax": "最高",
|
|
735
675
|
"reasoningLevelOff": "关闭"
|
|
736
676
|
};
|
|
737
677
|
// #51: live countdown to the quota window reset (account.quota.resetAt).
|
|
@@ -971,7 +911,7 @@ window.__ModuleLoader__.load({
|
|
|
971
911
|
if (d && d.ok && d.report) setDiagReport(d.report)
|
|
972
912
|
const txt = d && d.ok ? JSON.stringify(d.report, null, 2) : ('error: ' + ((d && d.error && d.error.message) || 'unknown'))
|
|
973
913
|
setDiag(txt)
|
|
974
|
-
|
|
914
|
+
copyText(txt)
|
|
975
915
|
})
|
|
976
916
|
.catch((e) => setDiag('error: ' + String(e && e.message || e)))
|
|
977
917
|
}
|
|
@@ -993,7 +933,7 @@ window.__ModuleLoader__.load({
|
|
|
993
933
|
fetch('/dsh-subscriptions/vault/sync/status')
|
|
994
934
|
.then((r) => r.json())
|
|
995
935
|
.then((d) => { if (d && d.ok) setVaultSyncInfo(d.status) })
|
|
996
|
-
.catch(() =>
|
|
936
|
+
.catch((e) => setBackupErr(cleanErrorMessage(e && e.message ? e.message : e)))
|
|
997
937
|
}
|
|
998
938
|
|
|
999
939
|
const doVaultSyncPush = async () => {
|
|
@@ -1064,7 +1004,7 @@ window.__ModuleLoader__.load({
|
|
|
1064
1004
|
const doExportBackup = async () => {
|
|
1065
1005
|
setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
|
|
1066
1006
|
try {
|
|
1067
|
-
const res = await fetch('/dsh-subscriptions/export
|
|
1007
|
+
const res = await fetch('/dsh-subscriptions/vault/export', {
|
|
1068
1008
|
method: 'POST',
|
|
1069
1009
|
headers: { 'Content-Type': 'application/json' },
|
|
1070
1010
|
body: JSON.stringify({ passphrase: backupPass, algorithm: 'pbkdf2' }),
|
|
@@ -1082,7 +1022,7 @@ window.__ModuleLoader__.load({
|
|
|
1082
1022
|
const doImportBackup = async () => {
|
|
1083
1023
|
setBackupErr(''); setBackupInfo(''); setBackupLoading(true)
|
|
1084
1024
|
try {
|
|
1085
|
-
const res = await fetch('/dsh-subscriptions/import
|
|
1025
|
+
const res = await fetch('/dsh-subscriptions/vault/import', {
|
|
1086
1026
|
method: 'POST',
|
|
1087
1027
|
headers: { 'Content-Type': 'application/json' },
|
|
1088
1028
|
body: JSON.stringify({ passphrase: backupPass, vault: backupData.trim() }),
|
|
@@ -1229,6 +1169,19 @@ window.__ModuleLoader__.load({
|
|
|
1229
1169
|
const [err, setErr] = React.useState('')
|
|
1230
1170
|
const [info, setInfo] = React.useState('')
|
|
1231
1171
|
const [revision, setRevision] = React.useState(null)
|
|
1172
|
+
const timersRef = React.useRef(new Set())
|
|
1173
|
+
React.useEffect(() => () => {
|
|
1174
|
+
for (const id of timersRef.current) clearTimeout(id)
|
|
1175
|
+
timersRef.current.clear()
|
|
1176
|
+
}, [])
|
|
1177
|
+
const later = (fn, ms) => {
|
|
1178
|
+
const id = setTimeout(() => {
|
|
1179
|
+
timersRef.current.delete(id)
|
|
1180
|
+
fn()
|
|
1181
|
+
}, ms)
|
|
1182
|
+
timersRef.current.add(id)
|
|
1183
|
+
return id
|
|
1184
|
+
}
|
|
1232
1185
|
|
|
1233
1186
|
const applyPayload = (data) => {
|
|
1234
1187
|
if (data && data.revision != null) setRevision(data.revision)
|
|
@@ -1238,19 +1191,29 @@ window.__ModuleLoader__.load({
|
|
|
1238
1191
|
setModelsCatalog((data && data.modelsCatalog) || [])
|
|
1239
1192
|
}
|
|
1240
1193
|
|
|
1194
|
+
let reloadSeq = 0
|
|
1195
|
+
let reloadAbort = null
|
|
1241
1196
|
const reload = () => {
|
|
1197
|
+
const seq = ++reloadSeq
|
|
1198
|
+
if (reloadAbort) reloadAbort.abort()
|
|
1199
|
+
const controller = new AbortController()
|
|
1200
|
+
reloadAbort = controller
|
|
1242
1201
|
const t0 = Date.now()
|
|
1243
|
-
return fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
1202
|
+
return fetch('/dsh-subscriptions/config', { cache: 'no-store', signal: controller.signal })
|
|
1244
1203
|
.then((res) => res.json())
|
|
1245
1204
|
.then((data) => {
|
|
1205
|
+
if (seq !== reloadSeq) return data
|
|
1246
1206
|
setHostPing({ ok: true, latencyMs: Date.now() - t0 })
|
|
1247
1207
|
applyPayload(data)
|
|
1248
1208
|
reloadTelemetry()
|
|
1249
1209
|
return data
|
|
1250
1210
|
})
|
|
1251
1211
|
.catch((err) => {
|
|
1252
|
-
|
|
1253
|
-
|
|
1212
|
+
if (seq === reloadSeq && err && err.name !== 'AbortError') {
|
|
1213
|
+
setHostPing({ ok: false, latencyMs: null })
|
|
1214
|
+
throw err
|
|
1215
|
+
}
|
|
1216
|
+
return null
|
|
1254
1217
|
})
|
|
1255
1218
|
}
|
|
1256
1219
|
|
|
@@ -1258,7 +1221,10 @@ window.__ModuleLoader__.load({
|
|
|
1258
1221
|
let alive = true
|
|
1259
1222
|
reload().catch((e) => { if (alive) setErr(String(e && e.message ? e.message : e)) })
|
|
1260
1223
|
checkUpdater().catch(() => {})
|
|
1261
|
-
return () => {
|
|
1224
|
+
return () => {
|
|
1225
|
+
alive = false
|
|
1226
|
+
if (reloadAbort) reloadAbort.abort()
|
|
1227
|
+
}
|
|
1262
1228
|
}, [])
|
|
1263
1229
|
|
|
1264
1230
|
// #90: poll device login while any slot is in 'pending' state.
|
|
@@ -1313,7 +1279,7 @@ window.__ModuleLoader__.load({
|
|
|
1313
1279
|
throw new Error((data.error && data.error.message) || ('HTTP ' + res.status))
|
|
1314
1280
|
}
|
|
1315
1281
|
applyPayload(data)
|
|
1316
|
-
setSaved(true);
|
|
1282
|
+
setSaved(true); later(() => setSaved(false), 2000)
|
|
1317
1283
|
}
|
|
1318
1284
|
|
|
1319
1285
|
const connect = async (provider, index) => {
|
|
@@ -1438,7 +1404,7 @@ window.__ModuleLoader__.load({
|
|
|
1438
1404
|
if (!res.ok) throw new Error((data.error && data.error.message) || ("HTTP " + res.status))
|
|
1439
1405
|
setAccounts(data.accounts || [])
|
|
1440
1406
|
setInfo(t("toastQuarantined"))
|
|
1441
|
-
|
|
1407
|
+
later(() => setInfo(""), 3000)
|
|
1442
1408
|
} catch (e) { setErr(cleanErrorMessage(e.message || e)) }
|
|
1443
1409
|
}
|
|
1444
1410
|
|
|
@@ -1457,7 +1423,7 @@ window.__ModuleLoader__.load({
|
|
|
1457
1423
|
setAccounts(data.accounts || [])
|
|
1458
1424
|
if (data.success || data.released) {
|
|
1459
1425
|
setInfo(t("toastReleased"))
|
|
1460
|
-
|
|
1426
|
+
later(() => setInfo(""), 3000)
|
|
1461
1427
|
} else if (data.error) {
|
|
1462
1428
|
setErr(cleanErrorMessage(data.error))
|
|
1463
1429
|
}
|
|
@@ -1509,7 +1475,7 @@ window.__ModuleLoader__.load({
|
|
|
1509
1475
|
if (data.config) applyPayload(data)
|
|
1510
1476
|
else await reload()
|
|
1511
1477
|
setInfo(t('importLocalSuccess'))
|
|
1512
|
-
|
|
1478
|
+
later(() => setInfo(''), 3000)
|
|
1513
1479
|
}
|
|
1514
1480
|
|
|
1515
1481
|
const importToken = async (provider, index) => {
|
|
@@ -2160,7 +2126,7 @@ window.__ModuleLoader__.load({
|
|
|
2160
2126
|
diag ? h('div', { className: 'dsub-row' },
|
|
2161
2127
|
h('button', {
|
|
2162
2128
|
type: 'button', className: 'dsub-mini',
|
|
2163
|
-
onClick: () => {
|
|
2129
|
+
onClick: () => { copyText(diag) },
|
|
2164
2130
|
}, t('diagCopy')),
|
|
2165
2131
|
h('pre', { className: 'dsub-diag' }, diag),
|
|
2166
2132
|
) : null,
|
|
@@ -2255,7 +2221,7 @@ window.__ModuleLoader__.load({
|
|
|
2255
2221
|
className: 'dsub-mini',
|
|
2256
2222
|
onClick: () => {
|
|
2257
2223
|
const txt = JSON.stringify(wizardModal.report, null, 2)
|
|
2258
|
-
|
|
2224
|
+
copyText(txt)
|
|
2259
2225
|
},
|
|
2260
2226
|
}, '📋 ' + t('wizardCopy')),
|
|
2261
2227
|
),
|
|
@@ -2327,7 +2293,7 @@ window.__ModuleLoader__.load({
|
|
|
2327
2293
|
h('button', {
|
|
2328
2294
|
type: 'button',
|
|
2329
2295
|
className: 'dsub-mini',
|
|
2330
|
-
onClick: () => {
|
|
2296
|
+
onClick: () => { copyText(backupData) },
|
|
2331
2297
|
}, t('diagCopy')),
|
|
2332
2298
|
h('button', {
|
|
2333
2299
|
type: 'button',
|
|
@@ -2504,11 +2470,11 @@ window.__ModuleLoader__.load({
|
|
|
2504
2470
|
type: 'button',
|
|
2505
2471
|
className: 'dsub-mini',
|
|
2506
2472
|
onClick: () => {
|
|
2507
|
-
|
|
2508
|
-
|
|
2473
|
+
copyText(headlessInfo[key].sshTunnelCommand).then((ok) => {
|
|
2474
|
+
if (!ok) return
|
|
2509
2475
|
setCopiedSsh((m) => Object.assign({}, m, { [key]: true }))
|
|
2510
|
-
|
|
2511
|
-
}
|
|
2476
|
+
later(() => setCopiedSsh((m) => Object.assign({}, m, { [key]: false })), 2000)
|
|
2477
|
+
})
|
|
2512
2478
|
}
|
|
2513
2479
|
}, copiedSsh[key] ? ('✓ ' + t('headlessCopied')) : ('📋 ' + t('headlessCopySsh')))
|
|
2514
2480
|
),
|
|
@@ -2580,7 +2546,7 @@ window.__ModuleLoader__.load({
|
|
|
2580
2546
|
stepsEn: ['Paste your token or API key into the field above and click "Save token".'],
|
|
2581
2547
|
};
|
|
2582
2548
|
// Determine language safely from current locale (fallback to zh if Chinese or en)
|
|
2583
|
-
const isZh = t('
|
|
2549
|
+
const isZh = t('lang') === 'zh';
|
|
2584
2550
|
const steps = (isZh && info.stepsZh) ? info.stepsZh : (info.stepsEn || []);
|
|
2585
2551
|
return h('div', { className: 'dsub-helpBox' },
|
|
2586
2552
|
h('div', { className: 'dsub-helpHead' },
|
|
@@ -2676,7 +2642,7 @@ window.__ModuleLoader__.load({
|
|
|
2676
2642
|
h('div', { className: 'dsub-row' },
|
|
2677
2643
|
h('button', {
|
|
2678
2644
|
type: 'button', className: 'dsub-mini',
|
|
2679
|
-
onClick: () => {
|
|
2645
|
+
onClick: () => { copyText(d.userCode) },
|
|
2680
2646
|
}, t('deviceCopy')),
|
|
2681
2647
|
h('a', { href: safeExternalUrl(d.authUrl) || undefined, target: '_blank', rel: 'noopener noreferrer', className: 'dsub-mini' }, t('verifyLink')),
|
|
2682
2648
|
),
|
|
@@ -2846,7 +2812,7 @@ window.__ModuleLoader__.load({
|
|
|
2846
2812
|
var agoStr = null
|
|
2847
2813
|
if (at) {
|
|
2848
2814
|
var m = Math.round((Date.now() - at) / 60000)
|
|
2849
|
-
if (m >= 1) agoStr = m + 'm ' + (t('
|
|
2815
|
+
if (m >= 1) agoStr = m + 'm ' + (t('lang') === 'zh' ? '前' : 'ago')
|
|
2850
2816
|
}
|
|
2851
2817
|
|
|
2852
2818
|
return h('div', { className: 'dsub-panel-box' },
|
|
@@ -3122,47 +3088,9 @@ window.__ModuleLoader__.load({
|
|
|
3122
3088
|
expiryNotifyDays: (data && data.expiryNotifyDays) || 7,
|
|
3123
3089
|
composerQuota: (data && data.composerQuota) || 'off',
|
|
3124
3090
|
active: (data && data.active) || null,
|
|
3091
|
+
error: null,
|
|
3125
3092
|
}
|
|
3126
|
-
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null } }
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
// #84: compact quota indicator of the active subscription in the input area.
|
|
3130
|
-
// The runway forecast uses a rolling window of /status samples.
|
|
3131
|
-
const fcSamples = { windows: {} }
|
|
3132
|
-
|
|
3133
|
-
function fcObserve(key, remaining, now) {
|
|
3134
|
-
const windows = fcSamples.windows
|
|
3135
|
-
const prev = windows[key]
|
|
3136
|
-
const samples = (prev && prev.samples) || []
|
|
3137
|
-
const last = samples[samples.length - 1]
|
|
3138
|
-
if (!last || (now > last.at && (Math.abs(remaining - last.pct) >= 0.1 || now - last.at >= 15 * 60 * 1000))) {
|
|
3139
|
-
samples.push({ at: now, pct: remaining })
|
|
3140
|
-
}
|
|
3141
|
-
windows[key] = { samples: samples.filter((x) => x.at >= now - 24 * 60 * 60 * 1000).slice(-192) }
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3144
|
-
function fcEstimate(key, remaining) {
|
|
3145
|
-
const rec = fcSamples.windows[key]
|
|
3146
|
-
if (!rec) return { status: 'calibrating' }
|
|
3147
|
-
const samples = rec.samples
|
|
3148
|
-
if (samples.length < 3) return { status: 'calibrating' }
|
|
3149
|
-
const first = samples[0]
|
|
3150
|
-
const last = samples[samples.length - 1]
|
|
3151
|
-
const span = last.at - first.at
|
|
3152
|
-
const consumed = first.pct - last.pct
|
|
3153
|
-
if (span < 30 * 60 * 1000 || consumed < 1) return { status: 'calibrating' }
|
|
3154
|
-
const t0 = first.at
|
|
3155
|
-
let sw = 0, sx = 0, sy = 0, sxx = 0, sxy = 0
|
|
3156
|
-
for (const x of samples) {
|
|
3157
|
-
const wx = (x.at - t0) / 3600000
|
|
3158
|
-
const wy = first.pct - x.pct
|
|
3159
|
-
const w = Math.exp((x.at - last.at) / (6 * 3600000))
|
|
3160
|
-
sw += w; sx += w * wx; sy += w * wy; sxx += w * wx * wx; sxy += w * wx * wy
|
|
3161
|
-
}
|
|
3162
|
-
const den = sw * sxx - sx * sx
|
|
3163
|
-
const pace = den > 0 ? (sw * sxy - sx * sy) / den : 0
|
|
3164
|
-
if (!Number.isFinite(pace) || pace < 0.02) return { status: 'idle' }
|
|
3165
|
-
return { status: 'ready', runwaySeconds: Math.round((remaining / pace) * 3600) }
|
|
3093
|
+
} catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null, error: 'status unavailable' } }
|
|
3166
3094
|
}
|
|
3167
3095
|
|
|
3168
3096
|
function ComposerQuota(props) {
|
|
@@ -3171,15 +3099,25 @@ window.__ModuleLoader__.load({
|
|
|
3171
3099
|
React.useEffect(() => {
|
|
3172
3100
|
let alive = true
|
|
3173
3101
|
const pull = () => {
|
|
3174
|
-
|
|
3102
|
+
Promise.all([
|
|
3103
|
+
refreshLoggedIn(),
|
|
3104
|
+
fetch('/dsh-subscriptions/analytics', { cache: 'no-store' })
|
|
3105
|
+
.then((res) => res.json())
|
|
3106
|
+
.catch(() => ({})),
|
|
3107
|
+
]).then(([r, analyticsPayload]) => {
|
|
3175
3108
|
if (!alive) return
|
|
3176
3109
|
const mode = r.composerQuota || 'off'
|
|
3177
3110
|
const a = r.active
|
|
3178
3111
|
if (mode !== 'off' && a && a.provider && a.provider !== 'ollama' && a.usagePercent != null) {
|
|
3179
3112
|
const remaining = Math.max(0, 100 - a.usagePercent)
|
|
3180
|
-
const
|
|
3181
|
-
|
|
3182
|
-
|
|
3113
|
+
const forecast = (analyticsPayload && analyticsPayload.analytics && analyticsPayload.analytics.forecast) || []
|
|
3114
|
+
const row = forecast.find((f) => f.provider === a.provider && (!f.ref || !a.ref || f.ref === a.ref))
|
|
3115
|
+
let est = { status: 'calibrating' }
|
|
3116
|
+
if (row) {
|
|
3117
|
+
est = row.hoursToExhaustion == null
|
|
3118
|
+
? { status: 'idle' }
|
|
3119
|
+
: { status: 'forecast', runwaySeconds: Math.max(0, Number(row.hoursToExhaustion) * 3600) }
|
|
3120
|
+
}
|
|
3183
3121
|
setState({ mode, active: a, remaining, est })
|
|
3184
3122
|
} else {
|
|
3185
3123
|
setState({ mode, active: a, remaining: null, est: null })
|
|
@@ -3287,9 +3225,15 @@ window.__ModuleLoader__.load({
|
|
|
3287
3225
|
return false
|
|
3288
3226
|
}
|
|
3289
3227
|
|
|
3228
|
+
let pluginSettingsGeneration = 0
|
|
3290
3229
|
function openPluginSettings(doc = document, schedule = (run, ms) => window.setTimeout(run, ms), navCtx) {
|
|
3291
3230
|
try {
|
|
3292
3231
|
if (findAndClickCard(doc)) return
|
|
3232
|
+
const generation = ++pluginSettingsGeneration
|
|
3233
|
+
const runIfCurrent = (fn, ms) => schedule(() => {
|
|
3234
|
+
if (generation !== pluginSettingsGeneration) return
|
|
3235
|
+
fn()
|
|
3236
|
+
}, ms)
|
|
3293
3237
|
|
|
3294
3238
|
const activeLabels = resolvePluginLabels(navCtx)
|
|
3295
3239
|
const checkEl = (el) => matchesPluginLabel(el, activeLabels)
|
|
@@ -3300,9 +3244,9 @@ window.__ModuleLoader__.load({
|
|
|
3300
3244
|
panel.click()
|
|
3301
3245
|
const poll = (left) => {
|
|
3302
3246
|
if (findAndClickCard(doc)) return
|
|
3303
|
-
if (left > 0)
|
|
3247
|
+
if (left > 0) runIfCurrent(() => poll(left - 1), 100)
|
|
3304
3248
|
}
|
|
3305
|
-
|
|
3249
|
+
runIfCurrent(() => poll(40), 100)
|
|
3306
3250
|
return
|
|
3307
3251
|
}
|
|
3308
3252
|
|
|
@@ -3318,15 +3262,15 @@ window.__ModuleLoader__.load({
|
|
|
3318
3262
|
section.click()
|
|
3319
3263
|
const poll = (cLeft) => {
|
|
3320
3264
|
if (findAndClickCard(doc)) return
|
|
3321
|
-
if (cLeft > 0)
|
|
3265
|
+
if (cLeft > 0) runIfCurrent(() => poll(cLeft - 1), 100)
|
|
3322
3266
|
}
|
|
3323
|
-
|
|
3267
|
+
runIfCurrent(() => poll(40), 100)
|
|
3324
3268
|
return
|
|
3325
3269
|
}
|
|
3326
3270
|
} catch { /* bestEffort */ }
|
|
3327
|
-
if (left > 0)
|
|
3271
|
+
if (left > 0) runIfCurrent(() => pollSection(left - 1), 80)
|
|
3328
3272
|
}
|
|
3329
|
-
|
|
3273
|
+
runIfCurrent(() => pollSection(20), 80)
|
|
3330
3274
|
}
|
|
3331
3275
|
}
|
|
3332
3276
|
} catch { /* bestEffort */ }
|
|
@@ -3340,6 +3284,13 @@ window.__ModuleLoader__.load({
|
|
|
3340
3284
|
const dismissToast = (id) => {
|
|
3341
3285
|
setToasts((prev) => prev.filter((t) => t.id !== id))
|
|
3342
3286
|
}
|
|
3287
|
+
const reportError = (message) => {
|
|
3288
|
+
const text = cleanErrorMessage(message)
|
|
3289
|
+
if (!text) return
|
|
3290
|
+
setToasts((prev) => prev.some((toast) => toast.message === text)
|
|
3291
|
+
? prev
|
|
3292
|
+
: [{ id: 'err_' + Date.now(), message: text, cls: 'dsub-toast-bad' }, ...prev].slice(0, 3))
|
|
3293
|
+
}
|
|
3343
3294
|
|
|
3344
3295
|
const pollAlerts = () => {
|
|
3345
3296
|
fetch('/dsh-subscriptions/alerts?limit=5', { cache: 'no-store' })
|
|
@@ -3359,22 +3310,25 @@ window.__ModuleLoader__.load({
|
|
|
3359
3310
|
}
|
|
3360
3311
|
}
|
|
3361
3312
|
})
|
|
3362
|
-
.catch(() =>
|
|
3313
|
+
.catch((e) => reportError(t('hostOffline') + ': ' + String(e && e.message || e)))
|
|
3363
3314
|
}
|
|
3364
3315
|
|
|
3365
3316
|
const pull = () => {
|
|
3366
3317
|
setState((s) => Object.assign({}, s, { loading: true }))
|
|
3367
3318
|
refreshLoggedIn().then((r) => {
|
|
3319
|
+
if (r.error) reportError(t('hostOffline') + ': ' + r.error)
|
|
3368
3320
|
const logged = r.logged || []
|
|
3369
3321
|
fetch('/dsh-subscriptions/config', { cache: 'no-store' })
|
|
3370
3322
|
.then((res) => res.json())
|
|
3371
3323
|
.then((cfg) => {
|
|
3372
3324
|
setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: (cfg && cfg.accounts) || [], active: r.active, loading: false }))
|
|
3373
3325
|
})
|
|
3374
|
-
.catch(() => {
|
|
3326
|
+
.catch((e) => {
|
|
3327
|
+
reportError(t('hostOffline') + ': ' + String(e && e.message || e))
|
|
3375
3328
|
setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: [], active: r.active, loading: false }))
|
|
3376
3329
|
})
|
|
3377
|
-
}).catch(() => {
|
|
3330
|
+
}).catch((e) => {
|
|
3331
|
+
reportError(t('hostOffline') + ': ' + String(e && e.message || e))
|
|
3378
3332
|
setState((s) => Object.assign({}, s, { loading: false }))
|
|
3379
3333
|
})
|
|
3380
3334
|
}
|
|
@@ -3684,7 +3638,7 @@ window.__ModuleLoader__.load({
|
|
|
3684
3638
|
if (target) {
|
|
3685
3639
|
const popup = window.open(target, '_blank', 'noopener')
|
|
3686
3640
|
if (!popup) {
|
|
3687
|
-
|
|
3641
|
+
copyText(target)
|
|
3688
3642
|
}
|
|
3689
3643
|
}
|
|
3690
3644
|
}).catch(() => {})
|
|
@@ -3725,7 +3679,7 @@ window.__ModuleLoader__.load({
|
|
|
3725
3679
|
}, 'dsh-subscriptions: slash commands')
|
|
3726
3680
|
}
|
|
3727
3681
|
|
|
3728
|
-
exports.inject = ['slots', 'locale'
|
|
3682
|
+
exports.inject = ['slots', 'locale']
|
|
3729
3683
|
exports.apply = function apply(ctx) {
|
|
3730
3684
|
if (typeof window !== 'undefined') {
|
|
3731
3685
|
window.__dshCtx = ctx
|
package/lib/config-schema.js
CHANGED
|
@@ -26,6 +26,14 @@ export const Slot = z.object({
|
|
|
26
26
|
.description('#590 Optional monthly subscription cost in USD for analytics. Defaults to 20.'),
|
|
27
27
|
maxQueueSize: z.number().default(0)
|
|
28
28
|
.description('#592 Per-account queue depth before overflow. 0 = use the global maxQueueSize.'),
|
|
29
|
+
vipOnly: z.boolean().default(false)
|
|
30
|
+
.description('#175 Reserve this account for VIP requests only.'),
|
|
31
|
+
tags: z.array(z.string()).default([])
|
|
32
|
+
.description('#173 Tags used by tag-filtered rotation.'),
|
|
33
|
+
weight: z.number().default(0)
|
|
34
|
+
.description('#171 Weighted rotation priority. 0 = derive from isPro.'),
|
|
35
|
+
isPro: z.boolean().default(false)
|
|
36
|
+
.description('#171 Treat this account as Pro for weighted rotation.'),
|
|
29
37
|
})
|
|
30
38
|
|
|
31
39
|
export const defaultSlots = PROVIDERS.map((provider) => ({ provider, index: 1, label: '' }))
|
package/lib/http.js
CHANGED
|
@@ -282,8 +282,23 @@ export function isSameOrigin(url1, url2) {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
export function headerSafeValue(value) {
|
|
286
|
+
return String(value == null ? '' : value)
|
|
287
|
+
.replace(/[\r\n]+/g, ' ')
|
|
288
|
+
.replace(/[^\t\x20-\x7e\x80-\xff]/g, '')
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function sanitizeHeaderValues(headers) {
|
|
292
|
+
const out = {}
|
|
293
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
294
|
+
if (value === undefined || value === null) continue
|
|
295
|
+
out[key] = headerSafeValue(value)
|
|
296
|
+
}
|
|
297
|
+
return out
|
|
298
|
+
}
|
|
299
|
+
|
|
285
300
|
export function sanitizeRequestHeaders(url, init = {}, options = {}) {
|
|
286
|
-
const headers =
|
|
301
|
+
const headers = sanitizeHeaderValues(init.headers || {})
|
|
287
302
|
if (!url) return headers
|
|
288
303
|
try {
|
|
289
304
|
const parsed = new URL(url)
|
package/lib/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { webCallbackUri } from './oauth.js'
|
|
|
11
11
|
import { getVendor, registerCustomVendor, clearCustomVendors } from './vendors/index.js'
|
|
12
12
|
import { generateOnce, SIZES as IMAGE_SIZES } from './images.js'
|
|
13
13
|
import { parseBlob } from './blob.js'
|
|
14
|
-
import { createVendorFromProfile,
|
|
14
|
+
import { createVendorFromProfile, validateProfiles } from './vendor-factory.js'
|
|
15
15
|
import { registerCustomProviderIds, registerDisplayName } from './refs.js'
|
|
16
16
|
import { createSubscriptionsService } from './subscriptions.js'
|
|
17
17
|
import { createAccountStore, normalizeSlots, vendorConfig } from './accounts.js'
|
|
@@ -86,13 +86,7 @@ export function apply(ctx, rawConfig) {
|
|
|
86
86
|
const live = () => cachedConfig
|
|
87
87
|
|
|
88
88
|
function syncCustomVendors() {
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
profiles = (live().customVendors || []).map(validateProfile)
|
|
92
|
-
} catch (e) {
|
|
93
|
-
if (logger && logger.warn) logger.warn('[dsh-subscriptions] customVendors: ' + String(e && e.message || e))
|
|
94
|
-
return
|
|
95
|
-
}
|
|
89
|
+
const profiles = validateProfiles(live().customVendors)
|
|
96
90
|
clearCustomVendors()
|
|
97
91
|
for (const profile of profiles) {
|
|
98
92
|
try {
|
package/lib/routes/accounts.js
CHANGED
|
@@ -5,11 +5,10 @@ import { normalizeSlots, vendorConfig } from '../accounts.js'
|
|
|
5
5
|
import { getVendor } from '../vendors/index.js'
|
|
6
6
|
import { notifyQuarantineEntered, notifyQuarantineReleased } from '../alerts.js'
|
|
7
7
|
import { Config, plainConfig, publicConfig, revisionOf } from '../config-schema.js'
|
|
8
|
-
import { decryptWithPassphrase, encryptWithPassphrase } from '../crypto.js'
|
|
9
8
|
import { isTrustedSettingsRequest, queryOf, readBody, safeJsonHandler, writeJson } from '../http.js'
|
|
10
9
|
import { discoverLocalCliSessions, loadLocalCliBlob } from '../import-auth.js'
|
|
11
10
|
import { validateCredentialBlob } from '../blob.js'
|
|
12
|
-
import { isProvider, oauthRef
|
|
11
|
+
import { isProvider, oauthRef } from '../refs.js'
|
|
13
12
|
|
|
14
13
|
export function registerAccountsRoutes(ctx, state) {
|
|
15
14
|
const {
|
|
@@ -308,100 +307,6 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
308
307
|
}), 'dsh-subscriptions: /import-local')
|
|
309
308
|
|
|
310
309
|
|
|
311
|
-
// Export of the encrypted token bundle. Tokens are never logged.
|
|
312
|
-
ctx.effect(() => ctx.webServer.register({
|
|
313
|
-
kind: 'exact',
|
|
314
|
-
path: '/dsh-subscriptions/export',
|
|
315
|
-
handler: safeJsonHandler(async (req, res) => {
|
|
316
|
-
if (req.method !== 'POST') {
|
|
317
|
-
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
318
|
-
return
|
|
319
|
-
}
|
|
320
|
-
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
321
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
let payload
|
|
325
|
-
try { payload = JSON.parse((await readBody(req, 8 * 1024)).toString('utf8') || '{}') } catch {
|
|
326
|
-
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
327
|
-
return
|
|
328
|
-
}
|
|
329
|
-
const passphrase = String(payload.passphrase || '').trim()
|
|
330
|
-
if (!passphrase || passphrase.length < 12) {
|
|
331
|
-
writeJson(res, 400, { ok: false, error: { code: 'passphrase', message: 'passphrase must be at least 12 characters' } })
|
|
332
|
-
return
|
|
333
|
-
}
|
|
334
|
-
const algorithm = payload.algorithm === 'scrypt' ? 'scrypt' : (payload.algorithm === 'scrypt-v2' || payload.algorithm === 'scrypt-hardened' ? payload.algorithm : 'pbkdf2')
|
|
335
|
-
try {
|
|
336
|
-
const accounts = []
|
|
337
|
-
for (const slot of normalizeSlots(live().slots)) {
|
|
338
|
-
try {
|
|
339
|
-
const blob = await store.loadBlob(slot.ref)
|
|
340
|
-
accounts.push({ ref: slot.ref, provider: slot.provider, index: slot.index, label: slot.label || blob.label || '', blob })
|
|
341
|
-
} catch { /* skip missing */ }
|
|
342
|
-
}
|
|
343
|
-
if (!accounts.length) {
|
|
344
|
-
writeJson(res, 200, { ok: false, error: { code: 'empty', message: 'no connected accounts' } })
|
|
345
|
-
return
|
|
346
|
-
}
|
|
347
|
-
const bundle = JSON.stringify({ v: 1, exportedAt: Date.now(), accounts })
|
|
348
|
-
const encrypted = encryptWithPassphrase(bundle, passphrase, { algorithm })
|
|
349
|
-
writeJson(res, 200, { ok: true, payload: encrypted, count: accounts.length })
|
|
350
|
-
} catch (e) {
|
|
351
|
-
writeJson(res, 500, { ok: false, error: { code: 'export', message: String(e && e.message || e) } })
|
|
352
|
-
}
|
|
353
|
-
}),
|
|
354
|
-
}), 'dsh-subscriptions: /export')
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// Import of the encrypted bundle.
|
|
358
|
-
ctx.effect(() => ctx.webServer.register({
|
|
359
|
-
kind: 'exact',
|
|
360
|
-
path: '/dsh-subscriptions/import',
|
|
361
|
-
handler: safeJsonHandler(async (req, res) => {
|
|
362
|
-
if (req.method !== 'POST') {
|
|
363
|
-
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
364
|
-
return
|
|
365
|
-
}
|
|
366
|
-
if (!isTrustedSettingsRequest(req, ctx)) {
|
|
367
|
-
writeJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'same-origin only' } })
|
|
368
|
-
return
|
|
369
|
-
}
|
|
370
|
-
let payload
|
|
371
|
-
try { payload = JSON.parse((await readBody(req, 1024 * 1024)).toString('utf8') || '{}') } catch {
|
|
372
|
-
writeJson(res, 400, { ok: false, error: { code: 'json', message: 'invalid json' } })
|
|
373
|
-
return
|
|
374
|
-
}
|
|
375
|
-
const { passphrase, payload: encrypted } = payload
|
|
376
|
-
if (!passphrase || !encrypted) {
|
|
377
|
-
writeJson(res, 400, { ok: false, error: { code: 'params', message: 'passphrase and payload are required' } })
|
|
378
|
-
return
|
|
379
|
-
}
|
|
380
|
-
let bundle
|
|
381
|
-
try {
|
|
382
|
-
bundle = JSON.parse(decryptWithPassphrase(encrypted, passphrase))
|
|
383
|
-
} catch {
|
|
384
|
-
writeJson(res, 400, { ok: false, error: { code: 'decrypt', message: 'wrong passphrase or corrupted bundle' } })
|
|
385
|
-
return
|
|
386
|
-
}
|
|
387
|
-
if (!bundle || !Array.isArray(bundle.accounts)) {
|
|
388
|
-
writeJson(res, 400, { ok: false, error: { code: 'format', message: 'invalid bundle format' } })
|
|
389
|
-
return
|
|
390
|
-
}
|
|
391
|
-
let imported = 0
|
|
392
|
-
for (const row of bundle.accounts) {
|
|
393
|
-
try {
|
|
394
|
-
if (!row || typeof row.ref !== 'string' || !parseOauthRef(row.ref) || !row.blob) continue
|
|
395
|
-
await store.saveBlob(row.ref, validateCredentialBlob(row.blob))
|
|
396
|
-
imported++
|
|
397
|
-
} catch { /* skip broken */ }
|
|
398
|
-
}
|
|
399
|
-
await syncAdapter()
|
|
400
|
-
writeJson(res, 200, { ok: true, imported, total: bundle.accounts.length, accounts: await accountsView() })
|
|
401
|
-
}),
|
|
402
|
-
}), 'dsh-subscriptions: /import')
|
|
403
|
-
|
|
404
|
-
|
|
405
310
|
// #45: import an existing refresh token / API key without an OAuth flow.
|
|
406
311
|
ctx.effect(() => ctx.webServer.register({
|
|
407
312
|
kind: 'exact',
|
|
@@ -659,12 +564,6 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
659
564
|
handler: handleVaultExport,
|
|
660
565
|
}), 'dsh-subscriptions: /vault/export')
|
|
661
566
|
|
|
662
|
-
ctx.effect(() => ctx.webServer.register({
|
|
663
|
-
kind: 'exact',
|
|
664
|
-
path: '/dsh-subscriptions/export-backup',
|
|
665
|
-
handler: handleVaultExport,
|
|
666
|
-
}), 'dsh-subscriptions: /export-backup')
|
|
667
|
-
|
|
668
567
|
const handleVaultImport = safeJsonHandler(async (req, res) => {
|
|
669
568
|
if (req.method !== 'POST') {
|
|
670
569
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'POST only' } })
|
|
@@ -728,12 +627,6 @@ export function registerAccountsRoutes(ctx, state) {
|
|
|
728
627
|
handler: handleVaultImport,
|
|
729
628
|
}), 'dsh-subscriptions: /vault/import')
|
|
730
629
|
|
|
731
|
-
ctx.effect(() => ctx.webServer.register({
|
|
732
|
-
kind: 'exact',
|
|
733
|
-
path: '/dsh-subscriptions/import-backup',
|
|
734
|
-
handler: handleVaultImport,
|
|
735
|
-
}), 'dsh-subscriptions: /import-backup')
|
|
736
|
-
|
|
737
630
|
|
|
738
631
|
// #562: Remote Vault Sync Push
|
|
739
632
|
ctx.effect(() => ctx.webServer.register({
|
package/lib/routes/proxy.js
CHANGED
|
@@ -40,7 +40,7 @@ export function registerProxyRoutes(ctx, state) {
|
|
|
40
40
|
// The token never leaves the process - only the provider's response goes out.
|
|
41
41
|
ctx.effect(() => ctx.webServer.register({
|
|
42
42
|
kind: 'prefix',
|
|
43
|
-
path: '/dsh-subscriptions/proxy',
|
|
43
|
+
path: '/dsh-subscriptions/proxy/',
|
|
44
44
|
handler: safeJsonHandler(async (req, res) => {
|
|
45
45
|
if (req.method !== 'POST' && req.method !== 'GET') {
|
|
46
46
|
writeJson(res, 405, { ok: false, error: { code: 'method', message: 'GET or POST' } })
|
package/lib/updater.js
CHANGED
|
@@ -4,60 +4,15 @@ import { readFile } from 'node:fs/promises'
|
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
5
|
import { basename, dirname, isAbsolute, resolve } from 'node:path'
|
|
6
6
|
import { fileURLToPath } from 'node:url'
|
|
7
|
-
import { isTrustedSettingsRequest, writeJson } from './http.js'
|
|
7
|
+
import { isLoopback, isTrustedSettingsRequest, writeJson } from './http.js'
|
|
8
8
|
|
|
9
|
-
const UPDATE_HEADER = 'x-dsh-plugin-update'
|
|
10
9
|
const UPDATE_TIMEOUT_MS = 10 * 60_000
|
|
11
10
|
const VERSION_CACHE_MS = 5 * 60_000
|
|
12
11
|
let latestCache
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
const value = request.headers?.[name]
|
|
16
|
-
return Array.isArray(value) ? value[0] : value
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function isLoopbackIPv4(address) {
|
|
20
|
-
if (!address || !address.startsWith('127.')) return false
|
|
21
|
-
const parts = address.split('.')
|
|
22
|
-
if (parts.length !== 4) return false
|
|
23
|
-
return parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function isLoopback(value) {
|
|
27
|
-
const address = value?.toLowerCase().replace(/^\[|\]$/g, '')
|
|
28
|
-
if (!address) return false
|
|
29
|
-
if (address === 'localhost' || address === 'localhost.' || address === '::1') return true
|
|
30
|
-
const ipv4 = address.startsWith('::ffff:') ? address.slice('::ffff:'.length) : address
|
|
31
|
-
return isLoopbackIPv4(ipv4)
|
|
32
|
-
}
|
|
13
|
+
export { isLoopback }
|
|
33
14
|
|
|
34
15
|
export function isTrustedUpdateRequest(request, targetCtx) {
|
|
35
|
-
const secFetchSite = String(header(request, 'sec-fetch-site') || '').toLowerCase()
|
|
36
|
-
if (secFetchSite === 'cross-site') return false
|
|
37
|
-
|
|
38
|
-
const origin = header(request, 'origin')
|
|
39
|
-
const host = header(request, 'host')
|
|
40
|
-
if (origin && host) {
|
|
41
|
-
try {
|
|
42
|
-
const u = new URL(origin)
|
|
43
|
-
if (u.protocol === 'http:' || u.protocol === 'https:') {
|
|
44
|
-
if (u.host.toLowerCase() !== host.toLowerCase()) return false
|
|
45
|
-
} else if (u.protocol === 'dsh-app:') {
|
|
46
|
-
const appHost = u.host.toLowerCase()
|
|
47
|
-
if (appHost && appHost !== 'app' && appHost !== 'localhost') return false
|
|
48
|
-
} else {
|
|
49
|
-
return false
|
|
50
|
-
}
|
|
51
|
-
} catch {
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const customHeader = header(request, UPDATE_HEADER)
|
|
57
|
-
if (customHeader === '1') {
|
|
58
|
-
const remote = request.socket?.remoteAddress
|
|
59
|
-
if (remote && isLoopback(remote)) return true
|
|
60
|
-
}
|
|
61
16
|
return isTrustedSettingsRequest(request, targetCtx)
|
|
62
17
|
}
|
|
63
18
|
|
package/lib/vendor-factory.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fetchWithTimeout } from "./http.js"
|
|
1
|
+
import { fetchWithTimeout, sanitizeHeaderValues } from "./http.js"
|
|
2
2
|
// Vendor factory from a declarative profile.
|
|
3
3
|
//
|
|
4
4
|
// Covers the OpenAI-Responses-compatible subscription family: PKCE auth,
|
|
@@ -12,6 +12,7 @@ import { buildAuthorizeUrl } from "./oauth.js"
|
|
|
12
12
|
import { codexResponsesBody, modelCatalog } from "./messages.js"
|
|
13
13
|
import { formTokenRequest, jsonTokenRequest, codexResponsesStream, readJson, tokenBlobFromOAuth, httpError , readErrorBody } from "./wire.js"
|
|
14
14
|
import { emailFromToken } from "./jwt.js"
|
|
15
|
+
import { BUILTIN_PROVIDERS } from "./refs.js"
|
|
15
16
|
|
|
16
17
|
function defaultModels(providerId, models) {
|
|
17
18
|
return modelCatalog(providerId, (models || []).map((row) => (typeof row === "string" ? { id: row, name: row } : row)))
|
|
@@ -29,11 +30,11 @@ export function createVendorFromProfile(profile) {
|
|
|
29
30
|
const tokenStyle = profile.tokenStyle === "json" ? "json" : "form"
|
|
30
31
|
|
|
31
32
|
function identityHeaders(blob, extra) {
|
|
32
|
-
return {
|
|
33
|
+
return sanitizeHeaderValues({
|
|
33
34
|
Authorization: `Bearer ${blob.accessToken}`,
|
|
34
35
|
...(profile.headers || {}),
|
|
35
36
|
...(extra || {}),
|
|
36
|
-
}
|
|
37
|
+
})
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
return {
|
|
@@ -108,12 +109,6 @@ export function createVendorFromProfile(profile) {
|
|
|
108
109
|
return catalog
|
|
109
110
|
}
|
|
110
111
|
},
|
|
111
|
-
async usage() {
|
|
112
|
-
// Arbitrary services have no universal usage endpoint - the quota
|
|
113
|
-
// snapshot is filled from ratelimit headers via the adapter quotaFetch.
|
|
114
|
-
return null
|
|
115
|
-
},
|
|
116
|
-
check: undefined,
|
|
117
112
|
async* streamOnce({ blob, options, fetchImpl, headers, signal }) {
|
|
118
113
|
const body = codexResponsesBody(options, "")
|
|
119
114
|
if (!body.instructions) delete body.instructions
|
|
@@ -141,6 +136,7 @@ export function validateProfile(raw) {
|
|
|
141
136
|
if (!p[key] || typeof p[key] !== "string") throw new Error(`customVendors[${p.id || "?"}]: field ${key} is required`)
|
|
142
137
|
}
|
|
143
138
|
if (!/^[a-z][a-z0-9_]*$/.test(p.id)) throw new Error(`customVendors: id "${p.id}" must match [a-z][a-z0-9_]*`)
|
|
139
|
+
if (BUILTIN_PROVIDERS.includes(p.id)) throw new Error(`customVendors: id "${p.id}" conflicts with a built-in provider`)
|
|
144
140
|
if (p.tokenStyle && !["form", "json"].includes(p.tokenStyle)) throw new Error(`customVendors[${p.id}]: tokenStyle must be form|json`)
|
|
145
141
|
const localHostnames = new Set(["localhost", "127.0.0.1", "::1", "[::1]"])
|
|
146
142
|
for (const key of ["authUrl", "tokenUrl", "baseUrl"]) {
|
|
@@ -170,4 +166,15 @@ export function validateProfile(raw) {
|
|
|
170
166
|
models: Array.isArray(p.models) ? p.models : [],
|
|
171
167
|
headers: p.headers && typeof p.headers === "object" ? p.headers : {},
|
|
172
168
|
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Validates a list of profiles, skipping invalid entries individually. */
|
|
172
|
+
export function validateProfiles(list) {
|
|
173
|
+
const out = []
|
|
174
|
+
for (const raw of Array.isArray(list) ? list : []) {
|
|
175
|
+
try {
|
|
176
|
+
out.push(validateProfile(raw))
|
|
177
|
+
} catch { /* skip invalid profile */ }
|
|
178
|
+
}
|
|
179
|
+
return out
|
|
173
180
|
}
|
package/lib/vendors/codex.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isTimeoutError, fetchWithTimeout } from "../http.js"
|
|
1
|
+
import { isTimeoutError, fetchWithTimeout, headerSafeValue } from "../http.js"
|
|
2
2
|
import { randomUUID, createHash } from 'node:crypto'
|
|
3
3
|
import { buildAuthorizeUrl } from '../oauth.js'
|
|
4
4
|
import { codexResponsesBody, modelCatalog } from '../messages.js'
|
|
@@ -61,10 +61,10 @@ export function authorizeUrl(cfg, pkce) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function identityHeaders(blob, config, extra) {
|
|
64
|
-
const originator = config.originator || 'codex_cli_rs'
|
|
65
|
-
const activeOrgId = blob.selectedOrgId || blob.accountId || ''
|
|
64
|
+
const originator = headerSafeValue(config.originator || 'codex_cli_rs')
|
|
65
|
+
const activeOrgId = headerSafeValue(blob.selectedOrgId || blob.accountId || '')
|
|
66
66
|
return {
|
|
67
|
-
Authorization: `Bearer ${blob.accessToken}`,
|
|
67
|
+
Authorization: `Bearer ${headerSafeValue(blob.accessToken)}`,
|
|
68
68
|
originator,
|
|
69
69
|
'chatgpt-account-id': activeOrgId,
|
|
70
70
|
'ChatGPT-Account-ID': activeOrgId,
|
|
@@ -260,7 +260,7 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
260
260
|
Accept: 'text/event-stream',
|
|
261
261
|
'session-id': sessionId,
|
|
262
262
|
'x-client-request-id': sessionId,
|
|
263
|
-
...(isFast ? { 'x-codex-routing-hint': `model=${(options && options.model) || 'gpt-5.6-luna'};tier=priority` } : {}),
|
|
263
|
+
...(isFast ? { 'x-codex-routing-hint': `model=${headerSafeValue((options && options.model) || 'gpt-5.6-luna')};tier=priority` } : {}),
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
const res = await fetchImpl(`${base}/responses`, {
|
package/lib/vendors/cody.js
CHANGED
|
@@ -39,10 +39,7 @@ export async function listModels() {
|
|
|
39
39
|
return CODY_MODELS
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
export async function usage(
|
|
43
|
-
if (blob && (blob.accessToken || blob.token)) {
|
|
44
|
-
return { usedPercent: null, plan: 'Cody Pro', timestamp: Date.now() }
|
|
45
|
-
}
|
|
42
|
+
export async function usage() {
|
|
46
43
|
return null
|
|
47
44
|
}
|
|
48
45
|
|
package/lib/vendors/copilot.js
CHANGED
|
@@ -81,6 +81,7 @@ export async function listModels(blob, cfg, fetchImpl) {
|
|
|
81
81
|
const impl = fetchImpl || fetch
|
|
82
82
|
const fallback = modelCatalog(id, (cfg && cfg.models) || COPILOT_MODELS)
|
|
83
83
|
if (!blob) return fallback
|
|
84
|
+
blob = { ...blob }
|
|
84
85
|
const githubToken = blob.refreshToken || blob.githubToken
|
|
85
86
|
let copilotToken = blob.accessToken
|
|
86
87
|
try {
|
|
@@ -279,6 +280,8 @@ export async function usage(blob, config, fetchImpl) {
|
|
|
279
280
|
const usedPercent = user && typeof user.used_percent === "number" && Number.isFinite(user.used_percent)
|
|
280
281
|
? user.used_percent
|
|
281
282
|
: null
|
|
283
|
+
|
|
284
|
+
if (usedPercent == null) return null
|
|
282
285
|
return {
|
|
283
286
|
usedPercent,
|
|
284
287
|
plan: user && user.plan ? (user.plan.name || String(user.plan)) : "Copilot",
|
|
@@ -290,6 +293,7 @@ export async function usage(blob, config, fetchImpl) {
|
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal, spawnImpl }) {
|
|
296
|
+
blob = blob ? { ...blob } : blob
|
|
293
297
|
const mode = (config && config.copilotMode) || (blob && blob.copilotMode) || 'auto'
|
|
294
298
|
const customPath = (config && config.copilotCliPath) || (blob && blob.copilotCliPath) || ''
|
|
295
299
|
|
package/lib/vendors/ernie.js
CHANGED
|
@@ -74,7 +74,6 @@ export async function* streamOnce({ blob, options, fetchImpl, headers, config, s
|
|
|
74
74
|
const auth = await refreshErnieToken(blob.apiKey, blob.secretKey, impl)
|
|
75
75
|
if (auth && auth.access_token) {
|
|
76
76
|
token = auth.access_token
|
|
77
|
-
blob.accessToken = token
|
|
78
77
|
}
|
|
79
78
|
}
|
|
80
79
|
if (!token) throw new LlmError('Baidu ERNIE requires access token or API Key + Secret Key', 'AUTH')
|
|
@@ -171,15 +171,16 @@ export async function usage(blob, _cfg, fetchImpl) {
|
|
|
171
171
|
})
|
|
172
172
|
if (res.ok) {
|
|
173
173
|
const d = await readJson(res)
|
|
174
|
+
if (typeof d.used_percent !== 'number') return null
|
|
174
175
|
return {
|
|
175
|
-
usedPercent:
|
|
176
|
+
usedPercent: d.used_percent,
|
|
176
177
|
plan: d.plan || blob.plan || 'Perplexity Pro',
|
|
177
178
|
timestamp: Date.now(),
|
|
178
179
|
quota: d.quota ? { used: d.quota.used, limit: d.quota.limit, resetAt: d.quota.reset_at } : undefined,
|
|
179
180
|
}
|
|
180
181
|
}
|
|
181
182
|
} catch { /* usage endpoint is optional */ }
|
|
182
|
-
return
|
|
183
|
+
return null
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
export async function* streamOnce({ blob, options, fetchImpl, headers, config, signal }) {
|
package/lib/vendors/replit.js
CHANGED
|
@@ -34,10 +34,7 @@ export async function listModels() {
|
|
|
34
34
|
return REPLIT_MODELS
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export async function usage(
|
|
38
|
-
if (blob && (blob.accessToken || blob.token)) {
|
|
39
|
-
return { usedPercent: null, plan: 'Replit Core', timestamp: Date.now() }
|
|
40
|
-
}
|
|
37
|
+
export async function usage() {
|
|
41
38
|
return null
|
|
42
39
|
}
|
|
43
40
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-subscriptions",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.88",
|
|
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",
|