dsh-multi-provider-usage 0.7.1 → 0.7.3
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 +77 -28
- package/README.zh.md +60 -23
- package/lib/auto-update.js +188 -26
- package/lib/client.js +544 -69
- 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;
|
|
@@ -556,8 +633,10 @@ window.__ModuleLoader__.load({
|
|
|
556
633
|
if (state.status === "updated" && state.restartRequired === true) {
|
|
557
634
|
return t("autoUpdate.updated", { v: state.version || state.latestVersion || "?" });
|
|
558
635
|
}
|
|
559
|
-
|
|
560
|
-
|
|
636
|
+
// The host only reports "available" after a successful check, so the
|
|
637
|
+
// version is real; the guard keeps a stale "v?" off the widget anyway.
|
|
638
|
+
if (state.status === "available" && typeof state.latestVersion === "string" && state.latestVersion.length > 0) {
|
|
639
|
+
return t("autoUpdate.available", { v: state.latestVersion });
|
|
561
640
|
}
|
|
562
641
|
return "";
|
|
563
642
|
}
|
|
@@ -643,9 +722,17 @@ window.__ModuleLoader__.load({
|
|
|
643
722
|
}
|
|
644
723
|
}
|
|
645
724
|
|
|
646
|
-
/**
|
|
647
|
-
|
|
648
|
-
|
|
725
|
+
/**
|
|
726
|
+
* True when the balance failed in a way the user can fix by signing in
|
|
727
|
+
* again (no console token, an API-key-only login, or an expired grant).
|
|
728
|
+
* The host marks these with `meta.balanceSetup`; the message pattern
|
|
729
|
+
* keeps older host payloads working.
|
|
730
|
+
*/
|
|
731
|
+
function isBalanceSetupError(provider) {
|
|
732
|
+
const setup = provider?.meta && typeof provider.meta.balanceSetup === "string" ? provider.meta.balanceSetup : "";
|
|
733
|
+
if (setup === "no-credential" || setup === "api-key" || setup === "expired") return true;
|
|
734
|
+
const message = provider?.meta && typeof provider.meta.balanceError === "string" ? provider.meta.balanceError : "";
|
|
735
|
+
return /no console credential|opencode auth login|not an OpenCode Console session|expired or not a Console login/i.test(message);
|
|
649
736
|
}
|
|
650
737
|
|
|
651
738
|
/** Open the plugin settings from the balance chip (never toggles the card). */
|
|
@@ -777,6 +864,15 @@ window.__ModuleLoader__.load({
|
|
|
777
864
|
});
|
|
778
865
|
};
|
|
779
866
|
|
|
867
|
+
/** Click/keyboard retry: force one fresh upstream read for all cards. */
|
|
868
|
+
const forceRefresh = () => setStamp(stamp + 1);
|
|
869
|
+
const retryKeyDown = (event) => {
|
|
870
|
+
if (event && (event.key === "Enter" || event.key === " " || event.key === "Spacebar")) {
|
|
871
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
872
|
+
forceRefresh();
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
|
|
780
876
|
// Collapsed rail: a single compact badge summarising all providers.
|
|
781
877
|
if (!props.wide) {
|
|
782
878
|
const maxPct = providers.reduce((max, provider) => {
|
|
@@ -800,7 +896,7 @@ window.__ModuleLoader__.load({
|
|
|
800
896
|
const balance = provider.balance && typeof provider.balance.amount === "number" ? provider.balance : null;
|
|
801
897
|
const delta = balanceDeltas[provider.id] && typeof balanceDeltas[provider.id].amount === "number" ? balanceDeltas[provider.id] : null;
|
|
802
898
|
const showDelta = delta !== null && Math.abs(delta.amount) >= 0.005;
|
|
803
|
-
const balanceClass = "dpu-provider-balance" + (showDelta ? " dpu-hit" : "");
|
|
899
|
+
const balanceClass = "dpu-provider-balance" + (showDelta ? " dpu-hit" : "") + balanceTone(balance);
|
|
804
900
|
// Command Code's balance is purchased + free credits; name the
|
|
805
901
|
// composition in the tooltip so the single number stays honest.
|
|
806
902
|
const balanceDetails = balance && balance.details && typeof balance.details === "object" && typeof balance.details.purchased === "number"
|
|
@@ -816,10 +912,13 @@ window.__ModuleLoader__.load({
|
|
|
816
912
|
})
|
|
817
913
|
: t("balance.title", { amount: balanceText, source: balance ? asText(balance.source) : "" }) + (balanceDetails === "" ? "" : " · " + balanceDetails);
|
|
818
914
|
const rawBalanceError = provider.meta && typeof provider.meta.balanceError === "string" ? provider.meta.balanceError : "";
|
|
819
|
-
// No console credential
|
|
820
|
-
// shows a "set up balance"
|
|
821
|
-
|
|
822
|
-
|
|
915
|
+
// No console credential (or an API-key-only / expired login) is
|
|
916
|
+
// actionable, not a failure: the header shows a "set up balance"
|
|
917
|
+
// chip that opens the plugin settings. Its tooltip carries the
|
|
918
|
+
// precise host reason when one exists.
|
|
919
|
+
const balanceSetup = isBalanceSetupError(provider);
|
|
920
|
+
const balanceSetupTitle = rawBalanceError === "" ? t("balance.setup.title") : t("balance.setup.title") + " · " + rawBalanceError;
|
|
921
|
+
const balanceError = rawBalanceError !== "" && !balanceSetup ? rawBalanceError : "";
|
|
823
922
|
// Command Code: credits answered without either credit pool, so
|
|
824
923
|
// there is no top-up balance to show (it is not a config problem).
|
|
825
924
|
const balanceMissing = provider.meta && provider.meta.balanceMissing === true;
|
|
@@ -906,8 +1005,12 @@ window.__ModuleLoader__.load({
|
|
|
906
1005
|
if (provider.status === "error") {
|
|
907
1006
|
const message = asText(provider.error) || t("status.error");
|
|
908
1007
|
body.push(react_jsx_runtime.jsx("div", {
|
|
909
|
-
className: "dpu-provider-error",
|
|
910
|
-
|
|
1008
|
+
className: "dpu-provider-error dpu-retry",
|
|
1009
|
+
role: "button",
|
|
1010
|
+
tabIndex: 0,
|
|
1011
|
+
title: message + " · " + t("error.retry"),
|
|
1012
|
+
onClick: forceRefresh,
|
|
1013
|
+
onKeyDown: retryKeyDown,
|
|
911
1014
|
children: message
|
|
912
1015
|
}, "error"));
|
|
913
1016
|
} else if (rows.length === 0) {
|
|
@@ -934,12 +1037,12 @@ window.__ModuleLoader__.load({
|
|
|
934
1037
|
title: t("balance.none.title", { error: balanceError }),
|
|
935
1038
|
children: t("balance.none")
|
|
936
1039
|
}, "balance-error")
|
|
937
|
-
:
|
|
1040
|
+
: balanceSetup
|
|
938
1041
|
? react_jsx_runtime.jsx("span", {
|
|
939
1042
|
className: "dpu-balance-setup",
|
|
940
1043
|
role: "button",
|
|
941
1044
|
tabIndex: 0,
|
|
942
|
-
title:
|
|
1045
|
+
title: balanceSetupTitle,
|
|
943
1046
|
onClick: activateBalanceSetup,
|
|
944
1047
|
onKeyDown: balanceSetupKeyDown,
|
|
945
1048
|
children: t("balance.setup")
|
|
@@ -969,10 +1072,6 @@ window.__ModuleLoader__.load({
|
|
|
969
1072
|
title: providerTitle(t, provider),
|
|
970
1073
|
"aria-expanded": !isCollapsed,
|
|
971
1074
|
children: [
|
|
972
|
-
react_jsx_runtime.jsx("span", {
|
|
973
|
-
className: "dpu-chevron",
|
|
974
|
-
children: isCollapsed ? "▸" : "▾"
|
|
975
|
-
}),
|
|
976
1075
|
react_jsx_runtime.jsx("span", { className: providerStatusClass(provider) }),
|
|
977
1076
|
react_jsx_runtime.jsx("span", {
|
|
978
1077
|
className: "dpu-provider-name",
|
|
@@ -1035,7 +1134,15 @@ window.__ModuleLoader__.load({
|
|
|
1035
1134
|
children: autoUpdateNotice(t, data && data.autoUpdate)
|
|
1036
1135
|
}),
|
|
1037
1136
|
sections.length > 0 ? react_jsx_runtime.jsx("div", { className: "dpu-providers", children: sections }) : null,
|
|
1038
|
-
error ? react_jsx_runtime.jsx("div", {
|
|
1137
|
+
error ? react_jsx_runtime.jsx("div", {
|
|
1138
|
+
className: "dpu-err dpu-retry",
|
|
1139
|
+
role: "button",
|
|
1140
|
+
tabIndex: 0,
|
|
1141
|
+
title: t("error.retry"),
|
|
1142
|
+
onClick: forceRefresh,
|
|
1143
|
+
onKeyDown: retryKeyDown,
|
|
1144
|
+
children: error
|
|
1145
|
+
}) : null,
|
|
1039
1146
|
!error && data === null ? react_jsx_runtime.jsx("div", { className: "dpu-ok", children: t("loading") }) : null,
|
|
1040
1147
|
data !== null && providers.length === 0
|
|
1041
1148
|
? react_jsx_runtime.jsx("div", { className: "dpu-err", children: t("noProviders") })
|
|
@@ -1057,6 +1164,8 @@ window.__ModuleLoader__.load({
|
|
|
1057
1164
|
// auto-generated "Plugin configuration" form (they keep their defaults)
|
|
1058
1165
|
// so this page never scrolls.
|
|
1059
1166
|
const SETTINGS_PATH = "/provider-usage/settings";
|
|
1167
|
+
/** Same-origin usage snapshot the settings page renders live. */
|
|
1168
|
+
const USAGE_PATH = "/provider-usage/usage";
|
|
1060
1169
|
const PROVIDER_FORMS = [
|
|
1061
1170
|
{
|
|
1062
1171
|
id: "opencode-go",
|
|
@@ -1223,7 +1332,38 @@ window.__ModuleLoader__.load({
|
|
|
1223
1332
|
fontSize: 12,
|
|
1224
1333
|
fontWeight: 500
|
|
1225
1334
|
},
|
|
1226
|
-
buttonDisabled: { opacity: 0.4, cursor: "not-allowed" }
|
|
1335
|
+
buttonDisabled: { opacity: 0.4, cursor: "not-allowed" },
|
|
1336
|
+
// live-usage inset of a provider card
|
|
1337
|
+
usageBox: {
|
|
1338
|
+
display: "flex",
|
|
1339
|
+
flexDirection: "column",
|
|
1340
|
+
gap: 6,
|
|
1341
|
+
padding: "8px 10px",
|
|
1342
|
+
borderRadius: 8,
|
|
1343
|
+
background: "var(--dsw-alias-bg-layer-2, #101216)"
|
|
1344
|
+
},
|
|
1345
|
+
usageHead: { display: "flex", alignItems: "center", gap: 8 },
|
|
1346
|
+
usageLabel: { fontSize: 11, fontWeight: 600, color: "var(--dsw-alias-label-tertiary, #8a8f98)" },
|
|
1347
|
+
usageRow: { display: "flex", flexDirection: "column", gap: 3 },
|
|
1348
|
+
usageRowLabel: { display: "flex", justifyContent: "space-between", gap: 8, fontSize: 12, lineHeight: 1.5, minWidth: 0 },
|
|
1349
|
+
usageRowName: { fontSize: 12, color: "var(--dsw-alias-label-secondary, #a8adb8)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
|
|
1350
|
+
usageRowValue: { flex: "none", fontSize: 12, fontWeight: 500, color: "var(--dsw-alias-label-primary, #e8e8e8)" },
|
|
1351
|
+
usageTrack: { height: 4, borderRadius: 2, background: "var(--dsw-alias-border-l2, #2a2d34)", overflow: "hidden" },
|
|
1352
|
+
usageFill: { height: "100%", borderRadius: 2, transition: "width .4s ease" },
|
|
1353
|
+
usageHint: { margin: 0, fontSize: 11, lineHeight: 1.5, color: "var(--dsw-alias-label-tertiary, #8a8f98)", overflowWrap: "anywhere" },
|
|
1354
|
+
// balance setup guide (OpenCode GO card)
|
|
1355
|
+
guideTitle: { fontSize: 12, fontWeight: 600, color: "var(--dsw-alias-label-secondary, #a8adb8)" },
|
|
1356
|
+
guideStep: { display: "flex", flexDirection: "column", gap: 4 },
|
|
1357
|
+
guideCode: {
|
|
1358
|
+
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
|
|
1359
|
+
fontSize: 11,
|
|
1360
|
+
color: "var(--dsw-alias-label-primary, #e8e8e8)",
|
|
1361
|
+
background: "var(--dsw-alias-bg-layer-3, #16181d)",
|
|
1362
|
+
border: "1px solid var(--dsw-alias-border-l2, #2a2d34)",
|
|
1363
|
+
borderRadius: 6,
|
|
1364
|
+
padding: "2px 6px",
|
|
1365
|
+
overflowWrap: "anywhere"
|
|
1366
|
+
}
|
|
1227
1367
|
};
|
|
1228
1368
|
|
|
1229
1369
|
function Field(props) {
|
|
@@ -1275,11 +1415,13 @@ window.__ModuleLoader__.load({
|
|
|
1275
1415
|
}
|
|
1276
1416
|
|
|
1277
1417
|
/**
|
|
1278
|
-
*
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1418
|
+
* A small copy button with the transient "Copied / Copy failed" note.
|
|
1419
|
+
* Shared by the referral row and the balance setup guide. `value` is
|
|
1420
|
+
* what lands on the clipboard; `label`/`copiedKey`/`failedKey` let the
|
|
1421
|
+
* caller pick the wording. Returns a boolean path so the caller can
|
|
1422
|
+
* show a failure hint instead of a lie.
|
|
1281
1423
|
*/
|
|
1282
|
-
function
|
|
1424
|
+
function CopyButton(props) {
|
|
1283
1425
|
const t = props.t;
|
|
1284
1426
|
const [status, setStatus] = react.useState("idle");
|
|
1285
1427
|
const timer = react.useRef(null);
|
|
@@ -1287,12 +1429,38 @@ window.__ModuleLoader__.load({
|
|
|
1287
1429
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
1288
1430
|
}, []);
|
|
1289
1431
|
const copy = async () => {
|
|
1290
|
-
const ok = await copyText(props.
|
|
1432
|
+
const ok = await copyText(props.value);
|
|
1291
1433
|
setStatus(ok ? "copied" : "failed");
|
|
1292
1434
|
if (timer.current !== null) clearTimeout(timer.current);
|
|
1293
1435
|
timer.current = setTimeout(() => setStatus("idle"), 1800);
|
|
1294
1436
|
};
|
|
1295
|
-
const note = status === "copied"
|
|
1437
|
+
const note = status === "copied"
|
|
1438
|
+
? t(props.copiedKey ?? "cfg.invite.copied")
|
|
1439
|
+
: status === "failed" ? t(props.failedKey ?? "cfg.invite.copyFailed") : "";
|
|
1440
|
+
return react_jsx_runtime.jsx("div", {
|
|
1441
|
+
style: { display: "flex", alignItems: "center", gap: 6, flexWrap: "wrap" },
|
|
1442
|
+
children: [
|
|
1443
|
+
react_jsx_runtime.jsx("button", {
|
|
1444
|
+
type: "button",
|
|
1445
|
+
style: { ...PS.button, padding: "3px 10px" },
|
|
1446
|
+
onClick: copy,
|
|
1447
|
+
children: props.label ?? t("cfg.invite.copy")
|
|
1448
|
+
}, "copy"),
|
|
1449
|
+
note === "" ? null : react_jsx_runtime.jsx("span", {
|
|
1450
|
+
style: status === "failed" ? PS.warn : PS.ok,
|
|
1451
|
+
children: note
|
|
1452
|
+
}, "copy-note")
|
|
1453
|
+
]
|
|
1454
|
+
});
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* The GLM card's referral row: the invitation URL and a Copy button.
|
|
1459
|
+
* Purely local — no host route reads it and it is never part of the
|
|
1460
|
+
* saved settings value, so saving settings cannot rewrite it.
|
|
1461
|
+
*/
|
|
1462
|
+
function InviteLink(props) {
|
|
1463
|
+
const t = props.t;
|
|
1296
1464
|
return react_jsx_runtime.jsx("div", {
|
|
1297
1465
|
style: PS.inviteRow,
|
|
1298
1466
|
children: [
|
|
@@ -1304,16 +1472,206 @@ window.__ModuleLoader__.load({
|
|
|
1304
1472
|
rel: "noreferrer",
|
|
1305
1473
|
children: props.url
|
|
1306
1474
|
}),
|
|
1307
|
-
react_jsx_runtime.jsx(
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1475
|
+
react_jsx_runtime.jsx(CopyButton, { t: t, value: props.url }, "invite-copy")
|
|
1476
|
+
]
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* The balance setup guide on the OpenCode GO card, shown whenever the
|
|
1482
|
+
* console read is not configured or names a fixable reason (API-key
|
|
1483
|
+
* login, expired sign-in). Three steps, each with an actionable row:
|
|
1484
|
+
* the CLI sign-in command (copyable), the manual credential path with
|
|
1485
|
+
* a console link, and a re-check that re-reads the sign-in state and
|
|
1486
|
+
* refreshes the balance without restarting DSH. The lead-in names the
|
|
1487
|
+
* state-specific fix.
|
|
1488
|
+
*/
|
|
1489
|
+
function ConsoleGuide(props) {
|
|
1490
|
+
const t = props.t;
|
|
1491
|
+
const [checking, setChecking] = react.useState(false);
|
|
1492
|
+
const recheck = async () => {
|
|
1493
|
+
if (checking) return;
|
|
1494
|
+
setChecking(true);
|
|
1495
|
+
try {
|
|
1496
|
+
await props.onRecheck();
|
|
1497
|
+
} finally {
|
|
1498
|
+
setChecking(false);
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
const leadIn = props.reason === "api-key"
|
|
1502
|
+
? t("cfg.console.apiKey")
|
|
1503
|
+
: props.reason === "expired" ? t("cfg.console.expired") : t("cfg.console.hint");
|
|
1504
|
+
const credentialRef = typeof props.credentialRef === "string" && props.credentialRef !== ""
|
|
1505
|
+
? props.credentialRef
|
|
1506
|
+
: "OPENCODE_CONSOLE_TOKEN";
|
|
1507
|
+
const command = "opencode auth login";
|
|
1508
|
+
const copyKeys = { copiedKey: "cfg.console.copied", failedKey: "cfg.console.copyFailed" };
|
|
1509
|
+
const stepRow = (key, text) => react_jsx_runtime.jsx("p", { style: PS.hint, children: text }, key);
|
|
1510
|
+
return react_jsx_runtime.jsx("div", {
|
|
1511
|
+
style: PS.usageBox,
|
|
1512
|
+
children: [
|
|
1513
|
+
react_jsx_runtime.jsx("div", { style: PS.guideTitle, children: t("cfg.console.guide.title") }, "guide-title"),
|
|
1514
|
+
react_jsx_runtime.jsx("p", { style: PS.hint, children: leadIn }, "guide-lead"),
|
|
1515
|
+
react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
|
|
1516
|
+
stepRow("guide-step1-text", t("cfg.console.guide.step1")),
|
|
1517
|
+
react_jsx_runtime.jsx("div", {
|
|
1518
|
+
style: PS.inviteRow,
|
|
1519
|
+
children: [
|
|
1520
|
+
react_jsx_runtime.jsx("code", { style: PS.guideCode, children: command }, "guide-command"),
|
|
1521
|
+
react_jsx_runtime.jsx(CopyButton, {
|
|
1522
|
+
t: t,
|
|
1523
|
+
value: command,
|
|
1524
|
+
label: t("cfg.console.copy.command"),
|
|
1525
|
+
...copyKeys
|
|
1526
|
+
}, "guide-step1-copy")
|
|
1527
|
+
]
|
|
1528
|
+
}, "guide-step1-row")
|
|
1529
|
+
] }, "guide-step1"),
|
|
1530
|
+
react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
|
|
1531
|
+
stepRow("guide-step2-text", t("cfg.console.guide.step2")),
|
|
1532
|
+
react_jsx_runtime.jsx("div", {
|
|
1533
|
+
style: PS.inviteRow,
|
|
1534
|
+
children: [
|
|
1535
|
+
react_jsx_runtime.jsx("code", { style: PS.guideCode, children: credentialRef }, "guide-ref"),
|
|
1536
|
+
react_jsx_runtime.jsx(CopyButton, {
|
|
1537
|
+
t: t,
|
|
1538
|
+
value: credentialRef,
|
|
1539
|
+
label: t("cfg.console.copy.ref"),
|
|
1540
|
+
...copyKeys
|
|
1541
|
+
}, "guide-step2-copy")
|
|
1542
|
+
]
|
|
1543
|
+
}, "guide-step2-row"),
|
|
1544
|
+
typeof props.consoleUrl === "string" && props.consoleUrl !== ""
|
|
1545
|
+
? react_jsx_runtime.jsx("a", {
|
|
1546
|
+
style: PS.inviteLink,
|
|
1547
|
+
href: props.consoleUrl,
|
|
1548
|
+
target: "_blank",
|
|
1549
|
+
rel: "noreferrer",
|
|
1550
|
+
children: t("cfg.console.guide.link")
|
|
1551
|
+
}, "guide-link")
|
|
1552
|
+
: null
|
|
1553
|
+
] }, "guide-step2"),
|
|
1554
|
+
react_jsx_runtime.jsx("div", { style: PS.guideStep, children: [
|
|
1555
|
+
stepRow("guide-step3-text", t("cfg.console.guide.step3")),
|
|
1556
|
+
react_jsx_runtime.jsx("button", {
|
|
1557
|
+
type: "button",
|
|
1558
|
+
style: checking || props.disabled === true
|
|
1559
|
+
? { ...PS.button, ...PS.buttonDisabled }
|
|
1560
|
+
: PS.button,
|
|
1561
|
+
disabled: checking || props.disabled === true,
|
|
1562
|
+
onClick: recheck,
|
|
1563
|
+
children: checking ? t("cfg.console.rechecking") : t("cfg.console.recheck")
|
|
1564
|
+
}, "guide-recheck")
|
|
1565
|
+
] }, "guide-step3")
|
|
1566
|
+
]
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* The live-usage inset of one provider card. `SettingsPage` fetches the
|
|
1572
|
+
* canonical usage snapshot (the same payload the widget renders) and
|
|
1573
|
+
* passes each provider's slice in; no hooks here, so it is safe to call
|
|
1574
|
+
* from any render path. `provider === null` covers not-loaded-yet,
|
|
1575
|
+
* a failed snapshot and a disabled-in-payload provider.
|
|
1576
|
+
*/
|
|
1577
|
+
function UsageSection(props) {
|
|
1578
|
+
const t = props.t;
|
|
1579
|
+
if (props.enabled === false) {
|
|
1580
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.disabled") });
|
|
1581
|
+
}
|
|
1582
|
+
const provider = props.provider === undefined ? null : props.provider;
|
|
1583
|
+
if (provider === null) {
|
|
1584
|
+
if (props.phase === "loading" || props.phase === "idle") {
|
|
1585
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.loading") });
|
|
1586
|
+
}
|
|
1587
|
+
if (props.phase === "error") {
|
|
1588
|
+
return react_jsx_runtime.jsx("p", { style: PS.err, children: t("cfg.usage.loadFailed", { error: asText(props.error) || "?" }) });
|
|
1589
|
+
}
|
|
1590
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.noData") });
|
|
1591
|
+
}
|
|
1592
|
+
if (provider.status === "error") {
|
|
1593
|
+
return react_jsx_runtime.jsx("p", { style: PS.err, children: t("cfg.usage.error", { error: asText(provider.error) || "?" }) });
|
|
1594
|
+
}
|
|
1595
|
+
const rows = [];
|
|
1596
|
+
for (const win of providerWindows(provider)) {
|
|
1597
|
+
const pct = typeof win.percent === "number" && Number.isFinite(win.percent) ? win.percent : null;
|
|
1598
|
+
const reset = win.resetsAt ? timeUntil(win.resetsAt, t) : "";
|
|
1599
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
1600
|
+
style: PS.usageRow,
|
|
1601
|
+
children: [
|
|
1602
|
+
react_jsx_runtime.jsx("div", {
|
|
1603
|
+
style: PS.usageRowLabel,
|
|
1604
|
+
children: [
|
|
1605
|
+
react_jsx_runtime.jsx("span", { style: PS.usageRowName, title: windowLabel(t, win), children: windowLabel(t, win) }),
|
|
1606
|
+
react_jsx_runtime.jsx("span", {
|
|
1607
|
+
style: PS.usageRowValue,
|
|
1608
|
+
children: (pct === null ? "—" : formatPercent(pct) + "%") + (reset === "" ? "" : " · " + reset)
|
|
1609
|
+
})
|
|
1610
|
+
]
|
|
1611
|
+
}),
|
|
1612
|
+
react_jsx_runtime.jsx("div", {
|
|
1613
|
+
style: PS.usageTrack,
|
|
1614
|
+
children: react_jsx_runtime.jsx("div", {
|
|
1615
|
+
style: { ...PS.usageFill, width: Math.max(0, Math.min(100, pct ?? 0)) + "%", background: fillColor(pct ?? 0) }
|
|
1616
|
+
})
|
|
1617
|
+
})
|
|
1618
|
+
]
|
|
1619
|
+
}, win.key));
|
|
1620
|
+
}
|
|
1621
|
+
// The top-up balance row mirrors the widget's chip: value when one
|
|
1622
|
+
// exists, otherwise the reason it does not (setup needed, no pools,
|
|
1623
|
+
// or a failed read). A provider with no balance concept at all (GLM)
|
|
1624
|
+
// carries no flags, so the row is skipped entirely.
|
|
1625
|
+
const meta = provider.meta && typeof provider.meta === "object" ? provider.meta : {};
|
|
1626
|
+
const balance = provider.balance && typeof provider.balance.amount === "number" ? provider.balance : null;
|
|
1627
|
+
const balanceValue = formatMoney(balance);
|
|
1628
|
+
const balanceDetails = balance && balance.details && typeof balance.details === "object" && typeof balance.details.purchased === "number"
|
|
1629
|
+
? t("balance.details.extra", {
|
|
1630
|
+
purchased: formatMoney({ amount: balance.details.purchased, currency: balance.currency }),
|
|
1631
|
+
free: formatMoney({ amount: typeof balance.details.free === "number" ? balance.details.free : 0, currency: balance.currency })
|
|
1632
|
+
})
|
|
1633
|
+
: "";
|
|
1634
|
+
const balanceMissing = meta.balanceMissing === true;
|
|
1635
|
+
const balanceSetup = meta.balanceSetup === "no-credential" || meta.balanceSetup === "api-key" || meta.balanceSetup === "expired";
|
|
1636
|
+
const balanceError = balanceValue === "" && !balanceMissing && !balanceSetup && typeof meta.balanceError === "string" && meta.balanceError !== ""
|
|
1637
|
+
? meta.balanceError
|
|
1638
|
+
: "";
|
|
1639
|
+
if (balanceValue !== "" || balanceMissing || balanceSetup || balanceError !== "") {
|
|
1640
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
1641
|
+
style: PS.usageRow,
|
|
1642
|
+
children: [
|
|
1643
|
+
react_jsx_runtime.jsx("div", {
|
|
1644
|
+
style: PS.usageRowLabel,
|
|
1645
|
+
children: [
|
|
1646
|
+
react_jsx_runtime.jsx("span", { style: PS.usageRowName, children: t("cfg.usage.balance") }),
|
|
1647
|
+
react_jsx_runtime.jsx("span", {
|
|
1648
|
+
style: { ...PS.usageRowValue, color: balanceValue === "" ? "var(--dsw-alias-label-tertiary, #8a8f98)" : PS.usageRowValue.color },
|
|
1649
|
+
children: balanceValue !== ""
|
|
1650
|
+
? balanceValue
|
|
1651
|
+
: balanceMissing ? t("balance.missing") : t("cfg.usage.noBalance")
|
|
1652
|
+
})
|
|
1653
|
+
]
|
|
1654
|
+
}),
|
|
1655
|
+
balanceValue !== "" && balanceDetails !== ""
|
|
1656
|
+
? react_jsx_runtime.jsx("span", { style: PS.usageHint, children: balanceDetails })
|
|
1657
|
+
: null,
|
|
1658
|
+
balanceError !== ""
|
|
1659
|
+
? react_jsx_runtime.jsx("span", { style: PS.usageHint, children: balanceError })
|
|
1660
|
+
: null
|
|
1661
|
+
]
|
|
1662
|
+
}, "balance"));
|
|
1663
|
+
}
|
|
1664
|
+
if (rows.length === 0) {
|
|
1665
|
+
return react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.usage.noData") });
|
|
1666
|
+
}
|
|
1667
|
+
return react_jsx_runtime.jsx("div", {
|
|
1668
|
+
style: PS.usageBox,
|
|
1669
|
+
children: [
|
|
1670
|
+
react_jsx_runtime.jsx("div", {
|
|
1671
|
+
style: PS.usageHead,
|
|
1672
|
+
children: react_jsx_runtime.jsx("span", { style: PS.usageLabel, children: t("cfg.usage") })
|
|
1673
|
+
}, "usage-head"),
|
|
1674
|
+
...rows
|
|
1317
1675
|
]
|
|
1318
1676
|
});
|
|
1319
1677
|
}
|
|
@@ -1333,6 +1691,11 @@ window.__ModuleLoader__.load({
|
|
|
1333
1691
|
const [busy, setBusy] = react.useState({ save: false, reload: false });
|
|
1334
1692
|
const [message, setMessage] = react.useState(null);
|
|
1335
1693
|
const [draggingId, setDraggingId] = react.useState(null);
|
|
1694
|
+
// Live usage snapshot for the per-card 实时用量 sections. `phase`
|
|
1695
|
+
// gates the placeholders; on a failed refresh the previous data
|
|
1696
|
+
// stays visible and only the error line appears. `at` stamps when
|
|
1697
|
+
// the numbers were read, shown as 「更新于 HH:MM」.
|
|
1698
|
+
const [usage, setUsage] = react.useState({ phase: "idle", data: null, error: null, at: 0 });
|
|
1336
1699
|
const tRef = react.useRef(t);
|
|
1337
1700
|
tRef.current = t;
|
|
1338
1701
|
// Id of the provider card currently being dragged (drag reordering).
|
|
@@ -1369,9 +1732,50 @@ window.__ModuleLoader__.load({
|
|
|
1369
1732
|
}
|
|
1370
1733
|
}, []);
|
|
1371
1734
|
|
|
1735
|
+
/**
|
|
1736
|
+
* Fetch the canonical usage snapshot; `force` appends `?refresh=1`
|
|
1737
|
+
* so the host re-reads upstream and the numbers reflect a just-
|
|
1738
|
+
* saved credential change immediately.
|
|
1739
|
+
*/
|
|
1740
|
+
const loadUsage = react.useCallback(async (force) => {
|
|
1741
|
+
setUsage((prev) => ({ ...prev, phase: "loading" }));
|
|
1742
|
+
try {
|
|
1743
|
+
const res = await callApi("GET", USAGE_PATH + (force ? "?refresh=1" : ""));
|
|
1744
|
+
const payload = res.payload;
|
|
1745
|
+
if (res.status === 200 && payload !== null && Array.isArray(payload.providers)) {
|
|
1746
|
+
setUsage({ phase: "ready", data: payload, error: null, at: Date.now() });
|
|
1747
|
+
} else {
|
|
1748
|
+
const detail = (payload && payload.error) || "HTTP " + res.status;
|
|
1749
|
+
setUsage((prev) => ({ phase: "error", data: prev.data, error: detail }));
|
|
1750
|
+
}
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
setUsage((prev) => ({ phase: "error", data: prev.data, error: (error && error.message) || String(error) }));
|
|
1753
|
+
}
|
|
1754
|
+
}, []);
|
|
1755
|
+
|
|
1372
1756
|
react.useEffect(() => {
|
|
1373
1757
|
reload(false);
|
|
1374
|
-
|
|
1758
|
+
void loadUsage(false);
|
|
1759
|
+
}, [reload, loadUsage]);
|
|
1760
|
+
|
|
1761
|
+
// While the settings page is open it tracks the widget: poll every
|
|
1762
|
+
// 30 s (cheap — the host cache collapses reads) and catch up the
|
|
1763
|
+
// moment the tab becomes visible again.
|
|
1764
|
+
react.useEffect(() => {
|
|
1765
|
+
if (typeof window === "undefined" || typeof window.setInterval !== "function") return undefined;
|
|
1766
|
+
const hidden = () => typeof document !== "undefined" && document.hidden === true;
|
|
1767
|
+
const timer = window.setInterval(() => { if (!hidden()) void loadUsage(false); }, 30_000);
|
|
1768
|
+
const onVisible = () => { if (typeof document !== "undefined" && !document.hidden) void loadUsage(false); };
|
|
1769
|
+
if (typeof document !== "undefined" && typeof document.addEventListener === "function") {
|
|
1770
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
1771
|
+
}
|
|
1772
|
+
return () => {
|
|
1773
|
+
window.clearInterval(timer);
|
|
1774
|
+
if (typeof document !== "undefined" && typeof document.removeEventListener === "function") {
|
|
1775
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
}, [loadUsage]);
|
|
1375
1779
|
|
|
1376
1780
|
const update = (key, value) => {
|
|
1377
1781
|
setForm((prev) => ({ ...prev, [key]: value }));
|
|
@@ -1398,6 +1802,10 @@ window.__ModuleLoader__.load({
|
|
|
1398
1802
|
if (res.status === 200 && payload !== null) {
|
|
1399
1803
|
await reload(false);
|
|
1400
1804
|
usageRefreshStore.publish();
|
|
1805
|
+
// A saved credential change (apiKeyEnv) must show its own
|
|
1806
|
+
// numbers right away: force one upstream re-read instead
|
|
1807
|
+
// of waiting for the next TTL.
|
|
1808
|
+
void loadUsage(true);
|
|
1401
1809
|
setMessage({ kind: "ok", text: tt("cfg.saved") });
|
|
1402
1810
|
} else if (res.status === 409) {
|
|
1403
1811
|
setMessage({ kind: "warn", text: tt("cfg.conflict") });
|
|
@@ -1416,6 +1824,7 @@ window.__ModuleLoader__.load({
|
|
|
1416
1824
|
setBusy((b) => ({ ...b, reload: true }));
|
|
1417
1825
|
try {
|
|
1418
1826
|
await reload(true);
|
|
1827
|
+
void loadUsage(false);
|
|
1419
1828
|
} finally {
|
|
1420
1829
|
setBusy((b) => ({ ...b, reload: false }));
|
|
1421
1830
|
}
|
|
@@ -1448,6 +1857,28 @@ window.__ModuleLoader__.load({
|
|
|
1448
1857
|
const server = formFromValue(state.value || {});
|
|
1449
1858
|
const dirty = Object.keys(form).some((key) => !fieldEquals(form[key], server[key]));
|
|
1450
1859
|
|
|
1860
|
+
// The balance guide's manual path: the configured console credential
|
|
1861
|
+
// ref (default OPENCODE_CONSOLE_TOKEN) and the console website
|
|
1862
|
+
// derived from the configured console origin.
|
|
1863
|
+
const consoleValue = state.value && typeof state.value === "object" ? state.value : {};
|
|
1864
|
+
const consoleRef = typeof consoleValue.opencodeConsoleApiKeyEnv === "string" && consoleValue.opencodeConsoleApiKeyEnv !== ""
|
|
1865
|
+
? consoleValue.opencodeConsoleApiKeyEnv
|
|
1866
|
+
: "OPENCODE_CONSOLE_TOKEN";
|
|
1867
|
+
const consoleLink = String(consoleValue.opencodeConsoleBaseUrl || "https://opencode.ai").replace(/\/+$/, "") + "/console";
|
|
1868
|
+
|
|
1869
|
+
/**
|
|
1870
|
+
* Re-check the console balance: re-read the detection report
|
|
1871
|
+
* (configured / reason) and force one upstream usage read, so a
|
|
1872
|
+
* sign-in the user just completed shows up without restarting DSH.
|
|
1873
|
+
* The settings reload is skipped while the form is dirty so
|
|
1874
|
+
* unsaved edits are not discarded — the forced usage read alone
|
|
1875
|
+
* still refreshes the balance row.
|
|
1876
|
+
*/
|
|
1877
|
+
const recheckBalance = async () => {
|
|
1878
|
+
if (!dirty) await reload(false);
|
|
1879
|
+
await loadUsage(true);
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1451
1882
|
// Cards render in the saved order. Reordering works three ways, so a
|
|
1452
1883
|
// shell without HTML5 drag (touch, Firefox proton, embedded webviews)
|
|
1453
1884
|
// always has a path: drag a card header onto another card, drop it
|
|
@@ -1588,15 +2019,23 @@ window.__ModuleLoader__.load({
|
|
|
1588
2019
|
// Reading the top-up balance is always on (the host default);
|
|
1589
2020
|
// the page only reports whether a credential was found.
|
|
1590
2021
|
if (consoleInfo !== null) {
|
|
2022
|
+
const consoleFixable = consoleInfo.reason === "api-key" || consoleInfo.reason === "expired";
|
|
1591
2023
|
children.push(react_jsx_runtime.jsx(StatusLine, {
|
|
1592
2024
|
error: typeof consoleInfo.error === "string" && consoleInfo.error !== "",
|
|
1593
2025
|
children: t("cfg.console.status") + ": " + (consoleInfo.configured === true ? t("cfg.status.configured") : t("cfg.status.notConfigured")) + (consoleInfo.source ? " · " + consoleInfo.source : "")
|
|
1594
2026
|
}, "console-status"));
|
|
1595
|
-
if (consoleInfo.configured !== true) {
|
|
1596
|
-
children.push(react_jsx_runtime.jsx(
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2027
|
+
if (consoleInfo.configured !== true || consoleFixable) {
|
|
2028
|
+
children.push(react_jsx_runtime.jsx(ConsoleGuide, {
|
|
2029
|
+
t: t,
|
|
2030
|
+
// A specific reason (API-key login, expired
|
|
2031
|
+
// session) names the fix in the guide's lead-in
|
|
2032
|
+
// instead of the generic hint.
|
|
2033
|
+
reason: consoleFixable ? consoleInfo.reason : "",
|
|
2034
|
+
credentialRef: consoleRef,
|
|
2035
|
+
consoleUrl: consoleLink,
|
|
2036
|
+
disabled: disabled,
|
|
2037
|
+
onRecheck: recheckBalance
|
|
2038
|
+
}, "console-guide"));
|
|
1600
2039
|
}
|
|
1601
2040
|
}
|
|
1602
2041
|
}
|
|
@@ -1606,6 +2045,19 @@ window.__ModuleLoader__.load({
|
|
|
1606
2045
|
children: statusLines.join(" · ")
|
|
1607
2046
|
}));
|
|
1608
2047
|
|
|
2048
|
+
// Live usage for this card, matched from the canonical snapshot
|
|
2049
|
+
// by provider id (the snapshot only carries enabled providers).
|
|
2050
|
+
const usageInfo = usage.data && Array.isArray(usage.data.providers)
|
|
2051
|
+
? usage.data.providers.find((candidate) => candidate && candidate.id === definition.id) ?? null
|
|
2052
|
+
: null;
|
|
2053
|
+
children.push(react_jsx_runtime.jsx(UsageSection, {
|
|
2054
|
+
t: t,
|
|
2055
|
+
enabled: enabled,
|
|
2056
|
+
provider: usageInfo,
|
|
2057
|
+
phase: usage.phase,
|
|
2058
|
+
error: usage.error
|
|
2059
|
+
}, "usage"));
|
|
2060
|
+
|
|
1609
2061
|
if (typeof definition.inviteUrl === "string" && definition.inviteUrl !== "") {
|
|
1610
2062
|
children.push(react_jsx_runtime.jsx(InviteLink, { t: t, url: definition.inviteUrl }, "invite"));
|
|
1611
2063
|
}
|
|
@@ -1633,7 +2085,27 @@ window.__ModuleLoader__.load({
|
|
|
1633
2085
|
children: [
|
|
1634
2086
|
react_jsx_runtime.jsx("div", { style: PS.title, children: t("cfg.title") }),
|
|
1635
2087
|
react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.lead") }),
|
|
1636
|
-
react_jsx_runtime.jsx("div", {
|
|
2088
|
+
react_jsx_runtime.jsx("div", {
|
|
2089
|
+
style: PS.row,
|
|
2090
|
+
children: [
|
|
2091
|
+
react_jsx_runtime.jsx("div", { style: PS.title, children: t("cfg.providers") }),
|
|
2092
|
+
typeof usage.at === "number" && usage.at > 0
|
|
2093
|
+
? react_jsx_runtime.jsx("span", {
|
|
2094
|
+
style: { ...PS.hint, marginLeft: "auto" },
|
|
2095
|
+
children: t("cfg.usage.updatedAt", { time: formatClock(usage.at) })
|
|
2096
|
+
}, "usage-updated")
|
|
2097
|
+
: react_jsx_runtime.jsx("span", { style: { ...PS.hint, marginLeft: "auto" } }, "usage-updated"),
|
|
2098
|
+
react_jsx_runtime.jsx("button", {
|
|
2099
|
+
type: "button",
|
|
2100
|
+
style: usage.phase === "loading"
|
|
2101
|
+
? { ...PS.button, padding: "3px 10px", ...PS.buttonDisabled }
|
|
2102
|
+
: { ...PS.button, padding: "3px 10px" },
|
|
2103
|
+
disabled: usage.phase === "loading",
|
|
2104
|
+
onClick: () => { void loadUsage(true); },
|
|
2105
|
+
children: usage.phase === "loading" ? t("cfg.usage.refreshing") : t("cfg.usage.refresh")
|
|
2106
|
+
}, "usage-refresh")
|
|
2107
|
+
]
|
|
2108
|
+
}),
|
|
1637
2109
|
react_jsx_runtime.jsx("div", {
|
|
1638
2110
|
style: { display: "flex", flexDirection: "column", gap: 8 },
|
|
1639
2111
|
children: providerCards
|
|
@@ -1673,7 +2145,7 @@ window.__ModuleLoader__.load({
|
|
|
1673
2145
|
}
|
|
1674
2146
|
|
|
1675
2147
|
/** One-line description the panel may render as the bundle's summary. */
|
|
1676
|
-
const SUMMARY = "OpenCode GO / Command Code / GLM Code Plan
|
|
2148
|
+
const SUMMARY = "OpenCode GO / Command Code / GLM Code Plan 套餐用量:一个侧边栏组件、同源代理与 /provider-usage 命令。";
|
|
1677
2149
|
|
|
1678
2150
|
/**
|
|
1679
2151
|
* The `plugins.bundle.config` occupant. The `view` branch is a component
|
|
@@ -1743,7 +2215,10 @@ window.__ModuleLoader__.load({
|
|
|
1743
2215
|
exports.Widget = Widget;
|
|
1744
2216
|
exports.BundleConfig = BundleConfig;
|
|
1745
2217
|
exports.SettingsPage = SettingsPage;
|
|
2218
|
+
exports.UsageSection = UsageSection;
|
|
1746
2219
|
exports.InviteLink = InviteLink;
|
|
2220
|
+
exports.CopyButton = CopyButton;
|
|
2221
|
+
exports.ConsoleGuide = ConsoleGuide;
|
|
1747
2222
|
exports.GLM_INVITE_URL = GLM_INVITE_URL;
|
|
1748
2223
|
exports.balanceDeltasBetween = balanceDeltasBetween;
|
|
1749
2224
|
exports.formatSignedMoney = formatSignedMoney;
|