dsh-multi-provider-usage 0.7.1 → 0.7.2
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 +66 -19
- package/README.zh.md +53 -17
- package/lib/auto-update.js +12 -1
- package/lib/client.js +540 -67
- package/lib/index.js +13 -1
- package/lib/opencode-console.js +329 -94
- package/lib/opencode-session.js +5 -1
- package/lib/providers.js +24 -7
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -51,11 +51,12 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
".dpu-provider:first-child{border-top:none;padding-top:0}",
|
|
52
52
|
".dpu-provider-head{display:flex;align-items:center;gap:2px;width:100%;min-width:0;border:none;background:transparent;border-radius:6px;padding:1px 1px;font:inherit;color:inherit;cursor:pointer;text-align:left}",
|
|
53
53
|
".dpu-provider-head:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
54
|
-
".dpu-chevron{flex:none;width:9px;font-size:9px;line-height:16px;color:var(--dsw-alias-label-tertiary)}",
|
|
55
54
|
".dpu-provider-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600}",
|
|
56
55
|
".dpu-provider-pct{flex:none;font-size:10px;font-weight:700;line-height:16px;color:var(--dsw-alias-label-primary)}",
|
|
57
56
|
".dpu-provider-balance{flex:none;max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 3px;border-radius:6px;font-size:10px;line-height:16px;font-weight:700;color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 16%, transparent)}",
|
|
58
57
|
".dpu-provider-balance.dpu-muted{color:var(--dsw-alias-label-tertiary)}",
|
|
58
|
+
".dpu-provider-balance.dpu-low{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 16%, transparent)}",
|
|
59
|
+
".dpu-provider-balance.dpu-critical{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent)}",
|
|
59
60
|
".dpu-provider-balance.dpu-hit{animation:dpu-balance-pop 1.4s ease}",
|
|
60
61
|
".dpu-balance-setup{flex:none;max-width:96px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 4px;border-radius:6px;font-size:10px;line-height:16px;font-weight:600;color:var(--dsw-alias-label-secondary);background:transparent;border:1px dashed color-mix(in srgb, var(--dsw-alias-label-secondary) 55%, transparent);cursor:pointer}",
|
|
61
62
|
".dpu-balance-setup:hover{color:var(--dsw-alias-label-primary);border-color:currentColor}",
|
|
@@ -65,9 +66,9 @@ window.__ModuleLoader__.load({
|
|
|
65
66
|
".dpu-balance-float.dpu-up-text{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 20%, transparent)}",
|
|
66
67
|
"@keyframes dpu-balance-pop{0%{transform:scale(.86);opacity:.35}35%{transform:scale(1.16);opacity:1}65%{transform:scale(1.02)}100%{transform:scale(1)}}",
|
|
67
68
|
"@keyframes dpu-balance-fly{0%{opacity:0;transform:translateY(5px) scale(.9)}18%{opacity:1;transform:translateY(0) scale(1)}100%{opacity:0;transform:translateY(-12px) scale(.95)}}",
|
|
68
|
-
".dpu-provider-body{display:flex;flex-direction:column;gap:4px;min-width:0;padding:2px 0 3px
|
|
69
|
+
".dpu-provider-body{display:flex;flex-direction:column;gap:4px;min-width:0;padding:2px 0 3px 11px}",
|
|
69
70
|
".dpu-provider-error{font-size:10px;line-height:14px;color:var(--dsw-alias-label-secondary);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}",
|
|
70
|
-
".dpu-dot{width:5px;height:5px;border-radius:50%;flex:none;background:var(--dsw-alias-label-tertiary)}",
|
|
71
|
+
".dpu-dot{width:5px;height:5px;border-radius:50%;flex:none;margin-right:3px;background:var(--dsw-alias-label-tertiary)}",
|
|
71
72
|
".dpu-dot.dpu-ok{background:var(--dsw-alias-state-success-primary)}",
|
|
72
73
|
".dpu-dot.dpu-error{background:var(--dsw-alias-state-error-primary)}",
|
|
73
74
|
".dpu-dot.dpu-warn{background:var(--dsw-alias-state-warn-primary)}",
|
|
@@ -87,6 +88,8 @@ window.__ModuleLoader__.load({
|
|
|
87
88
|
".dpu-timeelapsed{height:100%;flex:none;background:var(--dsw-alias-border-l2);border-radius:3px;transition:width .4s ease}",
|
|
88
89
|
".dpu-timeremain{height:100%;flex:1 1 auto;min-width:0;background:var(--dsw-static-deepseek-450, #5686fe);border-radius:3px;transition:width .4s ease}",
|
|
89
90
|
".dpu-err{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}",
|
|
91
|
+
".dpu-err.dpu-retry{cursor:pointer}",
|
|
92
|
+
".dpu-err.dpu-retry:hover{color:var(--dsw-alias-label-primary)}",
|
|
90
93
|
".dpu-ok{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}",
|
|
91
94
|
".dpu-rail{box-sizing:border-box;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:8px;font-size:10px;font-weight:700;color:var(--dsw-alias-label-primary);cursor:default}",
|
|
92
95
|
".dpu-rail:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
@@ -103,7 +106,7 @@ window.__ModuleLoader__.load({
|
|
|
103
106
|
"window.five-hour": "5 小时窗口",
|
|
104
107
|
"window.weekly": "周窗口",
|
|
105
108
|
"window.monthly": "月窗口",
|
|
106
|
-
"window.mcp": "MCP 工具
|
|
109
|
+
"window.mcp": "MCP 工具 (月)",
|
|
107
110
|
"window.token": "Token 窗口",
|
|
108
111
|
"window.other": "其他窗口",
|
|
109
112
|
"window.five-hour.hint": "5h",
|
|
@@ -112,7 +115,7 @@ window.__ModuleLoader__.load({
|
|
|
112
115
|
"time.minutes": "{n}分",
|
|
113
116
|
"time.hours": "{n}小时{m}分",
|
|
114
117
|
"time.days": "{n}天{m}小时",
|
|
115
|
-
"time.elapsed.title": "
|
|
118
|
+
"time.elapsed.title": "本窗口时间进度:已流逝 {pct}%,剩余 {rest}%",
|
|
116
119
|
"refresh": "刷新",
|
|
117
120
|
"settings": "设置",
|
|
118
121
|
"settings.title": "打开插件设置",
|
|
@@ -129,26 +132,26 @@ window.__ModuleLoader__.load({
|
|
|
129
132
|
"status.disabled": "已禁用",
|
|
130
133
|
"status.unknown": "未知",
|
|
131
134
|
"rail.plain": "用量",
|
|
132
|
-
"rail.all": "
|
|
133
|
-
"rail.error": "{provider}
|
|
135
|
+
"rail.all": "服务商用量:{summary}",
|
|
136
|
+
"rail.error": "{provider}: 读取失败",
|
|
134
137
|
"rail.title": "{provider} · {pct}%",
|
|
135
138
|
"rail.title.account": "{provider} · {account} · {pct}%",
|
|
136
139
|
"widget.brand": "服务商用量",
|
|
137
|
-
"widget.account.title": "
|
|
140
|
+
"widget.account.title": "当前账号:{name}",
|
|
138
141
|
"widget.fallback": "内置",
|
|
139
|
-
"widget.fallback.title": "
|
|
140
|
-
"widget.partial.title": "
|
|
142
|
+
"widget.fallback.title": "实际来源:{source};请求来源:{requested};回退原因:{reason}{message}",
|
|
143
|
+
"widget.partial.title": "部分接口失败:{error}",
|
|
141
144
|
"balance.extra": "充值余额",
|
|
142
145
|
"balance.zen": "充值余额",
|
|
143
146
|
"balance.short": "余额",
|
|
144
147
|
"balance.none": "余额 —",
|
|
145
|
-
"balance.title": "订阅外充值余额 {amount}
|
|
146
|
-
"balance.delta.title": "
|
|
147
|
-
"balance.none.title": "
|
|
148
|
+
"balance.title": "订阅外充值余额 {amount}(来源:{source})",
|
|
149
|
+
"balance.delta.title": "本轮余额变化:{from} → {to}",
|
|
150
|
+
"balance.none.title": "充值余额不可用:{error}",
|
|
148
151
|
"balance.setup": "配置余额",
|
|
149
|
-
"balance.setup.title": "
|
|
152
|
+
"balance.setup.title": "没有可用的 OpenCode 控制台 OAuth 登录,无法读取充值余额(API Key 登录不含余额,令牌过期也会如此)。点击打开设置页查看分步引导,或运行 opencode auth login 并选择 OpenCode Console 账号。",
|
|
150
153
|
"balance.missing": "无充值额度",
|
|
151
|
-
"balance.missing.title": "Command Code 的 credits
|
|
154
|
+
"balance.missing.title": "Command Code 的 credits 接口未返回购买或免费额度,因此没有充值余额可显示。",
|
|
152
155
|
"balance.details.extra": "购买 {purchased} + 免费 {free}",
|
|
153
156
|
"autoUpdate.updated": "插件已更新到 v{v},重启 DSH 生效",
|
|
154
157
|
"autoUpdate.available": "插件有新版本 v{v},自动更新未成功,请手动更新",
|
|
@@ -163,23 +166,48 @@ window.__ModuleLoader__.load({
|
|
|
163
166
|
"reason.unknown": "未知原因",
|
|
164
167
|
"update.available": "新版本 v{v}",
|
|
165
168
|
"update.title": "点击查看升级说明",
|
|
169
|
+
"error.retry": "点击重试",
|
|
166
170
|
|
|
167
171
|
// settings page (Plugins panel)
|
|
168
172
|
"cfg.summary": "在一个侧边栏组件中查看 OpenCode GO、Command Code 与 GLM Code Plan 的套餐用量。",
|
|
169
173
|
"cfg.title": "服务商用量设置",
|
|
170
|
-
"cfg.lead": "
|
|
174
|
+
"cfg.lead": "选择要显示的服务商与凭据引用。保存后立即生效,无需重启。API Key 保存在 DSH 凭据域中,不会出现在这里。",
|
|
171
175
|
"cfg.providers": "服务商",
|
|
172
176
|
"cfg.console.status": "充值余额",
|
|
173
|
-
"cfg.console.hint": "运行 opencode auth login 登录 OpenCode
|
|
177
|
+
"cfg.console.hint": "运行 opencode auth login 登录 OpenCode 控制台,或把控制台令牌保存为 OPENCODE_CONSOLE_TOKEN 凭据,即可显示充值余额。",
|
|
178
|
+
"cfg.console.apiKey": "本机 OpenCode 当前是 API Key 登录,不含控制台余额;请运行 opencode auth login 并选择 OpenCode Console 账号。",
|
|
179
|
+
"cfg.console.expired": "控制台登录已过期,请重新运行 opencode auth login 后再刷新。",
|
|
180
|
+
"cfg.console.guide.title": "如何显示充值余额",
|
|
181
|
+
"cfg.console.guide.step1": "在终端运行以下命令,选择 OpenCode Console 账号登录:",
|
|
182
|
+
"cfg.console.guide.step2": "或从控制台网页复制令牌,保存为 DSH 凭据:",
|
|
183
|
+
"cfg.console.guide.step3": "完成后点击重新检测,插件会重读登录状态并立即刷新余额,无需重启 DSH。",
|
|
184
|
+
"cfg.console.guide.link": "打开 OpenCode 控制台 ↗",
|
|
185
|
+
"cfg.console.copy.command": "复制命令",
|
|
186
|
+
"cfg.console.copy.ref": "复制凭据名",
|
|
187
|
+
"cfg.console.recheck": "重新检测",
|
|
188
|
+
"cfg.console.rechecking": "检测中…",
|
|
189
|
+
"cfg.console.copied": "已复制",
|
|
190
|
+
"cfg.console.copyFailed": "复制失败,请手动复制。",
|
|
191
|
+
"cfg.usage": "实时用量",
|
|
192
|
+
"cfg.usage.refresh": "刷新用量",
|
|
193
|
+
"cfg.usage.refreshing": "刷新中…",
|
|
194
|
+
"cfg.usage.loading": "正在读取用量…",
|
|
195
|
+
"cfg.usage.loadFailed": "用量读取失败:{error}",
|
|
196
|
+
"cfg.usage.disabled": "已禁用,启用并保存后显示。",
|
|
197
|
+
"cfg.usage.noData": "暂无数据",
|
|
198
|
+
"cfg.usage.error": "读取失败:{error}",
|
|
199
|
+
"cfg.usage.balance": "充值余额",
|
|
200
|
+
"cfg.usage.noBalance": "暂无充值余额",
|
|
201
|
+
"cfg.usage.updatedAt": "更新于 {time}",
|
|
174
202
|
"cfg.enabled": "启用",
|
|
175
203
|
"cfg.order.hint": "拖动此处调整显示顺序",
|
|
176
204
|
"cfg.move.up": "上移",
|
|
177
205
|
"cfg.move.down": "下移",
|
|
178
|
-
"cfg.credential": "凭据引用
|
|
206
|
+
"cfg.credential": "凭据引用 (apiKeyEnv)",
|
|
179
207
|
"cfg.invite": "推广链接",
|
|
180
208
|
"cfg.invite.copy": "复制",
|
|
181
209
|
"cfg.invite.copied": "已复制",
|
|
182
|
-
"cfg.invite.copyFailed": "
|
|
210
|
+
"cfg.invite.copyFailed": "复制失败,请手动选中链接复制。",
|
|
183
211
|
"cfg.status.configured": "已配置",
|
|
184
212
|
"cfg.status.notConfigured": "未配置",
|
|
185
213
|
"cfg.status.enabled": "已启用",
|
|
@@ -192,13 +220,13 @@ window.__ModuleLoader__.load({
|
|
|
192
220
|
"cfg.reloading": "加载中…",
|
|
193
221
|
"cfg.reloaded": "已重新加载。",
|
|
194
222
|
"cfg.retry": "重试",
|
|
195
|
-
"cfg.saved": "
|
|
223
|
+
"cfg.saved": "已保存,设置已更新。",
|
|
196
224
|
"cfg.noChanges": "没有需要保存的更改。",
|
|
197
225
|
"cfg.unsaved": "未保存",
|
|
198
|
-
"cfg.conflict": "
|
|
199
|
-
"cfg.saveFailed": "
|
|
200
|
-
"cfg.loadFailed": "
|
|
201
|
-
"cfg.unsupported": "宿主未提供设置描述符
|
|
226
|
+
"cfg.conflict": "保存冲突:设置已被其他会话修改,请点击「重新加载」后再保存。",
|
|
227
|
+
"cfg.saveFailed": "保存失败:{error}",
|
|
228
|
+
"cfg.loadFailed": "加载设置失败:{error}",
|
|
229
|
+
"cfg.unsupported": "宿主未提供设置描述符 (503)。请确认插件以 profile 条目安装,且 schemastery 版本 ≥ 3.18.4。",
|
|
202
230
|
"cfg.revision": "revision {n}"
|
|
203
231
|
};
|
|
204
232
|
const en = {
|
|
@@ -249,7 +277,7 @@ window.__ModuleLoader__.load({
|
|
|
249
277
|
"balance.delta.title": "Balance change this turn: {from} → {to}",
|
|
250
278
|
"balance.none.title": "Top-up balance unavailable: {error}",
|
|
251
279
|
"balance.setup": "Set up",
|
|
252
|
-
"balance.setup.title": "No OpenCode console
|
|
280
|
+
"balance.setup.title": "No usable OpenCode console OAuth login, so the top-up balance cannot be read (API-key logins carry no balance, and expired sessions fail too). Click to open the plugin settings for the step-by-step guide, or run opencode auth login and choose the OpenCode Console account.",
|
|
253
281
|
"balance.missing": "No top-up",
|
|
254
282
|
"balance.missing.title": "Command Code's credits response carried no purchased or free credits, so there is no top-up balance to show.",
|
|
255
283
|
"balance.details.extra": "purchased {purchased} + free {free}",
|
|
@@ -266,6 +294,7 @@ window.__ModuleLoader__.load({
|
|
|
266
294
|
"reason.unknown": "unknown reason",
|
|
267
295
|
"update.available": "v{v} available",
|
|
268
296
|
"update.title": "Click for upgrade instructions",
|
|
297
|
+
"error.retry": "Click to retry",
|
|
269
298
|
|
|
270
299
|
// settings page (Plugins panel)
|
|
271
300
|
"cfg.summary": "OpenCode GO, Command Code and GLM Code Plan plan usage in one sidebar widget.",
|
|
@@ -274,6 +303,30 @@ window.__ModuleLoader__.load({
|
|
|
274
303
|
"cfg.providers": "Providers",
|
|
275
304
|
"cfg.console.status": "Top-up balance",
|
|
276
305
|
"cfg.console.hint": "Run opencode auth login to sign in to the OpenCode console, or store the console token as the OPENCODE_CONSOLE_TOKEN credential, to show the top-up balance.",
|
|
306
|
+
"cfg.console.apiKey": "The local OpenCode login is an API key, which carries no console balance; run opencode auth login and choose the OpenCode Console account.",
|
|
307
|
+
"cfg.console.expired": "The console sign-in expired; run opencode auth login again and refresh.",
|
|
308
|
+
"cfg.console.guide.title": "How to show the top-up balance",
|
|
309
|
+
"cfg.console.guide.step1": "Run this command in a terminal and choose the OpenCode Console account:",
|
|
310
|
+
"cfg.console.guide.step2": "Or copy the token from the console website and store it as a DSH credential:",
|
|
311
|
+
"cfg.console.guide.step3": "When done, click Re-check — the plugin re-reads the sign-in state and refreshes the balance immediately; no DSH restart needed.",
|
|
312
|
+
"cfg.console.guide.link": "Open the OpenCode console ↗",
|
|
313
|
+
"cfg.console.copy.command": "Copy command",
|
|
314
|
+
"cfg.console.copy.ref": "Copy credential ref",
|
|
315
|
+
"cfg.console.recheck": "Re-check",
|
|
316
|
+
"cfg.console.rechecking": "Checking…",
|
|
317
|
+
"cfg.console.copied": "Copied",
|
|
318
|
+
"cfg.console.copyFailed": "Copy failed; copy it manually.",
|
|
319
|
+
"cfg.usage": "Live usage",
|
|
320
|
+
"cfg.usage.refresh": "Refresh usage",
|
|
321
|
+
"cfg.usage.refreshing": "Refreshing…",
|
|
322
|
+
"cfg.usage.loading": "Loading usage…",
|
|
323
|
+
"cfg.usage.loadFailed": "Failed to load usage: {error}",
|
|
324
|
+
"cfg.usage.disabled": "Disabled; enable it and save to show usage.",
|
|
325
|
+
"cfg.usage.noData": "No data",
|
|
326
|
+
"cfg.usage.error": "read failed: {error}",
|
|
327
|
+
"cfg.usage.balance": "Top-up balance",
|
|
328
|
+
"cfg.usage.noBalance": "no top-up balance",
|
|
329
|
+
"cfg.usage.updatedAt": "Updated {time}",
|
|
277
330
|
"cfg.enabled": "Enabled",
|
|
278
331
|
"cfg.order.hint": "Drag here to reorder",
|
|
279
332
|
"cfg.move.up": "Move up",
|
|
@@ -329,6 +382,16 @@ window.__ModuleLoader__.load({
|
|
|
329
382
|
return t("time.days", { n: Math.floor(hours / 24), m: hours % 24 });
|
|
330
383
|
}
|
|
331
384
|
|
|
385
|
+
/** Compact local clock "HH:MM" for the 「更新于」 stamp; "" when invalid. */
|
|
386
|
+
function formatClock(ms) {
|
|
387
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0) return "";
|
|
388
|
+
try {
|
|
389
|
+
return new Date(ms).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
390
|
+
} catch (_error) {
|
|
391
|
+
return "";
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
332
395
|
/** Fill color by usage percent — themed alias tokens (light/dark safe). */
|
|
333
396
|
function fillColor(percent) {
|
|
334
397
|
if (percent >= 90) return "var(--dsw-alias-state-error-primary)";
|
|
@@ -364,6 +427,20 @@ window.__ModuleLoader__.load({
|
|
|
364
427
|
return (change.amount < 0 ? "-" : "+") + text;
|
|
365
428
|
}
|
|
366
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Balance tone class (leading space included): at or below the critical
|
|
432
|
+
* threshold the chip turns red, at or below the low threshold amber,
|
|
433
|
+
* otherwise the usual green. Thresholds are in the currency unit and
|
|
434
|
+
* apply to USD and CNY plans alike — single-digit balances are exactly
|
|
435
|
+
* when "how much is left" starts to matter.
|
|
436
|
+
*/
|
|
437
|
+
function balanceTone(balance) {
|
|
438
|
+
if (!balance || typeof balance.amount !== "number" || !Number.isFinite(balance.amount)) return "";
|
|
439
|
+
if (balance.amount <= 1) return " dpu-critical";
|
|
440
|
+
if (balance.amount <= 5) return " dpu-low";
|
|
441
|
+
return "";
|
|
442
|
+
}
|
|
443
|
+
|
|
367
444
|
/** Animate a balance from its previous value to the refreshed value. */
|
|
368
445
|
function AnimatedBalance({ value, currency, delta }) {
|
|
369
446
|
const from = delta && typeof delta.from === "number" ? delta.from : value;
|
|
@@ -643,9 +720,17 @@ window.__ModuleLoader__.load({
|
|
|
643
720
|
}
|
|
644
721
|
}
|
|
645
722
|
|
|
646
|
-
/**
|
|
647
|
-
|
|
648
|
-
|
|
723
|
+
/**
|
|
724
|
+
* True when the balance failed in a way the user can fix by signing in
|
|
725
|
+
* again (no console token, an API-key-only login, or an expired grant).
|
|
726
|
+
* The host marks these with `meta.balanceSetup`; the message pattern
|
|
727
|
+
* keeps older host payloads working.
|
|
728
|
+
*/
|
|
729
|
+
function isBalanceSetupError(provider) {
|
|
730
|
+
const setup = provider?.meta && typeof provider.meta.balanceSetup === "string" ? provider.meta.balanceSetup : "";
|
|
731
|
+
if (setup === "no-credential" || setup === "api-key" || setup === "expired") return true;
|
|
732
|
+
const message = provider?.meta && typeof provider.meta.balanceError === "string" ? provider.meta.balanceError : "";
|
|
733
|
+
return /no console credential|opencode auth login|not an OpenCode Console session|expired or not a Console login/i.test(message);
|
|
649
734
|
}
|
|
650
735
|
|
|
651
736
|
/** Open the plugin settings from the balance chip (never toggles the card). */
|
|
@@ -777,6 +862,15 @@ window.__ModuleLoader__.load({
|
|
|
777
862
|
});
|
|
778
863
|
};
|
|
779
864
|
|
|
865
|
+
/** Click/keyboard retry: force one fresh upstream read for all cards. */
|
|
866
|
+
const forceRefresh = () => setStamp(stamp + 1);
|
|
867
|
+
const retryKeyDown = (event) => {
|
|
868
|
+
if (event && (event.key === "Enter" || event.key === " " || event.key === "Spacebar")) {
|
|
869
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
870
|
+
forceRefresh();
|
|
871
|
+
}
|
|
872
|
+
};
|
|
873
|
+
|
|
780
874
|
// Collapsed rail: a single compact badge summarising all providers.
|
|
781
875
|
if (!props.wide) {
|
|
782
876
|
const maxPct = providers.reduce((max, provider) => {
|
|
@@ -800,7 +894,7 @@ window.__ModuleLoader__.load({
|
|
|
800
894
|
const balance = provider.balance && typeof provider.balance.amount === "number" ? provider.balance : null;
|
|
801
895
|
const delta = balanceDeltas[provider.id] && typeof balanceDeltas[provider.id].amount === "number" ? balanceDeltas[provider.id] : null;
|
|
802
896
|
const showDelta = delta !== null && Math.abs(delta.amount) >= 0.005;
|
|
803
|
-
const balanceClass = "dpu-provider-balance" + (showDelta ? " dpu-hit" : "");
|
|
897
|
+
const balanceClass = "dpu-provider-balance" + (showDelta ? " dpu-hit" : "") + balanceTone(balance);
|
|
804
898
|
// Command Code's balance is purchased + free credits; name the
|
|
805
899
|
// composition in the tooltip so the single number stays honest.
|
|
806
900
|
const balanceDetails = balance && balance.details && typeof balance.details === "object" && typeof balance.details.purchased === "number"
|
|
@@ -816,10 +910,13 @@ window.__ModuleLoader__.load({
|
|
|
816
910
|
})
|
|
817
911
|
: t("balance.title", { amount: balanceText, source: balance ? asText(balance.source) : "" }) + (balanceDetails === "" ? "" : " · " + balanceDetails);
|
|
818
912
|
const rawBalanceError = provider.meta && typeof provider.meta.balanceError === "string" ? provider.meta.balanceError : "";
|
|
819
|
-
// No console credential
|
|
820
|
-
// shows a "set up balance"
|
|
821
|
-
|
|
822
|
-
|
|
913
|
+
// No console credential (or an API-key-only / expired login) is
|
|
914
|
+
// actionable, not a failure: the header shows a "set up balance"
|
|
915
|
+
// chip that opens the plugin settings. Its tooltip carries the
|
|
916
|
+
// precise host reason when one exists.
|
|
917
|
+
const balanceSetup = isBalanceSetupError(provider);
|
|
918
|
+
const balanceSetupTitle = rawBalanceError === "" ? t("balance.setup.title") : t("balance.setup.title") + " · " + rawBalanceError;
|
|
919
|
+
const balanceError = rawBalanceError !== "" && !balanceSetup ? rawBalanceError : "";
|
|
823
920
|
// Command Code: credits answered without either credit pool, so
|
|
824
921
|
// there is no top-up balance to show (it is not a config problem).
|
|
825
922
|
const balanceMissing = provider.meta && provider.meta.balanceMissing === true;
|
|
@@ -906,8 +1003,12 @@ window.__ModuleLoader__.load({
|
|
|
906
1003
|
if (provider.status === "error") {
|
|
907
1004
|
const message = asText(provider.error) || t("status.error");
|
|
908
1005
|
body.push(react_jsx_runtime.jsx("div", {
|
|
909
|
-
className: "dpu-provider-error",
|
|
910
|
-
|
|
1006
|
+
className: "dpu-provider-error dpu-retry",
|
|
1007
|
+
role: "button",
|
|
1008
|
+
tabIndex: 0,
|
|
1009
|
+
title: message + " · " + t("error.retry"),
|
|
1010
|
+
onClick: forceRefresh,
|
|
1011
|
+
onKeyDown: retryKeyDown,
|
|
911
1012
|
children: message
|
|
912
1013
|
}, "error"));
|
|
913
1014
|
} else if (rows.length === 0) {
|
|
@@ -934,12 +1035,12 @@ window.__ModuleLoader__.load({
|
|
|
934
1035
|
title: t("balance.none.title", { error: balanceError }),
|
|
935
1036
|
children: t("balance.none")
|
|
936
1037
|
}, "balance-error")
|
|
937
|
-
:
|
|
1038
|
+
: balanceSetup
|
|
938
1039
|
? react_jsx_runtime.jsx("span", {
|
|
939
1040
|
className: "dpu-balance-setup",
|
|
940
1041
|
role: "button",
|
|
941
1042
|
tabIndex: 0,
|
|
942
|
-
title:
|
|
1043
|
+
title: balanceSetupTitle,
|
|
943
1044
|
onClick: activateBalanceSetup,
|
|
944
1045
|
onKeyDown: balanceSetupKeyDown,
|
|
945
1046
|
children: t("balance.setup")
|
|
@@ -969,10 +1070,6 @@ window.__ModuleLoader__.load({
|
|
|
969
1070
|
title: providerTitle(t, provider),
|
|
970
1071
|
"aria-expanded": !isCollapsed,
|
|
971
1072
|
children: [
|
|
972
|
-
react_jsx_runtime.jsx("span", {
|
|
973
|
-
className: "dpu-chevron",
|
|
974
|
-
children: isCollapsed ? "▸" : "▾"
|
|
975
|
-
}),
|
|
976
1073
|
react_jsx_runtime.jsx("span", { className: providerStatusClass(provider) }),
|
|
977
1074
|
react_jsx_runtime.jsx("span", {
|
|
978
1075
|
className: "dpu-provider-name",
|
|
@@ -1035,7 +1132,15 @@ window.__ModuleLoader__.load({
|
|
|
1035
1132
|
children: autoUpdateNotice(t, data && data.autoUpdate)
|
|
1036
1133
|
}),
|
|
1037
1134
|
sections.length > 0 ? react_jsx_runtime.jsx("div", { className: "dpu-providers", children: sections }) : null,
|
|
1038
|
-
error ? react_jsx_runtime.jsx("div", {
|
|
1135
|
+
error ? react_jsx_runtime.jsx("div", {
|
|
1136
|
+
className: "dpu-err dpu-retry",
|
|
1137
|
+
role: "button",
|
|
1138
|
+
tabIndex: 0,
|
|
1139
|
+
title: t("error.retry"),
|
|
1140
|
+
onClick: forceRefresh,
|
|
1141
|
+
onKeyDown: retryKeyDown,
|
|
1142
|
+
children: error
|
|
1143
|
+
}) : null,
|
|
1039
1144
|
!error && data === null ? react_jsx_runtime.jsx("div", { className: "dpu-ok", children: t("loading") }) : null,
|
|
1040
1145
|
data !== null && providers.length === 0
|
|
1041
1146
|
? react_jsx_runtime.jsx("div", { className: "dpu-err", children: t("noProviders") })
|
|
@@ -1057,6 +1162,8 @@ window.__ModuleLoader__.load({
|
|
|
1057
1162
|
// auto-generated "Plugin configuration" form (they keep their defaults)
|
|
1058
1163
|
// so this page never scrolls.
|
|
1059
1164
|
const SETTINGS_PATH = "/provider-usage/settings";
|
|
1165
|
+
/** Same-origin usage snapshot the settings page renders live. */
|
|
1166
|
+
const USAGE_PATH = "/provider-usage/usage";
|
|
1060
1167
|
const PROVIDER_FORMS = [
|
|
1061
1168
|
{
|
|
1062
1169
|
id: "opencode-go",
|
|
@@ -1223,7 +1330,38 @@ window.__ModuleLoader__.load({
|
|
|
1223
1330
|
fontSize: 12,
|
|
1224
1331
|
fontWeight: 500
|
|
1225
1332
|
},
|
|
1226
|
-
buttonDisabled: { opacity: 0.4, cursor: "not-allowed" }
|
|
1333
|
+
buttonDisabled: { opacity: 0.4, cursor: "not-allowed" },
|
|
1334
|
+
// live-usage inset of a provider card
|
|
1335
|
+
usageBox: {
|
|
1336
|
+
display: "flex",
|
|
1337
|
+
flexDirection: "column",
|
|
1338
|
+
gap: 6,
|
|
1339
|
+
padding: "8px 10px",
|
|
1340
|
+
borderRadius: 8,
|
|
1341
|
+
background: "var(--dsw-alias-bg-layer-2, #101216)"
|
|
1342
|
+
},
|
|
1343
|
+
usageHead: { display: "flex", alignItems: "center", gap: 8 },
|
|
1344
|
+
usageLabel: { fontSize: 11, fontWeight: 600, color: "var(--dsw-alias-label-tertiary, #8a8f98)" },
|
|
1345
|
+
usageRow: { display: "flex", flexDirection: "column", gap: 3 },
|
|
1346
|
+
usageRowLabel: { display: "flex", justifyContent: "space-between", gap: 8, fontSize: 12, lineHeight: 1.5, minWidth: 0 },
|
|
1347
|
+
usageRowName: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #a8adb8)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
1348
|
+
usageRowValue: { flex: "none", fontSize: 12, fontWeight: 500, color: "var(--dsw-alias-label-primary, #e8e8e8)" },
|
|
1349
|
+
usageTrack: { height: 4, borderRadius: 2, background: "var(--dsw-alias-border-l2, #2a2d34)", overflow: "hidden" },
|
|
1350
|
+
usageFill: { height: "100%", borderRadius: 2, transition: "width .4s ease" },
|
|
1351
|
+
usageHint: { margin: 0, fontSize: 11, lineHeight: 1.5, color: "var(--dsw-alias-label-tertiary, #8a8f98)", overflowWrap: "anywhere" },
|
|
1352
|
+
// balance setup guide (OpenCode GO card)
|
|
1353
|
+
guideTitle: { fontSize: 12, fontWeight: 600, color: "var(--dsw-alias-label-secondary, #a8adb8)" },
|
|
1354
|
+
guideStep: { display: "flex", flexDirection: "column", gap: 4 },
|
|
1355
|
+
guideCode: {
|
|
1356
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
1357
|
+
fontSize: 11,
|
|
1358
|
+
color: "var(--dsw-alias-label-primary, #e8e8e8)",
|
|
1359
|
+
background: "var(--dsw-alias-bg-layer-3, #16181d)",
|
|
1360
|
+
border: "1px solid var(--dsw-alias-border-l2, #2a2d34)",
|
|
1361
|
+
borderRadius: 6,
|
|
1362
|
+
padding: "2px 6px",
|
|
1363
|
+
overflowWrap: "anywhere"
|
|
1364
|
+
}
|
|
1227
1365
|
};
|
|
1228
1366
|
|
|
1229
1367
|
function Field(props) {
|
|
@@ -1275,11 +1413,13 @@ window.__ModuleLoader__.load({
|
|
|
1275
1413
|
}
|
|
1276
1414
|
|
|
1277
1415
|
/**
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1416
|
+
* A small copy button with the transient "Copied / Copy failed" note.
|
|
1417
|
+
* Shared by the referral row and the balance setup guide. `value` is
|
|
1418
|
+
* what lands on the clipboard; `label`/`copiedKey`/`failedKey` let the
|
|
1419
|
+
* caller pick the wording. Returns a boolean path so the caller can
|
|
1420
|
+
* show a failure hint instead of a lie.
|
|
1281
1421
|
*/
|
|
1282
|
-
function
|
|
1422
|
+
function CopyButton(props) {
|
|
1283
1423
|
const t = props.t;
|
|
1284
1424
|
const [status, setStatus] = react.useState("idle");
|
|
1285
1425
|
const timer = react.useRef(null);
|
|
@@ -1287,12 +1427,38 @@ window.__ModuleLoader__.load({
|
|
|
1287
1427
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
1288
1428
|
}, []);
|
|
1289
1429
|
const copy = async () => {
|
|
1290
|
-
const ok = await copyText(props.
|
|
1430
|
+
const ok = await copyText(props.value);
|
|
1291
1431
|
setStatus(ok ? "copied" : "failed");
|
|
1292
1432
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
1293
1433
|
timer.current = setTimeout(() => setStatus("idle"), 1800);
|
|
1294
1434
|
};
|
|
1295
|
-
const note = status === "copied"
|
|
1435
|
+
const note = status === "copied"
|
|
1436
|
+
? t(props.copiedKey ?? "cfg.invite.copied")
|
|
1437
|
+
: status === "failed" ? t(props.failedKey ?? "cfg.invite.copyFailed") : "";
|
|
1438
|
+
return react_jsx_runtime.jsx("div", {
|
|
1439
|
+
style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
|
|
1440
|
+
children: [
|
|
1441
|
+
react_jsx_runtime.jsx("button", {
|
|
1442
|
+
type: "button",
|
|
1443
|
+
style: { ...PS.button, padding: "3px 10px" },
|
|
1444
|
+
onClick: copy,
|
|
1445
|
+
children: props.label ?? t("cfg.invite.copy")
|
|
1446
|
+
}, "copy"),
|
|
1447
|
+
note === "" ? null : react_jsx_runtime.jsx("span", {
|
|
1448
|
+
style: status === "failed" ? PS.warn : PS.ok,
|
|
1449
|
+
children: note
|
|
1450
|
+
}, "copy-note")
|
|
1451
|
+
]
|
|
1452
|
+
});
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* The GLM card's referral row: the invitation URL and a Copy button.
|
|
1457
|
+
* Purely local — no host route reads it and it is never part of the
|
|
1458
|
+
* saved settings value, so saving settings cannot rewrite it.
|
|
1459
|
+
*/
|
|
1460
|
+
function InviteLink(props) {
|
|
1461
|
+
const t = props.t;
|
|
1296
1462
|
return react_jsx_runtime.jsx("div", {
|
|
1297
1463
|
style: PS.inviteRow,
|
|
1298
1464
|
children: [
|
|
@@ -1304,16 +1470,206 @@ window.__ModuleLoader__.load({
|
|
|
1304
1470
|
rel: "noreferrer",
|
|
1305
1471
|
children: props.url
|
|
1306
1472
|
}),
|
|
1307
|
-
react_jsx_runtime.jsx(
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1473
|
+
react_jsx_runtime.jsx(CopyButton, { t: t, value: props.url }, "invite-copy")
|
|
1474
|
+
]
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
/**
|
|
1479
|
+
* The balance setup guide on the OpenCode GO card, shown whenever the
|
|
1480
|
+
* console read is not configured or names a fixable reason (API-key
|
|
1481
|
+
* login, expired sign-in). Three steps, each with an actionable row:
|
|
1482
|
+
* the CLI sign-in command (copyable), the manual credential path with
|
|
1483
|
+
* a console link, and a re-check that re-reads the sign-in state and
|
|
1484
|
+
* refreshes the balance without restarting DSH. The lead-in names the
|
|
1485
|
+
* state-specific fix.
|
|
1486
|
+
*/
|
|
1487
|
+
function ConsoleGuide(props) {
|
|
1488
|
+
const t = props.t;
|
|
1489
|
+
const [checking, setChecking] = react.useState(false);
|
|
1490
|
+
const recheck = async () => {
|
|
1491
|
+
if (checking) return;
|
|
1492
|
+
setChecking(true);
|
|
1493
|
+
try {
|
|
1494
|
+
await props.onRecheck();
|
|
1495
|
+
} finally {
|
|
1496
|
+
setChecking(false);
|
|
1497
|
+
}
|
|
1498
|
+
};
|
|
1499
|
+
const leadIn = props.reason === "api-key"
|
|
1500
|
+
? t("cfg.console.apiKey")
|
|
1501
|
+
: props.reason === "expired" ? t("cfg.console.expired") : t("cfg.console.hint");
|
|
1502
|
+
const credentialRef = typeof props.credentialRef === "string" && props.credentialRef !== ""
|
|
1503
|
+
? props.credentialRef
|
|
1504
|
+
: "OPENCODE_CONSOLE_TOKEN";
|
|
1505
|
+
const command = "opencode auth login";
|
|
1506
|
+
const copyKeys = { copiedKey: "cfg.console.copied", failedKey: "cfg.console.copyFailed" };
|
|
1507
|
+
const stepRow = (key, text) => react_jsx_runtime.jsx("p", { style: PS.hint, children: text }, key);
|
|
1508
|
+
return react_jsx_runtime.jsx("div", {
|
|
1509
|
+
style: PS.usageBox,
|
|
1510
|
+
children: [
|
|
1511
|
+
react_jsx_runtime.jsx("div", { style: PS.guideTitle, children: t("cfg.console.guide.title") }, "guide-title"),
|
|
1512
|
+
react_jsx_runtime.jsx("p", { style: PS.hint, children: leadIn }, "guide-lead"),
|
|
1513
|
+
react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
|
|
1514
|
+
stepRow("guide-step1-text", t("cfg.console.guide.step1")),
|
|
1515
|
+
react_jsx_runtime.jsx("div", {
|
|
1516
|
+
style: PS.inviteRow,
|
|
1517
|
+
children: [
|
|
1518
|
+
react_jsx_runtime.jsx("code", { style: PS.guideCode, children: command }, "guide-command"),
|
|
1519
|
+
react_jsx_runtime.jsx(CopyButton, {
|
|
1520
|
+
t: t,
|
|
1521
|
+
value: command,
|
|
1522
|
+
label: t("cfg.console.copy.command"),
|
|
1523
|
+
...copyKeys
|
|
1524
|
+
}, "guide-step1-copy")
|
|
1525
|
+
]
|
|
1526
|
+
}, "guide-step1-row")
|
|
1527
|
+
] }, "guide-step1"),
|
|
1528
|
+
react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
|
|
1529
|
+
stepRow("guide-step2-text", t("cfg.console.guide.step2")),
|
|
1530
|
+
react_jsx_runtime.jsx("div", {
|
|
1531
|
+
style: PS.inviteRow,
|
|
1532
|
+
children: [
|
|
1533
|
+
react_jsx_runtime.jsx("code", { style: PS.guideCode, children: credentialRef }, "guide-ref"),
|
|
1534
|
+
react_jsx_runtime.jsx(CopyButton, {
|
|
1535
|
+
t: t,
|
|
1536
|
+
value: credentialRef,
|
|
1537
|
+
label: t("cfg.console.copy.ref"),
|
|
1538
|
+
...copyKeys
|
|
1539
|
+
}, "guide-step2-copy")
|
|
1540
|
+
]
|
|
1541
|
+
}, "guide-step2-row"),
|
|
1542
|
+
typeof props.consoleUrl === "string" && props.consoleUrl !== ""
|
|
1543
|
+
? react_jsx_runtime.jsx("a", {
|
|
1544
|
+
style: PS.inviteLink,
|
|
1545
|
+
href: props.consoleUrl,
|
|
1546
|
+
target: "_blank",
|
|
1547
|
+
rel: "noreferrer",
|
|
1548
|
+
children: t("cfg.console.guide.link")
|
|
1549
|
+
}, "guide-link")
|
|
1550
|
+
: null
|
|
1551
|
+
] }, "guide-step2"),
|
|
1552
|
+
react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
|
|
1553
|
+
stepRow("guide-step3-text", t("cfg.console.guide.step3")),
|
|
1554
|
+
react_jsx_runtime.jsx("button", {
|
|
1555
|
+
type: "button",
|
|
1556
|
+
style: checking || props.disabled === true
|
|
1557
|
+
? { ...PS.button, ...PS.buttonDisabled }
|
|
1558
|
+
: PS.button,
|
|
1559
|
+
disabled: checking || props.disabled === true,
|
|
1560
|
+
onClick: recheck,
|
|
1561
|
+
children: checking ? t("cfg.console.rechecking") : t("cfg.console.recheck")
|
|
1562
|
+
}, "guide-recheck")
|
|
1563
|
+
] }, "guide-step3")
|
|
1564
|
+
]
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/**
|
|
1569
|
+
* The live-usage inset of one provider card. `SettingsPage` fetches the
|
|
1570
|
+
* canonical usage snapshot (the same payload the widget renders) and
|
|
1571
|
+
* passes each provider's slice in; no hooks here, so it is safe to call
|
|
1572
|
+
* from any render path. `provider === null` covers not-loaded-yet,
|
|
1573
|
+
* a failed snapshot and a disabled-in-payload provider.
|
|
1574
|
+
*/
|
|
1575
|
+
function UsageSection(props) {
|
|
1576
|
+
const t = props.t;
|
|
1577
|
+
if (props.enabled === false) {
|
|
1578
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.disabled") });
|
|
1579
|
+
}
|
|
1580
|
+
const provider = props.provider === undefined ? null : props.provider;
|
|
1581
|
+
if (provider === null) {
|
|
1582
|
+
if (props.phase === "loading" || props.phase === "idle") {
|
|
1583
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.loading") });
|
|
1584
|
+
}
|
|
1585
|
+
if (props.phase === "error") {
|
|
1586
|
+
return react_jsx_runtime.jsx("p", { style: PS.err, children: t("cfg.usage.loadFailed", { error: asText(props.error) || "?" }) });
|
|
1587
|
+
}
|
|
1588
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.noData") });
|
|
1589
|
+
}
|
|
1590
|
+
if (provider.status === "error") {
|
|
1591
|
+
return react_jsx_runtime.jsx("p", { style: PS.err, children: t("cfg.usage.error", { error: asText(provider.error) || "?" }) });
|
|
1592
|
+
}
|
|
1593
|
+
const rows = [];
|
|
1594
|
+
for (const win of providerWindows(provider)) {
|
|
1595
|
+
const pct = typeof win.percent === "number" && Number.isFinite(win.percent) ? win.percent : null;
|
|
1596
|
+
const reset = win.resetsAt ? timeUntil(win.resetsAt, t) : "";
|
|
1597
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
1598
|
+
style: PS.usageRow,
|
|
1599
|
+
children: [
|
|
1600
|
+
react_jsx_runtime.jsx("div", {
|
|
1601
|
+
style: PS.usageRowLabel,
|
|
1602
|
+
children: [
|
|
1603
|
+
react_jsx_runtime.jsx("span", { style: PS.usageRowName, title: windowLabel(t, win), children: windowLabel(t, win) }),
|
|
1604
|
+
react_jsx_runtime.jsx("span", {
|
|
1605
|
+
style: PS.usageRowValue,
|
|
1606
|
+
children: (pct === null ? "—" : formatPercent(pct) + "%") + (reset === "" ? "" : " · " + reset)
|
|
1607
|
+
})
|
|
1608
|
+
]
|
|
1609
|
+
}),
|
|
1610
|
+
react_jsx_runtime.jsx("div", {
|
|
1611
|
+
style: PS.usageTrack,
|
|
1612
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1613
|
+
style: { ...PS.usageFill, width: Math.max(0, Math.min(100, pct ?? 0)) + "%", background: fillColor(pct ?? 0) }
|
|
1614
|
+
})
|
|
1615
|
+
})
|
|
1616
|
+
]
|
|
1617
|
+
}, win.key));
|
|
1618
|
+
}
|
|
1619
|
+
// The top-up balance row mirrors the widget's chip: value when one
|
|
1620
|
+
// exists, otherwise the reason it does not (setup needed, no pools,
|
|
1621
|
+
// or a failed read). A provider with no balance concept at all (GLM)
|
|
1622
|
+
// carries no flags, so the row is skipped entirely.
|
|
1623
|
+
const meta = provider.meta && typeof provider.meta === "object" ? provider.meta : {};
|
|
1624
|
+
const balance = provider.balance && typeof provider.balance.amount === "number" ? provider.balance : null;
|
|
1625
|
+
const balanceValue = formatMoney(balance);
|
|
1626
|
+
const balanceDetails = balance && balance.details && typeof balance.details === "object" && typeof balance.details.purchased === "number"
|
|
1627
|
+
? t("balance.details.extra", {
|
|
1628
|
+
purchased: formatMoney({ amount: balance.details.purchased, currency: balance.currency }),
|
|
1629
|
+
free: formatMoney({ amount: typeof balance.details.free === "number" ? balance.details.free : 0, currency: balance.currency })
|
|
1630
|
+
})
|
|
1631
|
+
: "";
|
|
1632
|
+
const balanceMissing = meta.balanceMissing === true;
|
|
1633
|
+
const balanceSetup = meta.balanceSetup === "no-credential" || meta.balanceSetup === "api-key" || meta.balanceSetup === "expired";
|
|
1634
|
+
const balanceError = balanceValue === "" && !balanceMissing && !balanceSetup && typeof meta.balanceError === "string" && meta.balanceError !== ""
|
|
1635
|
+
? meta.balanceError
|
|
1636
|
+
: "";
|
|
1637
|
+
if (balanceValue !== "" || balanceMissing || balanceSetup || balanceError !== "") {
|
|
1638
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
1639
|
+
style: PS.usageRow,
|
|
1640
|
+
children: [
|
|
1641
|
+
react_jsx_runtime.jsx("div", {
|
|
1642
|
+
style: PS.usageRowLabel,
|
|
1643
|
+
children: [
|
|
1644
|
+
react_jsx_runtime.jsx("span", { style: PS.usageRowName, children: t("cfg.usage.balance") }),
|
|
1645
|
+
react_jsx_runtime.jsx("span", {
|
|
1646
|
+
style: { ...PS.usageRowValue, color: balanceValue === "" ? "var(--dsw-alias-label-tertiary, #8a8f98)" : PS.usageRowValue.color },
|
|
1647
|
+
children: balanceValue !== ""
|
|
1648
|
+
? balanceValue
|
|
1649
|
+
: balanceMissing ? t("balance.missing") : t("cfg.usage.noBalance")
|
|
1650
|
+
})
|
|
1651
|
+
]
|
|
1652
|
+
}),
|
|
1653
|
+
balanceValue !== "" && balanceDetails !== ""
|
|
1654
|
+
? react_jsx_runtime.jsx("span", { style: PS.usageHint, children: balanceDetails })
|
|
1655
|
+
: null,
|
|
1656
|
+
balanceError !== ""
|
|
1657
|
+
? react_jsx_runtime.jsx("span", { style: PS.usageHint, children: balanceError })
|
|
1658
|
+
: null
|
|
1659
|
+
]
|
|
1660
|
+
}, "balance"));
|
|
1661
|
+
}
|
|
1662
|
+
if (rows.length === 0) {
|
|
1663
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.noData") });
|
|
1664
|
+
}
|
|
1665
|
+
return react_jsx_runtime.jsx("div", {
|
|
1666
|
+
style: PS.usageBox,
|
|
1667
|
+
children: [
|
|
1668
|
+
react_jsx_runtime.jsx("div", {
|
|
1669
|
+
style: PS.usageHead,
|
|
1670
|
+
children: react_jsx_runtime.jsx("span", { style: PS.usageLabel, children: t("cfg.usage") })
|
|
1671
|
+
}, "usage-head"),
|
|
1672
|
+
...rows
|
|
1317
1673
|
]
|
|
1318
1674
|
});
|
|
1319
1675
|
}
|
|
@@ -1333,6 +1689,11 @@ window.__ModuleLoader__.load({
|
|
|
1333
1689
|
const [busy, setBusy] = react.useState({ save: false, reload: false });
|
|
1334
1690
|
const [message, setMessage] = react.useState(null);
|
|
1335
1691
|
const [draggingId, setDraggingId] = react.useState(null);
|
|
1692
|
+
// Live usage snapshot for the per-card 实时用量 sections. `phase`
|
|
1693
|
+
// gates the placeholders; on a failed refresh the previous data
|
|
1694
|
+
// stays visible and only the error line appears. `at` stamps when
|
|
1695
|
+
// the numbers were read, shown as 「更新于 HH:MM」.
|
|
1696
|
+
const [usage, setUsage] = react.useState({ phase: "idle", data: null, error: null, at: 0 });
|
|
1336
1697
|
const tRef = react.useRef(t);
|
|
1337
1698
|
tRef.current = t;
|
|
1338
1699
|
// Id of the provider card currently being dragged (drag reordering).
|
|
@@ -1369,9 +1730,50 @@ window.__ModuleLoader__.load({
|
|
|
1369
1730
|
}
|
|
1370
1731
|
}, []);
|
|
1371
1732
|
|
|
1733
|
+
/**
|
|
1734
|
+
* Fetch the canonical usage snapshot; `force` appends `?refresh=1`
|
|
1735
|
+
* so the host re-reads upstream and the numbers reflect a just-
|
|
1736
|
+
* saved credential change immediately.
|
|
1737
|
+
*/
|
|
1738
|
+
const loadUsage = react.useCallback(async (force) => {
|
|
1739
|
+
setUsage((prev) => ({ ...prev, phase: "loading" }));
|
|
1740
|
+
try {
|
|
1741
|
+
const res = await callApi("GET", USAGE_PATH + (force ? "?refresh=1" : ""));
|
|
1742
|
+
const payload = res.payload;
|
|
1743
|
+
if (res.status === 200 && payload !== null && Array.isArray(payload.providers)) {
|
|
1744
|
+
setUsage({ phase: "ready", data: payload, error: null, at: Date.now() });
|
|
1745
|
+
} else {
|
|
1746
|
+
const detail = (payload && payload.error) || "HTTP " + res.status;
|
|
1747
|
+
setUsage((prev) => ({ phase: "error", data: prev.data, error: detail }));
|
|
1748
|
+
}
|
|
1749
|
+
} catch (error) {
|
|
1750
|
+
setUsage((prev) => ({ phase: "error", data: prev.data, error: (error && error.message) || String(error) }));
|
|
1751
|
+
}
|
|
1752
|
+
}, []);
|
|
1753
|
+
|
|
1372
1754
|
react.useEffect(() => {
|
|
1373
1755
|
reload(false);
|
|
1374
|
-
|
|
1756
|
+
void loadUsage(false);
|
|
1757
|
+
}, [reload, loadUsage]);
|
|
1758
|
+
|
|
1759
|
+
// While the settings page is open it tracks the widget: poll every
|
|
1760
|
+
// 30 s (cheap — the host cache collapses reads) and catch up the
|
|
1761
|
+
// moment the tab becomes visible again.
|
|
1762
|
+
react.useEffect(() => {
|
|
1763
|
+
if (typeof window === "undefined" || typeof window.setInterval !== "function") return undefined;
|
|
1764
|
+
const hidden = () => typeof document !== "undefined" && document.hidden === true;
|
|
1765
|
+
const timer = window.setInterval(() => { if (!hidden()) void loadUsage(false); }, 30_000);
|
|
1766
|
+
const onVisible = () => { if (typeof document !== "undefined" && !document.hidden) void loadUsage(false); };
|
|
1767
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
1768
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
1769
|
+
}
|
|
1770
|
+
return () => {
|
|
1771
|
+
window.clearInterval(timer);
|
|
1772
|
+
if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
|
|
1773
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}, [loadUsage]);
|
|
1375
1777
|
|
|
1376
1778
|
const update = (key, value) => {
|
|
1377
1779
|
setForm((prev) => ({ ...prev, [key]: value }));
|
|
@@ -1398,6 +1800,10 @@ window.__ModuleLoader__.load({
|
|
|
1398
1800
|
if (res.status === 200 && payload !== null) {
|
|
1399
1801
|
await reload(false);
|
|
1400
1802
|
usageRefreshStore.publish();
|
|
1803
|
+
// A saved credential change (apiKeyEnv) must show its own
|
|
1804
|
+
// numbers right away: force one upstream re-read instead
|
|
1805
|
+
// of waiting for the next TTL.
|
|
1806
|
+
void loadUsage(true);
|
|
1401
1807
|
setMessage({ kind: "ok", text: tt("cfg.saved") });
|
|
1402
1808
|
} else if (res.status === 409) {
|
|
1403
1809
|
setMessage({ kind: "warn", text: tt("cfg.conflict") });
|
|
@@ -1416,6 +1822,7 @@ window.__ModuleLoader__.load({
|
|
|
1416
1822
|
setBusy((b) => ({ ...b, reload: true }));
|
|
1417
1823
|
try {
|
|
1418
1824
|
await reload(true);
|
|
1825
|
+
void loadUsage(false);
|
|
1419
1826
|
} finally {
|
|
1420
1827
|
setBusy((b) => ({ ...b, reload: false }));
|
|
1421
1828
|
}
|
|
@@ -1448,6 +1855,28 @@ window.__ModuleLoader__.load({
|
|
|
1448
1855
|
const server = formFromValue(state.value || {});
|
|
1449
1856
|
const dirty = Object.keys(form).some((key) => !fieldEquals(form[key], server[key]));
|
|
1450
1857
|
|
|
1858
|
+
// The balance guide's manual path: the configured console credential
|
|
1859
|
+
// ref (default OPENCODE_CONSOLE_TOKEN) and the console website
|
|
1860
|
+
// derived from the configured console origin.
|
|
1861
|
+
const consoleValue = state.value && typeof state.value === "object" ? state.value : {};
|
|
1862
|
+
const consoleRef = typeof consoleValue.opencodeConsoleApiKeyEnv === "string" && consoleValue.opencodeConsoleApiKeyEnv !== ""
|
|
1863
|
+
? consoleValue.opencodeConsoleApiKeyEnv
|
|
1864
|
+
: "OPENCODE_CONSOLE_TOKEN";
|
|
1865
|
+
const consoleLink = String(consoleValue.opencodeConsoleBaseUrl || "https://opencode.ai").replace(/\/+$/, "") + "/console";
|
|
1866
|
+
|
|
1867
|
+
/**
|
|
1868
|
+
* Re-check the console balance: re-read the detection report
|
|
1869
|
+
* (configured / reason) and force one upstream usage read, so a
|
|
1870
|
+
* sign-in the user just completed shows up without restarting DSH.
|
|
1871
|
+
* The settings reload is skipped while the form is dirty so
|
|
1872
|
+
* unsaved edits are not discarded — the forced usage read alone
|
|
1873
|
+
* still refreshes the balance row.
|
|
1874
|
+
*/
|
|
1875
|
+
const recheckBalance = async () => {
|
|
1876
|
+
if (!dirty) await reload(false);
|
|
1877
|
+
await loadUsage(true);
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1451
1880
|
// Cards render in the saved order. Reordering works three ways, so a
|
|
1452
1881
|
// shell without HTML5 drag (touch, Firefox proton, embedded webviews)
|
|
1453
1882
|
// always has a path: drag a card header onto another card, drop it
|
|
@@ -1588,15 +2017,23 @@ window.__ModuleLoader__.load({
|
|
|
1588
2017
|
// Reading the top-up balance is always on (the host default);
|
|
1589
2018
|
// the page only reports whether a credential was found.
|
|
1590
2019
|
if (consoleInfo !== null) {
|
|
2020
|
+
const consoleFixable = consoleInfo.reason === "api-key" || consoleInfo.reason === "expired";
|
|
1591
2021
|
children.push(react_jsx_runtime.jsx(StatusLine, {
|
|
1592
2022
|
error: typeof consoleInfo.error === "string" && consoleInfo.error !== "",
|
|
1593
2023
|
children: t("cfg.console.status") + ": " + (consoleInfo.configured === true ? t("cfg.status.configured") : t("cfg.status.notConfigured")) + (consoleInfo.source ? " · " + consoleInfo.source : "")
|
|
1594
2024
|
}, "console-status"));
|
|
1595
|
-
if (consoleInfo.configured !== true) {
|
|
1596
|
-
children.push(react_jsx_runtime.jsx(
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2025
|
+
if (consoleInfo.configured !== true || consoleFixable) {
|
|
2026
|
+
children.push(react_jsx_runtime.jsx(ConsoleGuide, {
|
|
2027
|
+
t: t,
|
|
2028
|
+
// A specific reason (API-key login, expired
|
|
2029
|
+
// session) names the fix in the guide's lead-in
|
|
2030
|
+
// instead of the generic hint.
|
|
2031
|
+
reason: consoleFixable ? consoleInfo.reason : "",
|
|
2032
|
+
credentialRef: consoleRef,
|
|
2033
|
+
consoleUrl: consoleLink,
|
|
2034
|
+
disabled: disabled,
|
|
2035
|
+
onRecheck: recheckBalance
|
|
2036
|
+
}, "console-guide"));
|
|
1600
2037
|
}
|
|
1601
2038
|
}
|
|
1602
2039
|
}
|
|
@@ -1606,6 +2043,19 @@ window.__ModuleLoader__.load({
|
|
|
1606
2043
|
children: statusLines.join(" · ")
|
|
1607
2044
|
}));
|
|
1608
2045
|
|
|
2046
|
+
// Live usage for this card, matched from the canonical snapshot
|
|
2047
|
+
// by provider id (the snapshot only carries enabled providers).
|
|
2048
|
+
const usageInfo = usage.data && Array.isArray(usage.data.providers)
|
|
2049
|
+
? usage.data.providers.find((candidate) => candidate && candidate.id === definition.id) ?? null
|
|
2050
|
+
: null;
|
|
2051
|
+
children.push(react_jsx_runtime.jsx(UsageSection, {
|
|
2052
|
+
t: t,
|
|
2053
|
+
enabled: enabled,
|
|
2054
|
+
provider: usageInfo,
|
|
2055
|
+
phase: usage.phase,
|
|
2056
|
+
error: usage.error
|
|
2057
|
+
}, "usage"));
|
|
2058
|
+
|
|
1609
2059
|
if (typeof definition.inviteUrl === "string" && definition.inviteUrl !== "") {
|
|
1610
2060
|
children.push(react_jsx_runtime.jsx(InviteLink, { t: t, url: definition.inviteUrl }, "invite"));
|
|
1611
2061
|
}
|
|
@@ -1633,7 +2083,27 @@ window.__ModuleLoader__.load({
|
|
|
1633
2083
|
children: [
|
|
1634
2084
|
react_jsx_runtime.jsx("div", { style: PS.title, children: t("cfg.title") }),
|
|
1635
2085
|
react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.lead") }),
|
|
1636
|
-
react_jsx_runtime.jsx("div", {
|
|
2086
|
+
react_jsx_runtime.jsx("div", {
|
|
2087
|
+
style: PS.row,
|
|
2088
|
+
children: [
|
|
2089
|
+
react_jsx_runtime.jsx("div", { style: PS.title, children: t("cfg.providers") }),
|
|
2090
|
+
typeof usage.at === "number" && usage.at > 0
|
|
2091
|
+
? react_jsx_runtime.jsx("span", {
|
|
2092
|
+
style: { ...PS.hint, marginLeft: "auto" },
|
|
2093
|
+
children: t("cfg.usage.updatedAt", { time: formatClock(usage.at) })
|
|
2094
|
+
}, "usage-updated")
|
|
2095
|
+
: react_jsx_runtime.jsx("span", { style: { ...PS.hint, marginLeft: "auto" } }, "usage-updated"),
|
|
2096
|
+
react_jsx_runtime.jsx("button", {
|
|
2097
|
+
type: "button",
|
|
2098
|
+
style: usage.phase === "loading"
|
|
2099
|
+
? { ...PS.button, padding: "3px 10px", ...PS.buttonDisabled }
|
|
2100
|
+
: { ...PS.button, padding: "3px 10px" },
|
|
2101
|
+
disabled: usage.phase === "loading",
|
|
2102
|
+
onClick: () => { void loadUsage(true); },
|
|
2103
|
+
children: usage.phase === "loading" ? t("cfg.usage.refreshing") : t("cfg.usage.refresh")
|
|
2104
|
+
}, "usage-refresh")
|
|
2105
|
+
]
|
|
2106
|
+
}),
|
|
1637
2107
|
react_jsx_runtime.jsx("div", {
|
|
1638
2108
|
style: { display: "flex", flexDirection: "column", gap: 8 },
|
|
1639
2109
|
children: providerCards
|
|
@@ -1673,7 +2143,7 @@ window.__ModuleLoader__.load({
|
|
|
1673
2143
|
}
|
|
1674
2144
|
|
|
1675
2145
|
/** One-line description the panel may render as the bundle's summary. */
|
|
1676
|
-
const SUMMARY = "OpenCode GO / Command Code / GLM Code Plan
|
|
2146
|
+
const SUMMARY = "OpenCode GO / Command Code / GLM Code Plan 套餐用量:一个侧边栏组件、同源代理与 /provider-usage 命令。";
|
|
1677
2147
|
|
|
1678
2148
|
/**
|
|
1679
2149
|
* The `plugins.bundle.config` occupant. The `view` branch is a component
|
|
@@ -1743,7 +2213,10 @@ window.__ModuleLoader__.load({
|
|
|
1743
2213
|
exports.Widget = Widget;
|
|
1744
2214
|
exports.BundleConfig = BundleConfig;
|
|
1745
2215
|
exports.SettingsPage = SettingsPage;
|
|
2216
|
+
exports.UsageSection = UsageSection;
|
|
1746
2217
|
exports.InviteLink = InviteLink;
|
|
2218
|
+
exports.CopyButton = CopyButton;
|
|
2219
|
+
exports.ConsoleGuide = ConsoleGuide;
|
|
1747
2220
|
exports.GLM_INVITE_URL = GLM_INVITE_URL;
|
|
1748
2221
|
exports.balanceDeltasBetween = balanceDeltasBetween;
|
|
1749
2222
|
exports.formatSignedMoney = formatSignedMoney;
|