dsh-deepseek-balance-widget 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,29 +2,53 @@
2
2
 
3
3
  [English](README_EN.md) | 中文
4
4
 
5
- 在 dsh Web 侧边栏显示 AI 余额的插件:内置 DeepSeek,可添加 MiMo 等平台,实时展示**余额 / 今日消费 / 今日 Token**,每 30 秒自动刷新。
5
+ 在 dsh Web 侧边栏显示 AI 余额的多平台插件:内置 **DeepSeek**,可添加 **MiMo(小米)** 等平台,每 30 秒自动刷新。
6
6
 
7
7
  ## 功能
8
8
 
9
9
  ![AI 余额侧边栏展示](assets/screenshot.png)
10
10
 
11
- - 侧边栏显示余额 / 今日消费 / 今日 Token,30 秒自动刷新
12
- - 点击弹出详情:累计消费、本月消耗、Tokens
13
- - 多平台 Tab 切换(DeepSeek / MiMo…),可「+ 添加」新平台
11
+ - 侧边栏实时显示余额 / 今日消费 / 今日 Token,30 秒自动刷新,并随当前平台切换
12
+ - 弹窗顶部 **Tab** 切换平台(DeepSeek / MiMo…),侧边栏数值同步更新
13
+ - 弹窗右上角「**+ 添加**」:随时加入 MiMo 等其他 AI 平台
14
+ - 详情字段:余额、累计消费、今日消费、今日 Token、本月消耗(本月消费 / Tokens)
15
+ - 「**AI 帮我配置**」:凭证抓取与写入全交给智能体,无需手动翻 Cookies
16
+ - 弹窗底部显示版本号,有新版时一键自动更新
14
17
 
15
18
  ## 安装
16
19
 
20
+ 需要:已安装 dsh(可用 `dsh web`)。
21
+
17
22
  ```bash
18
23
  dsh plugin --profile web add dsh-deepseek-balance-widget
19
24
  ```
20
25
 
21
- 从 npm 拉取安装,dsh 自动注册,**重启 `dsh web`** 后侧边栏出现余额按钮。
26
+ 从 npm 拉取安装,dsh 自动注册到 `dsh.profile.bundles`,完成后**重启 `dsh web`** 即可。
22
27
 
23
28
  也可以直接对 AI 说:*帮我用 npm 安装 dsh-deepseek-balance-widget 插件。*
24
29
 
25
30
  ## 配置
26
31
 
27
- 打开弹窗 →「+ 添加」→ 选平台 →「AI 帮我配置」→ 把提示语发给智能体,AI 会引导你完成配置并写入本机。
32
+ 插件首次使用会自动创建 DeepSeek 条目,读取你本机的 `DEEPSEEK_API_KEY`。其他平台通过弹窗「+ 添加」加入。两种方式任选:
33
+
34
+ **方式一:弹窗内添加**
35
+ 1. 打开弹窗,点右上角「+ 添加」。
36
+ 2. 选择平台,点「AI 帮我配置」,把提示语发给智能体。
37
+ 3. AI 一步步引导你获取凭证,自动写入本机配置。
38
+
39
+ **方式二:直接对 AI 说**
40
+
41
+ > 帮我配置 dsh-deepseek-balance-widget。
42
+
43
+ AI 会接管全部:问 API Key / 引导获取平台 Cookie → 写入本机 → 提醒重启 `dsh web`。
44
+
45
+ | 平台 | 凭据 | 备注 |
46
+ |---|---|---|
47
+ | DeepSeek | `DEEPSEEK_API_KEY` | 必填,余额 |
48
+ | DeepSeek 用量 | `DEEPSEEK_PLATFORM_TOKEN` | 可选,统计累计消费 / 本月 / 今日 |
49
+ | MiMo(小米) | 登录 Cookie | AI 引导获取 |
50
+
51
+ 凭据保存在你本机的 `~/.dsh/ai-balances.json`,不随插件分发。
28
52
 
29
53
  ## 卸载
30
54
 
@@ -32,6 +56,8 @@ dsh plugin --profile web add dsh-deepseek-balance-widget
32
56
  dsh plugin --profile web rm dsh-deepseek-balance-widget
33
57
  ```
34
58
 
59
+ 或者直接对 AI 说:*帮我用 npm 卸载 dsh-deepseek-balance-widget 插件。*
60
+
35
61
  ## License
36
62
 
37
- [MIT](LICENSE)
63
+ [MIT](LICENSE)
package/README_EN.md CHANGED
@@ -2,18 +2,23 @@
2
2
 
3
3
  English | [中文](README.md)
4
4
 
5
- A plugin that shows AI balances in the dsh web sidebar: DeepSeek built in, add MiMo and more, with live **balance / today spend / today tokens**, auto-refreshed every 30 s.
5
+ A multi-provider AI balance widget for the dsh web sidebar. **DeepSeek** is built in; add **MiMo (Xiaomi)** and others with one click. Auto-refreshes every 30 seconds.
6
6
 
7
7
  ## Features
8
8
 
9
- ![AI balance sidebar](assets/screenshot.png)
9
+ ![AI balance sidebar](assets/screenshot-en.png)
10
10
 
11
- - Sidebar widget: Balance / Today spend / Today tokens, auto-refresh every 30 s
12
- - Click for details: cumulative spend, monthly usage, tokens
13
- - Multi-provider tabs (DeepSeek / MiMo…), "+ Add" to add new providers
11
+ - Sidebar shows Balance / Today spend / Today tokens, auto-refresh every 30 s; values follow the selected provider
12
+ - **Tabs** in the popover switch between providers (DeepSeek / MiMo…), sidebar updates with the tab
13
+ - **"+ Add"** button: add MiMo or any other AI provider at any time
14
+ - Details: balance, cumulative spend, today spend, today tokens, monthly usage (monthly spend / tokens)
15
+ - **"AI 帮我配置"**: credentials are handled by your AI — no digging through cookies by hand
16
+ - Version shown in the popover footer; one-click auto-update when a new version is available
14
17
 
15
18
  ## Install
16
19
 
20
+ Requires: dsh installed (with `dsh web` working).
21
+
17
22
  ```bash
18
23
  dsh plugin --profile web add dsh-deepseek-balance-widget
19
24
  ```
@@ -24,7 +29,26 @@ Or just tell your AI: *Install the dsh-deepseek-balance-widget plugin for me via
24
29
 
25
30
  ## Configuration
26
31
 
27
- Open the popover "+ Add" → pick a provider "AI 帮我配置" → send the copied prompt to your AI; it will guide you through the setup and write the config locally.
32
+ On first use the plugin creates a DeepSeek entry and reads your local `DEEPSEEK_API_KEY`. Other providers are added via "+ Add". Either:
33
+
34
+ **Option 1: Add inside the popover**
35
+ 1. Open the popover and click "+ Add".
36
+ 2. Pick a provider, then click "AI 帮我配置" and send the copied prompt to your AI.
37
+ 3. Your AI walks you through getting the credentials and writes them locally.
38
+
39
+ **Option 2: Just tell your AI**
40
+
41
+ > Configure dsh-deepseek-balance-widget for me.
42
+
43
+ The AI takes care of everything: asks for your API key / guides you through getting the platform cookie → writes the local config → reminds you to **restart `dsh web`**.
44
+
45
+ | Provider | Credential | Notes |
46
+ |---|---|---|
47
+ | DeepSeek | `DEEPSEEK_API_KEY` | Required, balance |
48
+ | DeepSeek usage | `DEEPSEEK_PLATFORM_TOKEN` | Optional, cumulative / monthly / today stats |
49
+ | MiMo (Xiaomi) | Login cookie | Guided by your AI |
50
+
51
+ Credentials are stored locally in `~/.dsh/ai-balances.json`; nothing is shipped with the plugin.
28
52
 
29
53
  ## Uninstall
30
54
 
@@ -32,6 +56,8 @@ Open the popover → "+ Add" → pick a provider → "AI 帮我配置" → sen
32
56
  dsh plugin --profile web rm dsh-deepseek-balance-widget
33
57
  ```
34
58
 
59
+ Or just tell your AI: *Uninstall the dsh-deepseek-balance-widget plugin for me via npm.*
60
+
35
61
  ## License
36
62
 
37
- [MIT](LICENSE)
63
+ [MIT](LICENSE)
package/lib/client.js CHANGED
@@ -19,11 +19,261 @@ window.__ModuleLoader__.load({
19
19
  /** One-shot sync from dsh settings to the local balance store. */
20
20
  const SYNC_ENDPOINT = "/ai-balances-sync";
21
21
  /** Client-side provider metadata (label only; balance logic lives on host). */
22
- const PROVIDER_META = { deepseek: { label: "DeepSeek" }, mimo: { label: "MiMo" } };
22
+ const PROVIDER_META = {
23
+ deepseek: { label: { zh: "DeepSeek", en: "DeepSeek" } },
24
+ mimo: { label: { zh: "MiMo(小米)", en: "MiMo" } }
25
+ };
23
26
  /** Auto-refresh cadence, milliseconds. */
24
27
  const REFRESH_MS = 30000;
25
- /** How long the "update succeeded, restart dsh" message stays visible, milliseconds. */
26
- const UPDATE_SUCCESS_MS = 5000;
28
+ /** LocalStorage key for the UI language preference (no longer used; dsh settings drive language). */
29
+ const LANG_KEY = null;
30
+ /** Supported UI languages. */
31
+ const I18N = {
32
+ zh: {
33
+ entryAriaLabel: "AI 余额(余额 / 今日消费 / 今日Token)",
34
+ balance: "余额",
35
+ todaySpend: "今日消费",
36
+ todayTokens: "今日Token",
37
+ checking: "检查中…",
38
+ checkFailed: "检查失败",
39
+ upToDate: "已是最新",
40
+ updating: "更新中…",
41
+ updateSuccess: "成功,请重启 dsh",
42
+ updateFailed: "更新失败",
43
+ failed: "失败",
44
+ usageUnavailable: "用量接口不可用({0})",
45
+ usageNotConfigured: "未配置 DEEPSEEK_PLATFORM_TOKEN,仅显示余额。对 AI 说「帮我配置用量统计」,AI 会帮你设置(可选)。",
46
+ usageThisMonth: "本月消耗",
47
+ usageDisabled: "未启用",
48
+ periodSpend: "{0}消耗",
49
+ all: "全部",
50
+ thisMonthSpend: "本月消费",
51
+ tokens: "Tokens",
52
+ yi: "亿",
53
+ wan: "万",
54
+ error: "错误",
55
+ cumulativeSpend: "累计消费",
56
+ items: "项",
57
+ mimoFieldLabels: {
58
+ code: "状态码", message: "消息", frozenBalance: "冻结余额", overdraftLimit: "透支额度",
59
+ remainingOverdraftLimit: "剩余透支额度", giftBalance: "赠送余额", cashBalance: "现金余额",
60
+ rechargeBalance: "充值余额", accountBalance: "账户余额", walletBalance: "钱包余额",
61
+ plan: "套餐", expiredAt: "过期时间"
62
+ },
63
+ dailyUsage: "每日消耗",
64
+ date: "日期",
65
+ requests: "请求",
66
+ spend: "消费",
67
+ needSetup: "需设置",
68
+ remove: "移除",
69
+ mimoCookieHint: "MiMo 余额需用浏览器登录 Cookie 查询。请重新在浏览器登录 platform.xiaomimimo.com 后,将 Cookie 更新到 ~/.dsh/ai-balances.json 对应条目中。",
70
+ readFailed: "读取失败:{0}",
71
+ rawPrefix: "原始响应:",
72
+ aiGuide: "AI 帮我配置",
73
+ configMethod: "配置方式",
74
+ aiGuideNote: "点「AI 帮我配置」会复制一句话,发给智能体后它会读取本机教程文件、一步步引导你抓取凭证并写入插件。数据仅存本机,不上传。",
75
+ localKeyNote: "密钥仅保存在本机 ~/.dsh/ai-balances.json,不会上传。",
76
+ platform: "平台",
77
+ mimoOption: "MiMo(小米)",
78
+ deepseekOption: "DeepSeek",
79
+ name: "名称",
80
+ namePlaceholderDeepseek: "例如 DeepSeek",
81
+ namePlaceholderMimo: "例如 MiMo",
82
+ save: "保存",
83
+ cancel: "取消",
84
+ saving: "保存中…",
85
+ aiChatGuideTitle: "让 AI 在聊天里帮你配置",
86
+ step1: "Step 1",
87
+ step2: "Step 2",
88
+ openMimoBalance: "打开 platform.xiaomimimo.com/#/console/balance",
89
+ copyPromptToChat: "复制提示语 → 和智能体对话",
90
+ step1Desc: "点击下方链接打开 MiMo 余额页并登录",
91
+ step2Desc: "复制下方提示语,发送给智能体,AI 会在对话中一步步引导你完成配置",
92
+ manualPasteCookie: "我要手动粘贴 Cookie",
93
+ manualCookieTitle: "手动粘贴 Cookie",
94
+ cookiePlaceholder: "粘贴 Cookie / cURL / 请求头…",
95
+ extractResult: "识别结果:",
96
+ cookieNotFound: "未识别到 Cookie,请粘贴完整 cURL 或请求头",
97
+ backToAiGuide: "返回 AI 引导",
98
+ syncFailed: "同步失败:{0}",
99
+ syncAddedN: "已从 dsh 设置同步 {0} 个平台",
100
+ aiBalance: "AI 余额",
101
+ add: "+ 添加",
102
+ noProviders: "还没有任何 AI 余额,点右上角「+ 添加」。",
103
+ updatedAt: "更新于 ",
104
+ checkUpdate: "检查更新",
105
+ update: "更新",
106
+ fillName: "请填写名称",
107
+ saveFailed: "保存失败:{0}",
108
+ confirmRemove: "确定移除该 AI 余额?",
109
+ pasteCookieHint: "请粘贴 Cookie 或 cURL,AI 会自动提取",
110
+ copied: "已复制",
111
+ copyFailed: "复制失败",
112
+ refresh: "刷新"
113
+ },
114
+ en: {
115
+ entryAriaLabel: "AI Balance (Balance / Today Spend / Today Tokens)",
116
+ balance: "Balance",
117
+ todaySpend: "Today Spend",
118
+ todayTokens: "Today Tokens",
119
+ checking: "Checking…",
120
+ checkFailed: "Check failed",
121
+ upToDate: "Up to date",
122
+ updating: "Updating…",
123
+ updateSuccess: "Success, please restart dsh",
124
+ updateFailed: "Update failed",
125
+ failed: "Failed",
126
+ usageUnavailable: "Usage API unavailable ({0})",
127
+ usageNotConfigured: "DEEPSEEK_PLATFORM_TOKEN not configured, showing balance only. Tell the AI 'help me configure usage stats' to set it up (optional).",
128
+ usageThisMonth: "This Month",
129
+ usageDisabled: "Disabled",
130
+ periodSpend: "{0} Usage",
131
+ all: "All",
132
+ thisMonthSpend: "This Month Spend",
133
+ tokens: "Tokens",
134
+ yi: "B",
135
+ wan: "M",
136
+ error: "Error",
137
+ cumulativeSpend: "Cumulative Spend",
138
+ items: "items",
139
+ mimoFieldLabels: {
140
+ code: "Code", message: "Message", frozenBalance: "Frozen Balance", overdraftLimit: "Overdraft Limit",
141
+ remainingOverdraftLimit: "Remaining Overdraft", giftBalance: "Gift Balance", cashBalance: "Cash Balance",
142
+ rechargeBalance: "Recharge Balance", accountBalance: "Account Balance", walletBalance: "Wallet Balance",
143
+ plan: "Plan", expiredAt: "Expires At"
144
+ },
145
+ dailyUsage: "Daily Usage",
146
+ date: "Date",
147
+ requests: "Requests",
148
+ spend: "Spend",
149
+ needSetup: "Setup",
150
+ remove: "Remove",
151
+ mimoCookieHint: "MiMo balance requires a browser login Cookie. Please log in to platform.xiaomimimo.com again, then update the Cookie in ~/.dsh/ai-balances.json.",
152
+ readFailed: "Read failed: {0}",
153
+ rawPrefix: "Raw response: ",
154
+ aiGuide: "AI Configure",
155
+ configMethod: "Configuration",
156
+ aiGuideNote: "Clicking 'AI Configure' copies a prompt. Send it to the AI and it will read the local guide, walk you through capturing credentials, and write them to the plugin. Data stays local.",
157
+ localKeyNote: "Keys are only stored locally in ~/.dsh/ai-balances.json and are never uploaded.",
158
+ platform: "Platform",
159
+ mimoOption: "MiMo (Xiaomi)",
160
+ deepseekOption: "DeepSeek",
161
+ name: "Name",
162
+ namePlaceholderDeepseek: "e.g. DeepSeek",
163
+ namePlaceholderMimo: "e.g. MiMo",
164
+ save: "Save",
165
+ cancel: "Cancel",
166
+ saving: "Saving…",
167
+ aiChatGuideTitle: "Let the AI configure it in chat",
168
+ step1: "Step 1",
169
+ step2: "Step 2",
170
+ openMimoBalance: "Open platform.xiaomimimo.com/#/console/balance",
171
+ copyPromptToChat: "Copy prompt → chat with AI",
172
+ step1Desc: "Click the link below to open the MiMo balance page and log in",
173
+ step2Desc: "Copy the prompt below and send it to the AI. It will guide you step by step.",
174
+ manualPasteCookie: "I'll paste the Cookie manually",
175
+ manualCookieTitle: "Paste Cookie manually",
176
+ cookiePlaceholder: "Paste Cookie / cURL / headers…",
177
+ extractResult: "Extracted: ",
178
+ cookieNotFound: "No Cookie found. Paste the full cURL or headers.",
179
+ backToAiGuide: "Back to AI guide",
180
+ syncFailed: "Sync failed: {0}",
181
+ syncAddedN: "Synced {0} providers from dsh settings",
182
+ aiBalance: "AI Balance",
183
+ add: "+ Add",
184
+ noProviders: "No AI balances yet. Click '+ Add' in the top right.",
185
+ updatedAt: "Updated ",
186
+ checkUpdate: "Check update",
187
+ update: "Update",
188
+ fillName: "Please enter a name",
189
+ saveFailed: "Save failed: {0}",
190
+ confirmRemove: "Remove this AI balance?",
191
+ pasteCookieHint: "Paste a Cookie or cURL; the AI will extract it",
192
+ copied: "Copied",
193
+ copyFailed: "Copy failed",
194
+ refresh: "Refresh"
195
+ }
196
+ };
197
+ /** Authoritative source: host route reads ~/.dsh/settings.yaml locale.preference. */
198
+ const LOCALE_ENDPOINT = "/deepseek-balance-locale";
199
+ async function fetchDshLang() {
200
+ try {
201
+ const res = await fetch(LOCALE_ENDPOINT, { method: "GET", headers: { Accept: "application/json" } });
202
+ if (!res.ok) return null;
203
+ const env = await res.json();
204
+ if (env && (env.lang === "en" || env.lang === "zh")) return env.lang;
205
+ } catch { /* ignore */ }
206
+ return null;
207
+ }
208
+ /** Fallback: read dsh's <html lang> attribute (only useful if dsh updates it). */
209
+ function detectLangFromHtml() {
210
+ const raw = (document?.documentElement?.lang || "").trim().toLowerCase();
211
+ if (raw.startsWith("en")) return "en";
212
+ if (raw.startsWith("zh")) return "zh";
213
+ return null;
214
+ }
215
+ let currentLang = "zh";
216
+ function applyLangChange(next) {
217
+ if (next !== currentLang) {
218
+ currentLang = next;
219
+ if (typeof remountEntry === "function") remountEntry();
220
+ if (typeof updateEntryLabels === "function") updateEntryLabels();
221
+ if (lastEnvelope !== void 0) renderValue(lastEnvelope, lastUsage);
222
+ if (popRef !== null) renderPopover();
223
+ }
224
+ }
225
+ /** Poll the host route (authoritative settings.yaml) and re-render on change. */
226
+ async function pollDshLang() {
227
+ const next = await fetchDshLang();
228
+ if (next !== null) applyLangChange(next);
229
+ else {
230
+ const fb = detectLangFromHtml();
231
+ if (fb !== null) applyLangChange(fb);
232
+ }
233
+ }
234
+ if (typeof document !== "undefined" && document.documentElement) {
235
+ // 启动时立即问宿主端;之后每 8 秒问一次。
236
+ pollDshLang();
237
+ setInterval(pollDshLang, 8000);
238
+ // dsh 偶尔改 <html lang> 时也能跟上。
239
+ new MutationObserver(() => {
240
+ const fb = detectLangFromHtml();
241
+ if (fb !== null) applyLangChange(fb);
242
+ }).observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
243
+ }
244
+ /** Translate a key, optionally replacing {0}, {1}... placeholders. */
245
+ function t(key, ...args) {
246
+ const dict = I18N[currentLang] || I18N.zh;
247
+ let text = dict[key];
248
+ if (text === void 0) text = I18N.zh[key] ?? key;
249
+ if (args.length && typeof text === "string") {
250
+ text = text.replace(/\{(\d+)\}/g, (_, n) => String(args[Number(n)] ?? ""));
251
+ }
252
+ return text;
253
+ }
254
+ /** Update sidebar entry stat labels to the current language. */
255
+ function updateEntryLabels() {
256
+ // 用 data-stat-key 按语义查找 label,避免位置错乱(即便 dsh 注入了其他 .dshBalanceStatLabel 也不会错位)。
257
+ const entry = document.querySelector("[data-dsh-balance-entry]");
258
+ if (!entry) return;
259
+ const spendLabel = entry.querySelector('[data-stat-key="todaySpend"] .dshBalanceStatLabel');
260
+ const tokenLabel = entry.querySelector('[data-stat-key="todayTokens"] .dshBalanceStatLabel');
261
+ if (spendLabel) spendLabel.textContent = t("todaySpend");
262
+ if (tokenLabel) tokenLabel.textContent = t("todayTokens");
263
+ entry.setAttribute("aria-label", t("entryAriaLabel"));
264
+ }
265
+ /** Remove the entry from DOM; dsh's rootObserver (childList:true) will rebuild it via createEntry(),
266
+ * which calls statHtml with the current currentLang — so the new entry comes up in the right language. */
267
+ function remountEntry() {
268
+ const entry = document.querySelector("[data-dsh-balance-entry]");
269
+ if (!entry) return;
270
+ entry.remove();
271
+ }
272
+ /** How long the "update succeeded, restart dsh" message stays visible, milliseconds.
273
+ * Set to infinite so the message persists until dsh is restarted.
274
+ * dsh restart re-executes client.js, resetting all variables (including updateSuccessUntil),
275
+ * which makes the success message disappear and restores the normal version check button. */
276
+ const UPDATE_SUCCESS_MS = Number.MAX_SAFE_INTEGER;
27
277
  /** Absolute path to the AI-setup guide file the chat AI should read before guiding the user. */
28
278
  const AI_GUIDE_FILE = "D:/deepseek harness内置余额显示功能/main program/dsh-deepseek-balance-widget/AI_BALANCE_SETUP_GUIDE.md";
29
279
  /** Prompt copied to the chat when the user asks the AI to configure MiMo Cookie. */
@@ -74,7 +324,9 @@ window.__ModuleLoader__.load({
74
324
  ".dshBalanceRow{display:flex;justify-content:space-between;gap:12px;padding:3px 0}",
75
325
  ".dshBalanceRow span:first-child{color:#9aa4b2}",
76
326
  ".dshBalanceRow span:last-child{font-variant-numeric:tabular-nums}",
77
- ".dshBalanceFoot{display:flex;justify-content:space-between;align-items:center;margin-top:12px;padding-top:10px;border-top:1px solid rgba(255,255,255,.08);color:#7b8494;font-size:11px}",
327
+ // Footer 内的按钮不许被 flex 压缩成单字符宽度导致文字竖排(英文 label 更长时容易触发)。
328
+ ".dshBalanceFoot .dshBalanceBtn,.dshBalanceFoot .dshBalanceVersionBtn{white-space:nowrap;flex-shrink:0}",
329
+ ".dshBalanceFoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:6px;margin-top:12px;padding-top:10px;border-top:1px solid rgba(255,255,255,.08);color:#7b8494;font-size:11px}",
78
330
  ".dshBalanceBtn{appearance:none;cursor:pointer;background:#2d333b;color:#e6e9ef;border:1px solid rgba(255,255,255,.12);border-radius:7px;padding:4px 10px;font:inherit;font-size:12px}",
79
331
  ".dshBalanceBtn:hover{background:#363d47}",
80
332
  ".dshBalanceVersion{display:flex;align-items:center;gap:6px;font-size:11px;color:#7b8494}",
@@ -194,9 +446,9 @@ window.__ModuleLoader__.load({
194
446
  for (const child of root.children) if (child.tagName === "BUTTON") return child;
195
447
  }
196
448
 
197
- /** One labeled stat column (label above value). */
198
- function statHtml(label, valueHtml) {
199
- return '<span class="dshBalanceStat"><span class="dshBalanceStatLabel">' + label + "</span>" + valueHtml + "</span>";
449
+ /** One labeled stat column (label above value). key 用作 data-stat-key 标记,便于按语义查找。 */
450
+ function statHtml(label, valueHtml, key) {
451
+ return '<span class="dshBalanceStat" data-stat-key="' + key + '"><span class="dshBalanceStatLabel">' + label + "</span>" + valueHtml + "</span>";
200
452
  }
201
453
 
202
454
  /** Build the entry row (a detached button). */
@@ -205,11 +457,11 @@ window.__ModuleLoader__.load({
205
457
  entry.type = "button";
206
458
  entry.dataset.dshBalanceEntry = "";
207
459
  entry.className = "dshBalanceEntry" + (variant === "compact" ? " compact" : "");
208
- entry.setAttribute("aria-label", "AI 余额(余额 / 今日消费 / 今日Token)");
460
+ entry.setAttribute("aria-label", t("entryAriaLabel"));
209
461
  entry.innerHTML =
210
- statHtml('<span class="dshBalanceStatLabel dshBalanceProviderLabel">余额</span>', '<span class="dshBalanceValue" data-state="loading">…</span>') +
211
- statHtml("今日消费", '<span class="dshBalanceToday" data-state="loading">…</span>') +
212
- statHtml("今日Token", '<span class="dshBalanceTodayTok" data-state="loading">…</span>');
462
+ statHtml('<span class="dshBalanceStatLabel dshBalanceProviderLabel">' + t("balance") + "</span>", '<span class="dshBalanceValue" data-state="loading">…</span>', "balance") +
463
+ statHtml(t("todaySpend"), '<span class="dshBalanceToday" data-state="loading">…</span>', "todaySpend") +
464
+ statHtml(t("todayTokens"), '<span class="dshBalanceTodayTok" data-state="loading">…</span>', "todayTokens");
213
465
  return entry;
214
466
  }
215
467
 
@@ -261,31 +513,31 @@ window.__ModuleLoader__.load({
261
513
  async function handleUpdateClick(btn) {
262
514
  btn.disabled = true;
263
515
  const originalText = btn.textContent;
264
- btn.textContent = "检查中…";
516
+ btn.textContent = t("checking");
265
517
  const version = await fetchVersion();
266
518
  lastVersion = version;
267
519
  if (!version || version.ok === false) {
268
- btn.textContent = "检查失败";
520
+ btn.textContent = t("checkFailed");
269
521
  btn.title = version ? String(version.error || "") : "";
270
522
  setTimeout(() => { btn.textContent = originalText; btn.disabled = false; btn.title = ""; }, 2000);
271
523
  return;
272
524
  }
273
525
  if (!version.updateAvailable) {
274
- btn.textContent = "已是最新";
526
+ btn.textContent = t("upToDate");
275
527
  setTimeout(() => { btn.textContent = originalText; btn.disabled = false; }, 1500);
276
528
  return;
277
529
  }
278
- btn.textContent = "更新中…";
530
+ btn.textContent = t("updating");
279
531
  const result = await updatePackage();
280
532
  if (result.ok) {
281
- btn.textContent = "成功,请重启 dsh";
533
+ btn.textContent = t("updateSuccess");
282
534
  btn.disabled = true;
283
535
  // Keep the success message visible for a while before reverting to version info.
284
536
  updateSuccessUntil = Date.now() + UPDATE_SUCCESS_MS;
285
537
  setTimeout(() => checkVersion(), UPDATE_SUCCESS_MS);
286
538
  } else {
287
- btn.textContent = "更新失败";
288
- btn.title = String(result.error || "更新失败") + (result.output ? "\n" + result.output.slice(-200) : "");
539
+ btn.textContent = t("updateFailed");
540
+ btn.title = String(result.error || t("updateFailed")) + (result.output ? "\n" + result.output.slice(-200) : "");
289
541
  setTimeout(() => { btn.textContent = originalText; btn.disabled = false; btn.title = ""; }, 3000);
290
542
  }
291
543
  }
@@ -389,8 +641,8 @@ window.__ModuleLoader__.load({
389
641
  if (n === void 0 || n === null || n === "") return "—";
390
642
  const v = Number(String(n).replace(/,/g, ""));
391
643
  if (!Number.isFinite(v)) return String(n);
392
- if (v >= 1e8) return (v / 1e8).toFixed(1) + "亿";
393
- if (v >= 1e4) return (v / 1e4).toFixed(1) + "";
644
+ if (v >= 1e8) return (v / 1e8).toFixed(1) + t("yi");
645
+ if (v >= 1e4) return (v / 1e4).toFixed(1) + t("wan");
394
646
  return v.toLocaleString();
395
647
  }
396
648
 
@@ -472,6 +724,18 @@ window.__ModuleLoader__.load({
472
724
 
473
725
  /** Render balance + today stats into the entry spans. */
474
726
  function renderValue(envelope, usageEnvelope) {
727
+ // 每次刷新都从 DOM 重新拿 entry,并按当前语言同步所有 stat label —— 避免 dsh 自愈重建或 React 重渲染导致 label 不同步。
728
+ // 直接问宿主端读 settings.yaml(权威)。fire-and-forget:不等返回,下次轮询或下次 refresh 时自动同步。
729
+ pollDshLang();
730
+ const entry = document.querySelector("[data-dsh-balance-entry]");
731
+ if (entry) {
732
+ entryRef = entry;
733
+ const spendLabel = entry.querySelector('[data-stat-key="todaySpend"] .dshBalanceStatLabel');
734
+ const tokenLabel = entry.querySelector('[data-stat-key="todayTokens"] .dshBalanceStatLabel');
735
+ if (spendLabel) spendLabel.textContent = t("todaySpend");
736
+ if (tokenLabel) tokenLabel.textContent = t("todayTokens");
737
+ entry.setAttribute("aria-label", t("entryAriaLabel"));
738
+ }
475
739
  const provider = activeProvider();
476
740
  const isDeepseek = provider && provider.kind === "deepseek";
477
741
  const bal = isDeepseek ? envelope : (balances[provider.id] || null);
@@ -481,26 +745,22 @@ window.__ModuleLoader__.load({
481
745
  const todayEl = entryRef && entryRef.querySelector(".dshBalanceToday");
482
746
  const tokEl = entryRef && entryRef.querySelector(".dshBalanceTodayTok");
483
747
 
484
- // 平台名称(提醒这是哪个 provider 的余额)
748
+ // 平台名称(提醒这是哪个 provider 的余额);按当前语言查 PROVIDER_META,避免宿主端硬编码的中文混入英文界面。
485
749
  if (labelEl !== null) {
486
- labelEl.textContent = provider ? (provider.label || provider.kind) : "余额";
750
+ const meta = provider ? PROVIDER_META[provider.kind] : null;
751
+ const metaLabel = meta ? meta.label[currentLang] : null;
752
+ const fallbackLabel = provider ? (provider.label || provider.kind) : t("balance");
753
+ labelEl.textContent = metaLabel || fallbackLabel;
487
754
  }
488
755
 
489
- // 余额(入口处不显示货币符号)
756
+ // 余额(统一显示货币符号,与 MiMo 入口保持一致)
490
757
  if (valueEl !== null) {
491
758
  if (!bal || bal.ok === false) {
492
759
  valueEl.dataset.state = "err";
493
- valueEl.textContent = "失败";
760
+ valueEl.textContent = t("failed");
494
761
  } else {
495
- const info = (bal.data && bal.data.balance_infos && bal.data.balance_infos[0]) || null;
496
- let total = void 0;
497
- if (info !== null) {
498
- total = info.total_balance;
499
- } else if (bal.data && bal.data.total !== void 0) {
500
- total = bal.data.total;
501
- }
502
762
  valueEl.dataset.state = "ok";
503
- valueEl.textContent = total !== void 0 ? total : "—";
763
+ valueEl.textContent = formatProviderBalance(bal.data);
504
764
  }
505
765
  }
506
766
 
@@ -540,8 +800,10 @@ window.__ModuleLoader__.load({
540
800
  if (popRef === null) return;
541
801
  popRef.innerHTML = popoverHtml();
542
802
  positionPopover();
543
- const btn = popRef.querySelector(".dshBalanceBtn");
544
- if (btn !== null) btn.addEventListener("click", () => { refresh(true); checkVersion(); });
803
+ const refreshBtn = popRef.querySelector("[data-refresh]");
804
+ if (refreshBtn !== null) refreshBtn.addEventListener("click", () => { refresh(true); checkVersion(); });
805
+ const langBtn = popRef.querySelector("[data-lang-toggle]");
806
+ if (langBtn !== null) langBtn.remove();
545
807
  const updateBtn = popRef.querySelector("[data-update-cmd]");
546
808
  if (updateBtn !== null) {
547
809
  updateBtn.addEventListener("click", () => handleUpdateClick(updateBtn));
@@ -570,11 +832,11 @@ window.__ModuleLoader__.load({
570
832
  const prompt = AI_GUIDE_PROMPT_BY_KIND[addFormState.kind] || MIMO_COOKIE_GUIDE_PROMPT;
571
833
  try {
572
834
  await navigator.clipboard.writeText(prompt);
573
- aiGuideBtn.textContent = "已复制";
835
+ aiGuideBtn.textContent = t("copied");
574
836
  } catch {
575
- aiGuideBtn.textContent = "复制失败";
837
+ aiGuideBtn.textContent = t("copyFailed");
576
838
  }
577
- setTimeout(() => aiGuideBtn.textContent = "AI 帮我配置", 1500);
839
+ setTimeout(() => aiGuideBtn.textContent = t("aiGuide"), 1500);
578
840
  });
579
841
  }
580
842
  const cookieCancel = popRef.querySelector("[data-cookie-cancel]");
@@ -589,9 +851,9 @@ window.__ModuleLoader__.load({
589
851
  const previewEl = popRef.querySelector(".dshBalanceCookiePreview");
590
852
  if (previewEl !== null) {
591
853
  previewEl.dataset.found = String(!!cookieExtracted);
592
- previewEl.innerHTML = '<b>识别结果:</b>' + (cookieExtracted
854
+ previewEl.innerHTML = '<b>' + t("extractResult") + "</b>" + (cookieExtracted
593
855
  ? '<code>' + escapeHtml(cookieExtracted.slice(0, 180) + (cookieExtracted.length > 180 ? '…' : '')) + '</code>'
594
- : '未识别到 Cookie,请粘贴完整 cURL 或请求头');
856
+ : t("cookieNotFound"));
595
857
  }
596
858
  });
597
859
  }
@@ -603,11 +865,11 @@ window.__ModuleLoader__.load({
603
865
  const prompt = MIMO_COOKIE_GUIDE_PROMPT;
604
866
  try {
605
867
  await navigator.clipboard.writeText(prompt);
606
- copyStep2.textContent = "已复制";
868
+ copyStep2.textContent = t("copied");
607
869
  } catch {
608
- copyStep2.textContent = "复制失败";
870
+ copyStep2.textContent = t("copyFailed");
609
871
  }
610
- setTimeout(() => copyStep2.textContent = "复制提示语 → 和智能体对话", 1500);
872
+ setTimeout(() => copyStep2.textContent = t("copyPromptToChat"), 1500);
611
873
  });
612
874
  }
613
875
  const cookieManual = popRef.querySelector("[data-cookie-manual]");
@@ -621,18 +883,19 @@ window.__ModuleLoader__.load({
621
883
  const usage = usageStateOf(usageEnvelope);
622
884
  if (!usage.src) {
623
885
  const hint = usage.state === "err"
624
- ? "用量接口不可用(" + String(usage.error || "").slice(0, 60) + ")"
625
- : "未配置 DEEPSEEK_PLATFORM_TOKEN,仅显示余额。对 AI 说「帮我配置用量统计」,AI 会帮你设置(可选)。";
886
+ ? t("usageUnavailable", String(usage.error || "").slice(0, 60))
887
+ : t("usageNotConfigured");
626
888
  return '<div class="dshBalanceUsage">' +
627
- '<div class="dshBalanceUsageTitle"><b>本月消耗</b><span>未启用</span></div>' +
889
+ '<div class="dshBalanceUsageTitle"><b>' + t("usageThisMonth") + "</b><span>" + t("usageDisabled") + "</span></div>" +
628
890
  '<div class="dshBalanceHint">' + hint + "</div>" +
629
891
  "</div>";
630
892
  }
631
893
  const src = usage.src;
632
894
  return '<div class="dshBalanceUsage">' +
633
- '<div class="dshBalanceUsageTitle"><b>' + (src.period || "本月") + "消耗</b><span>" + (src.apiKey || "全部") + "</span></div>" +
634
- '<div class="dshBalanceRow"><span>本月消费</span><span>' + String(src.totalSpend ?? "") + "</span></div>" +
635
- '<div class="dshBalanceRow"><span>Tokens</span><span>' + String(src.tokens ?? "—") + "</span></div>" +
895
+ // 宿主端 src.apiKey 是硬编码的中文「API Key 全部」,英文下不能用;DeepSeek 当前只有 1 API Key,直接用 i18n key。
896
+ '<div class="dshBalanceUsageTitle"><b>' + t("usageThisMonth") + "</b><span>" + t("all") + "</span></div>" +
897
+ '<div class="dshBalanceRow"><span>' + t("thisMonthSpend") + "</span><span>" + String(src.totalSpend ?? "—") + "</span></div>" +
898
+ '<div class="dshBalanceRow"><span>' + t("tokens") + "</span><span>" + String(src.tokens ?? "—") + "</span></div>" +
636
899
  "</div>";
637
900
  }
638
901
 
@@ -643,24 +906,24 @@ window.__ModuleLoader__.load({
643
906
  const cur = info ? (sym(info.currency) || "¥") : "¥";
644
907
  let rows = "";
645
908
  if (envelope && envelope.error && !info) {
646
- rows += '<div class="dshBalanceRow"><span>错误</span><span>' + String(envelope.error).slice(0, 80) + "</span></div>";
909
+ rows += '<div class="dshBalanceRow"><span>' + t("error") + "</span><span>" + String(envelope.error).slice(0, 80) + "</span></div>";
647
910
  } else if (!info) {
648
- rows += '<div class="dshBalanceRow"><span>余额</span><span>—</span></div>';
911
+ rows += '<div class="dshBalanceRow"><span>' + t("balance") + "</span><span>—</span></div>";
649
912
  } else {
650
913
  const total = info.total_balance;
651
- rows += '<div class="dshBalanceRow"><span>余额</span><span>' + cur + (total !== void 0 ? total : "—") + "</span></div>";
914
+ rows += '<div class="dshBalanceRow"><span>' + t("balance") + "</span><span>" + cur + (total !== void 0 ? total : "—") + "</span></div>";
652
915
  }
653
916
  // 累计消费(全部时间)——放在余额下面,不属于「本月消耗」区块
654
917
  const usage = usageStateOf(usageEnvelope);
655
918
  const usageSrc = usage.src;
656
919
  if (usageSrc && usageSrc.cumulativeSpend) {
657
- rows += '<div class="dshBalanceRow"><span>累计消费</span><span>' + String(usageSrc.cumulativeSpend) + "</span></div>";
920
+ rows += '<div class="dshBalanceRow"><span>' + t("cumulativeSpend") + "</span><span>" + String(usageSrc.cumulativeSpend) + "</span></div>";
658
921
  }
659
922
  if (usageSrc && usageSrc.todaySpend) {
660
- rows += '<div class="dshBalanceRow"><span>今日消费</span><span>' + fmtYuan(usageSrc.todaySpend) + "</span></div>";
923
+ rows += '<div class="dshBalanceRow"><span>' + t("todaySpend") + "</span><span>" + fmtYuan(usageSrc.todaySpend) + "</span></div>";
661
924
  }
662
925
  if (usageSrc && usageSrc.todayTokens) {
663
- rows += '<div class="dshBalanceRow"><span>今日Token</span><span>' + String(usageSrc.todayTokens) + "</span></div>";
926
+ rows += '<div class="dshBalanceRow"><span>' + t("todayTokens") + "</span><span>" + String(usageSrc.todayTokens) + "</span></div>";
664
927
  }
665
928
  return rows + usageHtmlOf(usageEnvelope);
666
929
  }
@@ -676,28 +939,17 @@ window.__ModuleLoader__.load({
676
939
  const s = JSON.stringify(v);
677
940
  out.push([k, s.length > 48 ? s.slice(0, 48) + "…" : s]);
678
941
  } else if (Array.isArray(v)) {
679
- out.push([k, "[" + v.length + " ]"]);
942
+ out.push([k, "[" + v.length + " " + t("items") + "]"]);
680
943
  } else {
681
944
  out.push([k, String(v)]);
682
945
  }
683
946
  }
684
947
  }
685
948
 
686
- /** Chinese labels for known MiMo balance response fields. */
687
- const MIMO_FIELD_LABELS = {
688
- code: "状态码",
689
- message: "消息",
690
- frozenBalance: "冻结余额",
691
- overdraftLimit: "透支额度",
692
- remainingOverdraftLimit: "剩余透支额度",
693
- giftBalance: "赠送余额",
694
- cashBalance: "现金余额",
695
- rechargeBalance: "充值余额",
696
- accountBalance: "账户余额",
697
- walletBalance: "钱包余额",
698
- plan: "套餐",
699
- expiredAt: "过期时间"
700
- };
949
+ /** Labels for known MiMo balance response fields (localized). */
950
+ function mimoFieldLabel(key) {
951
+ return (I18N[currentLang]?.mimoFieldLabels ?? I18N.zh.mimoFieldLabels)[key] || key;
952
+ }
701
953
 
702
954
  /** Build the MiMo usage block (from /api/v1/usage): 与 DeepSeek「本月消耗」一模一样. */
703
955
  function mimoUsageHtml(usage) {
@@ -710,9 +962,9 @@ window.__ModuleLoader__.load({
710
962
  const tokens = token.totalToken !== void 0 ? fmtInt(token.totalToken) : "—";
711
963
  if (totalSpend === "—" && tokens === "—") return "";
712
964
  return '<div class="dshBalanceUsage">' +
713
- '<div class="dshBalanceUsageTitle"><b>本月消耗</b><span>全部</span></div>' +
714
- '<div class="dshBalanceRow"><span>本月消费</span><span>' + totalSpend + "</span></div>" +
715
- '<div class="dshBalanceRow"><span>Tokens</span><span>' + tokens + "</span></div>" +
965
+ '<div class="dshBalanceUsageTitle"><b>' + t("usageThisMonth") + "</b><span>" + t("all") + "</span></div>" +
966
+ '<div class="dshBalanceRow"><span>' + t("thisMonthSpend") + "</span><span>" + totalSpend + "</span></div>" +
967
+ '<div class="dshBalanceRow"><span>' + t("tokens") + "</span><span>" + tokens + "</span></div>" +
716
968
  "</div>";
717
969
  }
718
970
 
@@ -722,16 +974,16 @@ window.__ModuleLoader__.load({
722
974
  const data = bal.data || {};
723
975
  const cur = sym(data.currency) || "¥";
724
976
  // 余额(首行,与 DeepSeek 一致)
725
- let rows = '<div class="dshBalanceRow"><span>余额</span><span>' + cur + (data.total !== void 0 ? data.total : "—") + "</span></div>";
977
+ let rows = '<div class="dshBalanceRow"><span>' + t("balance") + "</span><span>" + cur + (data.total !== void 0 ? data.total : "—") + "</span></div>";
726
978
  const usage = data.usage || null;
727
979
  const cost = usage ? (usage.costUsage || {}) : {};
728
980
  // 与 DeepSeek 同款:累计消费 + 今日消费/今日Token(来自 /usage/detail 逐日明细;无今日记录则显示 0)
729
981
  if (cost.totalCost !== void 0) {
730
- rows += '<div class="dshBalanceRow"><span>累计消费</span><span>¥' + String(cost.totalCost) + "</span></div>";
982
+ rows += '<div class="dshBalanceRow"><span>' + t("cumulativeSpend") + "</span><span>¥" + String(cost.totalCost) + "</span></div>";
731
983
  }
732
984
  const today = data.today || null;
733
- rows += '<div class="dshBalanceRow"><span>今日消费</span><span>' + (today ? fmtYuan(today.cost) : "¥0.00") + "</span></div>";
734
- rows += '<div class="dshBalanceRow"><span>今日Token</span><span>' + (today ? fmtInt(today.tokens) : "0") + "</span></div>";
985
+ rows += '<div class="dshBalanceRow"><span>' + t("todaySpend") + "</span><span>" + (today ? fmtYuan(today.cost) : "¥0.00") + "</span></div>";
986
+ rows += '<div class="dshBalanceRow"><span>' + t("todayTokens") + "</span><span>" + (today ? fmtInt(today.tokens) : "0") + "</span></div>";
735
987
  // 本月消耗区块(与 DeepSeek 同款样式)
736
988
  rows += mimoUsageHtml(usage);
737
989
  return rows;
@@ -742,9 +994,9 @@ window.__ModuleLoader__.load({
742
994
  if (!Array.isArray(daily) || daily.length === 0) return "";
743
995
  // 接口按日期升序返回,全部展示(一个月最多 31 行)。
744
996
  let html = '<div class="dshBalanceUsage">' +
745
- '<div class="dshBalanceUsageTitle"><b>每日消耗</b><span>MiMo</span></div>' +
997
+ '<div class="dshBalanceUsageTitle"><b>' + t("dailyUsage") + "</b><span>MiMo</span></div>" +
746
998
  '<div class="dshBalanceDaily">' +
747
- '<div class="dshBalanceDailyHead"><span>日期</span><span>Token</span><span>请求</span><span>消费</span></div>';
999
+ '<div class="dshBalanceDailyHead"><span>' + t("date") + "</span><span>Token</span><span>" + t("requests") + "</span><span>" + t("spend") + "</span></div>";
748
1000
  for (const d of daily) {
749
1001
  const costStr = Number(d.cost) > 0 ? String(d.cost) : "0";
750
1002
  html += '<div class="dshBalanceDailyRow">' +
@@ -765,7 +1017,7 @@ window.__ModuleLoader__.load({
765
1017
  let balanceText = "—";
766
1018
  let stateAttr = "";
767
1019
  if (bal) {
768
- if (bal.ok === false) { balanceText = bal.needCookie ? "需设置" : "失败"; stateAttr = ' data-state="err"'; }
1020
+ if (bal.ok === false) { balanceText = bal.needCookie ? t("needSetup") : t("failed"); stateAttr = ' data-state="err"'; }
769
1021
  else balanceText = formatProviderBalance(bal.data);
770
1022
  }
771
1023
  return '<button class="dshBalanceTab' + (isActive ? " active" : "") + '" type="button" data-tab="' + escapeHtml(p.id) + '">' +
@@ -781,16 +1033,16 @@ window.__ModuleLoader__.load({
781
1033
  let actions = "";
782
1034
  // 所有 provider 详情操作区统一只保留「× 移除」(AI 引导配置入口保留在添加表单里)。
783
1035
  actions = '<div class="dshBalanceProvActions">' +
784
- '<button class="dshBalanceProvRemove" type="button" data-remove="' + escapeHtml(p.id) + '" title="移除">× 移除</button></div>';
1036
+ '<button class="dshBalanceProvRemove" type="button" data-remove="' + escapeHtml(p.id) + '" title="' + t("remove") + '"' + t("remove") + "</button></div>";
785
1037
 
786
1038
  let errHint = "";
787
1039
  if (bal && bal.ok === false) {
788
1040
  if (bal.needCookie) {
789
- errHint = '<div class="dshBalanceHint" style="margin-top:8px">MiMo 余额需用浏览器登录 Cookie 查询。请重新在浏览器登录 platform.xiaomimimo.com 后,将 Cookie 更新到 ~/.dsh/ai-balances.json 对应条目中。</div>';
1041
+ errHint = '<div class="dshBalanceHint" style="margin-top:8px">' + t("mimoCookieHint") + "</div>";
790
1042
  } else {
791
1043
  const raw = bal.data && bal.data.raw ? String(bal.data.raw).slice(0, 260) : "";
792
- errHint = '<div class="dshBalanceHint" style="margin-top:8px">读取失败:' + escapeHtml(String(bal.error || "").slice(0, 50)) +
793
- (raw ? '<br><span class="dshBalanceRawHint">' + escapeHtml(raw) + "</span>" : "") + "</div>";
1044
+ errHint = '<div class="dshBalanceHint" style="margin-top:8px">' + t("readFailed", String(bal.error || "").slice(0, 50)) +
1045
+ (raw ? '<br><span class="dshBalanceRawHint">' + t("rawPrefix") + escapeHtml(raw) + "</span>" : "") + "</div>";
794
1046
  }
795
1047
  }
796
1048
  return detail + actions + errHint;
@@ -803,50 +1055,50 @@ window.__ModuleLoader__.load({
803
1055
  const isMimo = addFormState.kind === "mimo";
804
1056
  const isDeepseek = addFormState.kind === "deepseek";
805
1057
  const aiGuide = (isMimo || isDeepseek)
806
- ? '<div class="dshBalanceField"><label>配置方式</label>' +
807
- '<button class="dshBalanceBtn" type="button" data-ai-guide style="width:100%;justify-content:center">AI 帮我配置</button></div>'
1058
+ ? '<div class="dshBalanceField"><label>' + t("configMethod") + "</label>" +
1059
+ '<button class="dshBalanceBtn" type="button" data-ai-guide style="width:100%;justify-content:center">' + t("aiGuide") + "</button></div>"
808
1060
  : "";
809
1061
  const note = (isMimo || isDeepseek)
810
- ? '<div class="dshBalanceHint">点「AI 帮我配置」会复制一句话,发给智能体后它会读取本机教程文件、一步步引导你抓取凭证并写入插件。数据仅存本机,不上传。</div>'
811
- : '<div class="dshBalanceHint">密钥仅保存在本机 ~/.dsh/ai-balances.json,不会上传。</div>';
1062
+ ? '<div class="dshBalanceHint">' + t("aiGuideNote") + "</div>"
1063
+ : '<div class="dshBalanceHint">' + t("localKeyNote") + "</div>";
812
1064
  return '<div class="dshBalanceAddForm">' +
813
- '<div class="dshBalanceField"><label>平台</label><select class="dshBalanceInput" data-f-kind>' +
814
- '<option value="mimo"' + mimoSelected + '>MiMo(小米)</option>' +
815
- '<option value="deepseek"' + deepseekSelected + '>DeepSeek</option></select></div>' +
816
- '<div class="dshBalanceField"><label>名称</label><input class="dshBalanceInput" data-f-label value="' + escapeHtml(addFormState.label) + '" placeholder="' + (addFormState.kind === "deepseek" ? "例如 DeepSeek" : "例如 MiMo") + '" /></div>' +
1065
+ '<div class="dshBalanceField"><label>' + t("platform") + '</label><select class="dshBalanceInput" data-f-kind>' +
1066
+ '<option value="mimo"' + mimoSelected + ">" + t("mimoOption") + "</option>" +
1067
+ '<option value="deepseek"' + deepseekSelected + ">" + t("deepseekOption") + "</option></select></div>" +
1068
+ '<div class="dshBalanceField"><label>' + t("name") + "</label><input class=\"dshBalanceInput\" data-f-label value=\"" + escapeHtml(addFormState.label) + "\" placeholder=\"" + (addFormState.kind === "deepseek" ? t("namePlaceholderDeepseek") : t("namePlaceholderMimo")) + "\" /></div>" +
817
1069
  aiGuide +
818
- '<div class="dshBalanceFormBtns"><button class="dshBalanceBtn" type="button" data-save>保存</button>' +
819
- '<button class="dshBalanceBtn" type="button" data-cancel>取消</button></div>' +
1070
+ '<div class="dshBalanceFormBtns"><button class="dshBalanceBtn" type="button" data-save>' + t("save") + "</button>" +
1071
+ '<button class="dshBalanceBtn" type="button" data-cancel>' + t("cancel") + "</button></div>" +
820
1072
  note + "</div>";
821
1073
  }
822
1074
 
823
1075
  /** Build the chat-based AI guide panel for MiMo Cookie configuration. */
824
1076
  function aiGuideCookiePanelHtml() {
825
1077
  return '<div class="dshBalanceCookiePanel">' +
826
- '<div class="dshBalanceHint" style="margin-bottom:4px;font-weight:600">让 AI 在聊天里帮你配置</div>' +
1078
+ '<div class="dshBalanceHint" style="margin-bottom:4px;font-weight:600">' + t("aiChatGuideTitle") + "</div>" +
827
1079
  '<div class="dshBalanceWizard">' +
828
- '<div class="dshBalanceWizardStep"><b>Step 1</b><span>点击下方链接打开 MiMo 余额页并登录</span>' +
829
- '<span class="dshBalanceWizardLink" data-open-mimo>打开 platform.xiaomimimo.com/#/console/balance</span></div>' +
830
- '<div class="dshBalanceWizardStep"><b>Step 2</b><span>复制下方提示语,发送给智能体,AI 会在对话中一步步引导你完成配置</span>' +
831
- '<span class="dshBalanceWizardLink" data-copy-step2>复制提示语 和智能体对话</span></div>' +
1080
+ '<div class="dshBalanceWizardStep"><b>' + t("step1") + "</b><span>" + t("step1Desc") + "</span>" +
1081
+ '<span class="dshBalanceWizardLink" data-open-mimo>' + t("openMimoBalance") + "</span></div>" +
1082
+ '<div class="dshBalanceWizardStep"><b>' + t("step2") + "</b><span>" + t("step2Desc") + "</span>" +
1083
+ '<span class="dshBalanceWizardLink" data-copy-step2>' + t("copyPromptToChat") + "</span></div>" +
832
1084
  '</div>' +
833
- '<div class="dshBalanceFormBtns"><button class="dshBalanceBtn" type="button" data-cookie-cancel>取消</button></div>' +
834
- '<div style="margin-top:6px"><span class="dshBalanceWizardLink" data-cookie-manual>我要手动粘贴 Cookie</span></div></div>';
1085
+ '<div class="dshBalanceFormBtns"><button class="dshBalanceBtn" type="button" data-cookie-cancel>' + t("cancel") + "</button></div>" +
1086
+ '<div style="margin-top:6px"><span class="dshBalanceWizardLink" data-cookie-manual>' + t("manualPasteCookie") + "</span></div></div>";
835
1087
  }
836
1088
 
837
1089
  /** Build the manual fallback textarea for Cookie configuration. */
838
1090
  function manualCookiePanelHtml() {
839
1091
  const preview = cookieExtracted
840
1092
  ? '<code>' + escapeHtml(cookieExtracted.slice(0, 180) + (cookieExtracted.length > 180 ? '…' : '')) + '</code>'
841
- : '未识别到 Cookie,请粘贴完整 cURL 或请求头';
1093
+ : t("cookieNotFound");
842
1094
  const found = !!cookieExtracted;
843
1095
  return '<div class="dshBalanceCookiePanel">' +
844
- '<div class="dshBalanceHint" style="margin-bottom:4px;font-weight:600">手动粘贴 Cookie</div>' +
845
- '<textarea class="dshBalanceInput dshBalanceTextarea" data-cookie-value placeholder="粘贴 Cookie / cURL / 请求头…">' + escapeHtml(cookieEditValue) + '</textarea>' +
846
- '<div class="dshBalanceCookiePreview" data-found="' + found + '"><b>识别结果:</b>' + preview + '</div>' +
847
- '<div class="dshBalanceFormBtns"><button class="dshBalanceBtn" type="button" data-cookie-save>保存</button>' +
848
- '<button class="dshBalanceBtn" type="button" data-cookie-cancel>取消</button></div>' +
849
- '<div style="margin-top:6px"><span class="dshBalanceWizardLink" data-cookie-guide>返回 AI 引导</span></div></div>';
1096
+ '<div class="dshBalanceHint" style="margin-bottom:4px;font-weight:600">' + t("manualCookieTitle") + "</div>" +
1097
+ '<textarea class="dshBalanceInput dshBalanceTextarea" data-cookie-value placeholder="' + t("cookiePlaceholder") + '">' + escapeHtml(cookieEditValue) + '</textarea>' +
1098
+ '<div class="dshBalanceCookiePreview" data-found="' + found + '"><b>' + t("extractResult") + "</b>" + preview + '</div>' +
1099
+ '<div class="dshBalanceFormBtns"><button class="dshBalanceBtn" type="button" data-cookie-save>' + t("save") + '</button>' +
1100
+ '<button class="dshBalanceBtn" type="button" data-cookie-cancel>' + t("cancel") + '</button></div>' +
1101
+ '<div style="margin-top:6px"><span class="dshBalanceWizardLink" data-cookie-guide>' + t("backToAiGuide") + "</span></div></div>";
850
1102
  }
851
1103
 
852
1104
  /** Build the MiMo Cookie configuration panel (AI guide or manual fallback). */
@@ -858,17 +1110,17 @@ window.__ModuleLoader__.load({
858
1110
  function syncHintHtml() {
859
1111
  if (!lastSyncResult) return "";
860
1112
  if (!lastSyncResult.ok) {
861
- return '<div class="dshBalanceSyncHint" data-state="err">同步失败:' + escapeHtml(String(lastSyncResult.error || "").slice(0, 60)) + "</div>";
1113
+ return '<div class="dshBalanceSyncHint" data-state="err">' + t("syncFailed", escapeHtml(String(lastSyncResult.error || "").slice(0, 60))) + "</div>";
862
1114
  }
863
1115
  if (lastSyncResult.added > 0) {
864
- return '<div class="dshBalanceSyncHint" data-state="ok">已从 dsh 设置同步 ' + lastSyncResult.added + " 个平台</div>";
1116
+ return '<div class="dshBalanceSyncHint" data-state="ok">' + t("syncAddedN", lastSyncResult.added) + "</div>";
865
1117
  }
866
1118
  return "";
867
1119
  }
868
1120
 
869
1121
  /** Build the popover HTML from current state. */
870
1122
  function popoverHtml() {
871
- const header = '<h4>AI 余额<button class="dshBalanceAddBtn" type="button" data-add>+ 添加</button></h4>';
1123
+ const header = '<h4>' + t("aiBalance") + '<button class="dshBalanceAddBtn" type="button" data-add>' + t("add") + "</button></h4>";
872
1124
  const syncHint = showAddForm ? "" : syncHintHtml();
873
1125
  let body;
874
1126
  if (showAddForm) {
@@ -878,7 +1130,7 @@ window.__ModuleLoader__.load({
878
1130
  } else {
879
1131
  body = '<div class="dshBalanceProviders">';
880
1132
  if (providers.length === 0) {
881
- body += '<div class="dshBalanceHint">还没有任何 AI 余额,点右上角「+ 添加」。</div>';
1133
+ body += '<div class="dshBalanceHint">' + t("noProviders") + "</div>";
882
1134
  } else {
883
1135
  const activeP = providers.find((p) => p.id === activeProviderId) || activeProvider();
884
1136
  body += '<div class="dshBalanceTabs">';
@@ -894,30 +1146,30 @@ window.__ModuleLoader__.load({
894
1146
  return header + syncHint + body +
895
1147
  '<div class="dshBalanceFoot">' + versionHtml +
896
1148
  '<span style="display:flex;align-items:center;gap:8px">' +
897
- '<span>更新于 ' + updated + "</span>" +
898
- '<button class="dshBalanceBtn" type="button">刷新</button></span></div>';
1149
+ '<button class="dshBalanceBtn" type="button" data-refresh>' + t("refresh") + "</button>" +
1150
+ '<span>' + t("updatedAt") + updated + "</span></span></div>";
899
1151
  }
900
1152
 
901
1153
  /** Build the npm version snippet shown in the footer (always a clickable button). */
902
1154
  function versionHtmlOf(versionEnvelope) {
903
1155
  if (Date.now() < updateSuccessUntil) {
904
- return '<span class="dshBalanceVersion" data-state="ok">成功,请重启 dsh</span>';
1156
+ return '<span class="dshBalanceVersion" data-state="ok">' + t("updateSuccess") + "</span>";
905
1157
  }
906
1158
  if (!versionEnvelope) {
907
- return '<span class="dshBalanceVersion" data-state="ok"><button class="dshBalanceVersionBtn" type="button" data-update-cmd>检查更新</button></span>';
1159
+ return '<span class="dshBalanceVersion" data-state="ok"><button class="dshBalanceVersionBtn" type="button" data-update-cmd>' + t("checkUpdate") + "</button></span>";
908
1160
  }
909
1161
  if (versionEnvelope.ok === false) {
910
- return '<span class="dshBalanceVersion" data-state="err"><button class="dshBalanceVersionBtn" type="button" data-update-cmd>检查更新</button></span>';
1162
+ return '<span class="dshBalanceVersion" data-state="err"><button class="dshBalanceVersionBtn" type="button" data-update-cmd>' + t("checkUpdate") + "</button></span>";
911
1163
  }
912
1164
  const local = versionEnvelope.local || "?";
913
1165
  if (versionEnvelope.updateAvailable && versionEnvelope.latest) {
914
1166
  return '<span class="dshBalanceVersion" data-state="update">' +
915
1167
  'v' + local + " → v" + versionEnvelope.latest +
916
- '<button class="dshBalanceVersionBtn" type="button" data-update-cmd>更新</button>' +
1168
+ '<button class="dshBalanceVersionBtn" type="button" data-update-cmd>' + t("update") + "</button>" +
917
1169
  "</span>";
918
1170
  }
919
1171
  return '<span class="dshBalanceVersion" data-state="ok">' +
920
- 'v' + local + '<button class="dshBalanceVersionBtn" type="button" data-update-cmd>检查更新</button>' +
1172
+ 'v' + local + '<button class="dshBalanceVersionBtn" type="button" data-update-cmd>' + t("checkUpdate") + "</button>" +
921
1173
  "</span>";
922
1174
  }
923
1175
 
@@ -927,11 +1179,11 @@ window.__ModuleLoader__.load({
927
1179
  const kind = (popRef.querySelector("[data-f-kind]")?.value || "mimo");
928
1180
  const label = (popRef.querySelector("[data-f-label]")?.value || "").trim();
929
1181
  if (!label) {
930
- window.alert("请填写名称");
1182
+ window.alert(t("fillName"));
931
1183
  return;
932
1184
  }
933
1185
  const saveBtn = popRef.querySelector("[data-save]");
934
- if (saveBtn !== null) { saveBtn.disabled = true; saveBtn.textContent = "保存中…"; }
1186
+ if (saveBtn !== null) { saveBtn.disabled = true; saveBtn.textContent = t("saving"); }
935
1187
  const body = { label, kind };
936
1188
  const result = await saveProvider(body);
937
1189
  if (result && result.ok) {
@@ -939,15 +1191,15 @@ window.__ModuleLoader__.load({
939
1191
  showAddForm = false;
940
1192
  await refresh(true);
941
1193
  } else {
942
- if (saveBtn !== null) { saveBtn.disabled = false; saveBtn.textContent = "保存"; }
943
- window.alert("保存失败:" + (result && result.error ? result.error : "未知错误"));
1194
+ if (saveBtn !== null) { saveBtn.disabled = false; saveBtn.textContent = t("save"); }
1195
+ window.alert(t("saveFailed", result && result.error ? result.error : "unknown"));
944
1196
  }
945
1197
  }
946
1198
 
947
1199
  /** Handle the "remove" action for a provider. */
948
1200
  async function onRemoveProvider(id) {
949
1201
  if (id === "deepseek") return;
950
- if (!window.confirm("确定移除该 AI 余额?")) return;
1202
+ if (!window.confirm(t("confirmRemove"))) return;
951
1203
  await removeProvider(id);
952
1204
  if (activeProviderId === id) activeProviderId = null;
953
1205
  await refresh(true);
@@ -956,16 +1208,16 @@ window.__ModuleLoader__.load({
956
1208
  /** Handle the "save cookie" action from the MiMo cookie panel. */
957
1209
  async function onSaveCookie() {
958
1210
  if (popRef === null || !cookieEditId) return;
959
- if (!cookieExtracted) { window.alert("请粘贴 Cookie 或 cURL,AI 会自动提取"); return; }
1211
+ if (!cookieExtracted) { window.alert(t("pasteCookieHint")); return; }
960
1212
  const saveBtn = popRef.querySelector("[data-cookie-save]");
961
- if (saveBtn !== null) { saveBtn.disabled = true; saveBtn.textContent = "保存中…"; }
1213
+ if (saveBtn !== null) { saveBtn.disabled = true; saveBtn.textContent = t("saving"); }
962
1214
  const result = await saveProvider({ id: cookieEditId, kind: "mimo", cookie: cookieExtracted });
963
1215
  if (result && result.ok) {
964
1216
  cookieEditId = null; cookieEditValue = ""; cookieExtracted = "";
965
1217
  await refresh(true);
966
1218
  } else {
967
- if (saveBtn !== null) { saveBtn.disabled = false; saveBtn.textContent = "保存"; }
968
- window.alert("保存失败:" + (result && result.error ? result.error : "未知错误"));
1219
+ if (saveBtn !== null) { saveBtn.disabled = false; saveBtn.textContent = t("save"); }
1220
+ window.alert(t("saveFailed", result && result.error ? result.error : "unknown"));
969
1221
  }
970
1222
  }
971
1223
 
@@ -1084,7 +1336,7 @@ window.__ModuleLoader__.load({
1084
1336
  } catch (e) {
1085
1337
  if (entryRef) {
1086
1338
  const v = entryRef.querySelector(".dshBalanceValue");
1087
- if (v) { v.dataset.state = "err"; v.textContent = "失败"; }
1339
+ if (v) { v.dataset.state = "err"; v.textContent = t("failed"); }
1088
1340
  }
1089
1341
  lastEnvelope = { ok: false, error: String(e?.message ?? e) };
1090
1342
  renderPopover();
@@ -1105,6 +1357,22 @@ window.__ModuleLoader__.load({
1105
1357
  function apply(ctx) {
1106
1358
  ensureStyle();
1107
1359
  if (typeof document !== "undefined" && document.querySelector("[data-dsh-balance-entry]") !== null) return () => {};
1360
+ // 用 dsh 的 LocaleRuntime 服务读取/订阅语言(ctx.locale 由 @deepseek-ai/dsh-client-locale provide)。
1361
+ let offLocaleChange = null;
1362
+ try {
1363
+ const snap = ctx.locale && typeof ctx.locale.getLocale === "function" ? ctx.locale.getLocale() : null;
1364
+ if (snap && typeof snap.active === "string") currentLang = snap.active === "en" ? "en" : "zh";
1365
+ if (ctx.locale && typeof ctx.locale.subscribe === "function") {
1366
+ offLocaleChange = ctx.locale.subscribe(() => {
1367
+ const s = ctx.locale.getLocale();
1368
+ const next = s && s.active === "en" ? "en" : "zh";
1369
+ if (next !== currentLang) {
1370
+ currentLang = next;
1371
+ applyLangChange(next);
1372
+ }
1373
+ });
1374
+ }
1375
+ } catch { /* locale service optional */ }
1108
1376
  const effect = ctx.effect(() => {
1109
1377
  const entry = createEntry("compact");
1110
1378
  entryRef = entry;
@@ -1156,6 +1424,7 @@ window.__ModuleLoader__.load({
1156
1424
  return () => {
1157
1425
  waitObserver.disconnect();
1158
1426
  rootObserver.disconnect();
1427
+ if (offLocaleChange !== null) { try { offLocaleChange(); } catch { /* ignore */ } offLocaleChange = null; }
1159
1428
  if (timer !== null) clearInterval(timer);
1160
1429
  if (versionTimer !== null) clearInterval(versionTimer);
1161
1430
  if (outsideHandler !== null) {
@@ -1173,7 +1442,8 @@ window.__ModuleLoader__.load({
1173
1442
  }
1174
1443
 
1175
1444
  exports.apply = apply;
1176
- exports.inject = [];
1445
+ /** Declare the locale service dependency so ctx.locale is available (dsh i18n). */
1446
+ exports.inject = ["locale"];
1177
1447
  return module.exports;
1178
1448
  }
1179
1449
  });
package/lib/index.js CHANGED
@@ -885,10 +885,6 @@ function apply(ctx) {
885
885
  return;
886
886
  }
887
887
  const result = await queryBalance(key);
888
- // 入口不展示货币符号:把币种字段清空,任何版本的前端都不会拼出 ¥。
889
- if (result.ok && Array.isArray(result.data?.balance_infos)) {
890
- for (const info of result.data.balance_infos) info.currency = "";
891
- }
892
888
  res.writeHead(result.ok ? 200 : 502, { "content-type": "application/json; charset=utf-8" });
893
889
  res.end(JSON.stringify(result));
894
890
  } catch (error) {
@@ -996,6 +992,40 @@ function apply(ctx) {
996
992
  return dispose;
997
993
  }, "deepseek-balance-widget: update route");
998
994
 
995
+ /** Read the user's locale from dsh settings.yaml (locale.preference). */
996
+ ctx.effect(() => {
997
+ const dispose = ctx.webServer.register({
998
+ kind: "prefix",
999
+ path: "/deepseek-balance-locale",
1000
+ handler: async (req, res) => {
1001
+ const url = new URL(req.url ?? "/", "http://x");
1002
+ if (req.method !== "GET" || url.pathname !== "/deepseek-balance-locale") {
1003
+ res.writeHead(404, { "content-type": "application/json; charset=utf-8" });
1004
+ res.end(JSON.stringify({ ok: false, error: "not found" }));
1005
+ return;
1006
+ }
1007
+ try {
1008
+ let lang = "zh";
1009
+ try {
1010
+ const text = await readFile(DSH_SETTINGS_PATH, "utf8");
1011
+ // 解析 YAML 顶层 locale: 段下的 preference: 字段(宽松匹配缩进)。
1012
+ const match = text.match(/^locale:\s*\r?\n(\s+preference:\s*(\S+))/m);
1013
+ if (match) {
1014
+ const value = match[2].replace(/^["']|["']$/g, "").toLowerCase();
1015
+ if (value === "en" || value === "zh") lang = value;
1016
+ }
1017
+ } catch { /* settings file missing/unreadable -> default zh */ }
1018
+ res.writeHead(200, { "content-type": "application/json; charset=utf-8" });
1019
+ res.end(JSON.stringify({ ok: true, lang }));
1020
+ } catch (error) {
1021
+ res.writeHead(500, { "content-type": "application/json; charset=utf-8" });
1022
+ res.end(JSON.stringify({ ok: false, error: String(error?.message ?? error) }));
1023
+ }
1024
+ }
1025
+ });
1026
+ return dispose;
1027
+ }, "deepseek-balance-widget: locale route");
1028
+
999
1029
  /** Provider list (GET), add/update (POST), remove (DELETE). */
1000
1030
  ctx.effect(() => {
1001
1031
  const dispose = ctx.webServer.register({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-deepseek-balance-widget",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "Multi-provider AI balance widget for the dsh web sidebar: a live, auto-refreshing balance pill plus a detail popover listing DeepSeek and any added providers (MiMo etc.). Keys are stored per-machine in ~/.dsh/ai-balances.json and resolved from the local credential seam, never hardcoded.",
6
6
  "keywords": [