dsh-multi-provider-usage 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +427 -2
- package/README.zh.md +377 -0
- package/cordis.patch.yml +15 -0
- package/docs/widget.png +0 -0
- package/lib/auto-update.js +570 -0
- package/lib/client.js +2226 -0
- package/lib/index.js +1290 -0
- package/lib/opencode-console.js +556 -0
- package/lib/opencode-session.js +139 -0
- package/lib/providers.js +919 -0
- package/lib/util.js +27 -0
- package/package.json +64 -4
package/lib/client.js
ADDED
|
@@ -0,0 +1,2226 @@
|
|
|
1
|
+
// dsh-multi-provider-usage — browser half.
|
|
2
|
+
//
|
|
3
|
+
// Hand-authored client bundle in the DSH module-loader factory format:
|
|
4
|
+
// `window.__ModuleLoader__.load({ id, factory })`. The factory receives the
|
|
5
|
+
// module-table `require`, so only shell-externalized modules may be imported
|
|
6
|
+
// (react / react/jsx-runtime are in the static table). It registers a widget
|
|
7
|
+
// into the sidebar's `sidebar.footer.action` list slot and polls the
|
|
8
|
+
// same-origin `/provider-usage/usage` proxy owned by the host half — API keys
|
|
9
|
+
// never enter the browser. UI strings are bilingual via the DSH locale
|
|
10
|
+
// service (dictionaries under the `dsh-multi-provider-usage` namespace).
|
|
11
|
+
//
|
|
12
|
+
// Every declaration that must not collide with another plugin's classic-script
|
|
13
|
+
// globals is kept INSIDE the factory: the shell evaluates all client bundles
|
|
14
|
+
// in one shared global lexical scope, so a top-level `const PACKAGE_NAME`
|
|
15
|
+
// would make the whole web boot fail with "Identifier has already been
|
|
16
|
+
// declared".
|
|
17
|
+
window.__ModuleLoader__.load({
|
|
18
|
+
id: "dsh-multi-provider-usage",
|
|
19
|
+
factory: (require) => {
|
|
20
|
+
const PACKAGE_NAME = "dsh-multi-provider-usage";
|
|
21
|
+
const CONFIG_KEY = PACKAGE_NAME;
|
|
22
|
+
// GLM Coding Plan invitation link (Z.ai "Invite Friends, Get Credits").
|
|
23
|
+
// It is public by nature and is not a credential, so the settings page
|
|
24
|
+
// renders it directly and it never enters the saved settings value.
|
|
25
|
+
const GLM_INVITE_URL = "https://zcode.z.ai/invite/SMBZKGEF";
|
|
26
|
+
var module = { exports: {} };
|
|
27
|
+
var exports = module.exports;
|
|
28
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
29
|
+
|
|
30
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
31
|
+
let react = require("react");
|
|
32
|
+
|
|
33
|
+
// ── styles (injected once, same pattern as built-in client plugins) ──
|
|
34
|
+
const CSS_ID = "dsh-multi-provider-usage/widget.css";
|
|
35
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + CSS_ID + "\"]") === null) {
|
|
36
|
+
const tag = document.createElement("style");
|
|
37
|
+
tag.dataset.plugin = "dsh-multi-provider-usage";
|
|
38
|
+
tag.dataset.pluginCss = CSS_ID;
|
|
39
|
+
tag.textContent = [
|
|
40
|
+
".dpu-widget{box-sizing:border-box;width:100%;min-width:0;padding:6px 6px 4px;display:flex;flex-direction:column;gap:5px;border-top:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}",
|
|
41
|
+
".dpu-widget:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
42
|
+
".dpu-head{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;font-weight:600;line-height:18px}",
|
|
43
|
+
".dpu-brand{white-space:nowrap;flex:none}",
|
|
44
|
+
".dpu-headright{display:flex;align-items:center;gap:6px;min-width:0}",
|
|
45
|
+
".dpu-refresh{cursor:pointer;border:none;background:none;padding:0;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:18px}",
|
|
46
|
+
".dpu-refresh:hover{color:var(--dsw-alias-label-primary)}",
|
|
47
|
+
".dpu-settings{cursor:pointer;border:none;background:none;padding:0;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:18px}",
|
|
48
|
+
".dpu-settings:hover{color:var(--dsw-alias-label-primary)}",
|
|
49
|
+
".dpu-providers{display:flex;flex-direction:column;gap:9px;min-width:0}",
|
|
50
|
+
".dpu-provider{position:relative;box-sizing:border-box;min-width:0;padding:6px 0 2px;border-top:1px solid var(--dsw-alias-border-l2)}",
|
|
51
|
+
".dpu-provider:first-child{border-top:none;padding-top:0}",
|
|
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
|
+
".dpu-provider-head:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
54
|
+
".dpu-provider-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:600}",
|
|
55
|
+
".dpu-provider-pct{flex:none;font-size:10px;font-weight:700;line-height:16px;color:var(--dsw-alias-label-primary)}",
|
|
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)}",
|
|
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)}",
|
|
60
|
+
".dpu-provider-balance.dpu-hit{animation:dpu-balance-pop 1.4s ease}",
|
|
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}",
|
|
62
|
+
".dpu-balance-setup:hover{color:var(--dsw-alias-label-primary);border-color:currentColor}",
|
|
63
|
+
".dpu-balance-setup:focus-visible{outline:1px solid var(--dsw-alias-label-primary);outline-offset:1px}",
|
|
64
|
+
".dpu-balance-float{position:absolute;right:4px;top:-3px;z-index:4;pointer-events:none;padding:0 3px;border-radius:6px;font-size:9px;line-height:14px;font-weight:700;white-space:nowrap;animation:dpu-balance-fly 1.6s ease-out forwards}",
|
|
65
|
+
".dpu-balance-float.dpu-down-text{color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 18%, transparent)}",
|
|
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)}",
|
|
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)}}",
|
|
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)}}",
|
|
69
|
+
".dpu-provider-body{display:flex;flex-direction:column;gap:4px;min-width:0;padding:2px 0 3px 11px}",
|
|
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}",
|
|
71
|
+
".dpu-dot{width:5px;height:5px;border-radius:50%;flex:none;margin-right:3px;background:var(--dsw-alias-label-tertiary)}",
|
|
72
|
+
".dpu-dot.dpu-ok{background:var(--dsw-alias-state-success-primary)}",
|
|
73
|
+
".dpu-dot.dpu-error{background:var(--dsw-alias-state-error-primary)}",
|
|
74
|
+
".dpu-dot.dpu-warn{background:var(--dsw-alias-state-warn-primary)}",
|
|
75
|
+
".dpu-sub{display:flex;align-items:center;gap:5px;min-width:0;font-size:10px;line-height:15px;color:var(--dsw-alias-label-secondary)}",
|
|
76
|
+
".dpu-account{flex:none;max-width:56px;overflow:hidden;text-overflow:ellipsis;padding:0 2px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);font-size:8px;line-height:14px;font-weight:600;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
|
|
77
|
+
".dpu-plan{flex:none;max-width:64px;overflow:hidden;text-overflow:ellipsis;padding:0 2px;border-radius:6px;background:var(--dsw-alias-interactive-bg-hover);font-size:8px;line-height:14px;font-weight:600;color:var(--dsw-alias-label-secondary);white-space:nowrap}",
|
|
78
|
+
".dpu-badge{flex:none;padding:0 4px;border-radius:6px;font-size:8px;line-height:14px;font-weight:600;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover);white-space:nowrap;cursor:default}",
|
|
79
|
+
".dpu-row{display:flex;flex-direction:column;gap:3px;min-width:0}",
|
|
80
|
+
".dpu-row-label{display:flex;justify-content:space-between;gap:8px;font-size:11px;line-height:15px;color:var(--dsw-alias-label-secondary)}",
|
|
81
|
+
".dpu-row-label b{font-weight:500;color:var(--dsw-alias-label-primary)}",
|
|
82
|
+
".dpu-meta{display:flex;align-items:center;gap:6px;min-width:0}",
|
|
83
|
+
".dpu-meta .dpu-bars{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}",
|
|
84
|
+
".dpu-meta>span{box-sizing:border-box;flex:0 0 64px;font-size:10px;line-height:14px;color:var(--dsw-alias-label-secondary);white-space:nowrap;text-align:right;overflow:hidden;text-overflow:ellipsis}",
|
|
85
|
+
".dpu-track{box-sizing:border-box;flex:none;height:5px;border-radius:3px;background:var(--dsw-alias-border-l2);overflow:hidden}",
|
|
86
|
+
".dpu-fill{height:100%;border-radius:3px;transition:width .4s ease}",
|
|
87
|
+
".dpu-timetrack{box-sizing:border-box;flex:none;display:flex;width:100%;height:5px;border-radius:3px;overflow:hidden;background:var(--dsw-alias-border-l2)}",
|
|
88
|
+
".dpu-timeelapsed{height:100%;flex:none;background:var(--dsw-alias-border-l2);border-radius:3px;transition:width .4s ease}",
|
|
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}",
|
|
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)}",
|
|
93
|
+
".dpu-ok{font-size:11px;line-height:16px;color:var(--dsw-alias-label-secondary)}",
|
|
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}",
|
|
95
|
+
".dpu-rail:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
96
|
+
".dpu-update{display:inline-block;padding:1px 6px;border-radius:8px;font-size:10px;line-height:16px;font-weight:600;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover);text-decoration:none;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:110px}",
|
|
97
|
+
".dpu-autoupdate{font-size:10px;line-height:15px;color:var(--dsw-alias-state-success-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
98
|
+
].join("");
|
|
99
|
+
document.head.appendChild(tag);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── locale dictionaries ──
|
|
103
|
+
const NS = "dsh-multi-provider-usage";
|
|
104
|
+
const zh = {
|
|
105
|
+
"window.rolling": "5 小时窗口",
|
|
106
|
+
"window.five-hour": "5 小时窗口",
|
|
107
|
+
"window.weekly": "周窗口",
|
|
108
|
+
"window.monthly": "月窗口",
|
|
109
|
+
"window.mcp": "MCP 工具 (月)",
|
|
110
|
+
"window.token": "Token 窗口",
|
|
111
|
+
"window.other": "其他窗口",
|
|
112
|
+
"window.five-hour.hint": "5h",
|
|
113
|
+
"window.mcp.hint": "月",
|
|
114
|
+
"time.soon": "即将重置",
|
|
115
|
+
"time.minutes": "{n}分",
|
|
116
|
+
"time.hours": "{n}小时{m}分",
|
|
117
|
+
"time.days": "{n}天{m}小时",
|
|
118
|
+
"time.elapsed.title": "本窗口时间进度:已流逝 {pct}%,剩余 {rest}%",
|
|
119
|
+
"refresh": "刷新",
|
|
120
|
+
"settings": "设置",
|
|
121
|
+
"settings.title": "打开插件设置",
|
|
122
|
+
"loading": "加载中…",
|
|
123
|
+
"error": "错误",
|
|
124
|
+
"noProviders": "未启用任何服务商。请在插件设置中启用。",
|
|
125
|
+
"provider.opencode-go": "OpenCode GO",
|
|
126
|
+
"provider.command-code": "Command Code",
|
|
127
|
+
"provider.glm": "GLM Code Plan",
|
|
128
|
+
"status.ok": "正常",
|
|
129
|
+
"status.exceeded": "已超限",
|
|
130
|
+
"status.rate-limited": "限流中",
|
|
131
|
+
"status.error": "失败",
|
|
132
|
+
"status.disabled": "已禁用",
|
|
133
|
+
"status.unknown": "未知",
|
|
134
|
+
"rail.plain": "用量",
|
|
135
|
+
"rail.all": "服务商用量:{summary}",
|
|
136
|
+
"rail.error": "{provider}: 读取失败",
|
|
137
|
+
"rail.title": "{provider} · {pct}%",
|
|
138
|
+
"rail.title.account": "{provider} · {account} · {pct}%",
|
|
139
|
+
"widget.brand": "服务商用量",
|
|
140
|
+
"widget.account.title": "当前账号:{name}",
|
|
141
|
+
"widget.fallback": "内置",
|
|
142
|
+
"widget.fallback.title": "实际来源:{source};请求来源:{requested};回退原因:{reason}{message}",
|
|
143
|
+
"widget.partial.title": "部分接口失败:{error}",
|
|
144
|
+
"balance.extra": "充值余额",
|
|
145
|
+
"balance.zen": "充值余额",
|
|
146
|
+
"balance.short": "余额",
|
|
147
|
+
"balance.none": "余额 —",
|
|
148
|
+
"balance.title": "订阅外充值余额 {amount}(来源:{source})",
|
|
149
|
+
"balance.delta.title": "本轮余额变化:{from} → {to}",
|
|
150
|
+
"balance.none.title": "充值余额不可用:{error}",
|
|
151
|
+
"balance.setup": "配置余额",
|
|
152
|
+
"balance.setup.title": "没有可用的 OpenCode 控制台 OAuth 登录,无法读取充值余额(API Key 登录不含余额,令牌过期也会如此)。点击打开设置页查看分步引导,或运行 opencode auth login 并选择 OpenCode Console 账号。",
|
|
153
|
+
"balance.missing": "无充值额度",
|
|
154
|
+
"balance.missing.title": "Command Code 的 credits 接口未返回购买或免费额度,因此没有充值余额可显示。",
|
|
155
|
+
"balance.details.extra": "购买 {purchased} + 免费 {free}",
|
|
156
|
+
"autoUpdate.updated": "插件已更新到 v{v},重启 DSH 生效",
|
|
157
|
+
"autoUpdate.available": "插件有新版本 v{v},自动更新未成功,请手动更新",
|
|
158
|
+
"widget.noData": "暂无数据",
|
|
159
|
+
"source.opencode-go": "dsh-opencode-go 当前账号",
|
|
160
|
+
"source.builtin": "DSH 内置提供商",
|
|
161
|
+
"reason.not-installed": "未安装 dsh-opencode-go",
|
|
162
|
+
"reason.entry-disabled": "dsh-opencode-go 已禁用",
|
|
163
|
+
"reason.service-unavailable": "宿主服务不可用",
|
|
164
|
+
"reason.read-failed": "读取额度失败",
|
|
165
|
+
"reason.invalid-response": "额度数据不合法",
|
|
166
|
+
"reason.unknown": "未知原因",
|
|
167
|
+
"update.available": "新版本 v{v}",
|
|
168
|
+
"update.title": "点击查看升级说明",
|
|
169
|
+
"error.retry": "点击重试",
|
|
170
|
+
|
|
171
|
+
// settings page (Plugins panel)
|
|
172
|
+
"cfg.summary": "在一个侧边栏组件中查看 OpenCode GO、Command Code 与 GLM Code Plan 的套餐用量。",
|
|
173
|
+
"cfg.title": "服务商用量设置",
|
|
174
|
+
"cfg.lead": "选择要显示的服务商与凭据引用。保存后立即生效,无需重启。API Key 保存在 DSH 凭据域中,不会出现在这里。",
|
|
175
|
+
"cfg.providers": "服务商",
|
|
176
|
+
"cfg.console.status": "充值余额",
|
|
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}",
|
|
202
|
+
"cfg.enabled": "启用",
|
|
203
|
+
"cfg.order.hint": "拖动此处调整显示顺序",
|
|
204
|
+
"cfg.move.up": "上移",
|
|
205
|
+
"cfg.move.down": "下移",
|
|
206
|
+
"cfg.credential": "凭据引用 (apiKeyEnv)",
|
|
207
|
+
"cfg.invite": "推广链接",
|
|
208
|
+
"cfg.invite.copy": "复制",
|
|
209
|
+
"cfg.invite.copied": "已复制",
|
|
210
|
+
"cfg.invite.copyFailed": "复制失败,请手动选中链接复制。",
|
|
211
|
+
"cfg.status.configured": "已配置",
|
|
212
|
+
"cfg.status.notConfigured": "未配置",
|
|
213
|
+
"cfg.status.enabled": "已启用",
|
|
214
|
+
"cfg.status.disabled": "已禁用",
|
|
215
|
+
"cfg.status.account": "账号",
|
|
216
|
+
"cfg.loading": "正在加载设置…",
|
|
217
|
+
"cfg.save": "保存",
|
|
218
|
+
"cfg.saving": "保存中…",
|
|
219
|
+
"cfg.reload": "重新加载",
|
|
220
|
+
"cfg.reloading": "加载中…",
|
|
221
|
+
"cfg.reloaded": "已重新加载。",
|
|
222
|
+
"cfg.retry": "重试",
|
|
223
|
+
"cfg.saved": "已保存,设置已更新。",
|
|
224
|
+
"cfg.noChanges": "没有需要保存的更改。",
|
|
225
|
+
"cfg.unsaved": "未保存",
|
|
226
|
+
"cfg.conflict": "保存冲突:设置已被其他会话修改,请点击「重新加载」后再保存。",
|
|
227
|
+
"cfg.saveFailed": "保存失败:{error}",
|
|
228
|
+
"cfg.loadFailed": "加载设置失败:{error}",
|
|
229
|
+
"cfg.unsupported": "宿主未提供设置描述符 (503)。请确认插件以 profile 条目安装,且 schemastery 版本 ≥ 3.18.4。",
|
|
230
|
+
"cfg.revision": "revision {n}"
|
|
231
|
+
};
|
|
232
|
+
const en = {
|
|
233
|
+
"window.rolling": "5-hour window",
|
|
234
|
+
"window.five-hour": "5-hour window",
|
|
235
|
+
"window.weekly": "Weekly",
|
|
236
|
+
"window.monthly": "Monthly",
|
|
237
|
+
"window.mcp": "MCP tools (monthly)",
|
|
238
|
+
"window.token": "Token window",
|
|
239
|
+
"window.other": "Other window",
|
|
240
|
+
"window.five-hour.hint": "5h",
|
|
241
|
+
"window.mcp.hint": "mo",
|
|
242
|
+
"time.soon": "resets soon",
|
|
243
|
+
"time.minutes": "{n}m",
|
|
244
|
+
"time.hours": "{n}h{m}m",
|
|
245
|
+
"time.days": "{n}d{m}h",
|
|
246
|
+
"time.elapsed.title": "Window time: {pct}% elapsed, {rest}% left",
|
|
247
|
+
"refresh": "Refresh",
|
|
248
|
+
"settings": "Settings",
|
|
249
|
+
"settings.title": "Open plugin settings",
|
|
250
|
+
"loading": "Loading…",
|
|
251
|
+
"error": "Error",
|
|
252
|
+
"noProviders": "No providers are enabled. Enable one in the plugin settings.",
|
|
253
|
+
"provider.opencode-go": "OpenCode GO",
|
|
254
|
+
"provider.command-code": "Command Code",
|
|
255
|
+
"provider.glm": "GLM Code Plan",
|
|
256
|
+
"status.ok": "ok",
|
|
257
|
+
"status.exceeded": "exceeded",
|
|
258
|
+
"status.rate-limited": "rate-limited",
|
|
259
|
+
"status.error": "failed",
|
|
260
|
+
"status.disabled": "disabled",
|
|
261
|
+
"status.unknown": "unknown",
|
|
262
|
+
"rail.plain": "Usage",
|
|
263
|
+
"rail.all": "Provider usage: {summary}",
|
|
264
|
+
"rail.error": "{provider}: read failed",
|
|
265
|
+
"rail.title": "{provider} · {pct}%",
|
|
266
|
+
"rail.title.account": "{provider} · {account} · {pct}%",
|
|
267
|
+
"widget.brand": "Provider usage",
|
|
268
|
+
"widget.account.title": "Active account: {name}",
|
|
269
|
+
"widget.fallback": "Built-in",
|
|
270
|
+
"widget.fallback.title": "Effective source: {source}; requested: {requested}; fallback reason: {reason}{message}",
|
|
271
|
+
"widget.partial.title": "Some endpoints failed: {error}",
|
|
272
|
+
"balance.extra": "Top-up",
|
|
273
|
+
"balance.zen": "Top-up",
|
|
274
|
+
"balance.short": "Bal.",
|
|
275
|
+
"balance.none": "Balance —",
|
|
276
|
+
"balance.title": "Top-up balance {amount} (source: {source})",
|
|
277
|
+
"balance.delta.title": "Balance change this turn: {from} → {to}",
|
|
278
|
+
"balance.none.title": "Top-up balance unavailable: {error}",
|
|
279
|
+
"balance.setup": "Set up",
|
|
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.",
|
|
281
|
+
"balance.missing": "No top-up",
|
|
282
|
+
"balance.missing.title": "Command Code's credits response carried no purchased or free credits, so there is no top-up balance to show.",
|
|
283
|
+
"balance.details.extra": "purchased {purchased} + free {free}",
|
|
284
|
+
"autoUpdate.updated": "Plugin updated to v{v}; restart DSH to apply",
|
|
285
|
+
"autoUpdate.available": "Plugin v{v} is available; automatic update failed — update it manually",
|
|
286
|
+
"widget.noData": "No data",
|
|
287
|
+
"source.opencode-go": "dsh-opencode-go current account",
|
|
288
|
+
"source.builtin": "DSH built-in provider",
|
|
289
|
+
"reason.not-installed": "dsh-opencode-go not installed",
|
|
290
|
+
"reason.entry-disabled": "dsh-opencode-go disabled",
|
|
291
|
+
"reason.service-unavailable": "host service unavailable",
|
|
292
|
+
"reason.read-failed": "quota read failed",
|
|
293
|
+
"reason.invalid-response": "invalid quota response",
|
|
294
|
+
"reason.unknown": "unknown reason",
|
|
295
|
+
"update.available": "v{v} available",
|
|
296
|
+
"update.title": "Click for upgrade instructions",
|
|
297
|
+
"error.retry": "Click to retry",
|
|
298
|
+
|
|
299
|
+
// settings page (Plugins panel)
|
|
300
|
+
"cfg.summary": "OpenCode GO, Command Code and GLM Code Plan plan usage in one sidebar widget.",
|
|
301
|
+
"cfg.title": "Provider usage settings",
|
|
302
|
+
"cfg.lead": "Choose which providers to show and their credential refs. Changes apply immediately; no restart needed. API keys stay in the DSH credentials domain and are never shown here.",
|
|
303
|
+
"cfg.providers": "Providers",
|
|
304
|
+
"cfg.console.status": "Top-up balance",
|
|
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}",
|
|
330
|
+
"cfg.enabled": "Enabled",
|
|
331
|
+
"cfg.order.hint": "Drag here to reorder",
|
|
332
|
+
"cfg.move.up": "Move up",
|
|
333
|
+
"cfg.move.down": "Move down",
|
|
334
|
+
"cfg.credential": "Credential ref (apiKeyEnv)",
|
|
335
|
+
"cfg.invite": "Referral link",
|
|
336
|
+
"cfg.invite.copy": "Copy",
|
|
337
|
+
"cfg.invite.copied": "Copied",
|
|
338
|
+
"cfg.invite.copyFailed": "Copy failed; select the link and copy it manually.",
|
|
339
|
+
"cfg.status.configured": "configured",
|
|
340
|
+
"cfg.status.notConfigured": "not configured",
|
|
341
|
+
"cfg.status.enabled": "enabled",
|
|
342
|
+
"cfg.status.disabled": "disabled",
|
|
343
|
+
"cfg.status.account": "Account",
|
|
344
|
+
"cfg.loading": "Loading settings…",
|
|
345
|
+
"cfg.save": "Save",
|
|
346
|
+
"cfg.saving": "Saving…",
|
|
347
|
+
"cfg.reload": "Reload",
|
|
348
|
+
"cfg.reloading": "Loading…",
|
|
349
|
+
"cfg.reloaded": "Reloaded.",
|
|
350
|
+
"cfg.retry": "Retry",
|
|
351
|
+
"cfg.saved": "Saved — settings updated.",
|
|
352
|
+
"cfg.noChanges": "Nothing to save.",
|
|
353
|
+
"cfg.unsaved": "unsaved",
|
|
354
|
+
"cfg.conflict": "Save conflict: the settings were changed by another session. Click Reload, then save again.",
|
|
355
|
+
"cfg.saveFailed": "Save failed: {error}",
|
|
356
|
+
"cfg.loadFailed": "Failed to load settings: {error}",
|
|
357
|
+
"cfg.unsupported": "The host exposes no settings descriptor (503). Make sure the plugin is installed as a profile entry and schemastery is ≥ 3.18.4.",
|
|
358
|
+
"cfg.revision": "revision {n}"
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// ── data helpers ──────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
/** Fraction of the window ALREADY elapsed, derived from its reset time. */
|
|
364
|
+
function timeElapsedFraction(resetsAt, windowMs) {
|
|
365
|
+
if (!resetsAt || !windowMs) return null;
|
|
366
|
+
const reset = new Date(resetsAt).getTime();
|
|
367
|
+
if (!Number.isFinite(reset)) return null;
|
|
368
|
+
const elapsed = (windowMs - (reset - Date.now())) / windowMs;
|
|
369
|
+
return Math.max(0, Math.min(1, elapsed));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Short human relative time until `iso`, localized. */
|
|
373
|
+
function timeUntil(iso, t) {
|
|
374
|
+
const target = new Date(iso).getTime();
|
|
375
|
+
if (!Number.isFinite(target)) return "";
|
|
376
|
+
const diff = target - Date.now();
|
|
377
|
+
if (diff <= 0) return t("time.soon");
|
|
378
|
+
const mins = Math.floor(diff / 60000);
|
|
379
|
+
if (mins < 60) return t("time.minutes", { n: mins });
|
|
380
|
+
const hours = Math.floor(mins / 60);
|
|
381
|
+
if (hours < 24) return t("time.hours", { n: hours, m: mins % 60 });
|
|
382
|
+
return t("time.days", { n: Math.floor(hours / 24), m: hours % 24 });
|
|
383
|
+
}
|
|
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
|
+
|
|
395
|
+
/** Fill color by usage percent — themed alias tokens (light/dark safe). */
|
|
396
|
+
function fillColor(percent) {
|
|
397
|
+
if (percent >= 90) return "var(--dsw-alias-state-error-primary)";
|
|
398
|
+
if (percent >= 70) return "var(--dsw-alias-state-warn-primary)";
|
|
399
|
+
return "var(--dsw-alias-state-success-primary)";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── defensive display helpers ─────────────────────────────────────
|
|
403
|
+
function asText(value) {
|
|
404
|
+
if (typeof value === "string") return value;
|
|
405
|
+
if (value === null || value === undefined) return "";
|
|
406
|
+
return String(value);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** One-decimal display percentage; null/NaN renders as an em dash. */
|
|
410
|
+
function formatPercent(value) {
|
|
411
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "—";
|
|
412
|
+
const rounded = Math.round(value * 10) / 10;
|
|
413
|
+
return String(rounded);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Format a provider balance for the badge. */
|
|
417
|
+
function formatMoney(balance) {
|
|
418
|
+
if (!balance || typeof balance.amount !== "number" || !Number.isFinite(balance.amount)) return "";
|
|
419
|
+
const amount = balance.amount.toFixed(2);
|
|
420
|
+
return balance.currency === "USD" || !balance.currency ? "$" + amount : amount + " " + balance.currency;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** Compact signed delta without the currency symbol, e.g. -0.1. */
|
|
424
|
+
function formatDeltaShort(change) {
|
|
425
|
+
if (!change || typeof change.amount !== "number" || !Number.isFinite(change.amount)) return "";
|
|
426
|
+
const text = Math.abs(change.amount).toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
|
|
427
|
+
return (change.amount < 0 ? "-" : "+") + text;
|
|
428
|
+
}
|
|
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
|
+
|
|
444
|
+
/** Animate a balance from its previous value to the refreshed value. */
|
|
445
|
+
function AnimatedBalance({ value, currency, delta }) {
|
|
446
|
+
const from = delta && typeof delta.from === "number" ? delta.from : value;
|
|
447
|
+
const [shown, setShown] = react.useState(from);
|
|
448
|
+
react.useEffect(() => {
|
|
449
|
+
const target = typeof value === "number" ? value : from;
|
|
450
|
+
if (!delta || typeof delta.from !== "number" || from === target) {
|
|
451
|
+
setShown(target);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
if (typeof window.requestAnimationFrame !== "function") {
|
|
455
|
+
setShown(target);
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
let raf = null;
|
|
459
|
+
const started = typeof window.performance?.now === "function" ? window.performance.now() : Date.now();
|
|
460
|
+
const duration = 650;
|
|
461
|
+
const step = (now) => {
|
|
462
|
+
const progress = Math.min(1, Math.max(0, (now - started) / duration));
|
|
463
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
464
|
+
setShown(from + (target - from) * eased);
|
|
465
|
+
if (progress < 1) raf = window.requestAnimationFrame(step);
|
|
466
|
+
else setShown(target);
|
|
467
|
+
};
|
|
468
|
+
raf = window.requestAnimationFrame(step);
|
|
469
|
+
return () => { if (raf !== null) window.cancelAnimationFrame(raf); };
|
|
470
|
+
}, [delta?.seq, value, currency, from]);
|
|
471
|
+
return formatMoney({ amount: shown, currency });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Snapshot the current balances keyed by provider id. */
|
|
475
|
+
function balancesFromProviders(providers) {
|
|
476
|
+
const out = {};
|
|
477
|
+
for (const provider of providers ?? []) {
|
|
478
|
+
const balance = provider?.balance;
|
|
479
|
+
if (!balance || typeof balance.amount !== "number" || !Number.isFinite(balance.amount)) continue;
|
|
480
|
+
out[provider.id] = {
|
|
481
|
+
amount: balance.amount,
|
|
482
|
+
currency: typeof balance.currency === "string" && balance.currency.length > 0 ? balance.currency : "USD"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Compare two balance snapshots and return only providers whose balance changed. */
|
|
489
|
+
function balanceDeltasBetween(previous, providers) {
|
|
490
|
+
const out = {};
|
|
491
|
+
for (const provider of providers ?? []) {
|
|
492
|
+
const balance = provider?.balance;
|
|
493
|
+
const before = previous?.[provider?.id];
|
|
494
|
+
if (!balance || typeof balance.amount !== "number" || !Number.isFinite(balance.amount)) continue;
|
|
495
|
+
if (!before || typeof before.amount !== "number" || !Number.isFinite(before.amount)) continue;
|
|
496
|
+
const currency = typeof balance.currency === "string" && balance.currency.length > 0 ? balance.currency : "USD";
|
|
497
|
+
const beforeCurrency = typeof before.currency === "string" && before.currency.length > 0 ? before.currency : "USD";
|
|
498
|
+
if (currency !== beforeCurrency) continue;
|
|
499
|
+
const raw = balance.amount - before.amount;
|
|
500
|
+
if (!Number.isFinite(raw)) continue;
|
|
501
|
+
// Money is displayed in cents; rounding here keeps 5.14 - 4.88 from
|
|
502
|
+
// surfacing as -0.2600000000000006.
|
|
503
|
+
const amount = Math.round(raw * 100) / 100;
|
|
504
|
+
if (Math.abs(amount) < 0.005) continue;
|
|
505
|
+
out[provider.id] = { amount, currency, from: before.amount, to: balance.amount };
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Signed money for the per-turn balance delta. */
|
|
511
|
+
function formatSignedMoney(change) {
|
|
512
|
+
if (!change || typeof change.amount !== "number" || !Number.isFinite(change.amount)) return "";
|
|
513
|
+
const amount = Math.abs(change.amount) < 0.005 ? 0 : change.amount;
|
|
514
|
+
const absolute = Math.abs(amount).toFixed(2);
|
|
515
|
+
const sign = amount < 0 ? "-" : amount > 0 ? "+" : "";
|
|
516
|
+
return change.currency === "USD" || !change.currency ? sign + "$" + absolute : sign + absolute + " " + change.currency;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/** The window whose percentage is shown on the collapsed rail. */
|
|
520
|
+
function primaryWindow(provider) {
|
|
521
|
+
if (!provider || !Array.isArray(provider.windows)) return null;
|
|
522
|
+
return provider.windows.find((window) => window.key === "monthly")
|
|
523
|
+
?? provider.windows.find((window) => window.key === "rolling" || window.key === "five-hour")
|
|
524
|
+
?? provider.windows[0]
|
|
525
|
+
?? null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function primaryPercent(provider) {
|
|
529
|
+
const window = primaryWindow(provider);
|
|
530
|
+
return window && typeof window.percent === "number" ? window.percent : null;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function statusText(t, status) {
|
|
534
|
+
const key = "status." + (typeof status === "string" && status !== "" ? status : "unknown");
|
|
535
|
+
const label = t(key);
|
|
536
|
+
if (label !== key) return label;
|
|
537
|
+
return typeof status === "string" && status !== "" ? status : t("status.unknown");
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function sourceLabel(t, source) {
|
|
541
|
+
if (source === "opencode-go") return t("source.opencode-go");
|
|
542
|
+
if (source === "builtin") return t("source.builtin");
|
|
543
|
+
return asText(source);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function reasonLabel(t, reason) {
|
|
547
|
+
const key = "reason." + (typeof reason === "string" && reason !== "" ? reason : "unknown");
|
|
548
|
+
const label = t(key);
|
|
549
|
+
return label === key ? t("reason.unknown") : label;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function fallbackTitle(t, fallback, source) {
|
|
553
|
+
const requested = fallback && (fallback.from === "builtin" || fallback.from === "opencode-go") ? fallback.from : source;
|
|
554
|
+
const raw = asText(fallback && fallback.message);
|
|
555
|
+
return t("widget.fallback.title", {
|
|
556
|
+
source: sourceLabel(t, source),
|
|
557
|
+
requested: sourceLabel(t, requested),
|
|
558
|
+
reason: reasonLabel(t, fallback && fallback.reason),
|
|
559
|
+
message: raw === "" ? "" : " · " + raw
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** Detect zh vs en for the tests and for hosts without a locale seat. */
|
|
564
|
+
function detectLang() {
|
|
565
|
+
try {
|
|
566
|
+
const lang = (typeof document !== "undefined" && document.documentElement && document.documentElement.lang)
|
|
567
|
+
|| (typeof navigator !== "undefined" && navigator.language)
|
|
568
|
+
|| "";
|
|
569
|
+
return String(lang).toLowerCase();
|
|
570
|
+
} catch (_error) {
|
|
571
|
+
return "";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function fallbackT(key, params) {
|
|
576
|
+
const table = detectLang().indexOf("zh") === 0 ? zh : en;
|
|
577
|
+
let raw = table[key] !== undefined ? table[key] : zh[key] !== undefined ? zh[key] : key;
|
|
578
|
+
if (params === null || params === undefined) return raw;
|
|
579
|
+
return String(raw).replace(/\{(\w+)\}/g, (match, name) => (params[name] === null || params[name] === undefined ? "" : String(params[name])));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function translateFor(props) {
|
|
583
|
+
return props && typeof props.t === "function" ? props.t : fallbackT;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** Applicable windows for a provider, skipping malformed rows. */
|
|
587
|
+
function providerWindows(provider) {
|
|
588
|
+
return Array.isArray(provider?.windows)
|
|
589
|
+
? provider.windows.filter((window) => window && typeof window.percent === "number")
|
|
590
|
+
: [];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function windowLabel(t, window) {
|
|
594
|
+
const key = typeof window.labelKey === "string" && window.labelKey.length > 0 ? window.labelKey : "window." + window.key;
|
|
595
|
+
const label = t(key);
|
|
596
|
+
const base = label === key ? asText(window.key) : label;
|
|
597
|
+
return typeof window.note === "string" && window.note.length > 0 ? base + " · " + window.note : base;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function providerStatusClass(provider) {
|
|
601
|
+
if (provider?.status === "error") return "dpu-dot dpu-error";
|
|
602
|
+
if (provider?.status === "ok") {
|
|
603
|
+
const partial = provider?.meta && Array.isArray(provider.meta.errors) ? provider.meta.errors.length : 0;
|
|
604
|
+
return partial > 0 ? "dpu-dot dpu-warn" : "dpu-dot dpu-ok";
|
|
605
|
+
}
|
|
606
|
+
return "dpu-dot dpu-warn";
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function providerPctText(provider) {
|
|
610
|
+
const pct = primaryPercent(provider);
|
|
611
|
+
return pct === null ? "—" : formatPercent(pct) + "%";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function providerTitle(t, provider) {
|
|
615
|
+
const parts = [provider.name || t("provider." + provider.id), providerPctText(provider)];
|
|
616
|
+
const account = provider.account && typeof provider.account.name === "string" ? provider.account.name.trim() : "";
|
|
617
|
+
if (account !== "") parts.push(account);
|
|
618
|
+
const plan = provider.plan && typeof provider.plan.name === "string" ? provider.plan.name.trim() : "";
|
|
619
|
+
if (plan !== "") parts.push(plan);
|
|
620
|
+
const money = formatMoney(provider.balance);
|
|
621
|
+
if (money !== "") parts.push(money);
|
|
622
|
+
const partial = provider?.meta && Array.isArray(provider.meta.errors)
|
|
623
|
+
? provider.meta.errors.filter((message) => typeof message === "string" && message !== "")
|
|
624
|
+
: [];
|
|
625
|
+
if (provider.status !== "error" && partial.length > 0) parts.push(t("widget.partial.title", { error: partial.join("; ") }));
|
|
626
|
+
if (provider.status === "error" && typeof provider.error === "string") parts.push(provider.error);
|
|
627
|
+
return parts.join(" · ");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** Short widget notice for an auto-update state (empty when nothing to say). */
|
|
631
|
+
function autoUpdateNotice(t, state) {
|
|
632
|
+
if (!state || typeof state !== "object") return "";
|
|
633
|
+
if (state.status === "updated" && state.restartRequired === true) {
|
|
634
|
+
return t("autoUpdate.updated", { v: state.version || state.latestVersion || "?" });
|
|
635
|
+
}
|
|
636
|
+
if (state.status === "available") {
|
|
637
|
+
return t("autoUpdate.available", { v: state.latestVersion || "?" });
|
|
638
|
+
}
|
|
639
|
+
return "";
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Hover text for the auto-update notice: the reason a self-update failed. */
|
|
643
|
+
function autoUpdateTitle(state) {
|
|
644
|
+
if (!state || typeof state !== "object") return "";
|
|
645
|
+
return state.status === "available" && typeof state.message === "string" ? state.message : "";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Compact balance label for a provider header (empty when unavailable). */
|
|
649
|
+
function formatBalanceShort(_t, provider) {
|
|
650
|
+
const balance = provider?.balance;
|
|
651
|
+
if (!balance || typeof balance.amount !== "number" || !Number.isFinite(balance.amount)) return "";
|
|
652
|
+
// The header is tight; the amount alone is enough, the tooltip carries
|
|
653
|
+
// the "balance" label and source.
|
|
654
|
+
return formatMoney(balance);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function collapseStorageKey() {
|
|
658
|
+
return PACKAGE_NAME + ":collapsed";
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function readCollapsedProviders() {
|
|
662
|
+
try {
|
|
663
|
+
const raw = window.localStorage.getItem(collapseStorageKey());
|
|
664
|
+
const parsed = raw === null ? [] : JSON.parse(raw);
|
|
665
|
+
return new Set(Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : []);
|
|
666
|
+
} catch (_error) {
|
|
667
|
+
return new Set();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function storeCollapsedProviders(value) {
|
|
672
|
+
try {
|
|
673
|
+
window.localStorage.setItem(collapseStorageKey(), JSON.stringify([...value]));
|
|
674
|
+
} catch (_error) {
|
|
675
|
+
// private mode / storage disabled: collapse state is session-only
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Apply the saved provider order to the displayed sections. */
|
|
680
|
+
function orderedProviders(data) {
|
|
681
|
+
const providers = data && Array.isArray(data.providers) ? data.providers : [];
|
|
682
|
+
const order = data && Array.isArray(data.providerOrder) ? data.providerOrder : [];
|
|
683
|
+
if (order.length === 0) return providers;
|
|
684
|
+
const rank = new Map();
|
|
685
|
+
order.forEach((id, index) => {
|
|
686
|
+
if (typeof id === "string" && !rank.has(id)) rank.set(id, index);
|
|
687
|
+
});
|
|
688
|
+
// Stable sort: providers missing from the order keep the host order.
|
|
689
|
+
return providers
|
|
690
|
+
.map((provider, index) => ({ provider, index }))
|
|
691
|
+
.sort((a, b) => ((rank.get(a.provider.id) ?? Infinity) - (rank.get(b.provider.id) ?? Infinity)) || (a.index - b.index))
|
|
692
|
+
.map((entry) => entry.provider);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function railSummary(t, providers) {
|
|
696
|
+
return providers.map((provider) => {
|
|
697
|
+
const name = provider.name || t("provider." + provider.id);
|
|
698
|
+
const pct = primaryPercent(provider);
|
|
699
|
+
const money = formatMoney(provider.balance);
|
|
700
|
+
return name + " " + (pct === null ? "—" : formatPercent(pct) + "%") + (money === "" ? "" : " · " + money);
|
|
701
|
+
}).join(" · ");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
let pluginContext = null;
|
|
705
|
+
|
|
706
|
+
/** Open this bundle's page in the Plugins panel (its configuration page). */
|
|
707
|
+
function openPluginSettings() {
|
|
708
|
+
const ctx = pluginContext;
|
|
709
|
+
if (!ctx || typeof ctx.get !== "function") return;
|
|
710
|
+
try {
|
|
711
|
+
const navigation = ctx.get("pluginNavigation");
|
|
712
|
+
if (navigation && typeof navigation.openBundle === "function") {
|
|
713
|
+
navigation.openBundle(PACKAGE_NAME);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
const layout = ctx.get("layout");
|
|
717
|
+
if (layout && typeof layout.selectPanel === "function") layout.selectPanel("plugins");
|
|
718
|
+
} catch (_error) {
|
|
719
|
+
// a host without the Plugins panel simply has no settings shortcut
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
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);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Open the plugin settings from the balance chip (never toggles the card). */
|
|
737
|
+
function activateBalanceSetup(event) {
|
|
738
|
+
if (event) {
|
|
739
|
+
if (typeof event.preventDefault === "function") event.preventDefault();
|
|
740
|
+
if (typeof event.stopPropagation === "function") event.stopPropagation();
|
|
741
|
+
}
|
|
742
|
+
openPluginSettings();
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/** Enter/Space activation for the keyboard-reachable balance chip. */
|
|
746
|
+
function balanceSetupKeyDown(event) {
|
|
747
|
+
if (event && (event.key === "Enter" || event.key === " " || event.key === "Spacebar")) activateBalanceSetup(event);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Cross-component ping: a settings save asks the widget to reload now. */
|
|
751
|
+
const usageRefreshStore = {
|
|
752
|
+
listeners: new Set(),
|
|
753
|
+
publish() {
|
|
754
|
+
for (const listener of this.listeners) listener();
|
|
755
|
+
},
|
|
756
|
+
subscribe(listener) {
|
|
757
|
+
this.listeners.add(listener);
|
|
758
|
+
return () => this.listeners.delete(listener);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
// ── widget component ──────────────────────────────────────────────
|
|
763
|
+
/**
|
|
764
|
+
* All enabled providers are stacked in one column. Each provider header
|
|
765
|
+
* collapses/expands its own body; collapse state is persisted locally.
|
|
766
|
+
* @param {{ wide?: boolean, t?: Function }} props - owner share from the
|
|
767
|
+
* sidebar (`sidebar.footer.action` is rendered with `{ wide }`) plus the
|
|
768
|
+
* locale translate seat declared by this registration.
|
|
769
|
+
*/
|
|
770
|
+
function Widget(props) {
|
|
771
|
+
const t = props.t || ((key) => key);
|
|
772
|
+
const [data, setData] = react.useState(null);
|
|
773
|
+
const [error, setError] = react.useState(null);
|
|
774
|
+
const [stamp, setStamp] = react.useState(0);
|
|
775
|
+
const [collapsed, setCollapsed] = react.useState(readCollapsedProviders);
|
|
776
|
+
const [balanceDeltas, setBalanceDeltas] = react.useState({});
|
|
777
|
+
const previousBalancesRef = react.useRef(null);
|
|
778
|
+
const deltaTimersRef = react.useRef(new Map());
|
|
779
|
+
|
|
780
|
+
react.useEffect(() => {
|
|
781
|
+
let alive = true;
|
|
782
|
+
/** True when the tab is hidden (guarded for hosts without DOM). */
|
|
783
|
+
const isHidden = () => typeof document !== "undefined" && document.hidden === true;
|
|
784
|
+
const load = async (force) => {
|
|
785
|
+
// A hidden tab has nobody looking at the numbers: skip the poll
|
|
786
|
+
// and catch up the moment it becomes visible again.
|
|
787
|
+
if (isHidden() && !force) return;
|
|
788
|
+
try {
|
|
789
|
+
const url = "/provider-usage/usage" + (force ? "?refresh=1" : "");
|
|
790
|
+
const res = await fetch(url, { cache: "no-store" });
|
|
791
|
+
const json = await res.json().catch(() => null);
|
|
792
|
+
if (!alive) return;
|
|
793
|
+
if (json === null || (typeof json.error === "string" && (!Array.isArray(json.providers) || json.providers.length === 0))) {
|
|
794
|
+
setError(asText(json?.error) || "HTTP " + res.status);
|
|
795
|
+
setData(null);
|
|
796
|
+
} else {
|
|
797
|
+
const providers = Array.isArray(json.providers) ? json.providers : [];
|
|
798
|
+
const previous = previousBalancesRef.current;
|
|
799
|
+
previousBalancesRef.current = balancesFromProviders(providers);
|
|
800
|
+
if (previous !== null) {
|
|
801
|
+
const changes = balanceDeltasBetween(previous, providers);
|
|
802
|
+
const ids = Object.keys(changes);
|
|
803
|
+
if (ids.length > 0) {
|
|
804
|
+
const stamped = {};
|
|
805
|
+
for (const id of ids) stamped[id] = { ...changes[id], seq: Date.now() + Math.random() };
|
|
806
|
+
setBalanceDeltas((current) => ({ ...current, ...stamped }));
|
|
807
|
+
for (const id of ids) {
|
|
808
|
+
const existing = deltaTimersRef.current.get(id);
|
|
809
|
+
if (existing !== undefined) window.clearTimeout(existing);
|
|
810
|
+
const seq = stamped[id].seq;
|
|
811
|
+
const timer = window.setTimeout(() => {
|
|
812
|
+
setBalanceDeltas((current) => {
|
|
813
|
+
if (current[id]?.seq !== seq) return current;
|
|
814
|
+
const next = { ...current };
|
|
815
|
+
delete next[id];
|
|
816
|
+
return next;
|
|
817
|
+
});
|
|
818
|
+
deltaTimersRef.current.delete(id);
|
|
819
|
+
}, 6000);
|
|
820
|
+
deltaTimersRef.current.set(id, timer);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
setData(json);
|
|
825
|
+
setError(null);
|
|
826
|
+
}
|
|
827
|
+
} catch (e) {
|
|
828
|
+
if (!alive) return;
|
|
829
|
+
setError(asText(e && e.message) || String(e));
|
|
830
|
+
setData(null);
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
// The manual refresh forces one immediate upstream read; the 30 s
|
|
834
|
+
// poll afterwards must use the host cache again.
|
|
835
|
+
load(stamp > 0);
|
|
836
|
+
const timer = window.setInterval(() => load(false), 30000);
|
|
837
|
+
const onVisibility = () => { if (!document.hidden) void load(false); };
|
|
838
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
839
|
+
return () => {
|
|
840
|
+
alive = false;
|
|
841
|
+
window.clearInterval(timer);
|
|
842
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
843
|
+
};
|
|
844
|
+
}, [stamp]);
|
|
845
|
+
|
|
846
|
+
react.useEffect(() => () => {
|
|
847
|
+
for (const timer of deltaTimersRef.current.values()) window.clearTimeout(timer);
|
|
848
|
+
deltaTimersRef.current.clear();
|
|
849
|
+
}, []);
|
|
850
|
+
|
|
851
|
+
react.useEffect(() => usageRefreshStore.subscribe(() => setStamp((n) => n + 1)), []);
|
|
852
|
+
|
|
853
|
+
const providers = orderedProviders(data);
|
|
854
|
+
|
|
855
|
+
const toggleProvider = (id) => {
|
|
856
|
+
setCollapsed((prev) => {
|
|
857
|
+
const next = new Set(prev);
|
|
858
|
+
if (next.has(id)) next.delete(id);
|
|
859
|
+
else next.add(id);
|
|
860
|
+
storeCollapsedProviders(next);
|
|
861
|
+
return next;
|
|
862
|
+
});
|
|
863
|
+
};
|
|
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
|
+
|
|
874
|
+
// Collapsed rail: a single compact badge summarising all providers.
|
|
875
|
+
if (!props.wide) {
|
|
876
|
+
const maxPct = providers.reduce((max, provider) => {
|
|
877
|
+
const pct = primaryPercent(provider);
|
|
878
|
+
return pct !== null && (max === null || pct > max) ? pct : max;
|
|
879
|
+
}, null);
|
|
880
|
+
const title = error !== null
|
|
881
|
+
? error
|
|
882
|
+
: providers.length === 0 ? t("rail.plain") : t("rail.all", { summary: railSummary(t, providers) });
|
|
883
|
+
return react_jsx_runtime.jsx("div", {
|
|
884
|
+
className: "dpu-rail",
|
|
885
|
+
title: title,
|
|
886
|
+
children: maxPct === null ? (providers.length === 0 ? "—" : t("rail.plain")) : formatPercent(maxPct) + "%"
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const sections = providers.map((provider) => {
|
|
891
|
+
const isCollapsed = collapsed.has(provider.id);
|
|
892
|
+
const pctText = providerPctText(provider);
|
|
893
|
+
const balanceText = formatBalanceShort(t, provider);
|
|
894
|
+
const balance = provider.balance && typeof provider.balance.amount === "number" ? provider.balance : null;
|
|
895
|
+
const delta = balanceDeltas[provider.id] && typeof balanceDeltas[provider.id].amount === "number" ? balanceDeltas[provider.id] : null;
|
|
896
|
+
const showDelta = delta !== null && Math.abs(delta.amount) >= 0.005;
|
|
897
|
+
const balanceClass = "dpu-provider-balance" + (showDelta ? " dpu-hit" : "") + balanceTone(balance);
|
|
898
|
+
// Command Code's balance is purchased + free credits; name the
|
|
899
|
+
// composition in the tooltip so the single number stays honest.
|
|
900
|
+
const balanceDetails = balance && balance.details && typeof balance.details === "object" && typeof balance.details.purchased === "number"
|
|
901
|
+
? t("balance.details.extra", {
|
|
902
|
+
purchased: formatMoney({ amount: balance.details.purchased, currency: balance.currency }),
|
|
903
|
+
free: formatMoney({ amount: typeof balance.details.free === "number" ? balance.details.free : 0, currency: balance.currency })
|
|
904
|
+
})
|
|
905
|
+
: "";
|
|
906
|
+
const balanceTitle = showDelta
|
|
907
|
+
? t("balance.delta.title", {
|
|
908
|
+
from: formatMoney({ amount: delta.from, currency: delta.currency }),
|
|
909
|
+
to: formatMoney({ amount: delta.to, currency: delta.currency })
|
|
910
|
+
})
|
|
911
|
+
: t("balance.title", { amount: balanceText, source: balance ? asText(balance.source) : "" }) + (balanceDetails === "" ? "" : " · " + balanceDetails);
|
|
912
|
+
const rawBalanceError = provider.meta && typeof provider.meta.balanceError === "string" ? provider.meta.balanceError : "";
|
|
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 : "";
|
|
920
|
+
// Command Code: credits answered without either credit pool, so
|
|
921
|
+
// there is no top-up balance to show (it is not a config problem).
|
|
922
|
+
const balanceMissing = provider.meta && provider.meta.balanceMissing === true;
|
|
923
|
+
|
|
924
|
+
const accountName = provider.account && typeof provider.account.name === "string" ? provider.account.name.trim() : "";
|
|
925
|
+
const planName = provider.plan && typeof provider.plan.name === "string" ? provider.plan.name.trim() : "";
|
|
926
|
+
const fallback = provider.fallback && typeof provider.fallback === "object" ? provider.fallback : null;
|
|
927
|
+
|
|
928
|
+
const accountBadge = accountName === "" ? null : react_jsx_runtime.jsx("span", {
|
|
929
|
+
className: "dpu-account",
|
|
930
|
+
title: t("widget.account.title", { name: accountName }),
|
|
931
|
+
children: accountName
|
|
932
|
+
}, "account");
|
|
933
|
+
const planBadge = planName === "" || planName.length > 12 ? null : react_jsx_runtime.jsx("span", {
|
|
934
|
+
className: "dpu-plan",
|
|
935
|
+
title: planName,
|
|
936
|
+
children: planName
|
|
937
|
+
}, "plan");
|
|
938
|
+
const fallbackBadge = fallback === null ? null : react_jsx_runtime.jsx("span", {
|
|
939
|
+
className: "dpu-badge",
|
|
940
|
+
title: fallbackTitle(t, fallback, provider.source),
|
|
941
|
+
children: t("widget.fallback")
|
|
942
|
+
}, "fallback");
|
|
943
|
+
|
|
944
|
+
const rows = [];
|
|
945
|
+
if (provider.status === "ok") {
|
|
946
|
+
for (const win of providerWindows(provider)) {
|
|
947
|
+
const elapsed = timeElapsedFraction(win.resetsAt, win.windowMs);
|
|
948
|
+
rows.push(react_jsx_runtime.jsx("div", {
|
|
949
|
+
className: "dpu-row",
|
|
950
|
+
children: [
|
|
951
|
+
react_jsx_runtime.jsx("div", {
|
|
952
|
+
className: "dpu-row-label",
|
|
953
|
+
children: [
|
|
954
|
+
react_jsx_runtime.jsx("b", { children: windowLabel(t, win) }),
|
|
955
|
+
react_jsx_runtime.jsx("b", { children: formatPercent(win.percent) + "%" })
|
|
956
|
+
]
|
|
957
|
+
}),
|
|
958
|
+
react_jsx_runtime.jsx("div", {
|
|
959
|
+
className: "dpu-meta",
|
|
960
|
+
children: [
|
|
961
|
+
react_jsx_runtime.jsx("div", {
|
|
962
|
+
className: "dpu-bars",
|
|
963
|
+
children: [
|
|
964
|
+
react_jsx_runtime.jsx("div", {
|
|
965
|
+
className: "dpu-track",
|
|
966
|
+
children: react_jsx_runtime.jsx("div", {
|
|
967
|
+
className: "dpu-fill",
|
|
968
|
+
style: {
|
|
969
|
+
width: Math.max(0, Math.min(100, win.percent)) + "%",
|
|
970
|
+
background: fillColor(win.percent)
|
|
971
|
+
}
|
|
972
|
+
})
|
|
973
|
+
}),
|
|
974
|
+
elapsed === null ? null : react_jsx_runtime.jsx("div", {
|
|
975
|
+
className: "dpu-timetrack",
|
|
976
|
+
title: t("time.elapsed.title", { pct: Math.round(elapsed * 100), rest: Math.round((1 - elapsed) * 100) }),
|
|
977
|
+
children: [
|
|
978
|
+
react_jsx_runtime.jsx("div", {
|
|
979
|
+
className: "dpu-timeelapsed",
|
|
980
|
+
style: { width: (elapsed * 100) + "%" }
|
|
981
|
+
}),
|
|
982
|
+
react_jsx_runtime.jsx("div", {
|
|
983
|
+
className: "dpu-timeremain",
|
|
984
|
+
style: { width: ((1 - elapsed) * 100) + "%" }
|
|
985
|
+
})
|
|
986
|
+
]
|
|
987
|
+
})
|
|
988
|
+
]
|
|
989
|
+
}),
|
|
990
|
+
react_jsx_runtime.jsx("span", {
|
|
991
|
+
children: win.status === "ok"
|
|
992
|
+
? (win.resetsAt ? timeUntil(win.resetsAt, t) : (win.hintKey ? t(win.hintKey) : ""))
|
|
993
|
+
: statusText(t, win.status)
|
|
994
|
+
})
|
|
995
|
+
]
|
|
996
|
+
})
|
|
997
|
+
]
|
|
998
|
+
}, win.key));
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const body = [];
|
|
1003
|
+
if (provider.status === "error") {
|
|
1004
|
+
const message = asText(provider.error) || t("status.error");
|
|
1005
|
+
body.push(react_jsx_runtime.jsx("div", {
|
|
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,
|
|
1012
|
+
children: message
|
|
1013
|
+
}, "error"));
|
|
1014
|
+
} else if (rows.length === 0) {
|
|
1015
|
+
body.push(react_jsx_runtime.jsx("div", { className: "dpu-provider-error", children: t("widget.noData") }, "empty"));
|
|
1016
|
+
} else {
|
|
1017
|
+
body.push(react_jsx_runtime.jsx(react.Fragment, { children: rows }, "rows"));
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const balanceBadge = balance !== null || showDelta
|
|
1021
|
+
? react_jsx_runtime.jsx("span", {
|
|
1022
|
+
className: balanceClass,
|
|
1023
|
+
title: balanceTitle,
|
|
1024
|
+
children: showDelta && balance !== null
|
|
1025
|
+
? react_jsx_runtime.jsx(AnimatedBalance, {
|
|
1026
|
+
value: balance.amount,
|
|
1027
|
+
currency: balance.currency,
|
|
1028
|
+
delta
|
|
1029
|
+
})
|
|
1030
|
+
: showDelta ? formatSignedMoney(delta) : balanceText
|
|
1031
|
+
}, "balance")
|
|
1032
|
+
: balanceError !== ""
|
|
1033
|
+
? react_jsx_runtime.jsx("span", {
|
|
1034
|
+
className: "dpu-provider-balance dpu-muted",
|
|
1035
|
+
title: t("balance.none.title", { error: balanceError }),
|
|
1036
|
+
children: t("balance.none")
|
|
1037
|
+
}, "balance-error")
|
|
1038
|
+
: balanceSetup
|
|
1039
|
+
? react_jsx_runtime.jsx("span", {
|
|
1040
|
+
className: "dpu-balance-setup",
|
|
1041
|
+
role: "button",
|
|
1042
|
+
tabIndex: 0,
|
|
1043
|
+
title: balanceSetupTitle,
|
|
1044
|
+
onClick: activateBalanceSetup,
|
|
1045
|
+
onKeyDown: balanceSetupKeyDown,
|
|
1046
|
+
children: t("balance.setup")
|
|
1047
|
+
}, "balance-setup")
|
|
1048
|
+
: balanceMissing
|
|
1049
|
+
? react_jsx_runtime.jsx("span", {
|
|
1050
|
+
className: "dpu-provider-balance dpu-muted",
|
|
1051
|
+
title: t("balance.missing.title"),
|
|
1052
|
+
children: t("balance.missing")
|
|
1053
|
+
}, "balance-missing")
|
|
1054
|
+
: null;
|
|
1055
|
+
const deltaChip = showDelta
|
|
1056
|
+
? react_jsx_runtime.jsx("span", {
|
|
1057
|
+
className: "dpu-balance-float " + (delta.amount < 0 ? "dpu-down-text" : "dpu-up-text"),
|
|
1058
|
+
title: balanceTitle,
|
|
1059
|
+
children: formatDeltaShort(delta)
|
|
1060
|
+
}, "delta")
|
|
1061
|
+
: null;
|
|
1062
|
+
|
|
1063
|
+
return react_jsx_runtime.jsx("div", {
|
|
1064
|
+
className: "dpu-provider",
|
|
1065
|
+
children: [
|
|
1066
|
+
react_jsx_runtime.jsx("button", {
|
|
1067
|
+
type: "button",
|
|
1068
|
+
className: "dpu-provider-head",
|
|
1069
|
+
onClick: () => toggleProvider(provider.id),
|
|
1070
|
+
title: providerTitle(t, provider),
|
|
1071
|
+
"aria-expanded": !isCollapsed,
|
|
1072
|
+
children: [
|
|
1073
|
+
react_jsx_runtime.jsx("span", { className: providerStatusClass(provider) }),
|
|
1074
|
+
react_jsx_runtime.jsx("span", {
|
|
1075
|
+
className: "dpu-provider-name",
|
|
1076
|
+
children: provider.name || t("provider." + provider.id)
|
|
1077
|
+
}),
|
|
1078
|
+
accountBadge,
|
|
1079
|
+
planBadge,
|
|
1080
|
+
fallbackBadge,
|
|
1081
|
+
react_jsx_runtime.jsx("span", {
|
|
1082
|
+
className: "dpu-provider-pct",
|
|
1083
|
+
children: pctText
|
|
1084
|
+
}),
|
|
1085
|
+
balanceBadge,
|
|
1086
|
+
deltaChip
|
|
1087
|
+
]
|
|
1088
|
+
}, "head"),
|
|
1089
|
+
isCollapsed ? null : react_jsx_runtime.jsx("div", { className: "dpu-provider-body", children: body }, "body")
|
|
1090
|
+
]
|
|
1091
|
+
}, provider.id);
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
return react_jsx_runtime.jsx("div", {
|
|
1095
|
+
className: "dpu-widget",
|
|
1096
|
+
children: [
|
|
1097
|
+
react_jsx_runtime.jsx("div", {
|
|
1098
|
+
className: "dpu-head",
|
|
1099
|
+
children: [
|
|
1100
|
+
react_jsx_runtime.jsx("span", { className: "dpu-brand", children: t("widget.brand") }),
|
|
1101
|
+
react_jsx_runtime.jsx("div", {
|
|
1102
|
+
className: "dpu-headright",
|
|
1103
|
+
children: [
|
|
1104
|
+
(data && data.update && data.update.available && data.update.latest
|
|
1105
|
+
? react_jsx_runtime.jsx("a", {
|
|
1106
|
+
className: "dpu-update",
|
|
1107
|
+
href: "https://www.npmjs.com/package/" + PACKAGE_NAME,
|
|
1108
|
+
target: "_blank",
|
|
1109
|
+
rel: "noreferrer",
|
|
1110
|
+
title: t("update.title"),
|
|
1111
|
+
children: t("update.available", { v: data.update.latest })
|
|
1112
|
+
})
|
|
1113
|
+
: null),
|
|
1114
|
+
react_jsx_runtime.jsx("button", {
|
|
1115
|
+
className: "dpu-refresh",
|
|
1116
|
+
onClick: () => setStamp(stamp + 1),
|
|
1117
|
+
children: t("refresh")
|
|
1118
|
+
}),
|
|
1119
|
+
react_jsx_runtime.jsx("button", {
|
|
1120
|
+
className: "dpu-settings",
|
|
1121
|
+
onClick: openPluginSettings,
|
|
1122
|
+
title: t("settings.title"),
|
|
1123
|
+
children: t("settings")
|
|
1124
|
+
})
|
|
1125
|
+
]
|
|
1126
|
+
})
|
|
1127
|
+
]
|
|
1128
|
+
}),
|
|
1129
|
+
autoUpdateNotice(t, data && data.autoUpdate) === "" ? null : react_jsx_runtime.jsx("div", {
|
|
1130
|
+
className: "dpu-autoupdate",
|
|
1131
|
+
title: autoUpdateTitle(data && data.autoUpdate),
|
|
1132
|
+
children: autoUpdateNotice(t, data && data.autoUpdate)
|
|
1133
|
+
}),
|
|
1134
|
+
sections.length > 0 ? react_jsx_runtime.jsx("div", { className: "dpu-providers", children: sections }) : null,
|
|
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,
|
|
1144
|
+
!error && data === null ? react_jsx_runtime.jsx("div", { className: "dpu-ok", children: t("loading") }) : null,
|
|
1145
|
+
data !== null && providers.length === 0
|
|
1146
|
+
? react_jsx_runtime.jsx("div", { className: "dpu-err", children: t("noProviders") })
|
|
1147
|
+
: null
|
|
1148
|
+
]
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// ── settings page (Plugins panel → this bundle) ───────────────────
|
|
1153
|
+
//
|
|
1154
|
+
// `plugins.bundle.config` is a KEYED slot: the plugin manager asks a
|
|
1155
|
+
// bundle for its configuration page by PACKAGE NAME and renders it above
|
|
1156
|
+
// the bundle's rows. The slot component branches on `props.view` before
|
|
1157
|
+
// any hook: `summary` is a plain string and `page` mounts SettingsPage.
|
|
1158
|
+
//
|
|
1159
|
+
// The form exposes the knobs touched most often only: one enable flag
|
|
1160
|
+
// and one credential ref per provider, plus the general options. End-
|
|
1161
|
+
// points, regions, CLI auth files and the console credential stay in the
|
|
1162
|
+
// auto-generated "Plugin configuration" form (they keep their defaults)
|
|
1163
|
+
// so this page never scrolls.
|
|
1164
|
+
const SETTINGS_PATH = "/provider-usage/settings";
|
|
1165
|
+
/** Same-origin usage snapshot the settings page renders live. */
|
|
1166
|
+
const USAGE_PATH = "/provider-usage/usage";
|
|
1167
|
+
const PROVIDER_FORMS = [
|
|
1168
|
+
{
|
|
1169
|
+
id: "opencode-go",
|
|
1170
|
+
nameKey: "provider.opencode-go",
|
|
1171
|
+
credentialKey: "opencodeGoApiKeyEnv",
|
|
1172
|
+
credentialDefault: "OPENCODE_GO_API_KEY",
|
|
1173
|
+
enabledKey: "opencodeGoEnabled",
|
|
1174
|
+
console: true
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
id: "command-code",
|
|
1178
|
+
nameKey: "provider.command-code",
|
|
1179
|
+
credentialKey: "commandCodeApiKeyEnv",
|
|
1180
|
+
credentialDefault: "COMMANDCODE_API_KEY",
|
|
1181
|
+
enabledKey: "commandCodeEnabled"
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
id: "glm",
|
|
1185
|
+
nameKey: "provider.glm",
|
|
1186
|
+
credentialKey: "glmApiKeyEnv",
|
|
1187
|
+
credentialDefault: "ZAI_API_KEY",
|
|
1188
|
+
enabledKey: "glmEnabled",
|
|
1189
|
+
inviteUrl: GLM_INVITE_URL
|
|
1190
|
+
}
|
|
1191
|
+
];
|
|
1192
|
+
|
|
1193
|
+
function emptyForm() {
|
|
1194
|
+
return {
|
|
1195
|
+
opencodeGoEnabled: true,
|
|
1196
|
+
opencodeGoApiKeyEnv: "OPENCODE_GO_API_KEY",
|
|
1197
|
+
commandCodeEnabled: true,
|
|
1198
|
+
commandCodeApiKeyEnv: "COMMANDCODE_API_KEY",
|
|
1199
|
+
glmEnabled: true,
|
|
1200
|
+
glmApiKeyEnv: "ZAI_API_KEY",
|
|
1201
|
+
providerOrder: PROVIDER_FORMS.map((definition) => definition.id)
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/** The known provider ids, canonically ordered (the form default). */
|
|
1206
|
+
function canonicalProviderOrder() {
|
|
1207
|
+
return PROVIDER_FORMS.map((definition) => definition.id);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** Fix up a saved order into a full permutation of the known ids. */
|
|
1211
|
+
function providerOrderFromForm(value) {
|
|
1212
|
+
const order = [];
|
|
1213
|
+
if (Array.isArray(value)) {
|
|
1214
|
+
for (const id of value) {
|
|
1215
|
+
if (typeof id === "string" && !order.includes(id) && canonicalProviderOrder().includes(id)) order.push(id);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
for (const id of canonicalProviderOrder()) {
|
|
1219
|
+
if (!order.includes(id)) order.push(id);
|
|
1220
|
+
}
|
|
1221
|
+
return order;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/** Field-level dirty check: reference equality, or per-member for the order array. */
|
|
1225
|
+
function fieldEquals(a, b) {
|
|
1226
|
+
if (a === b) return true;
|
|
1227
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1228
|
+
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
1229
|
+
}
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function formFromValue(value) {
|
|
1234
|
+
const form = emptyForm();
|
|
1235
|
+
if (value !== null && typeof value === "object") {
|
|
1236
|
+
for (const key of Object.keys(form)) {
|
|
1237
|
+
if (value[key] !== undefined) form[key] = value[key];
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
return form;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Call one of this plugin's routes. */
|
|
1244
|
+
async function callApi(method, path, body) {
|
|
1245
|
+
const init = { method: method, cache: "no-store", credentials: "same-origin" };
|
|
1246
|
+
if (body !== undefined) {
|
|
1247
|
+
init.headers = { "content-type": "application/json" };
|
|
1248
|
+
init.body = JSON.stringify(body);
|
|
1249
|
+
}
|
|
1250
|
+
const res = await fetch(path, init);
|
|
1251
|
+
let payload = null;
|
|
1252
|
+
try {
|
|
1253
|
+
payload = await res.json();
|
|
1254
|
+
} catch (_error) {
|
|
1255
|
+
payload = null;
|
|
1256
|
+
}
|
|
1257
|
+
return { status: res.status, payload: payload };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Inline styles on the DSH design tokens. Keeping them inline means the
|
|
1261
|
+
// page needs no extra CSS rule to be picked up.
|
|
1262
|
+
const PS = {
|
|
1263
|
+
wrap: { display: "flex", flexDirection: "column", gap: 12, padding: "4px 0 8px" },
|
|
1264
|
+
card: {
|
|
1265
|
+
background: "var(--dsw-alias-bg-layer-3, #16181d)",
|
|
1266
|
+
border: "1px solid var(--dsw-alias-border-l2, #2a2d34)",
|
|
1267
|
+
borderRadius: 10,
|
|
1268
|
+
padding: "14px 16px",
|
|
1269
|
+
display: "flex",
|
|
1270
|
+
flexDirection: "column",
|
|
1271
|
+
gap: 10
|
|
1272
|
+
},
|
|
1273
|
+
title: { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-label-primary, #e8e8e8)" },
|
|
1274
|
+
hint: { margin: 0, fontSize: 12, lineHeight: 1.6, color: "var(--dsw-alias-label-tertiary, #8a8f98)" },
|
|
1275
|
+
providerRow: {
|
|
1276
|
+
display: "flex",
|
|
1277
|
+
flexDirection: "column",
|
|
1278
|
+
gap: 8,
|
|
1279
|
+
padding: "10px 12px",
|
|
1280
|
+
borderRadius: 8,
|
|
1281
|
+
border: "1px solid var(--dsw-alias-border-l2, #2a2d34)"
|
|
1282
|
+
},
|
|
1283
|
+
providerHead: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 },
|
|
1284
|
+
providerName: { fontSize: 13, fontWeight: 600, color: "var(--dsw-alias-label-primary, #e8e8e8)" },
|
|
1285
|
+
status: { fontSize: 11, lineHeight: 1.5, color: "var(--dsw-alias-label-tertiary, #8a8f98)", overflowWrap: "anywhere" },
|
|
1286
|
+
field: { display: "flex", flexDirection: "column", gap: 4, minWidth: 0 },
|
|
1287
|
+
fieldLabel: { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #8a8f98)" },
|
|
1288
|
+
input: {
|
|
1289
|
+
font: "inherit",
|
|
1290
|
+
fontSize: 12,
|
|
1291
|
+
color: "var(--dsw-alias-label-primary, #e8e8e8)",
|
|
1292
|
+
background: "var(--dsw-alias-bg-layer-2, #101216)",
|
|
1293
|
+
border: "1px solid var(--dsw-alias-border-l2, #2a2d34)",
|
|
1294
|
+
borderRadius: 7,
|
|
1295
|
+
padding: "6px 8px",
|
|
1296
|
+
minWidth: 0
|
|
1297
|
+
},
|
|
1298
|
+
row: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" },
|
|
1299
|
+
inviteRow: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", minWidth: 0 },
|
|
1300
|
+
inviteLink: {
|
|
1301
|
+
fontSize: 12,
|
|
1302
|
+
lineHeight: 1.5,
|
|
1303
|
+
color: "var(--dsw-alias-state-info-primary, #6cb6ff)",
|
|
1304
|
+
textDecoration: "none",
|
|
1305
|
+
overflowWrap: "anywhere"
|
|
1306
|
+
},
|
|
1307
|
+
checkbox: { display: "flex", alignItems: "center", gap: 6, fontSize: 12, color: "var(--dsw-alias-label-secondary, #a8adb8)" },
|
|
1308
|
+
ok: { margin: 0, fontSize: 12, color: "var(--dsw-alias-state-success-primary, #5dbb76)" },
|
|
1309
|
+
warn: { margin: 0, fontSize: 12, color: "var(--dsw-alias-state-warn-primary, #d6a64b)" },
|
|
1310
|
+
err: { margin: 0, fontSize: 12, color: "var(--dsw-alias-state-error-primary, #e5484d)" },
|
|
1311
|
+
info: { margin: 0, fontSize: 12, color: "var(--dsw-alias-label-tertiary, #8a8f98)" },
|
|
1312
|
+
button: {
|
|
1313
|
+
font: "inherit",
|
|
1314
|
+
cursor: "pointer",
|
|
1315
|
+
background: "transparent",
|
|
1316
|
+
color: "var(--dsw-alias-label-secondary, #a8adb8)",
|
|
1317
|
+
border: "1px solid var(--dsw-alias-border-l2, #2a2d34)",
|
|
1318
|
+
borderRadius: 8,
|
|
1319
|
+
padding: "6px 14px",
|
|
1320
|
+
fontSize: 12
|
|
1321
|
+
},
|
|
1322
|
+
buttonPrimary: {
|
|
1323
|
+
font: "inherit",
|
|
1324
|
+
cursor: "pointer",
|
|
1325
|
+
background: "var(--dsw-alias-label-primary, #0f1115)",
|
|
1326
|
+
color: "var(--dsw-alias-bg-layer-3, #ffffff)",
|
|
1327
|
+
border: "1px solid transparent",
|
|
1328
|
+
borderRadius: 8,
|
|
1329
|
+
padding: "7px 16px",
|
|
1330
|
+
fontSize: 12,
|
|
1331
|
+
fontWeight: 500
|
|
1332
|
+
},
|
|
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
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
function Field(props) {
|
|
1368
|
+
return react_jsx_runtime.jsx("label", {
|
|
1369
|
+
style: PS.field,
|
|
1370
|
+
children: [
|
|
1371
|
+
react_jsx_runtime.jsx("span", { style: PS.fieldLabel, children: props.label }),
|
|
1372
|
+
props.children
|
|
1373
|
+
]
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function StatusLine(props) {
|
|
1378
|
+
return react_jsx_runtime.jsx("div", {
|
|
1379
|
+
style: props.error ? PS.err : PS.status,
|
|
1380
|
+
children: props.children
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Copy a short text, preferring the async clipboard and falling back to
|
|
1386
|
+
* a hidden selection for shells that only ship `execCommand`. Returns a
|
|
1387
|
+
* boolean so the caller can show a failure hint instead of a lie.
|
|
1388
|
+
*/
|
|
1389
|
+
async function copyText(text) {
|
|
1390
|
+
try {
|
|
1391
|
+
if (typeof navigator !== "undefined" && navigator.clipboard && typeof navigator.clipboard.writeText === "function") {
|
|
1392
|
+
await navigator.clipboard.writeText(text);
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
} catch (_error) {
|
|
1396
|
+
// fall through to the selection fallback
|
|
1397
|
+
}
|
|
1398
|
+
try {
|
|
1399
|
+
const area = document.createElement("textarea");
|
|
1400
|
+
area.value = text;
|
|
1401
|
+
area.setAttribute("readonly", "");
|
|
1402
|
+
area.style.position = "fixed";
|
|
1403
|
+
area.style.top = "-1000px";
|
|
1404
|
+
area.style.opacity = "0";
|
|
1405
|
+
document.body.appendChild(area);
|
|
1406
|
+
area.select();
|
|
1407
|
+
const ok = document.execCommand("copy");
|
|
1408
|
+
document.body.removeChild(area);
|
|
1409
|
+
return ok === true;
|
|
1410
|
+
} catch (_error) {
|
|
1411
|
+
return false;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
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.
|
|
1421
|
+
*/
|
|
1422
|
+
function CopyButton(props) {
|
|
1423
|
+
const t = props.t;
|
|
1424
|
+
const [status, setStatus] = react.useState("idle");
|
|
1425
|
+
const timer = react.useRef(null);
|
|
1426
|
+
react.useEffect(() => () => {
|
|
1427
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
1428
|
+
}, []);
|
|
1429
|
+
const copy = async () => {
|
|
1430
|
+
const ok = await copyText(props.value);
|
|
1431
|
+
setStatus(ok ? "copied" : "failed");
|
|
1432
|
+
if (timer.current !== null) clearTimeout(timer.current);
|
|
1433
|
+
timer.current = setTimeout(() => setStatus("idle"), 1800);
|
|
1434
|
+
};
|
|
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;
|
|
1462
|
+
return react_jsx_runtime.jsx("div", {
|
|
1463
|
+
style: PS.inviteRow,
|
|
1464
|
+
children: [
|
|
1465
|
+
react_jsx_runtime.jsx("span", { style: PS.fieldLabel, children: t("cfg.invite") }),
|
|
1466
|
+
react_jsx_runtime.jsx("a", {
|
|
1467
|
+
style: PS.inviteLink,
|
|
1468
|
+
href: props.url,
|
|
1469
|
+
target: "_blank",
|
|
1470
|
+
rel: "noreferrer",
|
|
1471
|
+
children: props.url
|
|
1472
|
+
}),
|
|
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
|
|
1673
|
+
]
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/**
|
|
1678
|
+
* The configuration page itself. Mounted only for `view: "page"`.
|
|
1679
|
+
*
|
|
1680
|
+
* Mount → GET /provider-usage/settings → `phase: "ready"` with the
|
|
1681
|
+
* redacted `value` and the non-network `detection` report. Save POSTs
|
|
1682
|
+
* only the changed fields; 409 shows a reload hint, 503 explains the
|
|
1683
|
+
* missing settings descriptor.
|
|
1684
|
+
*/
|
|
1685
|
+
function SettingsPage(props) {
|
|
1686
|
+
const t = translateFor(props);
|
|
1687
|
+
const [state, setState] = react.useState({ phase: "loading", revision: 0, value: {}, detection: null, error: null });
|
|
1688
|
+
const [form, setForm] = react.useState(emptyForm);
|
|
1689
|
+
const [busy, setBusy] = react.useState({ save: false, reload: false });
|
|
1690
|
+
const [message, setMessage] = react.useState(null);
|
|
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 });
|
|
1697
|
+
const tRef = react.useRef(t);
|
|
1698
|
+
tRef.current = t;
|
|
1699
|
+
// Id of the provider card currently being dragged (drag reordering).
|
|
1700
|
+
const dragProviderRef = react.useRef(null);
|
|
1701
|
+
|
|
1702
|
+
const reload = react.useCallback(async (announce) => {
|
|
1703
|
+
const tt = tRef.current;
|
|
1704
|
+
try {
|
|
1705
|
+
const res = await callApi("GET", SETTINGS_PATH);
|
|
1706
|
+
const payload = res.payload;
|
|
1707
|
+
if (res.status === 200 && payload !== null) {
|
|
1708
|
+
const value = payload.value && typeof payload.value === "object" ? payload.value : {};
|
|
1709
|
+
setState({
|
|
1710
|
+
phase: "ready",
|
|
1711
|
+
revision: typeof payload.revision === "number" ? payload.revision : 0,
|
|
1712
|
+
value: value,
|
|
1713
|
+
detection: payload.detection && typeof payload.detection === "object" ? payload.detection : null,
|
|
1714
|
+
error: null
|
|
1715
|
+
});
|
|
1716
|
+
setForm(formFromValue(value));
|
|
1717
|
+
setMessage(announce === true ? { kind: "ok", text: tt("cfg.reloaded") } : null);
|
|
1718
|
+
return true;
|
|
1719
|
+
}
|
|
1720
|
+
const detail = (payload && (payload.error || payload.hint)) || "";
|
|
1721
|
+
setState((prev) => ({
|
|
1722
|
+
...prev,
|
|
1723
|
+
phase: "error",
|
|
1724
|
+
error: (res.status === 503 ? tt("cfg.unsupported") : tt("cfg.loadFailed", { error: "HTTP " + res.status })) + (detail === "" ? "" : " [" + detail + "]")
|
|
1725
|
+
}));
|
|
1726
|
+
return false;
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
setState((prev) => ({ ...prev, phase: "error", error: tt("cfg.loadFailed", { error: (error && error.message) || String(error) }) }));
|
|
1729
|
+
return false;
|
|
1730
|
+
}
|
|
1731
|
+
}, []);
|
|
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
|
+
|
|
1754
|
+
react.useEffect(() => {
|
|
1755
|
+
reload(false);
|
|
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]);
|
|
1777
|
+
|
|
1778
|
+
const update = (key, value) => {
|
|
1779
|
+
setForm((prev) => ({ ...prev, [key]: value }));
|
|
1780
|
+
setMessage(null);
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
const save = async () => {
|
|
1784
|
+
if (busy.save) return;
|
|
1785
|
+
const tt = tRef.current;
|
|
1786
|
+
const server = formFromValue(state.value || {});
|
|
1787
|
+
const patch = {};
|
|
1788
|
+
for (const key of Object.keys(form)) {
|
|
1789
|
+
if (!fieldEquals(form[key], server[key])) patch[key] = form[key];
|
|
1790
|
+
}
|
|
1791
|
+
if (Object.keys(patch).length === 0) {
|
|
1792
|
+
setMessage({ kind: "info", text: tt("cfg.noChanges") });
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
setBusy((b) => ({ ...b, save: true }));
|
|
1796
|
+
setMessage(null);
|
|
1797
|
+
try {
|
|
1798
|
+
const res = await callApi("POST", SETTINGS_PATH, { patch: patch, expectedRevision: state.revision });
|
|
1799
|
+
const payload = res.payload;
|
|
1800
|
+
if (res.status === 200 && payload !== null) {
|
|
1801
|
+
await reload(false);
|
|
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);
|
|
1807
|
+
setMessage({ kind: "ok", text: tt("cfg.saved") });
|
|
1808
|
+
} else if (res.status === 409) {
|
|
1809
|
+
setMessage({ kind: "warn", text: tt("cfg.conflict") });
|
|
1810
|
+
} else {
|
|
1811
|
+
setMessage({ kind: "err", text: tt("cfg.saveFailed", { error: (payload && payload.error) || ("HTTP " + res.status) }) });
|
|
1812
|
+
}
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
setMessage({ kind: "err", text: tt("cfg.saveFailed", { error: (error && error.message) || String(error) }) });
|
|
1815
|
+
} finally {
|
|
1816
|
+
setBusy((b) => ({ ...b, save: false }));
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
|
|
1820
|
+
const onReload = async () => {
|
|
1821
|
+
if (busy.reload) return;
|
|
1822
|
+
setBusy((b) => ({ ...b, reload: true }));
|
|
1823
|
+
try {
|
|
1824
|
+
await reload(true);
|
|
1825
|
+
void loadUsage(false);
|
|
1826
|
+
} finally {
|
|
1827
|
+
setBusy((b) => ({ ...b, reload: false }));
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
if (state.phase === "loading") {
|
|
1832
|
+
return react_jsx_runtime.jsx("div", { style: PS.wrap, children: react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.loading") }) });
|
|
1833
|
+
}
|
|
1834
|
+
if (state.phase === "error") {
|
|
1835
|
+
return react_jsx_runtime.jsx("div", {
|
|
1836
|
+
style: PS.wrap,
|
|
1837
|
+
children: [
|
|
1838
|
+
react_jsx_runtime.jsx("p", { style: PS.err, children: state.error }),
|
|
1839
|
+
react_jsx_runtime.jsx("div", {
|
|
1840
|
+
style: PS.row,
|
|
1841
|
+
children: react_jsx_runtime.jsx("button", {
|
|
1842
|
+
style: busy.reload ? { ...PS.button, ...PS.buttonDisabled } : PS.button,
|
|
1843
|
+
disabled: busy.reload,
|
|
1844
|
+
onClick: onReload,
|
|
1845
|
+
children: t("cfg.retry")
|
|
1846
|
+
})
|
|
1847
|
+
})
|
|
1848
|
+
]
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
const detection = state.detection && typeof state.detection === "object" ? state.detection : {};
|
|
1853
|
+
const detected = detection.providers && typeof detection.providers === "object" ? detection.providers : {};
|
|
1854
|
+
const disabled = busy.save || busy.reload;
|
|
1855
|
+
const server = formFromValue(state.value || {});
|
|
1856
|
+
const dirty = Object.keys(form).some((key) => !fieldEquals(form[key], server[key]));
|
|
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
|
+
|
|
1880
|
+
// Cards render in the saved order. Reordering works three ways, so a
|
|
1881
|
+
// shell without HTML5 drag (touch, Firefox proton, embedded webviews)
|
|
1882
|
+
// always has a path: drag a card header onto another card, drop it
|
|
1883
|
+
// anywhere over a card, or tap the header's ▲/▼ buttons. The order is
|
|
1884
|
+
// written into `providerOrder` and saved with the next settings save.
|
|
1885
|
+
const orderedDefinitions = providerOrderFromForm(form.providerOrder)
|
|
1886
|
+
.map((id) => PROVIDER_FORMS.find((definition) => definition.id === id))
|
|
1887
|
+
.filter(Boolean);
|
|
1888
|
+
const currentOrder = orderedDefinitions.map((definition) => definition.id);
|
|
1889
|
+
const moveProvider = (fromId, toId) => {
|
|
1890
|
+
if (fromId === null || fromId === toId) return;
|
|
1891
|
+
const order = providerOrderFromForm(form.providerOrder);
|
|
1892
|
+
const from = order.indexOf(fromId);
|
|
1893
|
+
const to = order.indexOf(toId);
|
|
1894
|
+
if (from === -1 || to === -1) return;
|
|
1895
|
+
order.splice(to, 0, order.splice(from, 1)[0]);
|
|
1896
|
+
update("providerOrder", order);
|
|
1897
|
+
};
|
|
1898
|
+
const shiftProvider = (id, delta) => {
|
|
1899
|
+
const order = providerOrderFromForm(form.providerOrder);
|
|
1900
|
+
const from = order.indexOf(id);
|
|
1901
|
+
if (from === -1) return;
|
|
1902
|
+
const to = Math.max(0, Math.min(order.length - 1, from + delta));
|
|
1903
|
+
if (to === from) return;
|
|
1904
|
+
order.splice(to, 0, order.splice(from, 1)[0]);
|
|
1905
|
+
update("providerOrder", order);
|
|
1906
|
+
};
|
|
1907
|
+
/** Card-level drop zone: accepts a drop anywhere over the card. */
|
|
1908
|
+
const dropProps = (definition) => ({
|
|
1909
|
+
onDragOver: (event) => {
|
|
1910
|
+
try { event.preventDefault(); } catch (_error) {
|
|
1911
|
+
// dragging is best-effort on older shells
|
|
1912
|
+
}
|
|
1913
|
+
},
|
|
1914
|
+
onDrop: (event) => {
|
|
1915
|
+
try {
|
|
1916
|
+
event.preventDefault();
|
|
1917
|
+
// `text/plain` carries the id for engines that need payload.
|
|
1918
|
+
const fromId = event.dataTransfer && typeof event.dataTransfer.getData === "function"
|
|
1919
|
+
? event.dataTransfer.getData("text/plain")
|
|
1920
|
+
: "";
|
|
1921
|
+
moveProvider(fromId || dragProviderRef.current, definition.id);
|
|
1922
|
+
} catch (_error) {
|
|
1923
|
+
moveProvider(dragProviderRef.current, definition.id);
|
|
1924
|
+
}
|
|
1925
|
+
dragProviderRef.current = null;
|
|
1926
|
+
setDraggingId(null);
|
|
1927
|
+
}
|
|
1928
|
+
});
|
|
1929
|
+
/** Header drag handle: initiates the reorder drag. */
|
|
1930
|
+
const dragHandleProps = (definition) => ({
|
|
1931
|
+
draggable: !disabled,
|
|
1932
|
+
onDragStart: (event) => {
|
|
1933
|
+
dragProviderRef.current = definition.id;
|
|
1934
|
+
setDraggingId(definition.id);
|
|
1935
|
+
try {
|
|
1936
|
+
// Firefox refuses to start a drag without payload data.
|
|
1937
|
+
event.dataTransfer.effectAllowed = "move";
|
|
1938
|
+
event.dataTransfer.setData("text/plain", definition.id);
|
|
1939
|
+
} catch (_error) {
|
|
1940
|
+
// engines without dataTransfer still reorder via the drop target
|
|
1941
|
+
}
|
|
1942
|
+
},
|
|
1943
|
+
onDragEnd: () => {
|
|
1944
|
+
dragProviderRef.current = null;
|
|
1945
|
+
setDraggingId(null);
|
|
1946
|
+
}
|
|
1947
|
+
});
|
|
1948
|
+
/** Small ghost button that shifts one provider up/down. */
|
|
1949
|
+
const shiftButton = (definition, delta, glyph, titleKey, blocked) => react_jsx_runtime.jsx("button", {
|
|
1950
|
+
type: "button",
|
|
1951
|
+
"aria-label": t(titleKey),
|
|
1952
|
+
title: t(titleKey),
|
|
1953
|
+
disabled: disabled || blocked,
|
|
1954
|
+
onClick: () => shiftProvider(definition.id, delta),
|
|
1955
|
+
style: {
|
|
1956
|
+
border: "none",
|
|
1957
|
+
background: "transparent",
|
|
1958
|
+
padding: "0 3px",
|
|
1959
|
+
fontSize: "12px",
|
|
1960
|
+
lineHeight: "16px",
|
|
1961
|
+
cursor: blocked || disabled ? "default" : "pointer",
|
|
1962
|
+
color: blocked || disabled ? "var(--dsw-alias-label-tertiary, #8a8f98)" : "var(--dsw-alias-label-secondary, #a8adb8)"
|
|
1963
|
+
},
|
|
1964
|
+
children: glyph
|
|
1965
|
+
}, titleKey);
|
|
1966
|
+
const providerCards = orderedDefinitions.map((definition) => {
|
|
1967
|
+
const info = detected[definition.id] && typeof detected[definition.id] === "object" ? detected[definition.id] : {};
|
|
1968
|
+
const enabled = form[definition.enabledKey] !== false;
|
|
1969
|
+
const statusLines = [];
|
|
1970
|
+
statusLines.push(info.configured === true ? t("cfg.status.configured") : t("cfg.status.notConfigured"));
|
|
1971
|
+
statusLines.push(enabled ? t("cfg.status.enabled") : t("cfg.status.disabled"));
|
|
1972
|
+
if (typeof info.credentialRef === "string" && info.credentialRef !== "") statusLines.push(info.credentialRef);
|
|
1973
|
+
if (info.account && typeof info.account.name === "string" && info.account.name !== "") {
|
|
1974
|
+
statusLines.push(t("cfg.status.account") + ": " + info.account.name);
|
|
1975
|
+
}
|
|
1976
|
+
if (typeof info.error === "string" && info.error !== "") statusLines.push(info.error);
|
|
1977
|
+
|
|
1978
|
+
const orderIndex = currentOrder.indexOf(definition.id);
|
|
1979
|
+
const children = [
|
|
1980
|
+
react_jsx_runtime.jsx("div", {
|
|
1981
|
+
style: { ...PS.providerHead, cursor: disabled ? undefined : "grab" },
|
|
1982
|
+
title: disabled ? undefined : t("cfg.order.hint"),
|
|
1983
|
+
...dragHandleProps(definition),
|
|
1984
|
+
children: [
|
|
1985
|
+
react_jsx_runtime.jsx("span", { style: PS.providerName, children: t(definition.nameKey) }),
|
|
1986
|
+
shiftButton(definition, -1, "▲", "cfg.move.up", orderIndex <= 0),
|
|
1987
|
+
shiftButton(definition, 1, "▼", "cfg.move.down", orderIndex >= currentOrder.length - 1),
|
|
1988
|
+
react_jsx_runtime.jsx("label", {
|
|
1989
|
+
style: { ...PS.checkbox, marginLeft: "auto" },
|
|
1990
|
+
children: [
|
|
1991
|
+
react_jsx_runtime.jsx("input", {
|
|
1992
|
+
type: "checkbox",
|
|
1993
|
+
checked: enabled,
|
|
1994
|
+
disabled: disabled,
|
|
1995
|
+
onChange: (event) => update(definition.enabledKey, event.target.checked)
|
|
1996
|
+
}),
|
|
1997
|
+
t("cfg.enabled")
|
|
1998
|
+
]
|
|
1999
|
+
})
|
|
2000
|
+
]
|
|
2001
|
+
}),
|
|
2002
|
+
react_jsx_runtime.jsx(Field, {
|
|
2003
|
+
label: t("cfg.credential"),
|
|
2004
|
+
children: react_jsx_runtime.jsx("input", {
|
|
2005
|
+
style: PS.input,
|
|
2006
|
+
type: "text",
|
|
2007
|
+
value: asText(form[definition.credentialKey]),
|
|
2008
|
+
disabled: disabled || !enabled,
|
|
2009
|
+
placeholder: definition.credentialDefault,
|
|
2010
|
+
onChange: (event) => update(definition.credentialKey, event.target.value)
|
|
2011
|
+
})
|
|
2012
|
+
})
|
|
2013
|
+
];
|
|
2014
|
+
|
|
2015
|
+
if (definition.console === true) {
|
|
2016
|
+
const consoleInfo = info.console && typeof info.console === "object" ? info.console : null;
|
|
2017
|
+
// Reading the top-up balance is always on (the host default);
|
|
2018
|
+
// the page only reports whether a credential was found.
|
|
2019
|
+
if (consoleInfo !== null) {
|
|
2020
|
+
const consoleFixable = consoleInfo.reason === "api-key" || consoleInfo.reason === "expired";
|
|
2021
|
+
children.push(react_jsx_runtime.jsx(StatusLine, {
|
|
2022
|
+
error: typeof consoleInfo.error === "string" && consoleInfo.error !== "",
|
|
2023
|
+
children: t("cfg.console.status") + ": " + (consoleInfo.configured === true ? t("cfg.status.configured") : t("cfg.status.notConfigured")) + (consoleInfo.source ? " · " + consoleInfo.source : "")
|
|
2024
|
+
}, "console-status"));
|
|
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"));
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
children.push(react_jsx_runtime.jsx(StatusLine, {
|
|
2042
|
+
error: typeof info.error === "string" && info.error !== "",
|
|
2043
|
+
children: statusLines.join(" · ")
|
|
2044
|
+
}));
|
|
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
|
+
|
|
2059
|
+
if (typeof definition.inviteUrl === "string" && definition.inviteUrl !== "") {
|
|
2060
|
+
children.push(react_jsx_runtime.jsx(InviteLink, { t: t, url: definition.inviteUrl }, "invite"));
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
return react_jsx_runtime.jsx("div", {
|
|
2064
|
+
style: {
|
|
2065
|
+
...PS.providerRow,
|
|
2066
|
+
opacity: draggingId === definition.id ? 0.45 : undefined,
|
|
2067
|
+
transition: "opacity .15s ease"
|
|
2068
|
+
},
|
|
2069
|
+
...dropProps(definition),
|
|
2070
|
+
children: children
|
|
2071
|
+
}, definition.id);
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
const messageStyle = message === null
|
|
2075
|
+
? PS.info
|
|
2076
|
+
: message.kind === "ok" ? PS.ok : message.kind === "warn" ? PS.warn : message.kind === "err" ? PS.err : PS.info;
|
|
2077
|
+
|
|
2078
|
+
return react_jsx_runtime.jsx("div", {
|
|
2079
|
+
style: PS.wrap,
|
|
2080
|
+
children: [
|
|
2081
|
+
react_jsx_runtime.jsx("section", {
|
|
2082
|
+
style: PS.card,
|
|
2083
|
+
children: [
|
|
2084
|
+
react_jsx_runtime.jsx("div", { style: PS.title, children: t("cfg.title") }),
|
|
2085
|
+
react_jsx_runtime.jsx("p", { style: PS.hint, children: t("cfg.lead") }),
|
|
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
|
+
}),
|
|
2107
|
+
react_jsx_runtime.jsx("div", {
|
|
2108
|
+
style: { display: "flex", flexDirection: "column", gap: 8 },
|
|
2109
|
+
children: providerCards
|
|
2110
|
+
})
|
|
2111
|
+
]
|
|
2112
|
+
}),
|
|
2113
|
+
react_jsx_runtime.jsx("section", {
|
|
2114
|
+
style: PS.card,
|
|
2115
|
+
children: [
|
|
2116
|
+
react_jsx_runtime.jsx("div", {
|
|
2117
|
+
style: PS.row,
|
|
2118
|
+
children: [
|
|
2119
|
+
react_jsx_runtime.jsx("button", {
|
|
2120
|
+
style: disabled ? { ...PS.buttonPrimary, ...PS.buttonDisabled } : PS.buttonPrimary,
|
|
2121
|
+
disabled: disabled,
|
|
2122
|
+
onClick: save,
|
|
2123
|
+
children: busy.save ? t("cfg.saving") : t("cfg.save")
|
|
2124
|
+
}),
|
|
2125
|
+
react_jsx_runtime.jsx("button", {
|
|
2126
|
+
style: disabled ? { ...PS.button, ...PS.buttonDisabled } : PS.button,
|
|
2127
|
+
disabled: disabled,
|
|
2128
|
+
onClick: onReload,
|
|
2129
|
+
children: busy.reload ? t("cfg.reloading") : t("cfg.reload")
|
|
2130
|
+
}),
|
|
2131
|
+
dirty ? react_jsx_runtime.jsx("span", { style: PS.warn, children: t("cfg.unsaved") }) : null,
|
|
2132
|
+
react_jsx_runtime.jsx("span", {
|
|
2133
|
+
style: { ...PS.hint, marginLeft: "auto" },
|
|
2134
|
+
children: t("cfg.revision", { n: state.revision })
|
|
2135
|
+
})
|
|
2136
|
+
]
|
|
2137
|
+
}),
|
|
2138
|
+
message === null ? null : react_jsx_runtime.jsx("p", { style: messageStyle, children: message.text })
|
|
2139
|
+
]
|
|
2140
|
+
})
|
|
2141
|
+
]
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
/** One-line description the panel may render as the bundle's summary. */
|
|
2146
|
+
const SUMMARY = "OpenCode GO / Command Code / GLM Code Plan 套餐用量:一个侧边栏组件、同源代理与 /provider-usage 命令。";
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* The `plugins.bundle.config` occupant. The `view` branch is a component
|
|
2150
|
+
* boundary on purpose: SettingsPage opens with hooks, so the summary —
|
|
2151
|
+
* rendered as a plain string by the panel — must not pass through a
|
|
2152
|
+
* hook-bearing parent.
|
|
2153
|
+
*/
|
|
2154
|
+
function BundleConfig(props) {
|
|
2155
|
+
if (props !== null && props !== undefined && props.view === "summary") {
|
|
2156
|
+
try {
|
|
2157
|
+
return translateFor(props)("cfg.summary");
|
|
2158
|
+
} catch (_error) {
|
|
2159
|
+
return SUMMARY;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
return react_jsx_runtime.jsx(SettingsPage, props && typeof props.t === "function" ? { t: props.t } : {});
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// ── cordis plugin entry ──
|
|
2166
|
+
const inject = ["slots", "locale"];
|
|
2167
|
+
|
|
2168
|
+
function apply(ctx) {
|
|
2169
|
+
pluginContext = ctx;
|
|
2170
|
+
ctx.effect(() => () => {
|
|
2171
|
+
if (pluginContext === ctx) pluginContext = null;
|
|
2172
|
+
}, "dsh-multi-provider-usage: context");
|
|
2173
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-multi-provider-usage: dictionaries");
|
|
2174
|
+
const registerWidget = () => ctx.slots.register({
|
|
2175
|
+
name: "sidebar.footer.action",
|
|
2176
|
+
id: PACKAGE_NAME,
|
|
2177
|
+
order: 0,
|
|
2178
|
+
label: "Provider usage",
|
|
2179
|
+
locale: NS
|
|
2180
|
+
}, Widget);
|
|
2181
|
+
const registerConfig = () => ctx.slots.register({
|
|
2182
|
+
name: "plugins.bundle.config",
|
|
2183
|
+
key: CONFIG_KEY,
|
|
2184
|
+
locale: NS
|
|
2185
|
+
}, BundleConfig);
|
|
2186
|
+
// The settings page is additive: a host that predates the Plugins
|
|
2187
|
+
// panel (or a slots service without `inject`) must lose only this
|
|
2188
|
+
// page, never the widget.
|
|
2189
|
+
const registerConfigSafely = () => {
|
|
2190
|
+
try {
|
|
2191
|
+
registerConfig();
|
|
2192
|
+
} catch (_error) {
|
|
2193
|
+
// no plugins.bundle.config slot in this composition
|
|
2194
|
+
}
|
|
2195
|
+
};
|
|
2196
|
+
if (typeof ctx.slots.inject === "function") {
|
|
2197
|
+
ctx.slots.inject("sidebar.footer.action", registerWidget);
|
|
2198
|
+
try {
|
|
2199
|
+
ctx.slots.inject("plugins.bundle.config", registerConfigSafely);
|
|
2200
|
+
} catch (_error) {
|
|
2201
|
+
// older slots service: the widget alone still works
|
|
2202
|
+
}
|
|
2203
|
+
} else {
|
|
2204
|
+
ctx.effect(registerWidget, "dsh-multi-provider-usage: widget registration");
|
|
2205
|
+
ctx.effect(registerConfigSafely, "dsh-multi-provider-usage: settings page registration");
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
exports.apply = apply;
|
|
2210
|
+
exports.inject = inject;
|
|
2211
|
+
exports.CONFIG_KEY = CONFIG_KEY;
|
|
2212
|
+
exports.SUMMARY = SUMMARY;
|
|
2213
|
+
exports.Widget = Widget;
|
|
2214
|
+
exports.BundleConfig = BundleConfig;
|
|
2215
|
+
exports.SettingsPage = SettingsPage;
|
|
2216
|
+
exports.UsageSection = UsageSection;
|
|
2217
|
+
exports.InviteLink = InviteLink;
|
|
2218
|
+
exports.CopyButton = CopyButton;
|
|
2219
|
+
exports.ConsoleGuide = ConsoleGuide;
|
|
2220
|
+
exports.GLM_INVITE_URL = GLM_INVITE_URL;
|
|
2221
|
+
exports.balanceDeltasBetween = balanceDeltasBetween;
|
|
2222
|
+
exports.formatSignedMoney = formatSignedMoney;
|
|
2223
|
+
exports.formatDeltaShort = formatDeltaShort;
|
|
2224
|
+
return module.exports;
|
|
2225
|
+
}
|
|
2226
|
+
});
|