@linxin666/dsh-usage 0.3.22 → 0.3.24

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.
Files changed (40) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +6 -8
  3. package/README.zh.md +6 -8
  4. package/lib/client.js +749 -78
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +171 -201
  7. package/lib/types/client/UsageSectionCard.d.ts +0 -1
  8. package/lib/types/client/UsageSidebarPanel.d.ts +34 -0
  9. package/lib/types/client/body-mutations.d.ts +45 -0
  10. package/lib/types/client/locales.d.ts +9 -4
  11. package/lib/types/client/sidebar-entry-core.d.ts +87 -0
  12. package/lib/types/client/sidebar-entry.d.ts +35 -0
  13. package/lib/types/client/sidebar-panel-mount.d.ts +40 -0
  14. package/lib/types/client/voucher.d.ts +2 -2
  15. package/lib/types/core/provider-routes.d.ts +26 -0
  16. package/lib/types/core/types.d.ts +14 -1
  17. package/lib/types/host/access.d.ts +17 -0
  18. package/lib/types/host/pair-access.d.ts +34 -0
  19. package/lib/types/host/routes.d.ts +16 -7
  20. package/lib/types/host/usage-service.d.ts +28 -73
  21. package/lib/types/index.d.ts +0 -2
  22. package/package.json +4 -3
  23. package/src/client/UsageSectionCard.tsx +1 -15
  24. package/src/client/UsageSidebarPanel.tsx +196 -0
  25. package/src/client/body-mutations.ts +133 -0
  26. package/src/client/index.ts +22 -1
  27. package/src/client/locales.ts +20 -10
  28. package/src/client/sidebar-entry-core.ts +321 -0
  29. package/src/client/sidebar-entry.ts +101 -0
  30. package/src/client/sidebar-panel-mount.tsx +113 -0
  31. package/src/client/usage.module.css +252 -0
  32. package/src/client/voucher.ts +2 -2
  33. package/src/core/adapters.ts +40 -6
  34. package/src/core/provider-routes.ts +37 -0
  35. package/src/core/types.ts +14 -1
  36. package/src/host/access.ts +21 -0
  37. package/src/host/pair-access.ts +49 -0
  38. package/src/host/routes.ts +19 -10
  39. package/src/host/usage-service.ts +65 -214
  40. package/src/index.ts +1 -6
package/lib/client.js CHANGED
@@ -5,6 +5,7 @@ window.__ModuleLoader__.load({
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
+ let react_dom_client = require("react-dom/client");
8
9
  let react_jsx_runtime = require("react/jsx-runtime");
9
10
  //#region \0dsh-store-engine
10
11
  const platform = ["@deepseek-ai/dsh-client", "-store"].join("");
@@ -97,12 +98,8 @@ window.__ModuleLoader__.load({
97
98
  "usage.config.title": "设置",
98
99
  "usage.config.enabled": "启用插件",
99
100
  "usage.config.pollIntervalSec": "轮询间隔(秒)",
100
- "usage.config.bubbleMode": "宠物气泡",
101
- "usage.config.bubbleMode.always": "常驻显示",
102
- "usage.config.bubbleMode.change": "仅变化时",
103
- "usage.config.bubbleMode.off": "关闭",
104
101
  "usage.bank.title": "鲸元券",
105
- "usage.bank.hint": "官方 API 每消耗 1000 tokens 铸造 1 鲸元;保存或分享这张票券。",
102
+ "usage.bank.hint": "官方 API 每消耗 100 万 tokens 铸造 1 鲸元;保存或分享这张票券。",
106
103
  "usage.bank.noUsage": "暂无 DeepSeek 官方用量数据(统计自插件启用起)",
107
104
  "usage.bank.minted": "累计铸造 {minted} 鲸元({tokens} tokens)",
108
105
  "usage.bank.spend.observed": "官方余额实测花费 ¥{cost}(自 {since} 起)",
@@ -110,7 +107,16 @@ window.__ModuleLoader__.load({
110
107
  "usage.bank.window": "统计窗口 {from} ~ {to}",
111
108
  "usage.bank.save": "保存图片",
112
109
  "usage.bank.share": "分享",
113
- "usage.bank.drawError": "票券生成失败:{error}"
110
+ "usage.bank.drawError": "票券生成失败:{error}",
111
+ "usage.sidebar.entry.label": "用量",
112
+ "usage.sidebar.entry.tooltip": "用量面板:套餐配额与余额",
113
+ "usage.sidebar.toggle.collapse": "折叠",
114
+ "usage.sidebar.toggle.expand": "展开",
115
+ "usage.sidebar.empty": "没有已配置的套餐或余额数据。",
116
+ "usage.sidebar.error": "加载失败:{error}",
117
+ "usage.sidebar.loading": "正在加载用量数据…",
118
+ "usage.sidebar.balanceLeft": "剩余 {balance}",
119
+ "usage.sidebar.todayUsage": "今日 {tokens} tokens"
114
120
  };
115
121
  /** English mirror; every zh key present. */
116
122
  const en = {
@@ -153,12 +159,8 @@ window.__ModuleLoader__.load({
153
159
  "usage.config.title": "Settings",
154
160
  "usage.config.enabled": "Enable plugin",
155
161
  "usage.config.pollIntervalSec": "Poll interval (seconds)",
156
- "usage.config.bubbleMode": "Pet bubble",
157
- "usage.config.bubbleMode.always": "Always visible",
158
- "usage.config.bubbleMode.change": "On change",
159
- "usage.config.bubbleMode.off": "Off",
160
162
  "usage.bank.title": "Whale-yuan voucher",
161
- "usage.bank.hint": "Every 1,000 tokens spent on the official API mint one whale yuan; save or share the note.",
163
+ "usage.bank.hint": "Every 1,000,000 tokens spent on the official API mint one whale yuan; save or share the note.",
162
164
  "usage.bank.noUsage": "No official DeepSeek usage yet (counting starts when the plugin is enabled)",
163
165
  "usage.bank.minted": "Minted {minted} whale yuan ({tokens} tokens)",
164
166
  "usage.bank.spend.observed": "Spent ¥{cost} observed on the official balance (watching since {since})",
@@ -166,7 +168,16 @@ window.__ModuleLoader__.load({
166
168
  "usage.bank.window": "Window {from} - {to}",
167
169
  "usage.bank.save": "Save image",
168
170
  "usage.bank.share": "Share",
169
- "usage.bank.drawError": "Failed to render the voucher: {error}"
171
+ "usage.bank.drawError": "Failed to render the voucher: {error}",
172
+ "usage.sidebar.entry.label": "Usage",
173
+ "usage.sidebar.entry.tooltip": "Usage panel: plan quotas and balances",
174
+ "usage.sidebar.toggle.collapse": "Collapse",
175
+ "usage.sidebar.toggle.expand": "Expand",
176
+ "usage.sidebar.empty": "No plan or balance data configured.",
177
+ "usage.sidebar.error": "Loading failed: {error}",
178
+ "usage.sidebar.loading": "Loading usage data…",
179
+ "usage.sidebar.balanceLeft": "{balance} left",
180
+ "usage.sidebar.todayUsage": "{tokens} tokens today"
170
181
  };
171
182
  /**
172
183
  * Active dictionary, picked by the document language at call time. The
@@ -184,7 +195,7 @@ window.__ModuleLoader__.load({
184
195
  }
185
196
  //#endregion
186
197
  //#region \0dsh-css:packages/dsh-usage/src/client/usage.module.css.mjs
187
- const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_chart{flex-direction:column;gap:12px;display:flex}.cvtkAW_chartProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_chartHead{justify-content:space-between;align-items:baseline;gap:10px;font-size:13px;display:flex}.cvtkAW_chartTokens{font-variant-numeric:tabular-nums;opacity:.65;white-space:nowrap;font-size:11px}.cvtkAW_chartBar{background:color-mix(in srgb, currentColor 8%, transparent);border-radius:999px;height:8px;display:block;overflow:hidden}.cvtkAW_chartFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_chartModel{opacity:.8;grid-template-columns:minmax(80px,180px) 1fr auto;align-items:center;gap:8px;padding-left:14px;font-size:11px;display:grid}.cvtkAW_chartModelName{text-overflow:ellipsis;white-space:nowrap;opacity:.8;overflow:hidden}.cvtkAW_chartModelBar{background:color-mix(in srgb, currentColor 6%, transparent);border-radius:999px;height:4px;display:block;overflow:hidden}.cvtkAW_chartModelFill{background:color-mix(in srgb, currentColor 35%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_voucherPreview canvas{border-radius:8px;width:100%;height:auto;display:block}.cvtkAW_buttonRow{gap:8px;display:flex}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}";
198
+ const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_chart{flex-direction:column;gap:12px;display:flex}.cvtkAW_chartProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_chartHead{justify-content:space-between;align-items:baseline;gap:10px;font-size:13px;display:flex}.cvtkAW_chartTokens{font-variant-numeric:tabular-nums;opacity:.65;white-space:nowrap;font-size:11px}.cvtkAW_chartBar{background:color-mix(in srgb, currentColor 8%, transparent);border-radius:999px;height:8px;display:block;overflow:hidden}.cvtkAW_chartFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_chartModel{opacity:.8;grid-template-columns:minmax(80px,180px) 1fr auto;align-items:center;gap:8px;padding-left:14px;font-size:11px;display:grid}.cvtkAW_chartModelName{text-overflow:ellipsis;white-space:nowrap;opacity:.8;overflow:hidden}.cvtkAW_chartModelBar{background:color-mix(in srgb, currentColor 6%, transparent);border-radius:999px;height:4px;display:block;overflow:hidden}.cvtkAW_chartModelFill{background:color-mix(in srgb, currentColor 35%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_voucherPreview canvas{border-radius:8px;width:100%;height:auto;display:block}.cvtkAW_buttonRow{gap:8px;display:flex}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_entry{box-sizing:border-box;width:100%;height:36px;color:var(--dsw-alias-label-secondary);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 10px;font-size:13px;display:flex}.cvtkAW_entry:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}.cvtkAW_entryIcon{flex:none;justify-content:center;align-items:center;width:24px;height:24px;display:inline-flex}.cvtkAW_entryIcon svg{width:18px;height:18px;display:block}.cvtkAW_entryLabel{text-overflow:ellipsis;overflow:hidden}.cvtkAW_entryMain{appearance:none;min-width:0;height:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:none;flex:1;align-items:center;gap:8px;padding:0;display:flex}.cvtkAW_entryAction{appearance:none;width:22px;height:22px;color:inherit;cursor:pointer;opacity:.55;background:0 0;border:none;border-radius:6px;flex:none;justify-content:center;align-items:center;padding:0;transition:opacity .12s,background-color .12s;display:inline-flex}.cvtkAW_entry:hover .cvtkAW_entryAction{opacity:.8}.cvtkAW_entryAction:hover{opacity:1;background:color-mix(in srgb, currentColor 10%, transparent)}.cvtkAW_entryAction:focus-visible,.cvtkAW_entryMain:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_entryAction:disabled{cursor:default;opacity:.4}.cvtkAW_entryAction svg{width:14px;height:14px;display:block}.cvtkAW_entryAction:disabled svg{animation:.9s linear infinite cvtkAW_usageEntrySpin}@keyframes cvtkAW_usageEntrySpin{to{transform:rotate(360deg)}}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entry,[data-sidebar-collapsed] .cvtkAW_entry{border-radius:50%;justify-content:center;width:36px;height:36px;margin:0 auto 12px;padding:0}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entryMain,[data-sidebar-collapsed] .cvtkAW_entryMain{flex:none;justify-content:center;width:100%}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entryAction,[data-sidebar-collapsed] .cvtkAW_entryAction{display:none}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_sidebarSummary,[data-sidebar-collapsed] .cvtkAW_sidebarSummary{display:none}[data-dsh-frame][data-sidebar-collapsed] .cvtkAW_entryLabel,[data-sidebar-collapsed] .cvtkAW_entryLabel{display:none}.cvtkAW_sidebarSummary{color:inherit;opacity:.75;white-space:nowrap;align-items:baseline;gap:6px;margin:0 4px 12px;padding:0 10px;font-size:11px;display:flex;overflow:hidden}.cvtkAW_sidebarSummaryName{text-overflow:ellipsis;flex:none;max-width:40%;font-weight:600;overflow:hidden}.cvtkAW_sidebarSummaryFacts{text-overflow:ellipsis;font-variant-numeric:tabular-nums;overflow:hidden}.cvtkAW_sidebarPanel{border:1px solid color-mix(in srgb, currentColor 14%, transparent);color:inherit;background:color-mix(in srgb, currentColor 4%, transparent);border-radius:10px;flex-direction:column;gap:8px;margin:0 4px 12px;padding:10px 10px 12px;display:flex}.cvtkAW_sidebarProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_sidebarProviderHead{justify-content:space-between;align-items:baseline;gap:8px;font-size:12px;display:flex}.cvtkAW_sidebarProviderName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.cvtkAW_sidebarBalance{opacity:.75;flex:none}.cvtkAW_sidebarWindowRow{opacity:.85;align-items:center;gap:6px;font-size:11px;display:flex}.cvtkAW_sidebarBar{background:color-mix(in srgb, currentColor 14%, transparent);border-radius:2px;flex:1;height:4px;overflow:hidden}.cvtkAW_sidebarBarFill{background:currentColor;border-radius:2px;height:100%;display:block}.cvtkAW_sidebarMuted{opacity:.6;font-size:11px}";
188
199
  const tagId = "@linxin666/dsh-usage/packages/dsh-usage/src/client/usage.module.css";
189
200
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
190
201
  const tag = document.createElement("style");
@@ -213,6 +224,11 @@ window.__ModuleLoader__.load({
213
224
  "chartTokens": "cvtkAW_chartTokens",
214
225
  "currentBadge": "cvtkAW_currentBadge",
215
226
  "currentProvider": "cvtkAW_currentProvider",
227
+ "entry": "cvtkAW_entry",
228
+ "entryAction": "cvtkAW_entryAction",
229
+ "entryIcon": "cvtkAW_entryIcon",
230
+ "entryLabel": "cvtkAW_entryLabel",
231
+ "entryMain": "cvtkAW_entryMain",
216
232
  "errorLine": "cvtkAW_errorLine",
217
233
  "header": "cvtkAW_header",
218
234
  "muted": "cvtkAW_muted",
@@ -228,6 +244,18 @@ window.__ModuleLoader__.load({
228
244
  "section": "cvtkAW_section",
229
245
  "settingItem": "cvtkAW_settingItem",
230
246
  "settingsGrid": "cvtkAW_settingsGrid",
247
+ "sidebarBalance": "cvtkAW_sidebarBalance",
248
+ "sidebarBar": "cvtkAW_sidebarBar",
249
+ "sidebarBarFill": "cvtkAW_sidebarBarFill",
250
+ "sidebarMuted": "cvtkAW_sidebarMuted",
251
+ "sidebarPanel": "cvtkAW_sidebarPanel",
252
+ "sidebarProvider": "cvtkAW_sidebarProvider",
253
+ "sidebarProviderHead": "cvtkAW_sidebarProviderHead",
254
+ "sidebarProviderName": "cvtkAW_sidebarProviderName",
255
+ "sidebarSummary": "cvtkAW_sidebarSummary",
256
+ "sidebarSummaryFacts": "cvtkAW_sidebarSummaryFacts",
257
+ "sidebarSummaryName": "cvtkAW_sidebarSummaryName",
258
+ "sidebarWindowRow": "cvtkAW_sidebarWindowRow",
231
259
  "stat": "cvtkAW_stat",
232
260
  "statLabel": "cvtkAW_statLabel",
233
261
  "statRow": "cvtkAW_statRow",
@@ -236,11 +264,356 @@ window.__ModuleLoader__.load({
236
264
  "tabActive": "cvtkAW_tabActive",
237
265
  "tabs": "cvtkAW_tabs",
238
266
  "trendAxis": "cvtkAW_trendAxis",
267
+ "usageEntrySpin": "cvtkAW_usageEntrySpin",
239
268
  "voucherPreview": "cvtkAW_voucherPreview",
240
269
  "windowLabel": "cvtkAW_windowLabel",
241
270
  "windowRow": "cvtkAW_windowRow"
242
271
  };
243
272
  //#endregion
273
+ //#region src/client/body-mutations.ts
274
+ /** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
275
+ const HUB_KEY = Symbol.for("dsh-web.body-mutation-hub");
276
+ const INVALIDATION_ONLY = Symbol.for("dsh-web.body-mutation-invalidation");
277
+ function needsRecords(subscribers) {
278
+ for (const listener of subscribers) if (!listener[INVALIDATION_ONLY]) return true;
279
+ return false;
280
+ }
281
+ /**
282
+ * Subscribe to a coalesced DOM re-check without retaining mutation records.
283
+ * The marked wrapper also works with an older hub, which delivers records
284
+ * that it simply ignores until a page reload picks up the updated hub.
285
+ */
286
+ function subscribeBodyInvalidations(subscriber) {
287
+ const listener = () => {
288
+ subscriber();
289
+ };
290
+ listener[INVALIDATION_ONLY] = true;
291
+ return subscribeBodyMutations(listener);
292
+ }
293
+ /**
294
+ * Subscribe to body-level childList mutations.
295
+ * @param subscriber - called at most once per animation frame with the records
296
+ * collected since the previous flush; must be safe to run repeatedly.
297
+ * @returns the disposer removing this subscriber (and the observer when it was
298
+ * the last one).
299
+ */
300
+ function subscribeBodyMutations(subscriber) {
301
+ if (typeof globalThis === "undefined" || typeof document === "undefined") return () => {};
302
+ if (typeof MutationObserver !== "function") return () => {};
303
+ const registry = globalThis;
304
+ let hub = registry[HUB_KEY];
305
+ if (hub === void 0) {
306
+ const subscribers = /* @__PURE__ */ new Set();
307
+ const created = {
308
+ observer: void 0,
309
+ subscribers,
310
+ pending: [],
311
+ scheduled: false
312
+ };
313
+ const flush = () => {
314
+ created.frame = void 0;
315
+ created.scheduled = false;
316
+ const batch = created.pending;
317
+ created.pending = [];
318
+ for (const listener of [...subscribers]) {
319
+ if (!subscribers.has(listener)) continue;
320
+ try {
321
+ listener(batch);
322
+ } catch {}
323
+ }
324
+ };
325
+ const schedule = () => {
326
+ if (created.scheduled) return;
327
+ created.scheduled = true;
328
+ if (typeof requestAnimationFrame === "function") created.frame = requestAnimationFrame(flush);
329
+ else flush();
330
+ };
331
+ created.observer = new MutationObserver((records) => {
332
+ if (needsRecords(subscribers)) for (const record of records) created.pending.push(record);
333
+ schedule();
334
+ });
335
+ created.observer.observe(document.body ?? document.documentElement, {
336
+ childList: true,
337
+ subtree: true
338
+ });
339
+ registry[HUB_KEY] = created;
340
+ hub = created;
341
+ }
342
+ const active = hub;
343
+ active.subscribers.add(subscriber);
344
+ let subscribed = true;
345
+ return () => {
346
+ if (!subscribed) return;
347
+ subscribed = false;
348
+ active.subscribers.delete(subscriber);
349
+ if (!needsRecords(active.subscribers)) active.pending = [];
350
+ if (active.subscribers.size === 0 && registry[HUB_KEY] === active) {
351
+ active.observer.disconnect();
352
+ if (active.frame !== void 0 && typeof cancelAnimationFrame === "function") cancelAnimationFrame(active.frame);
353
+ active.frame = void 0;
354
+ active.pending = [];
355
+ active.scheduled = false;
356
+ delete registry[HUB_KEY];
357
+ }
358
+ };
359
+ }
360
+ //#endregion
361
+ //#region src/client/sidebar-entry-core.ts
362
+ /**
363
+ * Shared sidebar entry injection core.
364
+ *
365
+ * dsh's sidebar shell exposes no slot an external plugin can register into,
366
+ * so the entry row is injected between the shell's New Session button and the
367
+ * workspace browser. The injection self-heals: a MutationObserver watches the
368
+ * sidebar root and re-inserts the row whenever a React re-render displaces it
369
+ * (re-insertion happens in the same frame, before paint, so no flicker).
370
+ *
371
+ * The row is plain DOM (no React tree) so it can never disturb the shell's
372
+ * reconciliation; the view it toggles is a separate root owned by the caller.
373
+ *
374
+ * Packages receive this file as a generated copy via scripts/sync-shared.mjs;
375
+ * edit the shared source and re-run the sync instead of editing a copy.
376
+ */
377
+ /** Find the sidebar shell root element, or undefined while not yet mounted. */
378
+ function sidebarRoot() {
379
+ const column = document.querySelector("[data-pane=\"sidebar\"], [class*=\"sidebarCol\"]");
380
+ if (column === null) return void 0;
381
+ return column.querySelector("[class*=\"logoRow\"]")?.parentElement ?? column.firstElementChild;
382
+ }
383
+ /** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
384
+ function newSessionButton(root) {
385
+ const nested = root.querySelector("button[class*=\"newSession\"]");
386
+ if (nested !== null) return nested;
387
+ for (const child of root.children) if (child.tagName === "BUTTON") return child;
388
+ }
389
+ /** Build the entry row (detached; inserted once the shell is up). */
390
+ function createEntry(options) {
391
+ const actions = options.actions ?? [];
392
+ const composite = actions.length > 0;
393
+ const entry = document.createElement(composite ? "div" : "button");
394
+ if (!composite) entry.type = "button";
395
+ entry.setAttribute(options.rowAttribute, "");
396
+ if (options.plugin !== void 0) {
397
+ entry.setAttribute("data-dsh-plugin", options.plugin);
398
+ entry.setAttribute("data-dsh-part", "sidebar-entry");
399
+ }
400
+ entry.className = options.css["entry"] ?? "";
401
+ const labelSpan = document.createElement("span");
402
+ labelSpan.className = options.css["entryLabel"] ?? "";
403
+ const iconSpan = document.createElement("span");
404
+ iconSpan.className = options.css["entryIcon"] ?? "";
405
+ iconSpan.innerHTML = options.icon;
406
+ const main = composite ? document.createElement("button") : entry;
407
+ if (composite) {
408
+ main.type = "button";
409
+ main.className = options.css["entryMain"] ?? "";
410
+ main.append(iconSpan, labelSpan);
411
+ entry.append(main);
412
+ } else entry.append(iconSpan, labelSpan);
413
+ const actionButtons = [];
414
+ for (const action of actions) {
415
+ const button = document.createElement("button");
416
+ button.type = "button";
417
+ button.className = options.css["entryAction"] ?? "";
418
+ button.setAttribute("data-dsh-entry-action", action.id);
419
+ button.innerHTML = action.icon;
420
+ button.addEventListener("click", () => {
421
+ action.onClick(button);
422
+ });
423
+ entry.append(button);
424
+ actionButtons.push({
425
+ action,
426
+ button
427
+ });
428
+ }
429
+ let open = false;
430
+ const applyActions = () => {
431
+ for (const { action, button } of actionButtons) {
432
+ button.innerHTML = open || action.inactiveIcon === void 0 ? action.icon : action.inactiveIcon;
433
+ const text = action.label(open);
434
+ button.setAttribute("aria-label", text);
435
+ button.setAttribute("title", text);
436
+ if (action.inactiveIcon !== void 0) button.setAttribute("aria-expanded", String(open));
437
+ }
438
+ };
439
+ const applyLabel = () => {
440
+ main.setAttribute("aria-label", options.label());
441
+ if (options.tooltip !== void 0) main.setAttribute("title", options.tooltip());
442
+ labelSpan.textContent = options.label();
443
+ applyActions();
444
+ };
445
+ applyLabel();
446
+ main.addEventListener("click", options.onToggle);
447
+ if (composite) entry.addEventListener("click", (event) => {
448
+ if (event.target === entry) options.onToggle();
449
+ });
450
+ return {
451
+ entry,
452
+ applyLabel,
453
+ setOpen: (next) => {
454
+ open = next;
455
+ applyActions();
456
+ }
457
+ };
458
+ }
459
+ /** Re-insert the entry after the New Session row (before the browser region). */
460
+ function placeEntry(root, entry, options) {
461
+ const button = newSessionButton(root);
462
+ if (button === void 0) return false;
463
+ if (entry.parentElement !== root) {
464
+ const row = button.closest("[class*=\"logoRow\"]");
465
+ const base = row !== null && row.parentElement === root ? row : button;
466
+ const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", ")));
467
+ const anchor = options.position === "before" ? family.length > 0 ? family[0] : base.nextElementSibling : family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling;
468
+ root.insertBefore(entry, anchor);
469
+ }
470
+ return true;
471
+ }
472
+ /**
473
+ * Mount the sidebar entry, waiting for the shell to render and self-healing
474
+ * on later React re-renders.
475
+ * @param options - the row's attribute/icon/copy/action/ordering configuration.
476
+ * @returns disposer removing the entry and its observers.
477
+ */
478
+ function mountSidebarEntry$1(options) {
479
+ if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {};
480
+ const { entry, applyLabel, setOpen } = createEntry(options);
481
+ let root;
482
+ let placed = false;
483
+ let unsubscribeRefresh;
484
+ if (options.refresh !== void 0) try {
485
+ unsubscribeRefresh = options.refresh.subscribe(applyLabel);
486
+ } catch {}
487
+ const tryPlace = () => {
488
+ if (root !== void 0 && !root.isConnected) {
489
+ rootObserver.disconnect();
490
+ root = void 0;
491
+ placed = false;
492
+ }
493
+ if (placed) {
494
+ if (document.body.contains(entry)) return;
495
+ rootObserver.disconnect();
496
+ root = void 0;
497
+ placed = false;
498
+ }
499
+ root ??= sidebarRoot();
500
+ if (root === void 0) return;
501
+ placed = placeEntry(root, entry, options);
502
+ if (placed) rootObserver.observe(root, {
503
+ childList: true,
504
+ subtree: true
505
+ });
506
+ };
507
+ const unsubscribeBody = subscribeBodyInvalidations(() => {
508
+ tryPlace();
509
+ });
510
+ const rootObserver = new MutationObserver(() => {
511
+ if (root === void 0 || !root.isConnected) {
512
+ placed = false;
513
+ tryPlace();
514
+ return;
515
+ }
516
+ if (!root.contains(entry)) placed = placeEntry(root, entry, options);
517
+ });
518
+ const unsubscribeActive = options.active === void 0 ? void 0 : (() => {
519
+ const syncActive = () => {
520
+ const open = options.active.isOpen();
521
+ if (open) entry.dataset.active = "true";
522
+ else delete entry.dataset.active;
523
+ setOpen(open);
524
+ };
525
+ const unsubscribe = options.active.subscribe(syncActive);
526
+ syncActive();
527
+ return unsubscribe;
528
+ })();
529
+ tryPlace();
530
+ return () => {
531
+ unsubscribeBody();
532
+ rootObserver.disconnect();
533
+ unsubscribeRefresh?.();
534
+ unsubscribeActive?.();
535
+ entry.remove();
536
+ };
537
+ }
538
+ //#endregion
539
+ //#region src/client/sidebar-entry.ts
540
+ /**
541
+ * Sidebar entry injection for the usage panel (issue #1592) — wiring over the
542
+ * shared core. Unlike the panel-takeover family (task board / SSH / skill
543
+ * center) this row does not participate in the family block, so it is placed
544
+ * directly under the New Session row and ordered after those entries. The row
545
+ * seats the panel's controls as trailing actions: a refresh command and a
546
+ * collapse chevron mirroring the open state, so the panel below renders only
547
+ * its content.
548
+ * @module @linxin666/dsh-usage/client/sidebar-entry
549
+ */
550
+ /** Stable data attribute identifying the injected entry row. */
551
+ const ENTRY_SELECTOR = "[data-dsh-usage-entry]";
552
+ /** Inline gauge glyph normalized to the shell's 18px navigation glyph size. */
553
+ const ICON = "<svg viewBox=\"0 0 16 16\" width=\"18\" height=\"18\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M2.5 12.5a7 7 0 0 1 11 -4.3\"/><path d=\"M8 12.5V8.2l2.9-2.1\"/><circle cx=\"8\" cy=\"12.5\" r=\"1\"/></svg>";
554
+ /** Refresh glyph: a clockwise circular arrow, 14px like the sibling action icons. */
555
+ const REFRESH_ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M13.4 8a5.4 5.4 0 1 1-1.58-3.82\"/><path d=\"M13.4 2.6v2.7h-2.7\"/></svg>";
556
+ /** Disclosure chevrons: down while expanded, right while collapsed. */
557
+ const CHEVRON_OPEN_ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m4 6 4 4 4-4\"/></svg>";
558
+ const CHEVRON_CLOSED_ICON = "<svg viewBox=\"0 0 16 16\" width=\"14\" height=\"14\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"m6 4 4 4-4 4\"/></svg>";
559
+ /** Refresh cooldown: one forced probe cycle per click, the icon spins meanwhile. */
560
+ const REFRESH_COOLDOWN_MS = 3e3;
561
+ /** Trailing actions seated at the row's right edge. */
562
+ function entryActions(handlers) {
563
+ return [{
564
+ id: "refresh",
565
+ icon: REFRESH_ICON,
566
+ label: () => t("usage.refresh"),
567
+ onClick: (button) => {
568
+ handlers.onRefresh();
569
+ button.disabled = true;
570
+ window.setTimeout(() => {
571
+ button.disabled = false;
572
+ }, REFRESH_COOLDOWN_MS);
573
+ }
574
+ }, {
575
+ id: "collapse",
576
+ icon: CHEVRON_OPEN_ICON,
577
+ inactiveIcon: CHEVRON_CLOSED_ICON,
578
+ label: (open) => t(open ? "usage.sidebar.toggle.collapse" : "usage.sidebar.toggle.expand"),
579
+ onClick: () => {
580
+ handlers.onToggle();
581
+ }
582
+ }];
583
+ }
584
+ /**
585
+ * Mount the usage sidebar entry.
586
+ * @param handlers - the panel's toggle/refresh callbacks and open-state face.
587
+ * @param locale - locale-change source; when given, re-applies the label on a
588
+ * language switch (the plain-DOM row otherwise keeps the mount-time copy).
589
+ * @returns disposer removing the entry and its observers.
590
+ */
591
+ function mountSidebarEntry(handlers, locale) {
592
+ return mountSidebarEntry$1({
593
+ rowAttribute: "data-dsh-usage-entry",
594
+ rowSelector: ENTRY_SELECTOR,
595
+ plugin: "usage",
596
+ icon: ICON,
597
+ css: usage_module_css_default,
598
+ label: () => t("usage.sidebar.entry.label"),
599
+ tooltip: () => t("usage.sidebar.entry.tooltip"),
600
+ refresh: locale === void 0 ? void 0 : { subscribe: (listener) => locale.subscribe(listener) },
601
+ onToggle: handlers.onToggle,
602
+ actions: entryActions(handlers),
603
+ position: "after",
604
+ familySelectors: [
605
+ "[data-dsh-taskboard-entry]",
606
+ "[data-dsh-ssh-entry]",
607
+ "[data-dsh-skill-explorer-entry]",
608
+ "[data-dsh-usage-entry]"
609
+ ],
610
+ active: {
611
+ subscribe: handlers.subscribeOpen,
612
+ isOpen: handlers.isOpen
613
+ }
614
+ });
615
+ }
616
+ //#endregion
244
617
  //#region src/core/adapters.ts
245
618
  /** Parse a string/number into a finite number, else undefined. */
246
619
  function toNum(value) {
@@ -254,6 +627,10 @@ window.__ModuleLoader__.load({
254
627
  function str(value) {
255
628
  return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
256
629
  }
630
+ /** Clamp a provider percentage into the renderable 0-100 band. */
631
+ function clampPercent(value) {
632
+ return Math.max(0, Math.min(100, value));
633
+ }
257
634
  /** Format a number to a fixed 2-decimal display string. */
258
635
  function money(value) {
259
636
  return value.toFixed(2);
@@ -386,7 +763,28 @@ window.__ModuleLoader__.load({
386
763
  }
387
764
  }
388
765
  };
389
- /** GLM Coding Plan quota; auth is the RAW key without a Bearer prefix. */
766
+ /**
767
+ * GLM Coding Plan quota windows, keyed by the provider's own `unit` code
768
+ * rather than by row position so an added or reordered limit cannot shift a
769
+ * window's meaning: 3 = the rolling 5-hour window, 6 = the weekly window,
770
+ * 5 = the monthly one. Two row kinds carry quota — `TOKENS_LIMIT` (token
771
+ * plans) and `CREDIT_LIMIT` (credit plans, whose exact ratio is
772
+ * `currentValue` / `usage`) — while `TIME_LIMIT` is the MCP request cap and
773
+ * is not a plan window at all. Auth is the RAW key without a Bearer prefix.
774
+ */
775
+ const GLM_UNIT_KEYS = {
776
+ 3: "5h",
777
+ 5: "month",
778
+ 6: "week"
779
+ };
780
+ /** Percent from a credit row's exact ratio, else the provider's own percentage. */
781
+ function glmCreditPercent(row) {
782
+ const used = toNum(row.currentValue);
783
+ const limit = toNum(row.usage);
784
+ if (used !== void 0 && limit !== void 0 && limit > 0) return clampPercent(used / limit * 100);
785
+ const percentage = toNum(row.percentage);
786
+ return percentage === void 0 ? void 0 : clampPercent(percentage);
787
+ }
390
788
  function glmPlan(host, ids) {
391
789
  return {
392
790
  ids,
@@ -411,11 +809,17 @@ window.__ModuleLoader__.load({
411
809
  for (const entry of limits) {
412
810
  if (typeof entry !== "object" || entry === null) continue;
413
811
  const row = entry;
414
- const unit = toNum(row.unit);
415
- const percent = toNum(row.percentage);
812
+ const kind = str(row.type);
813
+ if (kind === "TIME_LIMIT") continue;
814
+ const key = GLM_UNIT_KEYS[toNum(row.unit) ?? NaN];
815
+ if (key === void 0) continue;
816
+ if (kind !== "TOKENS_LIMIT" && kind !== "CREDIT_LIMIT") continue;
416
817
  windows.push({
417
- key: unit === 3 ? "5h" : unit === 6 ? "week" : unit !== void 0 ? `unit-${unit}` : "window",
418
- percent: percent === void 0 ? void 0 : Math.max(0, Math.min(100, percent)),
818
+ key,
819
+ percent: kind === "CREDIT_LIMIT" ? glmCreditPercent(row) : (() => {
820
+ const percentage = toNum(row.percentage);
821
+ return percentage === void 0 ? void 0 : clampPercent(percentage);
822
+ })(),
419
823
  resetsAt: toIso(row.nextResetTime)
420
824
  });
421
825
  }
@@ -629,7 +1033,7 @@ window.__ModuleLoader__.load({
629
1033
  moonshotBalance("api.moonshot.ai", "USD", ["moonshotai"]),
630
1034
  KIMI_CODING,
631
1035
  glmPlan("open.bigmodel.cn", ["zai-coding-cn"]),
632
- glmPlan("api.z.ai", ["zai-coding"]),
1036
+ glmPlan("api.z.ai", ["zai", "zai-coding"]),
633
1037
  OPENCODE_GO,
634
1038
  minimaxPlan("api.minimaxi.com", ["minimax-cn"]),
635
1039
  minimaxPlan("api.minimax.io", ["minimax"]),
@@ -700,6 +1104,31 @@ window.__ModuleLoader__.load({
700
1104
  };
701
1105
  }
702
1106
  //#endregion
1107
+ //#region src/core/types.ts
1108
+ /** A zeroed totals bucket. */
1109
+ function emptyTotals() {
1110
+ return {
1111
+ inputTokens: 0,
1112
+ outputTokens: 0,
1113
+ cacheReadTokens: 0,
1114
+ cacheWriteTokens: 0,
1115
+ reasoningTokens: 0,
1116
+ calls: 0,
1117
+ cost: 0
1118
+ };
1119
+ }
1120
+ /** Add `right` into `left` in place. */
1121
+ function addTotals(left, right) {
1122
+ left.inputTokens += right.inputTokens;
1123
+ left.outputTokens += right.outputTokens;
1124
+ left.cacheReadTokens += right.cacheReadTokens;
1125
+ left.cacheWriteTokens += right.cacheWriteTokens;
1126
+ left.reasoningTokens += right.reasoningTokens;
1127
+ left.calls += right.calls;
1128
+ left.cost += right.cost;
1129
+ return left;
1130
+ }
1131
+ //#endregion
703
1132
  //#region src/core/ledger.ts
704
1133
  /** Total tokens of a bucket (billed input + output; reasoning is inside output). */
705
1134
  function totalTokens(totals) {
@@ -728,8 +1157,8 @@ window.__ModuleLoader__.load({
728
1157
  * section renders as its failure line.
729
1158
  * @module @linxin666/dsh-usage/client/voucher
730
1159
  */
731
- /** Anti-inflation exchange rate: 1000 tokens mint one whale yuan. */
732
- const TOKENS_PER_WHALE_YUAN = 1e3;
1160
+ /** Anti-inflation exchange rate: 1,000,000 tokens mint one whale yuan. */
1161
+ const TOKENS_PER_WHALE_YUAN = 1e6;
733
1162
  /** The note's face value in whale yuan; the smallest denomination is 1. */
734
1163
  function faceValue(tokens) {
735
1164
  return Math.max(1, Math.round(tokens / TOKENS_PER_WHALE_YUAN));
@@ -1320,7 +1749,7 @@ window.__ModuleLoader__.load({
1320
1749
  }
1321
1750
  /**
1322
1751
  * The Token 银行 card: the DeepSeek official family's retained-ledger usage
1323
- * minted onto the whale-yuan note at 1000 tokens per whale yuan. The window
1752
+ * minted onto the whale-yuan note at 1,000,000 tokens per whale yuan. The window
1324
1753
  * prefers the host's whole-ledger aggregate and falls back to the 30-day
1325
1754
  * trend when an older host serves no `all`; the spend line prefers the
1326
1755
  * official balance watch and falls back to the fold-time estimate; the
@@ -1510,11 +1939,6 @@ window.__ModuleLoader__.load({
1510
1939
  function SettingsRow(props) {
1511
1940
  const { settings, snapshot, value } = props;
1512
1941
  const disabled = snapshot === void 0 || !snapshot.writable;
1513
- const bubbleMode = typeof value.bubbleMode === "string" && [
1514
- "always",
1515
- "change",
1516
- "off"
1517
- ].includes(value.bubbleMode) ? value.bubbleMode : "always";
1518
1942
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1519
1943
  className: usage_module_css_default.card,
1520
1944
  "data-dsh-part": "settings-row",
@@ -1523,61 +1947,288 @@ window.__ModuleLoader__.load({
1523
1947
  children: t("usage.config.title")
1524
1948
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1525
1949
  className: usage_module_css_default.settingsGrid,
1526
- children: [
1527
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1528
- className: usage_module_css_default.settingItem,
1529
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1530
- type: "checkbox",
1531
- checked: value.enabled ?? true,
1532
- disabled,
1533
- onChange: (event) => {
1534
- settings.set("enabled", event.target.checked);
1535
- }
1536
- }), t("usage.config.enabled")]
1537
- }),
1538
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1539
- className: usage_module_css_default.settingItem,
1540
- children: [t("usage.config.pollIntervalSec"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1541
- type: "number",
1542
- min: 30,
1543
- max: 3600,
1544
- value: typeof value.pollIntervalSec === "number" ? value.pollIntervalSec : 60,
1545
- disabled,
1546
- onChange: (event) => {
1547
- const parsed = Number(event.target.value);
1548
- if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) settings.set("pollIntervalSec", Math.round(parsed));
1549
- }
1550
- })]
1551
- }),
1552
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1553
- className: usage_module_css_default.settingItem,
1554
- children: [t("usage.config.bubbleMode"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
1555
- value: bubbleMode,
1556
- disabled,
1557
- onChange: (event) => {
1558
- settings.set("bubbleMode", event.target.value);
1559
- },
1560
- children: [
1561
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1562
- value: "always",
1563
- children: t("usage.config.bubbleMode.always")
1564
- }),
1565
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1566
- value: "change",
1567
- children: t("usage.config.bubbleMode.change")
1568
- }),
1569
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1570
- value: "off",
1571
- children: t("usage.config.bubbleMode.off")
1572
- })
1573
- ]
1574
- })]
1575
- })
1576
- ]
1950
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1951
+ className: usage_module_css_default.settingItem,
1952
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1953
+ type: "checkbox",
1954
+ checked: value.enabled ?? true,
1955
+ disabled,
1956
+ onChange: (event) => {
1957
+ settings.set("enabled", event.target.checked);
1958
+ }
1959
+ }), t("usage.config.enabled")]
1960
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
1961
+ className: usage_module_css_default.settingItem,
1962
+ children: [t("usage.config.pollIntervalSec"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1963
+ type: "number",
1964
+ min: 30,
1965
+ max: 3600,
1966
+ value: typeof value.pollIntervalSec === "number" ? value.pollIntervalSec : 60,
1967
+ disabled,
1968
+ onChange: (event) => {
1969
+ const parsed = Number(event.target.value);
1970
+ if (Number.isFinite(parsed) && parsed >= 30 && parsed <= 3600) settings.set("pollIntervalSec", Math.round(parsed));
1971
+ }
1972
+ })]
1973
+ })]
1577
1974
  })]
1578
1975
  });
1579
1976
  }
1580
1977
  //#endregion
1978
+ //#region src/client/UsageSidebarPanel.tsx
1979
+ /**
1980
+ * Sidebar usage panel (issue #1592): the usage body seated directly under the
1981
+ * sidebar entry row. The row carries the panel's controls (refresh, collapse
1982
+ * chevron), so this component renders only the content: expanded, plan
1983
+ * providers show their quota windows and balance-only providers their
1984
+ * remaining amount; collapsed, a one-line strip shows the current session
1985
+ * provider's today usage (tokens, calls, and cost when priced) from the same
1986
+ * /api/dsh-usage/overview document the settings section renders. The open
1987
+ * state lives in the mount (persisted to localStorage) and arrives through
1988
+ * the open face; polling runs at 10s expanded, 30s collapsed, and pauses
1989
+ * while the page is hidden.
1990
+ * @module @linxin666/dsh-usage/client/UsageSidebarPanel
1991
+ */
1992
+ /** Poll cadence while the panel is expanded and the page visible. */
1993
+ const PANEL_POLL_MS = 1e4;
1994
+ /**
1995
+ * Poll cadence of the collapsed strip: the current session's usage should
1996
+ * stay visibly live, but a collapsed surface pays a relaxed cadence instead
1997
+ * of the expanded one.
1998
+ */
1999
+ const COLLAPSED_POLL_MS = 3e4;
2000
+ /** Bar tone, matching the settings section's thresholds. */
2001
+ function tone(percent) {
2002
+ if (percent >= 90) return usage_module_css_default.barLow;
2003
+ if (percent >= 70) return usage_module_css_default.barWarn;
2004
+ return usage_module_css_default.sidebarBarFill;
2005
+ }
2006
+ /**
2007
+ * The collapsed-strip facts for the current provider: display name plus
2008
+ * today's adapter-family totals (tokens, calls, cost). The host serves them
2009
+ * strip-ready inside `current`; an older host document is derived against
2010
+ * here. Undefined on a day without usage — a strip about nothing is noise.
2011
+ */
2012
+ function collapsedSummary(snapshot) {
2013
+ const provider = snapshot.current.provider;
2014
+ if (provider === void 0) return void 0;
2015
+ const name = snapshot.current.displayName ?? snapshot.providers.find((entry) => entry.provider === provider)?.displayName ?? provider;
2016
+ if (snapshot.current.today !== void 0) {
2017
+ const totals = snapshot.current.today;
2018
+ return totals.calls > 0 && totalTokens(totals) > 0 ? {
2019
+ name,
2020
+ totals
2021
+ } : void 0;
2022
+ }
2023
+ const family = adapterFor(provider);
2024
+ let merged = emptyTotals();
2025
+ for (const row of snapshot.usage.today.providers) if (family === void 0 ? row.provider === provider : adapterFor(row.provider) === family) merged = addTotals(merged, row.totals);
2026
+ return merged.calls > 0 && totalTokens(merged) > 0 ? {
2027
+ name,
2028
+ totals: merged
2029
+ } : void 0;
2030
+ }
2031
+ /** A provider is worth listing when it is configured and carries a fact. */
2032
+ function listable(provider) {
2033
+ return provider.credential !== "none" && (provider.plan !== void 0 || provider.balance !== void 0);
2034
+ }
2035
+ /** One provider row: plan windows when present, otherwise the balance line. */
2036
+ function ProviderBlock(props) {
2037
+ const { provider } = props;
2038
+ const windows = provider.plan?.windows ?? [];
2039
+ if (windows.length > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2040
+ className: usage_module_css_default.sidebarProvider,
2041
+ "data-dsh-part": "sidebar-provider",
2042
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2043
+ className: usage_module_css_default.sidebarProviderHead,
2044
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2045
+ className: usage_module_css_default.sidebarProviderName,
2046
+ children: provider.displayName
2047
+ })
2048
+ }), windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2049
+ className: usage_module_css_default.sidebarWindowRow,
2050
+ "data-dsh-part": "sidebar-window",
2051
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window.name ?? t(`usage.plan.windows.${window.key}`) }), window.percent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2052
+ className: usage_module_css_default.sidebarBar,
2053
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2054
+ className: tone(window.percent),
2055
+ style: { width: `${Math.min(100, Math.max(0, window.percent))}%` }
2056
+ })
2057
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [window.percent >= 10 ? Math.round(window.percent) : window.percent.toFixed(1), "%"] })] })]
2058
+ }, window.key))]
2059
+ });
2060
+ if (provider.balance !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2061
+ className: usage_module_css_default.sidebarProvider,
2062
+ "data-dsh-part": "sidebar-provider",
2063
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2064
+ className: usage_module_css_default.sidebarProviderHead,
2065
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2066
+ className: usage_module_css_default.sidebarProviderName,
2067
+ children: provider.displayName
2068
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2069
+ className: usage_module_css_default.sidebarBalance,
2070
+ children: t("usage.sidebar.balanceLeft", { balance: `${provider.balance.currency} ${provider.balance.totalBalance}` })
2071
+ })]
2072
+ })
2073
+ });
2074
+ return null;
2075
+ }
2076
+ /**
2077
+ * Render the sidebar usage panel body.
2078
+ * @param props - store, the poll callback, and the mount's open-state face.
2079
+ * @returns the panel content, or null while collapsed.
2080
+ */
2081
+ function UsageSidebarPanel(props) {
2082
+ const { store, poll, open: openState } = props;
2083
+ const ui = (0, react.useSyncExternalStore)(store.subscribe, store.getSnapshot);
2084
+ const open = (0, react.useSyncExternalStore)(openState.subscribe, openState.isOpen);
2085
+ (0, react.useEffect)(() => {
2086
+ poll();
2087
+ const cadence = open ? PANEL_POLL_MS : COLLAPSED_POLL_MS;
2088
+ let timer;
2089
+ const start = () => {
2090
+ if (timer === void 0 && document.visibilityState === "visible") timer = window.setInterval(poll, cadence);
2091
+ };
2092
+ const onVisibility = () => {
2093
+ if (document.visibilityState === "visible") {
2094
+ poll();
2095
+ start();
2096
+ } else if (timer !== void 0) {
2097
+ window.clearInterval(timer);
2098
+ timer = void 0;
2099
+ }
2100
+ };
2101
+ start();
2102
+ document.addEventListener("visibilitychange", onVisibility);
2103
+ return () => {
2104
+ if (timer !== void 0) window.clearInterval(timer);
2105
+ document.removeEventListener("visibilitychange", onVisibility);
2106
+ };
2107
+ }, [poll, open]);
2108
+ const snapshot = ui.snapshot;
2109
+ if (!open) {
2110
+ const summary = snapshot === null ? void 0 : collapsedSummary(snapshot);
2111
+ if (summary === void 0) return null;
2112
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2113
+ className: usage_module_css_default.sidebarSummary,
2114
+ "data-dsh-plugin": "usage",
2115
+ "data-dsh-part": "sidebar-summary",
2116
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2117
+ className: usage_module_css_default.sidebarSummaryName,
2118
+ children: summary.name
2119
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2120
+ className: usage_module_css_default.sidebarSummaryFacts,
2121
+ children: [
2122
+ t("usage.sidebar.todayUsage", { tokens: formatTokens(totalTokens(summary.totals)) }),
2123
+ " · ",
2124
+ t("usage.calls", { n: summary.totals.calls }),
2125
+ summary.totals.cost > 0 ? ` · ¥${summary.totals.cost.toFixed(2)}` : ""
2126
+ ]
2127
+ })]
2128
+ });
2129
+ }
2130
+ const providers = snapshot === null ? [] : snapshot.providers.filter(listable);
2131
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2132
+ className: usage_module_css_default.sidebarPanel,
2133
+ "data-dsh-plugin": "usage",
2134
+ "data-dsh-part": "sidebar-panel",
2135
+ children: ui.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2136
+ className: usage_module_css_default.sidebarMuted,
2137
+ children: t("usage.sidebar.error", { error: ui.error ?? "" })
2138
+ }) : snapshot === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2139
+ className: usage_module_css_default.sidebarMuted,
2140
+ children: t("usage.sidebar.loading")
2141
+ }) : providers.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2142
+ className: usage_module_css_default.sidebarMuted,
2143
+ children: t("usage.sidebar.empty")
2144
+ }) : providers.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderBlock, { provider }, provider.provider))
2145
+ });
2146
+ }
2147
+ //#endregion
2148
+ //#region src/client/sidebar-panel-mount.tsx
2149
+ /**
2150
+ * Sidebar usage panel mounting (issue #1592).
2151
+ *
2152
+ * The panel is a plain DOM row inserted into the sidebar right below the
2153
+ * usage entry row, carrying its own React root. Placement self-heals with the
2154
+ * same page-wide body-mutation hub the entry row uses; the entry row stays the
2155
+ * anchor, so the sidebar order never depends on this module's timing.
2156
+ *
2157
+ * The mount owns the panel's single open state: initialized from localStorage,
2158
+ * flipped by the entry row's toggle controls, and mirrored to the React body
2159
+ * through the subscribe/isOpen face (the same face the entry row's chevron
2160
+ * and highlight read).
2161
+ * @module @linxin666/dsh-usage/client/sidebar-panel-mount
2162
+ */
2163
+ /** localStorage key holding the collapsed flag ('1' = collapsed). */
2164
+ const COLLAPSED_STORAGE_KEY = "dsh-usage.sidebar.collapsed";
2165
+ /** Read the persisted collapsed flag (absent = expanded). */
2166
+ function readCollapsed() {
2167
+ try {
2168
+ return window.localStorage.getItem(COLLAPSED_STORAGE_KEY) === "1";
2169
+ } catch {
2170
+ return false;
2171
+ }
2172
+ }
2173
+ /** Persist the collapsed flag; storage failures keep the session state. */
2174
+ function writeCollapsed(collapsed) {
2175
+ try {
2176
+ window.localStorage.setItem(COLLAPSED_STORAGE_KEY, collapsed ? "1" : "0");
2177
+ } catch {}
2178
+ }
2179
+ /**
2180
+ * Mount the sidebar usage panel below its entry row.
2181
+ * @param props - the store and poll callback of the apply body.
2182
+ * @returns the panel controller.
2183
+ */
2184
+ function mountUsagePanel(props) {
2185
+ const container = document.createElement("div");
2186
+ container.setAttribute("data-dsh-usage-view", "");
2187
+ const listeners = /* @__PURE__ */ new Set();
2188
+ let open = !readCollapsed();
2189
+ const emit = () => {
2190
+ for (const listener of [...listeners]) listener();
2191
+ };
2192
+ const openState = {
2193
+ subscribe: (listener) => {
2194
+ listeners.add(listener);
2195
+ return () => {
2196
+ listeners.delete(listener);
2197
+ };
2198
+ },
2199
+ isOpen: () => open
2200
+ };
2201
+ const root = (0, react_dom_client.createRoot)(container);
2202
+ root.render((0, react.createElement)(UsageSidebarPanel, {
2203
+ store: props.store,
2204
+ poll: props.poll,
2205
+ open: openState
2206
+ }));
2207
+ /** Keep the panel as the row directly after the usage entry. */
2208
+ const place = () => {
2209
+ const entry = document.querySelector(ENTRY_SELECTOR);
2210
+ if (entry === null || entry.parentElement === null) return;
2211
+ if (container.parentElement !== entry.parentElement || entry.nextElementSibling !== container) entry.parentElement.insertBefore(container, entry.nextElementSibling);
2212
+ };
2213
+ place();
2214
+ const unsubscribeBody = subscribeBodyInvalidations(place);
2215
+ return {
2216
+ isOpen: () => open,
2217
+ subscribe: openState.subscribe,
2218
+ toggle: () => {
2219
+ open = !open;
2220
+ writeCollapsed(!open);
2221
+ emit();
2222
+ },
2223
+ dispose: () => {
2224
+ unsubscribeBody();
2225
+ root.unmount();
2226
+ container.remove();
2227
+ listeners.clear();
2228
+ }
2229
+ };
2230
+ }
2231
+ //#endregion
1581
2232
  //#region src/client/index.ts
1582
2233
  /** Hard ceiling for one usage API call; a stalled host must not pile up requests. */
1583
2234
  const USAGE_FETCH_TIMEOUT_MS = 2e4;
@@ -1653,6 +2304,26 @@ window.__ModuleLoader__.load({
1653
2304
  refresh,
1654
2305
  settings: settingsScope
1655
2306
  });
2307
+ const panel = mountUsagePanel({
2308
+ store,
2309
+ poll
2310
+ });
2311
+ const disposeEntry = mountSidebarEntry({
2312
+ onToggle: () => {
2313
+ panel.toggle();
2314
+ },
2315
+ onRefresh: () => {
2316
+ refresh();
2317
+ },
2318
+ isOpen: () => panel.isOpen(),
2319
+ subscribeOpen: (listener) => panel.subscribe(listener)
2320
+ }, ctx.locale);
2321
+ ctx.effect(() => () => {
2322
+ try {
2323
+ disposeEntry();
2324
+ } catch {}
2325
+ panel.dispose();
2326
+ }, "dsh-usage: sidebar panel");
1656
2327
  ctx.slots.inject("settings.section", () => {
1657
2328
  try {
1658
2329
  const unregister = ctx.slots.register({