dsh-deepseek-balance-widget 2.1.0 → 2.2.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
@@ -1,63 +1,62 @@
1
- # dsh-deepseek-balance-widget
2
-
3
- [English](README_EN.md) | 中文
4
-
5
- 在 dsh Web 侧边栏显示 AI 余额的多平台插件:内置 **DeepSeek**,可添加 **MiMo(小米)** 等平台,每 30 秒自动刷新。
6
-
7
- ## 功能
8
-
9
- ![AI 余额侧边栏展示](assets/screenshot.png)
10
-
11
- - 侧边栏实时显示余额 / 今日消费 / 今日 Token,30 秒自动刷新,并随当前平台切换
12
- - 弹窗顶部 **Tab** 切换平台(DeepSeek / MiMo…),侧边栏数值同步更新
13
- - 弹窗右上角「**+ 添加**」:随时加入 MiMo 等其他 AI 平台
14
- - 详情字段:余额、累计消费、今日消费、今日 Token、本月消耗(本月消费 / Tokens)
15
- - 「**AI 帮我配置**」:凭证抓取与写入全交给智能体,无需手动翻 Cookies
16
- - 弹窗底部显示版本号,有新版时一键自动更新
17
-
18
- ## 安装
19
-
20
- 需要:已安装 dsh(可用 `dsh web`)。
21
-
22
- ```bash
23
- dsh plugin --profile web add dsh-deepseek-balance-widget
24
- ```
25
-
26
- 从 npm 拉取安装,dsh 自动注册到 `dsh.profile.bundles`,完成后**重启 `dsh web`** 即可。
27
-
28
- 也可以直接对 AI 说:*帮我用 npm 安装 dsh-deepseek-balance-widget 插件。*
29
-
30
- ## 配置
31
-
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`,不随插件分发。
52
-
53
- ## 卸载
54
-
55
- ```bash
56
- dsh plugin --profile web rm dsh-deepseek-balance-widget
57
- ```
58
-
59
- 或者直接对 AI 说:*帮我用 npm 卸载 dsh-deepseek-balance-widget 插件。*
60
-
61
- ## License
62
-
63
- [MIT](LICENSE)
1
+ # dsh-deepseek-balance-widget
2
+
3
+ [English](README_EN.md) | 中文
4
+
5
+ 在 dsh Web 侧边栏显示 AI 余额的多平台插件:内置 **DeepSeek**,可添加 **MiMo(小米)**,每 30 秒自动刷新。
6
+
7
+ ## 功能
8
+
9
+ ![AI 余额侧边栏展示](assets/screenshot.png)
10
+
11
+ - 侧边栏实时显示余额 / 今日消费 / 今日 Token,30 秒自动刷新,并随当前平台切换
12
+ - 弹窗顶部显示当前平台标题:点「**更换**」在已添加的平台间切换,点「**×**」可移除该平台
13
+ - 弹窗右上角「**+ 添加**」:加入 MiMo
14
+ - 详情字段:余额、累计消费、今日消费、今日 Token、本月消耗(本月消费 / Tokens)
15
+ - 「**AI 帮我配置**」:复制提示语发给智能体,AI 会一步步指导你抓取凭证并完成配置。
16
+ - 弹窗底部显示版本号,有新版时一键自动更新
17
+
18
+ ## 安装
19
+
20
+ 需要:已安装 dsh(可用 `dsh web`)。
21
+
22
+ ```bash
23
+ dsh plugin --profile web add dsh-deepseek-balance-widget
24
+ ```
25
+
26
+ 从 npm 拉取安装,dsh 自动注册到 `dsh.profile.bundles`,完成后**重启 `dsh web`** 即可。
27
+
28
+ 也可以直接对 AI 说:
29
+
30
+ > 帮我用 npm 安装 dsh-deepseek-balance-widget 插件。
31
+
32
+ ## 配置
33
+
34
+ 插件首次使用会自动创建 DeepSeek 条目,读取你本机的 `DEEPSEEK_API_KEY`。
35
+
36
+ 要加入 MiMo,打开弹窗点右上角「+ 添加」,再点「AI 帮我配置」,把提示语发给智能体;也可以直接对 AI 说:
37
+
38
+ > 帮我配置 dsh-deepseek-balance-widget。
39
+
40
+ AI 会接管全部:问 API Key / 引导获取平台 Cookie → 写入本机 → 提醒重启 `dsh web`。
41
+
42
+ | 平台 | 凭据 | 备注 |
43
+ | ----------- | ------------------------- | ------------------- |
44
+ | DeepSeek | `DEEPSEEK_API_KEY` | 必填,余额 |
45
+ | DeepSeek 用量 | `DEEPSEEK_PLATFORM_TOKEN` | 可选,统计累计消费 / 本月 / 今日 |
46
+ | MiMo(小米) | 登录 Cookie | AI 引导获取 |
47
+
48
+ 凭据保存在你本机的 `~/.dsh/ai-balances.json`,不随插件分发、不上传。
49
+
50
+ ## 卸载
51
+
52
+ ```bash
53
+ dsh plugin --profile web rm dsh-deepseek-balance-widget
54
+ ```
55
+
56
+ 或者直接对 AI 说:
57
+
58
+ > 帮我用 npm 卸载 dsh-deepseek-balance-widget 插件。
59
+
60
+ ## License
61
+
62
+ [MIT](LICENSE)
package/README_EN.md CHANGED
@@ -2,17 +2,17 @@
2
2
 
3
3
  English | [中文](README.md)
4
4
 
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.
5
+ A multi-provider AI balance widget for the dsh web sidebar. **DeepSeek** is built in; add **MiMo (Xiaomi)** from the popover. Auto-refreshes every 30 seconds.
6
6
 
7
7
  ## Features
8
8
 
9
9
  ![AI balance sidebar](assets/screenshot-en.png)
10
10
 
11
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
12
+ - Popover header shows the current provider: a "**Switch**" menu changes to another added provider, and "**×**" removes it
13
+ - "**+ Add**" button (top right): add **MiMo**
14
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
15
+ - "**AI 帮我配置**": one click copies a prompt; send it to your AI and it guides you through fetching credentials and writing them locally — no digging through cookies by hand
16
16
  - Version shown in the popover footer; one-click auto-update when a new version is available
17
17
 
18
18
  ## Install
@@ -25,18 +25,15 @@ dsh plugin --profile web add dsh-deepseek-balance-widget
25
25
 
26
26
  Installed from npm and auto-registered by dsh. **Restart `dsh web`** and the balance button appears in the sidebar.
27
27
 
28
- Or just tell your AI: *Install the dsh-deepseek-balance-widget plugin for me via npm.*
28
+ Or just tell your AI:
29
29
 
30
- ## Configuration
30
+ > Install the dsh-deepseek-balance-widget plugin for me via npm.
31
31
 
32
- On first use the plugin creates a DeepSeek entry and reads your local `DEEPSEEK_API_KEY`. Other providers are added via "+ Add". Either:
32
+ ## Configuration
33
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.
34
+ On first use the plugin creates a DeepSeek entry and reads your local `DEEPSEEK_API_KEY`.
38
35
 
39
- **Option 2: Just tell your AI**
36
+ To add MiMo, open the popover, click "+ Add", then "AI 帮我配置", and send the copied prompt to your AI. Or just tell your AI:
40
37
 
41
38
  > Configure dsh-deepseek-balance-widget for me.
42
39
 
@@ -48,7 +45,7 @@ The AI takes care of everything: asks for your API key / guides you through gett
48
45
  | DeepSeek usage | `DEEPSEEK_PLATFORM_TOKEN` | Optional, cumulative / monthly / today stats |
49
46
  | MiMo (Xiaomi) | Login cookie | Guided by your AI |
50
47
 
51
- Credentials are stored locally in `~/.dsh/ai-balances.json`; nothing is shipped with the plugin.
48
+ Credentials are stored locally in `~/.dsh/ai-balances.json`; nothing is shipped with the plugin and nothing is uploaded.
52
49
 
53
50
  ## Uninstall
54
51
 
@@ -56,8 +53,10 @@ Credentials are stored locally in `~/.dsh/ai-balances.json`; nothing is shipped
56
53
  dsh plugin --profile web rm dsh-deepseek-balance-widget
57
54
  ```
58
55
 
59
- Or just tell your AI: *Uninstall the dsh-deepseek-balance-widget plugin for me via npm.*
56
+ Or just tell your AI:
57
+
58
+ > Uninstall the dsh-deepseek-balance-widget plugin for me via npm.
60
59
 
61
60
  ## License
62
61
 
63
- [MIT](LICENSE)
62
+ [MIT](LICENSE)
package/lib/client.js CHANGED
@@ -66,6 +66,7 @@ window.__ModuleLoader__.load({
66
66
  spend: "消费",
67
67
  needSetup: "需设置",
68
68
  remove: "移除",
69
+ switch: "更换",
69
70
  mimoCookieHint: "MiMo 余额需用浏览器登录 Cookie 查询。请重新在浏览器登录 platform.xiaomimimo.com 后,将 Cookie 更新到 ~/.dsh/ai-balances.json 对应条目中。",
70
71
  readFailed: "读取失败:{0}",
71
72
  rawPrefix: "原始响应:",
@@ -148,6 +149,7 @@ window.__ModuleLoader__.load({
148
149
  spend: "Spend",
149
150
  needSetup: "Setup",
150
151
  remove: "Remove",
152
+ switch: "Switch",
151
153
  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
154
  readFailed: "Read failed: {0}",
153
155
  rawPrefix: "Raw response: ",
@@ -317,16 +319,17 @@ window.__ModuleLoader__.load({
317
319
  ".dshBalanceToday[data-state=err],.dshBalanceTodayTok[data-state=err]{color:#f85149}",
318
320
  ".dshBalanceToday[data-state=loading],.dshBalanceTodayTok[data-state=loading]{color:var(--dsw-alias-label-tertiary,#7b8494)}",
319
321
  ".dshBalanceToday[data-state=muted],.dshBalanceTodayTok[data-state=muted]{color:var(--dsw-alias-label-tertiary,#7b8494)}",
320
- ".dshBalancePop{position:fixed;z-index:9999;width:280px;max-height:calc(100vh - 16px);overflow-y:auto;box-sizing:border-box;background:#1b1f27;border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.5);padding:14px 16px;color:#e6e9ef;font-size:13px;line-height:1.5}",
322
+ ".dshBalancePop{position:fixed;z-index:9999;width:280px;max-height:calc(100vh - 16px);overflow-x:hidden;overflow-y:auto;box-sizing:border-box;background:#1b1f27;border:1px solid rgba(255,255,255,.1);border-radius:12px;box-shadow:0 12px 40px rgba(0,0,0,.5);padding:14px 16px;color:#e6e9ef;font-size:13px;line-height:1.5}",
321
323
  ".dshBalancePop::-webkit-scrollbar{width:8px}",
322
324
  ".dshBalancePop::-webkit-scrollbar-thumb{background:rgba(255,255,255,.18);border-radius:4px}",
323
- ".dshBalancePop h4{margin:0 0 10px;font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px}",
325
+ ".dshBalancePop h4{margin:0 0 10px;padding-bottom:10px;border-bottom:1px solid rgba(255,255,255,.08);font-size:14px;font-weight:600;display:flex;align-items:center;gap:8px}",
324
326
  ".dshBalanceRow{display:flex;justify-content:space-between;gap:12px;padding:3px 0}",
325
327
  ".dshBalanceRow span:first-child{color:#9aa4b2}",
326
328
  ".dshBalanceRow span:last-child{font-variant-numeric:tabular-nums}",
329
+ ".dshBalanceDivider{height:0;margin:8px 0;border-top:1px solid rgba(255,255,255,.08)}",
327
330
  // Footer 内的按钮不许被 flex 压缩成单字符宽度导致文字竖排(英文 label 更长时容易触发)。
328
331
  ".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}",
332
+ ".dshBalanceFoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:6px;margin-top:12px;padding-top:10px;color:#7b8494;font-size:11px}",
330
333
  ".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}",
331
334
  ".dshBalanceBtn:hover{background:#363d47}",
332
335
  ".dshBalanceVersion{display:flex;align-items:center;gap:6px;font-size:11px;color:#7b8494}",
@@ -340,7 +343,7 @@ window.__ModuleLoader__.load({
340
343
  ".dshBalanceDot[data-state=ok]{background:#3fb950}",
341
344
  ".dshBalanceDot[data-state=err]{background:#f85149}",
342
345
  ".dshBalanceDot[data-state=loading],.dshBalanceDot[data-state=muted]{background:#d29922}",
343
- ".dshBalanceUsage{display:block;margin-top:12px;padding-top:11px;border-top:1px solid rgba(255,255,255,.08)}",
346
+ ".dshBalanceUsage{display:block;margin-top:6px}",
344
347
  ".dshBalanceUsageTitle{font-size:11px;color:#7b8494;letter-spacing:.04em;margin:0 0 2px;display:flex;align-items:baseline;gap:6px}",
345
348
  ".dshBalanceUsageTitle b{color:#9aa4b2;font-weight:600;text-transform:uppercase}",
346
349
  ".dshBalanceDaily{margin-top:6px;font-size:12px}",
@@ -372,11 +375,23 @@ window.__ModuleLoader__.load({
372
375
  ".dshBalanceProvCookie{appearance:none;cursor:pointer;background:transparent;border:0;color:#58a6ff;font:inherit;font-size:11px;text-decoration:underline;padding:0 4px;flex:none}",
373
376
  ".dshBalanceProvCookie:hover{color:#79b8ff}",
374
377
  ".dshBalanceTabs{display:flex;gap:6px;margin-bottom:10px;border-bottom:1px solid rgba(255,255,255,.08);padding-bottom:8px}",
375
- ".dshBalanceTab{appearance:none;cursor:pointer;background:transparent;border:0;color:#9aa4b2;font:inherit;font-size:13px;padding:5px 10px;border-radius:7px;display:flex;align-items:center;gap:6px;flex:1;justify-content:center;white-space:nowrap}",
376
- ".dshBalanceTab:hover{color:#e6e9ef;background:rgba(255,255,255,.05)}",
377
- ".dshBalanceTab.active{color:#e6e9ef;background:#2d333b;border:1px solid rgba(255,255,255,.12)}",
378
+ ".dshBalanceTabsRow{display:flex;align-items:flex-end;gap:8px;margin-bottom:0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.08)}",
379
+ ".dshBalanceTabsRow .dshBalanceTabs{margin-bottom:0;border-bottom:none;padding-bottom:0;flex:1}",
380
+ ".dshBalanceTabContent{padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,.08)}",
381
+ ".dshBalanceSwitchBtn{appearance:none;cursor:pointer;flex-shrink:0;background:#2d333b;border:1px solid rgba(255,255,255,.14);color:#e6e9ef;font:inherit;font-size:12px;padding:5px 10px;border-radius:7px;white-space:nowrap}",
382
+ ".dshBalanceSwitchBtn:hover{background:#353c47}",
383
+ ".dshBalanceTab{position:relative;appearance:none;cursor:default;background:transparent;border:0;color:#e6e9ef;font:inherit;font-size:13px;padding:5px 10px;border-radius:7px;display:flex;align-items:center;gap:6px;flex:0 1 auto;justify-content:flex-start;white-space:nowrap}",
384
+ ".dshBalanceTab.active{padding-left:0}",
378
385
  ".dshBalanceTabBal{font-size:12px;font-weight:600;color:#3fb950;font-variant-numeric:tabular-nums}",
379
386
  ".dshBalanceTabBal[data-state=err]{color:#f85149}",
387
+ ".dshBalanceTabTitle{font-size:14px;font-weight:600}",
388
+ ".dshBalanceTabMenu{appearance:none;cursor:pointer;margin-left:4px;font-size:16px;font-weight:600;line-height:1;color:#7b8494;user-select:none;width:18px;height:18px;display:flex;align-items:center;justify-content:center;border-radius:4px}",
389
+ ".dshBalanceTabMenu:hover{color:#f85149;background:rgba(248,81,73,.12)}",
390
+ ".dshBalanceCtxMenu{position:absolute;z-index:10000;background:#161b22;border:1px solid rgba(255,255,255,.12);border-radius:6px;box-shadow:0 8px 24px rgba(0,0,0,.35);padding:4px 0;min-width:120px}",
391
+ ".dshBalanceCtxMenuItem{width:100%;text-align:left;background:transparent;border:none;color:#c9d1d9;padding:6px 12px;font-size:13px;cursor:pointer;font:inherit}",
392
+ ".dshBalanceCtxMenuItem:hover{background:#21262d;color:#58a6ff}",
393
+ ".dshBalanceCtxMenuItem:disabled{cursor:default;color:#5b626a}",
394
+ ".dshBalanceCtxMenuItem:disabled:hover{background:transparent;color:#5b626a}",
380
395
  ".dshBalanceProvActions{display:flex;gap:8px;margin-top:10px;align-items:center}",
381
396
  ".dshBalanceProvActions .dshBalanceProvRemove{font-size:12px;color:#7b8494}",
382
397
  ".dshBalanceProvActions .dshBalanceProvRemove:hover{color:#f85149}",
@@ -815,10 +830,31 @@ window.__ModuleLoader__.load({
815
830
  const saveBtn = popRef.querySelector("[data-save]");
816
831
  if (saveBtn !== null) saveBtn.addEventListener("click", () => onSaveProvider());
817
832
  popRef.querySelectorAll("[data-tab]").forEach((tb) => {
818
- tb.addEventListener("click", () => { activeProviderId = tb.getAttribute("data-tab"); renderValue(lastEnvelope, lastUsage); renderPopover(); });
833
+ tb.addEventListener("click", () => {
834
+ closeContextMenu();
835
+ });
819
836
  });
820
- popRef.querySelectorAll("[data-remove]").forEach((rb) => {
821
- rb.addEventListener("click", () => onRemoveProvider(rb.getAttribute("data-remove")));
837
+ const tabsEl = popRef.querySelector("[data-tabs]");
838
+ if (tabsEl !== null) {
839
+ tabsEl.style.cursor = "default";
840
+ }
841
+ const switchBtn = popRef.querySelector("[data-switch]");
842
+ if (switchBtn !== null) {
843
+ switchBtn.addEventListener("click", () => {
844
+ if (providers.length <= 1) return;
845
+ const rect = switchBtn.getBoundingClientRect();
846
+ const popRect = popRef.getBoundingClientRect();
847
+ renderProviderSwitchMenu(rect.left - popRect.left, rect.bottom - popRect.top);
848
+ });
849
+ }
850
+ popRef.querySelectorAll("[data-tab-remove]").forEach((mb) => {
851
+ mb.addEventListener("click", (e) => {
852
+ e.preventDefault();
853
+ e.stopPropagation();
854
+ const tabEl = mb.closest("[data-tab]");
855
+ if (!tabEl) return;
856
+ onRemoveProvider(tabEl.getAttribute("data-tab"));
857
+ });
822
858
  });
823
859
  const kindSel = popRef.querySelector("[data-f-kind]");
824
860
  if (kindSel !== null) kindSel.addEventListener("change", (e) => { addFormState.kind = e.target.value; });
@@ -839,6 +875,14 @@ window.__ModuleLoader__.load({
839
875
  setTimeout(() => aiGuideBtn.textContent = t("aiGuide"), 1500);
840
876
  });
841
877
  }
878
+ const aiGuideCookieBtn = popRef.querySelector("[data-ai-guide-cookie]");
879
+ if (aiGuideCookieBtn !== null) {
880
+ aiGuideCookieBtn.addEventListener("click", () => {
881
+ cookieEditId = aiGuideCookieBtn.getAttribute("data-provider-id");
882
+ cookieManualMode = false;
883
+ renderPopover();
884
+ });
885
+ }
842
886
  const cookieCancel = popRef.querySelector("[data-cookie-cancel]");
843
887
  if (cookieCancel !== null) cookieCancel.addEventListener("click", () => { cookieEditId = null; cookieEditValue = ""; cookieExtracted = ""; cookieManualMode = false; renderPopover(); });
844
888
  const cookieSave = popRef.querySelector("[data-cookie-save]");
@@ -925,6 +969,7 @@ window.__ModuleLoader__.load({
925
969
  if (usageSrc && usageSrc.todayTokens) {
926
970
  rows += '<div class="dshBalanceRow"><span>' + t("todayTokens") + "</span><span>" + String(usageSrc.todayTokens) + "</span></div>";
927
971
  }
972
+ rows += '<div class="dshBalanceDivider"></div>';
928
973
  return rows + usageHtmlOf(usageEnvelope);
929
974
  }
930
975
 
@@ -985,7 +1030,10 @@ window.__ModuleLoader__.load({
985
1030
  rows += '<div class="dshBalanceRow"><span>' + t("todaySpend") + "</span><span>" + (today ? fmtYuan(today.cost) : "¥0.00") + "</span></div>";
986
1031
  rows += '<div class="dshBalanceRow"><span>' + t("todayTokens") + "</span><span>" + (today ? fmtInt(today.tokens) : "0") + "</span></div>";
987
1032
  // 本月消耗区块(与 DeepSeek 同款样式)
988
- rows += mimoUsageHtml(usage);
1033
+ const usageBlock = mimoUsageHtml(usage);
1034
+ if (usageBlock) {
1035
+ rows += '<div class="dshBalanceDivider"></div>' + usageBlock;
1036
+ }
989
1037
  return rows;
990
1038
  }
991
1039
 
@@ -1020,8 +1068,12 @@ window.__ModuleLoader__.load({
1020
1068
  if (bal.ok === false) { balanceText = bal.needCookie ? t("needSetup") : t("failed"); stateAttr = ' data-state="err"'; }
1021
1069
  else balanceText = formatProviderBalance(bal.data);
1022
1070
  }
1071
+ const removeBtn =
1072
+ '<span class="dshBalanceTabMenu" data-tab-remove type="button" role="button" aria-label="' + t("remove") + '" title="' + t("remove") + '">×</span>';
1073
+ const titleCls = isActive ? ' class="dshBalanceTabTitle"' : '';
1074
+ const balanceSpan = isActive ? '' : '<span class="dshBalanceTabBal"' + stateAttr + ">" + balanceText + "</span>";
1023
1075
  return '<button class="dshBalanceTab' + (isActive ? " active" : "") + '" type="button" data-tab="' + escapeHtml(p.id) + '">' +
1024
- escapeHtml(label) + '<span class="dshBalanceTabBal"' + stateAttr + ">" + balanceText + "</span></button>";
1076
+ '<span' + titleCls + '>' + escapeHtml(label) + '</span>' + balanceSpan + removeBtn + "</button>";
1025
1077
  }
1026
1078
 
1027
1079
  /** Build the detail pane for the currently active provider. */
@@ -1030,15 +1082,16 @@ window.__ModuleLoader__.load({
1030
1082
  if (p.kind === "deepseek") detail = deepseekDetailHtml(lastEnvelope, lastUsage);
1031
1083
  else if (p.kind === "mimo") detail = mimoDetailHtml(bal);
1032
1084
 
1085
+ // 移除操作已放到 provider tab 的右键菜单里,详情区不再显示。
1033
1086
  let actions = "";
1034
- // 所有 provider 详情操作区统一只保留「× 移除」(AI 引导配置入口保留在添加表单里)。
1035
- actions = '<div class="dshBalanceProvActions">' +
1036
- '<button class="dshBalanceProvRemove" type="button" data-remove="' + escapeHtml(p.id) + '" title="' + t("remove") + '">× ' + t("remove") + "</button></div>";
1037
1087
 
1038
1088
  let errHint = "";
1039
1089
  if (bal && bal.ok === false) {
1040
1090
  if (bal.needCookie) {
1041
- errHint = '<div class="dshBalanceHint" style="margin-top:8px">' + t("mimoCookieHint") + "</div>";
1091
+ errHint = '<div class="dshBalanceHint" style="margin-top:8px">' + t("mimoCookieHint") + "</div>" +
1092
+ '<div class="dshBalanceProvActions" style="margin-top:8px">' +
1093
+ '<button class="dshBalanceBtn" type="button" data-ai-guide-cookie data-provider-id="' + escapeHtml(p.id) + '">' + t("aiGuide") + "</button>" +
1094
+ "</div>";
1042
1095
  } else {
1043
1096
  const raw = bal.data && bal.data.raw ? String(bal.data.raw).slice(0, 260) : "";
1044
1097
  errHint = '<div class="dshBalanceHint" style="margin-top:8px">' + t("readFailed", String(bal.error || "").slice(0, 50)) +
@@ -1133,9 +1186,12 @@ window.__ModuleLoader__.load({
1133
1186
  body += '<div class="dshBalanceHint">' + t("noProviders") + "</div>";
1134
1187
  } else {
1135
1188
  const activeP = providers.find((p) => p.id === activeProviderId) || activeProvider();
1136
- body += '<div class="dshBalanceTabs">';
1137
- for (const p of providers) {
1138
- body += providerTabHtml(p, balances[p.id], p.id === activeP.id);
1189
+ body += '<div class="dshBalanceTabsRow">';
1190
+ body += '<div class="dshBalanceTabs" data-tabs>';
1191
+ body += providerTabHtml(activeP, balances[activeP.id], true);
1192
+ body += '</div>';
1193
+ if (providers.length > 1) {
1194
+ body += '<button class="dshBalanceSwitchBtn" type="button" data-switch title="' + t("switch") + '">' + t("switch") + "</button>";
1139
1195
  }
1140
1196
  body += '</div><div class="dshBalanceTabContent">' + providerContentHtml(activeP, balances[activeP.id]) + "</div>";
1141
1197
  }
@@ -1198,7 +1254,6 @@ window.__ModuleLoader__.load({
1198
1254
 
1199
1255
  /** Handle the "remove" action for a provider. */
1200
1256
  async function onRemoveProvider(id) {
1201
- if (id === "deepseek") return;
1202
1257
  if (!window.confirm(t("confirmRemove"))) return;
1203
1258
  await removeProvider(id);
1204
1259
  if (activeProviderId === id) activeProviderId = null;
@@ -1221,9 +1276,65 @@ window.__ModuleLoader__.load({
1221
1276
  }
1222
1277
  }
1223
1278
 
1279
+ /** Close the context menu if it is open. */
1280
+ function closeContextMenu() {
1281
+ if (contextMenuEl !== null) {
1282
+ contextMenuEl.remove();
1283
+ contextMenuEl = null;
1284
+ }
1285
+ }
1286
+
1287
+ /** Render a dropdown menu to switch between providers. */
1288
+ function renderProviderSwitchMenu(x, y) {
1289
+ closeContextMenu();
1290
+ if (popRef === null || providers.length <= 1) return;
1291
+ const menu = document.createElement("div");
1292
+ menu.className = "dshBalanceCtxMenu";
1293
+ for (const p of providers) {
1294
+ const meta = PROVIDER_META[p.kind] || { label: p.kind };
1295
+ const label = p.label || meta.label;
1296
+ const bal = balances[p.id];
1297
+ let balanceText = "—";
1298
+ if (bal) {
1299
+ if (bal.ok === false) balanceText = bal.needCookie ? t("needSetup") : t("failed");
1300
+ else balanceText = formatProviderBalance(bal.data);
1301
+ }
1302
+ const item = document.createElement("button");
1303
+ item.className = "dshBalanceCtxMenuItem";
1304
+ item.type = "button";
1305
+ item.textContent = label + " " + balanceText;
1306
+ if (p.id === activeProviderId) {
1307
+ item.disabled = true;
1308
+ item.title = currentLang === "en" ? "Currently viewing" : "当前正在查看";
1309
+ } else {
1310
+ item.addEventListener("click", () => {
1311
+ closeContextMenu();
1312
+ activeProviderId = p.id;
1313
+ renderValue(lastEnvelope, lastUsage);
1314
+ renderPopover();
1315
+ });
1316
+ }
1317
+ menu.appendChild(item);
1318
+ }
1319
+ popRef.appendChild(menu);
1320
+ contextMenuEl = menu;
1321
+ const popRect = popRef.getBoundingClientRect();
1322
+ let left = x;
1323
+ let top = y;
1324
+ const menuW = menu.offsetWidth || 160;
1325
+ const menuH = menu.offsetHeight || 100;
1326
+ if (left + menuW > popRect.width) left = Math.max(4, popRect.width - menuW - 4);
1327
+ if (left < 4) left = 4;
1328
+ if (top + menuH > popRect.height) top = Math.max(4, popRect.height - menuH - 4);
1329
+ if (top < 4) top = 4;
1330
+ menu.style.left = left + "px";
1331
+ menu.style.top = top + "px";
1332
+ }
1333
+
1224
1334
  /** Close the popover and detach the outside-click listener. */
1225
1335
  function closePopover() {
1226
1336
  if (popRef === null) return;
1337
+ closeContextMenu();
1227
1338
  popRef.remove();
1228
1339
  popRef = null;
1229
1340
  if (outsideHandler !== null) {
@@ -1271,6 +1382,10 @@ window.__ModuleLoader__.load({
1271
1382
  pop.className = "dshBalancePop";
1272
1383
  document.body.appendChild(pop);
1273
1384
  popRef = pop;
1385
+ // 点击弹窗内非菜单区域时收起右键菜单。
1386
+ pop.addEventListener("mousedown", (e) => {
1387
+ if (contextMenuEl && !contextMenuEl.contains(e.target)) closeContextMenu();
1388
+ });
1274
1389
  // renderPopover() 内部会按入口位置重新定位弹窗。
1275
1390
  renderPopover();
1276
1391
 
@@ -1306,6 +1421,10 @@ window.__ModuleLoader__.load({
1306
1421
  let cookieExtracted = "";
1307
1422
  /** When true, the cookie editor shows the manual textarea instead of the AI chat guide. */
1308
1423
  let cookieManualMode = false;
1424
+ /** Provider switch dropdown menu state. */
1425
+ let contextMenuX = 0;
1426
+ let contextMenuY = 0;
1427
+ let contextMenuEl = null;
1309
1428
  /** Result of the most recent /ai-balances-sync call (used for UI hint). */
1310
1429
  let lastSyncResult = null;
1311
1430
  let timer = null;
package/lib/index.js CHANGED
@@ -231,8 +231,12 @@ async function syncProvidersFromDshSettings() {
231
231
  skipped++;
232
232
  continue;
233
233
  }
234
- store.push({ ...cand, enabled: true });
235
- added++;
234
+ // 仅当 store 完全为空(首次初始化,或用户清空了全部 provider)时,才从 dsh 设置自动加回;
235
+ // 否则用户已主动删除该项,不自动复活,确保「删除」持久生效(重启 dsh 也不会回来)。
236
+ if (store.length === 0) {
237
+ store.push({ ...cand, enabled: true });
238
+ added++;
239
+ }
236
240
  }
237
241
  if (added > 0) await writeStore(store);
238
242
 
@@ -1086,11 +1090,6 @@ function apply(ctx) {
1086
1090
  res.end(JSON.stringify({ ok: false, error: "缺少 id" }));
1087
1091
  return;
1088
1092
  }
1089
- if (id === "deepseek") {
1090
- res.writeHead(400, { "content-type": "application/json; charset=utf-8" });
1091
- res.end(JSON.stringify({ ok: false, error: "DeepSeek 不能移除" }));
1092
- return;
1093
- }
1094
1093
  const store = await ensureStore();
1095
1094
  const next = store.filter((p) => p.id !== id);
1096
1095
  await writeStore(next);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-deepseek-balance-widget",
3
- "version": "2.1.0",
3
+ "version": "2.2.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": [