dsh-balance-widget 0.4.5 → 0.5.1
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.en.md +35 -15
- package/README.md +36 -16
- package/docs/screenshot-corner.png +0 -0
- package/docs/screenshot-popover.png +0 -0
- package/lib/client.js +97 -130
- package/lib/index.js +143 -47
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[]()
|
|
11
11
|
|
|
12
|
-
A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web GUI: a persistent sidebar footer card shows the account balance and
|
|
12
|
+
A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH) Web GUI: a persistent sidebar footer card shows the account balance and today's cost; clicking opens a five-tier cost breakdown (balance / last prompt with its session name / today-this-session / today-this-workspace / today-all-workspaces).
|
|
13
13
|
|
|
14
14
|
## Preview
|
|
15
15
|
|
|
16
|
-
| Sidebar card |
|
|
16
|
+
| Sidebar card | Five-tier cost popover |
|
|
17
17
|
| --- | --- |
|
|
18
18
|
|  |  |
|
|
19
19
|
|
|
@@ -24,7 +24,7 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
|
|
|
24
24
|
| **Zero external dependencies** | ✅ Imports no `@deepseek-ai/*` packages, no native modules | ❌ Most depend on dsh SDK packages |
|
|
25
25
|
| **Node 24 ready** | ✅ Works out of the box on any profile layout | ⚠️ Many community plugins still error on Node 24 |
|
|
26
26
|
| **Boot stability** | ✅ Registers routes via official `ctx.webServer`; never conflicts with apiproxy | ⚠️ Some self-host HTTP servers that crash `dsh web` on boot |
|
|
27
|
-
| **
|
|
27
|
+
| **Always-fresh auto refresh** | ✅ Sidebar card refreshes balance & costs every 60s; opening the popover triggers an immediate refresh | Some always-on badges refresh on a timer |
|
|
28
28
|
| **Peak/off-peak pricing** | ✅ Built-in official 2026-08-17 rate table, auto-switches by window | Partial support |
|
|
29
29
|
| **Security** | ✅ API key stays in the host process; loopback-only guard | Varies |
|
|
30
30
|
|
|
@@ -32,17 +32,18 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
|
|
|
32
32
|
|
|
33
33
|
## Features
|
|
34
34
|
|
|
35
|
-
- **Account balance** — On click, the host proxies DeepSeek's official `GET /user/balance` and shows the `¥` balance. The API key is resolved through the host credentials service and never leaves the host process; the browser only talks to same-origin routes.
|
|
36
|
-
- **
|
|
37
|
-
- **
|
|
38
|
-
- **Today
|
|
35
|
+
- **Account balance** — On click, the host proxies DeepSeek's official `GET /user/balance` and shows the `¥` balance; the balance number is color-coded by threshold (healthy / amber below `lowThreshold` / red below `criticalThreshold`). The API key is resolved through the host credentials service and never leaves the host process; the browser only talks to same-origin routes.
|
|
36
|
+
- **Last prompt cost (estimate)** — Parses the most recent session file and prices the last turn's token usage, answering "how much did that last prompt cost", with the **session name** labeled underneath.
|
|
37
|
+
- **Today · this session cost (estimate)** — The current session's usage today (calendar day) × DeepSeek's official peak/off-peak price table. Follows the configured model (default `deepseek-v4-flash`, switchable to `deepseek-v4-pro`) and the Beijing-time peak/off-peak windows automatically.
|
|
38
|
+
- **Today · this workspace cost (estimate)** — Sums today's token usage × price across every session in the current workspace (anchored by the current session).
|
|
39
|
+
- **Today · all workspaces cost (estimate)** — Walks every session under `~/.dsh/sessions/` and sums today's (calendar day) token usage × price.
|
|
40
|
+
- **Peak/off-peak status** — The card and popover borders are tinted by the current window (orange at peak / green at off-peak), a "Peak/Off-peak" tag sits next to the popover title, and hovering it shows the current price tier (input/output per 1M tokens).
|
|
39
41
|
- **Token usage** — Also shows the session's input (incl. cache hits) / output tokens.
|
|
40
42
|
- **One-click top-up** — a "Top up" link in the popover footer jumps to the official DeepSeek top-up page (platform.deepseek.com/top_up) in a new tab.
|
|
41
|
-
- **Sidebar card** — a persistent card at the sidebar footer shows balance
|
|
42
|
-
- **
|
|
43
|
+
- **Sidebar card** — a persistent card at the sidebar footer shows balance and today's cost; globally visible, auto-refreshes every 60s (balance via the official API, costs parsed locally), and opening the popover triggers an immediate refresh.
|
|
44
|
+
- **Balance color warning** — the balance number is tinted in three tiers: default (healthy) → amber (below `lowThreshold`) → red (below `criticalThreshold`).
|
|
43
45
|
- **Official price auto-sync** — fetches the DeepSeek official pricing page on startup and every 12h; falls back to built-in rates on failure.
|
|
44
46
|
- **Agent tool** — a `deepseek_billing` tool lets the model answer "how much balance do I have / how much did today cost".
|
|
45
|
-
- **On-demand refresh** — No polling, no background requests; endpoints are only hit when you click the icon. Costs zero tokens to use.
|
|
46
47
|
|
|
47
48
|
## Why this plugin
|
|
48
49
|
|
|
@@ -55,15 +56,15 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
|
|
|
55
56
|
host half (lib/index.js)
|
|
56
57
|
ctx.webServer.register:
|
|
57
58
|
GET /api/dsh-balance/balance → official /user/balance (loopback-only guard)
|
|
58
|
-
GET /api/dsh-balance/active-cost → last prompt + session
|
|
59
|
-
GET /api/dsh-balance/today-cost → today's costs (dual: current workspace + all)
|
|
59
|
+
GET /api/dsh-balance/active-cost → last prompt + today-this-session (with session name, most recent session)
|
|
60
|
+
GET /api/dsh-balance/today-cost → today's costs (dual: current workspace + all workspaces)
|
|
60
61
|
Zero @deepseek-ai/* imports; loads from any profile layout.
|
|
61
62
|
Also registers a deepseek_billing tool for model-driven queries.
|
|
62
63
|
|
|
63
64
|
client half (lib/client.js)
|
|
64
65
|
ctx.slots.inject("sidebar.footer.action")
|
|
65
|
-
→ persistent sidebar footer card (balance +
|
|
66
|
-
→ click opens
|
|
66
|
+
→ persistent sidebar footer card (balance + today's cost, peak-tinted border)
|
|
67
|
+
→ click opens five-tier cost popover + peak tag + ⓘ term explanations
|
|
67
68
|
```
|
|
68
69
|
|
|
69
70
|
## Installation
|
|
@@ -152,6 +153,23 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
|
|
|
152
153
|
|
|
153
154
|
## Changelog
|
|
154
155
|
|
|
156
|
+
### v0.5.1 — Cold-start speedup & cleanup
|
|
157
|
+
- 🚀 **Performance**: today-cost cold start dropped from ~5.4s to ~0.01s — only session files modified today are decompressed (mtime filter), parsed results are cached by (path, mtime), and parsing runs in parallel
|
|
158
|
+
- ⏰ **Refresh wording**: README now matches the code — the persistent card auto-refreshes every 60s (the old "no polling" claims contradicted the code and have been corrected)
|
|
159
|
+
- 🌏 **Timezone fix**: peak/off-peak windows are now computed in Beijing time (UTC+8) explicitly instead of the host's local timezone
|
|
160
|
+
- 🧹 **Cleanup**: removed a dead client-side pricing stack (PRICING/priceSession) that was never called; all pricing now goes through the host
|
|
161
|
+
- 📐 **Pricing parsing hardened**: peak rates are parsed explicitly from the official page (no more hard-coded "off-peak × 2"); if a cache-hit rate cannot be parsed the whole table falls back to built-in rates instead of silently pricing cache hits at 0
|
|
162
|
+
- 🏷️ **Card label**: the card footer now reads "Today · all" to make clear it is the global (all-workspaces) figure
|
|
163
|
+
- 🎨 **Visual refresh**: SVG wallet icon replaces 💰, press/pop micro-animations, muted secondary tiers in the popover
|
|
164
|
+
|
|
165
|
+
### v0.5.0 — Five-tier costs & peak/off-peak status
|
|
166
|
+
- ✨ **Added**: the cost breakdown is now five tiers — balance / last prompt / today-this-session / today-this-workspace / today-all-workspaces
|
|
167
|
+
- The last-prompt row labels the **session name** underneath (based on the most recent session)
|
|
168
|
+
- "Today · this session" = the current session's usage today; "Today · this workspace" = all sessions in the current workspace today (workspace anchored by the current session); "Today · all workspaces" = everything across all workspaces today
|
|
169
|
+
- ✨ **Added**: peak/off-peak status visuals — card and popover borders tinted by window (orange at peak / green at off-peak), a "Peak/Off-peak" tag next to the popover title with a hover tooltip showing the current price tier
|
|
170
|
+
- 🎨 **Changed**: removed the remaining-ratio bar; the balance number is now color-coded directly by threshold (healthy / amber / red)
|
|
171
|
+
- 🗑️ **Removed**: the "This session" row (all-time session total) from the popover
|
|
172
|
+
|
|
155
173
|
### v0.2.0 — Last prompt & today total cost
|
|
156
174
|
- ✨ **Added**: popover now shows "last prompt cost" and "today total cost"
|
|
157
175
|
- Last prompt: prices the current session's last turn from the session file
|
|
@@ -166,7 +184,9 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
|
|
|
166
184
|
|
|
167
185
|
- Config tree: `dsh --profile web --dump-config` should show a `balance-widget` entry.
|
|
168
186
|
- Balance route: after restarting dsh web, `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` should return `{ ok, balance_infos, modelId }`.
|
|
169
|
-
-
|
|
187
|
+
- Session costs: `curl -s http://127.0.0.1:3080/api/dsh-balance/active-cost` should return `{ lastPrompt, todaySession, title, sessionId, peak, workspaceName, ... }`; append `?session=SESSION_ID` to target a specific session.
|
|
188
|
+
- Today costs: `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` should return `{ workspace: { cost, ..., cwd }, all: { cost, ... }, modelId }`.
|
|
189
|
+
- Legacy route: `curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` still works and returns `{ cost, inputTokens, outputTokens, modelId }`.
|
|
170
190
|
|
|
171
191
|
## License
|
|
172
192
|
|
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[]()
|
|
11
11
|
|
|
12
|
-
DeepSeek Harness (DSH) Web GUI
|
|
12
|
+
DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常驻卡片显示账户余额与今日花费,点击弹出五层级成本明细(余额 / 最近提问·标注会话名 / 今日·本会话 / 今日·本工作区 / 今日·所有工作区)。
|
|
13
13
|
|
|
14
14
|
## 效果预览
|
|
15
15
|
|
|
16
|
-
| 侧边栏卡片(左下角常驻) |
|
|
16
|
+
| 侧边栏卡片(左下角常驻) | 点击弹出五层级成本 |
|
|
17
17
|
| --- | --- |
|
|
18
18
|
|  |  |
|
|
19
19
|
|
|
@@ -24,7 +24,7 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
|
|
|
24
24
|
| **零外部依赖** | ✅ 不 import 任何 `@deepseek-ai/*` 包,无原生模块 | ❌ 多数依赖 dsh SDK 包 |
|
|
25
25
|
| **Node 24 兼容** | ✅ 天然兼容(零依赖设计),任何 profile 布局可加载 | ⚠️ 不少社区插件在 Node 24 下报错 |
|
|
26
26
|
| **启动稳定性** | ✅ 用官方 `ctx.webServer` 注册路由,不与 apiproxy 冲突 | ⚠️ 有的自建 HTTP 服务导致 dsh web 启动崩溃 |
|
|
27
|
-
|
|
|
27
|
+
| **常驻自动刷新** | ✅ 侧边栏卡片每 60s 自动刷新余额与成本,弹框点击即强刷 | 有的常驻徽章定时刷新 |
|
|
28
28
|
| **峰谷定价** | ✅ 内置官方 2026-08-17 峰谷价表,自动按时段切换 | 部分支持 |
|
|
29
29
|
| **安全性** | ✅ API key 仅在宿主进程,loopback-only 守卫 | 参差不齐 |
|
|
30
30
|
|
|
@@ -32,17 +32,18 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
|
|
|
32
32
|
|
|
33
33
|
## 功能
|
|
34
34
|
|
|
35
|
-
- **账户余额** — 点击图标时经宿主代理查询 DeepSeek 官方 `GET /user/balance`,展示 `¥`
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
35
|
+
- **账户余额** — 点击图标时经宿主代理查询 DeepSeek 官方 `GET /user/balance`,展示 `¥` 余额;余额数字按阈值自动变色(充足 / 低于 `lowThreshold` 变黄 / 低于 `criticalThreshold` 变红);API key 只在宿主进程内读取(凭据服务),浏览器不接触密钥。
|
|
36
|
+
- **最近一次提问成本(估算)** — 从最近活跃会话文件解析最后一个 turn 的 token 用量 × 单价,回答"刚才那条提问花了多少";下方标注该会话的**会话名**。
|
|
37
|
+
- **今日·本会话成本(估算)** — 当前会话今天(自然日)产生的 token 用量 × DeepSeek 官方峰谷定价表计算,随当前会话模型(默认 `deepseek-v4-flash`,可在配置中改为 `deepseek-v4-pro`)与北京时间高峰/空闲时段自动切换。
|
|
38
|
+
- **今日·本工作区成本(估算)** — 遍历当前工作区(由当前会话锚定)下的所有会话,累加今天的 token 用量 × 单价。
|
|
39
|
+
- **今日·所有工作区成本(估算)** — 遍历 `~/.dsh/sessions/` 下所有工作区的所有会话,累加今天的 token 用量 × 单价。
|
|
40
|
+
- **峰谷状态标签** — 卡片与弹框边框按当前时段着色(峰时橙色 / 谷时绿色),弹框标题旁显示「峰时/谷时」标签,悬停可查看当前价格档位(输入/输出单价)。
|
|
41
|
+
- **Token 用量** — 同时展示输入(含缓存命中)/ 输出 token 数。
|
|
40
42
|
- **一键充值** — 弹层底部「去充值」链接直达 DeepSeek 官方充值页(platform.deepseek.com/top_up),新窗口打开。
|
|
41
|
-
- **侧边栏常驻卡片** — 侧边栏底部(设置上方)显示余额 +
|
|
42
|
-
-
|
|
43
|
+
- **侧边栏常驻卡片** — 侧边栏底部(设置上方)显示余额 + 今日花费,全局可见;每 60 秒自动刷新(余额走官方接口、成本为本地会话解析),打开弹框时也会立即刷新一次,无需手动操作。
|
|
44
|
+
- **余额数字变色预警** — 余额数字按三档着色:充足(默认色)→ 琥珀(低于 `lowThreshold`)→ 红色(低于 `criticalThreshold`),一眼判断余额健康度。
|
|
43
45
|
- **官方价格自动同步** — 启动时 + 每 12 小时抓取 DeepSeek 官方定价页,改价自动跟进;失败回退内置价目表。
|
|
44
46
|
- **模型工具查询** — 新增 `deepseek_billing` 工具,可直接问模型"余额多少/今天花了多少"。
|
|
45
|
-
- **按需刷新** — 无轮询、无后台请求;只有点击图标时才发起查询,不消耗任何 token。
|
|
46
47
|
|
|
47
48
|
## 架构
|
|
48
49
|
|
|
@@ -50,15 +51,15 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
|
|
|
50
51
|
host 半区 (lib/index.js)
|
|
51
52
|
ctx.webServer.register:
|
|
52
53
|
GET /api/dsh-balance/balance → 官方 /user/balance(loopback-only 守卫)
|
|
53
|
-
GET /api/dsh-balance/active-cost → 最近活跃会话的最近提问 +
|
|
54
|
-
GET /api/dsh-balance/today-cost → 今日成本(双值:当前工作区 +
|
|
54
|
+
GET /api/dsh-balance/active-cost → 最近活跃会话的最近提问 + 今日·本会话(含会话名)
|
|
55
|
+
GET /api/dsh-balance/today-cost → 今日成本(双值:当前工作区 + 所有工作区)
|
|
55
56
|
依赖:零外部 @deepseek-ai/* import,任何 profile 布局均可解析
|
|
56
57
|
另有 deepseek_billing 工具供模型直接查询余额/成本
|
|
57
58
|
|
|
58
59
|
client 半区 (lib/client.js)
|
|
59
60
|
ctx.slots.inject("sidebar.footer.action")
|
|
60
|
-
→ 侧边栏底部常驻卡片(余额 +
|
|
61
|
-
→
|
|
61
|
+
→ 侧边栏底部常驻卡片(余额 + 今日花费,峰/谷时段描边着色)
|
|
62
|
+
→ 点击弹出五层级成本明细 + 峰谷标签 + ⓘ 名词解释
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
## 安装
|
|
@@ -149,6 +150,23 @@ DSH 的插件配置统一放在这个文件里:
|
|
|
149
150
|
|
|
150
151
|
## 版本历史
|
|
151
152
|
|
|
153
|
+
### v0.5.1 — 冷启动提速与代码清理
|
|
154
|
+
- 🚀 **性能**:今日成本冷启动从 ~5.4s 降至 ~0.01s——只解压当天活跃的会话文件(按 mtime 过滤)+ 解压结果按 (path, mtime) 长期缓存 + 并行解析
|
|
155
|
+
- ⏰ **轮询口径**:README 与代码统一——常驻卡片每 60s 自动刷新(此前文档"无轮询"表述自相矛盾,已更正)
|
|
156
|
+
- 🌏 **峰谷时区修正**:峰/谷判断改为显式按北京时间(UTC+8)计算,不再依赖宿主机时区
|
|
157
|
+
- 🧹 **清理**:移除客户端一套从未调用的死计价代码(PRICING/priceSession),计价统一走宿主
|
|
158
|
+
- 📐 **定价解析加固**:峰价改为从官方页面显式解析(不再硬编码"谷价 ×2");解析不到缓存命中价时整表回退内置价目表,避免静默按 0 计价
|
|
159
|
+
- 🏷️ **卡片语义**:卡片底部"今日"改为"今日·全部",明确是全局(所有工作区)口径
|
|
160
|
+
- 🎨 **外观**:💰 换 SVG 钱包图标、弹框/卡片微交互动画、层级弱化(详情见 v0.5.1 diff)
|
|
161
|
+
|
|
162
|
+
### v0.5.0 — 五层级成本与峰谷状态
|
|
163
|
+
- ✨ **新增**:成本明细改为五层级——余额 / 最近一次提问 / 今日·本会话 / 今日·本工作区 / 今日·所有工作区
|
|
164
|
+
- 最近一次提问下方标注**会话名**(基于最近活跃会话)
|
|
165
|
+
- 「今日·本会话」= 当前会话今天产生的费用;「今日·本工作区」= 当前工作区今天所有会话合计(由当前会话锚定工作区);「今日·所有工作区」= 全部工作区今天合计
|
|
166
|
+
- ✨ **新增**:峰/谷时段状态可视化——卡片与弹框边框按时段着色(峰时橙色 / 谷时绿色),弹框标题旁显示「峰时/谷时」标签,悬停查看当前价格档位
|
|
167
|
+
- 🎨 **调整**:移除余额剩余比例条,改为余额数字按阈值直接变色(充足 / 黄 / 红)
|
|
168
|
+
- 🗑️ **移除**:弹框中的「本会话成本」(会话全程累计)行
|
|
169
|
+
|
|
152
170
|
### v0.2.0 — 最近一次提问与今日总成本
|
|
153
171
|
- ✨ **新增**:弹层增加「最近一次提问成本」与「今天总成本」两项
|
|
154
172
|
- 最近一次提问:解析当前会话最后一个 turn 的 token 用量 × 单价
|
|
@@ -163,7 +181,9 @@ DSH 的插件配置统一放在这个文件里:
|
|
|
163
181
|
|
|
164
182
|
- 配置树:`dsh --profile web --dump-config` 应出现 `balance-widget` 条目
|
|
165
183
|
- 余额路由:重启 dsh web 后 `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` 应返回 `{ ok, balance_infos, modelId }`
|
|
166
|
-
-
|
|
184
|
+
- 会话成本:`curl -s http://127.0.0.1:3080/api/dsh-balance/active-cost` 应返回 `{ lastPrompt, todaySession, title, sessionId, peak, workspaceName, ... }`;加 `?session=SESSION_ID` 可指定会话
|
|
185
|
+
- 今日成本:`curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` 应返回 `{ workspace: { cost, ..., cwd }, all: { cost, ... }, modelId }`
|
|
186
|
+
- 兼容旧路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` 仍可用,返回 `{ cost, inputTokens, outputTokens, modelId }`
|
|
167
187
|
|
|
168
188
|
## License
|
|
169
189
|
|
|
Binary file
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react = require("react");
|
|
9
9
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
10
|
//#region lib/types/client/styles.js
|
|
11
|
-
const css = ".
|
|
11
|
+
const css = ".dshbw_row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:2px 0}.dshbw_label{display:inline-flex;align-items:center;min-width:0}.dshbw_val{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_val[data-tier=\"sec\"]{font-weight:400;color:var(--dsw-alias-label-secondary)}.dshbw_val[data-tier=\"ter\"]{font-weight:400;color:var(--dsw-alias-label-tertiary)}.dshbw_val[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_val[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_err{color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:16px;margin-top:6px}.dshbw_link{background:none;border:none;cursor:pointer;color:var(--dsw-alias-state-business-primary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit;text-decoration:none}.dshbw_link:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline}.dshbw_sideTop{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;line-height:18px}.dshbw_sideAmount{font-weight:600}.dshbw_sideAmount[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary);font-weight:600}.dshbw_sideAmount[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_sideFoot{display:flex;justify-content:space-between;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.dshbw_sideWrap{position:relative}.dshbw_side{display:flex;flex-direction:column;gap:4px;width:100%;padding:6px 8px;margin:0 8px 4px;border:none;border-radius:6px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;text-align:left;transition:transform 120ms cubic-bezier(0.23,1,0.32,1),background 120ms cubic-bezier(0.23,1,0.32,1)}.dshbw_side:active{transform:scale(.97)}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sidePop{position:absolute;bottom:calc(100% + 8px);left:8px;z-index:70;min-width:240px;background:var(--dsw-alias-bg-layer-3);border:none;border-radius:12px;box-shadow:0 0 1px rgba(0,0,0,.3),0 4px 14px rgba(0,0,0,.1);padding:12px 12px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary);animation:dshbw-pop-in 160ms cubic-bezier(0.23,1,0.32,1) both}.dshbw_sidePopHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}.dshbw_sidePopTitle{font-weight:600}.dshbw_sidePopClose{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-tertiary);font-size:16px;line-height:1;padding:0 2px;font-family:inherit}.dshbw_sidePopClose:hover{color:var(--dsw-alias-label-primary)}.dshbw_sidePopFoot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px}.dshbw_sidePopRefresh{background:none;border:1px solid var(--dsw-alias-border-l2);cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:6px;padding:0;font-family:inherit}.dshbw_sidePopRefresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_side[data-peak=\"true\"]{border:1px solid var(--dsw-alias-state-warn-primary)}.dshbw_side[data-peak=\"false\"]{border:1px solid var(--dsw-alias-state-success-primary)}.dshbw_peakTag{display:inline-flex;align-items:center;gap:4px;font-size:12px;line-height:16px;padding:1px 7px;border-radius:6px}.dshbw_peakTag[data-peak=\"true\"]{color:var(--dsw-alias-state-warn-primary);background:var(--dsw-alias-state-warn-tertiary)}.dshbw_peakTag[data-peak=\"false\"]{color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-state-success-tertiary)}.dshbw_sessionNote{display:flex;align-items:center;gap:4px;margin-top:6px;padding-top:6px;border-top:1px solid var(--dsw-alias-border-l1);font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);min-width:0}.dshbw_sessionName{max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dshbw_ico{color:var(--dsw-alias-state-business-primary);display:inline-flex;align-items:center}@keyframes dshbw-pop-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.dshbw_sidePopRefresh,.dshbw_sidePopClose{transition:transform 120ms cubic-bezier(0.23,1,0.32,1),color 120ms cubic-bezier(0.23,1,0.32,1)}.dshbw_sidePopRefresh:active{transform:scale(.92)}.dshbw_sidePopClose:active{transform:scale(.92)}";
|
|
12
12
|
const tagId = "dsh-balance-widget/styles.css";
|
|
13
13
|
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
14
14
|
const tag = document.createElement("style");
|
|
@@ -18,113 +18,33 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
document.head.appendChild(tag);
|
|
19
19
|
}
|
|
20
20
|
var styles = {
|
|
21
|
-
"wrap": "dshbw_wrap",
|
|
22
|
-
"btn": "dshbw_btn",
|
|
23
|
-
"pop": "dshbw_pop",
|
|
24
|
-
"head": "dshbw_head",
|
|
25
|
-
"title": "dshbw_title",
|
|
26
|
-
"refresh": "dshbw_refresh",
|
|
27
21
|
"row": "dshbw_row",
|
|
28
22
|
"label": "dshbw_label",
|
|
29
23
|
"val": "dshbw_val",
|
|
30
24
|
"err": "dshbw_err",
|
|
31
|
-
"hint": "dshbw_hint",
|
|
32
|
-
"foot": "dshbw_foot",
|
|
33
25
|
"link": "dshbw_link",
|
|
34
26
|
"side": "dshbw_side",
|
|
35
27
|
"sideWrap": "dshbw_sideWrap",
|
|
36
28
|
"sideTop": "dshbw_sideTop",
|
|
37
29
|
"sideAmount": "dshbw_sideAmount",
|
|
38
|
-
"bar": "dshbw_bar",
|
|
39
|
-
"barFill": "dshbw_barFill",
|
|
40
30
|
"sideFoot": "dshbw_sideFoot",
|
|
41
31
|
"sidePop": "dshbw_sidePop",
|
|
42
32
|
"sidePopHead": "dshbw_sidePopHead",
|
|
43
33
|
"sidePopTitle": "dshbw_sidePopTitle",
|
|
44
34
|
"sidePopClose": "dshbw_sidePopClose",
|
|
45
35
|
"sidePopFoot": "dshbw_sidePopFoot",
|
|
46
|
-
"sidePopRefresh": "dshbw_sidePopRefresh"
|
|
36
|
+
"sidePopRefresh": "dshbw_sidePopRefresh",
|
|
37
|
+
"peakTag": "dshbw_peakTag",
|
|
38
|
+
"sessionNote": "dshbw_sessionNote",
|
|
39
|
+
"sessionName": "dshbw_sessionName"
|
|
47
40
|
};
|
|
48
41
|
//#endregion
|
|
49
|
-
//#region lib/types/client/pricing.js
|
|
50
|
-
/**
|
|
51
|
-
* DeepSeek official pricing, in CNY per 1M tokens, effective 2026-08-17
|
|
52
|
-
* (peak/off-peak two-tier billing). Peak windows are Beijing time:
|
|
53
|
-
* 09:00-12:00 and 14:00-18:00; off-peak is half the peak price.
|
|
54
|
-
* Prices are hard-coded from the official announcement and are estimates
|
|
55
|
-
* for display — actual billing is authoritative from the provider.
|
|
56
|
-
*/
|
|
57
|
-
const PRICING = {
|
|
58
|
-
"deepseek-v4-flash": {
|
|
59
|
-
peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 },
|
|
60
|
-
offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 }
|
|
61
|
-
},
|
|
62
|
-
"deepseek-v4-pro": {
|
|
63
|
-
peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 },
|
|
64
|
-
offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 }
|
|
65
|
-
},
|
|
66
|
-
"deepseek-chat": {
|
|
67
|
-
peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 },
|
|
68
|
-
offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 }
|
|
69
|
-
},
|
|
70
|
-
"deepseek-reasoner": {
|
|
71
|
-
peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 },
|
|
72
|
-
offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 }
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
/** Fall back to Flash pricing for unknown model ids. */
|
|
76
|
-
function pricingFor(modelId) {
|
|
77
|
-
return PRICING[modelId] ?? PRICING["deepseek-v4-flash"];
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Whether `date` falls inside a Beijing-time peak window.
|
|
81
|
-
* The host's local timezone is used; the widget is for a CN deployment.
|
|
82
|
-
*/
|
|
83
|
-
function isPeak(date) {
|
|
84
|
-
const hour = date.getHours();
|
|
85
|
-
return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Price one session's token usage into a CNY estimate.
|
|
89
|
-
* @param usage - tokenUsage projection value.
|
|
90
|
-
* @param modelId - provider model id used for pricing.
|
|
91
|
-
* @returns { input, output, total } in CNY, or null when usage is absent.
|
|
92
|
-
*/
|
|
93
|
-
function priceSession(usage, modelId) {
|
|
94
|
-
if (usage === void 0 || usage === null) return null;
|
|
95
|
-
const input = usage.uncachedInputTokens ?? 0;
|
|
96
|
-
const hit = usage.cacheReadTokens ?? 0;
|
|
97
|
-
const write = usage.cacheWriteTokens ?? 0;
|
|
98
|
-
const output = usage.outputTokens ?? 0;
|
|
99
|
-
if (input + hit + write + output <= 0) return null;
|
|
100
|
-
const table = isPeak(new Date()) ? pricingFor(modelId).peak : pricingFor(modelId).offPeak;
|
|
101
|
-
const inputCost = (input * table.inputMiss + (hit + write) * table.inputHit) / 1e6;
|
|
102
|
-
const outputCost = (output * table.output) / 1e6;
|
|
103
|
-
return {
|
|
104
|
-
input: inputCost,
|
|
105
|
-
output: outputCost,
|
|
106
|
-
total: inputCost + outputCost
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
42
|
/** Format a CNY amount: ¥ + 3 significant decimals (drop trailing zeros). */
|
|
110
43
|
function formatCny(value) {
|
|
111
44
|
if (value === void 0 || value === null || !Number.isFinite(value)) return "—";
|
|
112
45
|
const rounded = Math.round(value * 1e4) / 1e4;
|
|
113
46
|
return `¥${rounded.toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: 4 })}`;
|
|
114
47
|
}
|
|
115
|
-
/** Format token counts compactly: 1234 -> 1.2K. */
|
|
116
|
-
function formatTokens(value) {
|
|
117
|
-
if (value === void 0 || value === null) return "—";
|
|
118
|
-
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
119
|
-
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
120
|
-
return String(value);
|
|
121
|
-
}
|
|
122
|
-
/** Format an epoch ms as HH:MM local time. */
|
|
123
|
-
function formatTime(ms) {
|
|
124
|
-
if (ms === void 0 || ms === null) return "—";
|
|
125
|
-
const d = new Date(ms);
|
|
126
|
-
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
127
|
-
}
|
|
128
48
|
//#endregion
|
|
129
49
|
//#region lib/types/client/balance-api.js
|
|
130
50
|
/** Same-origin balance route (host half registers it over ctx.webServer). */
|
|
@@ -136,15 +56,29 @@ window.__ModuleLoader__.load({
|
|
|
136
56
|
/** Official DeepSeek platform top-up page (opens in a new tab). */
|
|
137
57
|
const TOP_UP_URL = "https://platform.deepseek.com/top_up";
|
|
138
58
|
/**
|
|
139
|
-
* Fetch the active session's costs (last prompt + session total).
|
|
140
|
-
* @
|
|
59
|
+
* Fetch the active session's costs (last prompt + today's session total).
|
|
60
|
+
* @param {string} [sessionId] - the currently selected session; when omitted
|
|
61
|
+
* the host falls back to its most recently active session.
|
|
62
|
+
* @returns { ok, lastPrompt?, todaySession?, sessionTitle?, workspaceName?, peak?, error? }
|
|
141
63
|
*/
|
|
142
|
-
async function fetchActiveCost() {
|
|
64
|
+
async function fetchActiveCost(sessionId) {
|
|
143
65
|
try {
|
|
144
|
-
const
|
|
66
|
+
const url = sessionId === void 0 || sessionId === ""
|
|
67
|
+
? ACTIVE_COST_ROUTE
|
|
68
|
+
: `${ACTIVE_COST_ROUTE}?session=${encodeURIComponent(sessionId)}`;
|
|
69
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
145
70
|
const payload = await response.json().catch(() => ({}));
|
|
146
71
|
if (!response.ok) return { ok: false, error: payload.error ?? `active-cost responded ${response.status}` };
|
|
147
|
-
return {
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
lastPrompt: payload.lastPrompt?.cost ?? 0,
|
|
75
|
+
todaySession: payload.todaySession?.cost ?? 0,
|
|
76
|
+
sessionTitle: payload.title,
|
|
77
|
+
workspaceName: payload.workspaceName,
|
|
78
|
+
cwd: payload.cwd,
|
|
79
|
+
sessionId: payload.sessionId,
|
|
80
|
+
peak: payload.peak
|
|
81
|
+
};
|
|
148
82
|
} catch (error) {
|
|
149
83
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
150
84
|
}
|
|
@@ -238,20 +172,30 @@ window.__ModuleLoader__.load({
|
|
|
238
172
|
* cost, top-up, refresh) using only global data.
|
|
239
173
|
* @param {Object} props - wide (sidebar expanded), t (locale).
|
|
240
174
|
*/
|
|
241
|
-
function SidebarBalanceCard({ wide, t }) {
|
|
175
|
+
function SidebarBalanceCard({ wide, t, useSessions }) {
|
|
242
176
|
const [open, setOpen] = (0, react.useState)(false);
|
|
243
177
|
const [balance, setBalance] = (0, react.useState)(null);
|
|
244
178
|
const [lastPrompt, setLastPrompt] = (0, react.useState)(null);
|
|
245
|
-
const [
|
|
179
|
+
const [todaySession, setTodaySession] = (0, react.useState)(null);
|
|
180
|
+
const [sessionTitle, setSessionTitle] = (0, react.useState)(null);
|
|
246
181
|
const [todayWs, setTodayWs] = (0, react.useState)(null);
|
|
247
182
|
const [todayAll, setTodayAll] = (0, react.useState)(null);
|
|
248
183
|
const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
|
|
249
184
|
const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
|
|
250
185
|
const [error, setError] = (0, react.useState)(null);
|
|
251
186
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
187
|
+
const [peak, setPeak] = (0, react.useState)(null);
|
|
188
|
+
// The currently selected session id (the session the user is viewing).
|
|
189
|
+
// This anchors "today · this workspace" to the actual current workspace.
|
|
190
|
+
const currentSessionId = typeof useSessions === "function"
|
|
191
|
+
? (0, useSessions)((s) => s.current)
|
|
192
|
+
: void 0;
|
|
252
193
|
/** Fetch today's costs (workspace + all) from the dual-value route. */
|
|
253
194
|
const fetchTodayCosts = async () => {
|
|
254
|
-
const
|
|
195
|
+
const url = currentSessionId !== void 0 && currentSessionId !== ""
|
|
196
|
+
? `${TODAY_COST_ROUTE}?session=${encodeURIComponent(currentSessionId)}`
|
|
197
|
+
: TODAY_COST_ROUTE;
|
|
198
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
255
199
|
const payload = await response.json().catch(() => ({}));
|
|
256
200
|
if (!response.ok) return { ok: false, error: payload.error ?? `today-cost responded ${response.status}` };
|
|
257
201
|
return {
|
|
@@ -265,7 +209,7 @@ window.__ModuleLoader__.load({
|
|
|
265
209
|
setError(null);
|
|
266
210
|
const [bResult, aResult, tResult] = await Promise.all([
|
|
267
211
|
fetchBalance(),
|
|
268
|
-
fetchActiveCost(),
|
|
212
|
+
fetchActiveCost(currentSessionId),
|
|
269
213
|
fetchTodayCosts()
|
|
270
214
|
]);
|
|
271
215
|
const failures = [];
|
|
@@ -278,7 +222,9 @@ window.__ModuleLoader__.load({
|
|
|
278
222
|
}
|
|
279
223
|
if (aResult.ok) {
|
|
280
224
|
setLastPrompt(aResult.lastPrompt);
|
|
281
|
-
|
|
225
|
+
setTodaySession(aResult.todaySession);
|
|
226
|
+
setSessionTitle(aResult.sessionTitle ?? null);
|
|
227
|
+
setPeak(aResult.peak ?? null);
|
|
282
228
|
} else {
|
|
283
229
|
failures.push(`会话: ${aResult.error}`);
|
|
284
230
|
}
|
|
@@ -295,7 +241,7 @@ window.__ModuleLoader__.load({
|
|
|
295
241
|
refresh(false);
|
|
296
242
|
const timer = setInterval(() => refresh(false), 60000);
|
|
297
243
|
return () => clearInterval(timer);
|
|
298
|
-
}, []);
|
|
244
|
+
}, [currentSessionId]);
|
|
299
245
|
// Close the popover when clicking outside the card.
|
|
300
246
|
const wrapRef = (0, react.useRef)(null);
|
|
301
247
|
(0, react.useEffect)(() => {
|
|
@@ -308,11 +254,6 @@ window.__ModuleLoader__.load({
|
|
|
308
254
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
309
255
|
}, [open]);
|
|
310
256
|
const level = balance === null ? 0 : balance.total < criticalThreshold ? 2 : balance.total < lowThreshold ? 1 : 0;
|
|
311
|
-
// Remaining-ratio: 100% at lowThreshold (or above), 0% at 0.
|
|
312
|
-
let percent = 100;
|
|
313
|
-
if (balance !== null && balance.total < lowThreshold) {
|
|
314
|
-
percent = lowThreshold <= 0 ? 100 : Math.max(0, Math.round(balance.total / lowThreshold * 100));
|
|
315
|
-
}
|
|
316
257
|
const toggle = async () => {
|
|
317
258
|
const next = !open;
|
|
318
259
|
setOpen(next);
|
|
@@ -325,28 +266,21 @@ window.__ModuleLoader__.load({
|
|
|
325
266
|
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
326
267
|
type: "button",
|
|
327
268
|
className: styles.side,
|
|
269
|
+
"data-peak": peak !== null ? String(peak.active) : undefined,
|
|
328
270
|
title: t("title"),
|
|
329
271
|
onClick: toggle,
|
|
330
272
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
331
273
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
332
274
|
className: styles.sideTop,
|
|
333
275
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
334
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
335
|
-
children: "
|
|
276
|
+
children: [(0, react_jsx_runtime.jsx)("span", { className: "dshbw_ico",
|
|
277
|
+
children: (0, react_jsx_runtime.jsx)("svg", { width: 16, height: 16, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.7, strokeLinecap: "round", strokeLinejoin: "round", children: [(0, react_jsx_runtime.jsx)("rect", { x: 3, y: 6, width: 18, height: 13, rx: 2 }), (0, react_jsx_runtime.jsx)("path", { d: "M3 10h18" })] })
|
|
336
278
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
337
279
|
className: styles.sideAmount,
|
|
338
280
|
"data-level": level > 0 ? level : undefined,
|
|
339
281
|
children: balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
340
282
|
})]
|
|
341
283
|
})
|
|
342
|
-
}), (0, react_jsx_runtime.jsx)("div", {
|
|
343
|
-
className: styles.bar,
|
|
344
|
-
"aria-label": t("side.bar"),
|
|
345
|
-
children: (0, react_jsx_runtime.jsx)("div", {
|
|
346
|
-
className: styles.barFill,
|
|
347
|
-
"data-level": level,
|
|
348
|
-
style: { width: `${percent}%` }
|
|
349
|
-
})
|
|
350
284
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
351
285
|
className: styles.sideFoot,
|
|
352
286
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
@@ -360,6 +294,7 @@ window.__ModuleLoader__.load({
|
|
|
360
294
|
})
|
|
361
295
|
}), open && (0, react_jsx_runtime.jsx)("div", {
|
|
362
296
|
className: styles.sidePop,
|
|
297
|
+
"data-peak": peak !== null ? String(peak.active) : undefined,
|
|
363
298
|
role: "dialog",
|
|
364
299
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
365
300
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
@@ -367,7 +302,20 @@ window.__ModuleLoader__.load({
|
|
|
367
302
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
368
303
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
369
304
|
className: styles.sidePopTitle,
|
|
370
|
-
children:
|
|
305
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
306
|
+
children: [t("title"), peak !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
307
|
+
label: peak.active
|
|
308
|
+
? t("peak.tooltip.active", { input: peak.inputMiss, output: peak.output })
|
|
309
|
+
: t("peak.tooltip.offpeak", { input: peak.inputMiss, output: peak.output }),
|
|
310
|
+
side: "top",
|
|
311
|
+
delayMs: 200,
|
|
312
|
+
children: (0, react_jsx_runtime.jsx)("span", {
|
|
313
|
+
className: styles.peakTag,
|
|
314
|
+
"data-peak": String(peak.active),
|
|
315
|
+
children: peak.active ? t("peak.active") : t("peak.offpeak")
|
|
316
|
+
})
|
|
317
|
+
})]
|
|
318
|
+
})
|
|
371
319
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
372
320
|
type: "button",
|
|
373
321
|
className: styles.sidePopClose,
|
|
@@ -409,24 +357,35 @@ window.__ModuleLoader__.load({
|
|
|
409
357
|
})]
|
|
410
358
|
})
|
|
411
359
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
412
|
-
className: styles.val,
|
|
360
|
+
className: styles.val, "data-tier": "ter",
|
|
413
361
|
children: loading ? "…" : lastPrompt !== null ? formatCny(lastPrompt) : "—"
|
|
414
362
|
})]
|
|
415
363
|
})
|
|
364
|
+
}), sessionTitle !== null && sessionTitle !== "" && (0, react_jsx_runtime.jsx)("div", {
|
|
365
|
+
className: styles.sessionNote,
|
|
366
|
+
title: sessionTitle,
|
|
367
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
368
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
369
|
+
children: t("sessionNote")
|
|
370
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
371
|
+
className: styles.sessionName,
|
|
372
|
+
children: sessionTitle
|
|
373
|
+
})]
|
|
374
|
+
})
|
|
416
375
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
417
376
|
className: styles.row,
|
|
418
377
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
419
378
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
420
379
|
className: styles.label,
|
|
421
380
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
422
|
-
children: [t("
|
|
423
|
-
label: t("explain.
|
|
381
|
+
children: [t("todaySession"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
382
|
+
label: t("explain.todaySession"),
|
|
424
383
|
t
|
|
425
384
|
})]
|
|
426
385
|
})
|
|
427
386
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
428
387
|
className: styles.val,
|
|
429
|
-
children: loading ? "…" :
|
|
388
|
+
children: loading ? "…" : todaySession !== null ? formatCny(todaySession) : "—"
|
|
430
389
|
})]
|
|
431
390
|
})
|
|
432
391
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -441,7 +400,7 @@ window.__ModuleLoader__.load({
|
|
|
441
400
|
})]
|
|
442
401
|
})
|
|
443
402
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
444
|
-
className: styles.val,
|
|
403
|
+
className: styles.val, "data-tier": "sec",
|
|
445
404
|
children: loading ? "…" : todayWs !== null ? formatCny(todayWs) : "—"
|
|
446
405
|
})]
|
|
447
406
|
})
|
|
@@ -457,7 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
457
416
|
})]
|
|
458
417
|
})
|
|
459
418
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
460
|
-
className: styles.val,
|
|
419
|
+
className: styles.val, "data-tier": "sec",
|
|
461
420
|
children: loading ? "…" : todayAll !== null ? formatCny(todayAll) : "—"
|
|
462
421
|
})]
|
|
463
422
|
})
|
|
@@ -501,21 +460,25 @@ window.__ModuleLoader__.load({
|
|
|
501
460
|
"refresh.title": "刷新余额与成本",
|
|
502
461
|
"balance": "账户余额",
|
|
503
462
|
"lastPrompt": "最近一次提问",
|
|
504
|
-
"
|
|
505
|
-
"todayWs": "
|
|
506
|
-
"todayAll": "
|
|
463
|
+
"todaySession": "今日·本会话",
|
|
464
|
+
"todayWs": "今日·本工作区",
|
|
465
|
+
"todayAll": "今日·所有工作区",
|
|
507
466
|
"explain.aria": "解释",
|
|
508
467
|
"explain.balance": "DeepSeek 账户可用余额,实时查询官方接口。",
|
|
509
468
|
"explain.lastPrompt": "最近活跃会话的最后一个问题(含 AI 回复)的费用估算。",
|
|
510
|
-
"explain.
|
|
469
|
+
"explain.todaySession": "当前会话今天产生的费用估算(从会话开始累计到今天)。",
|
|
511
470
|
"explain.todayWs": "当前工作区今天所有会话的费用合计估算。",
|
|
512
471
|
"explain.todayAll": "所有工作区今天所有会话的费用合计估算(含其他项目)。",
|
|
513
472
|
"warn.title.1": "余额不足 ¥5,建议及时充值",
|
|
514
473
|
"warn.title.2": "余额低于 ¥1,即将耗尽!",
|
|
515
474
|
"warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
|
|
516
475
|
"warn.text.2": "⛔ 余额低于 ¥1,即将耗尽,请立即充值!",
|
|
517
|
-
"
|
|
518
|
-
"
|
|
476
|
+
"todayCostShort": "今日·全部",
|
|
477
|
+
"peak.active": "峰时",
|
|
478
|
+
"peak.offpeak": "谷时",
|
|
479
|
+
"peak.tooltip.active": "当前为峰时(09:00-12:00、14:00-18:00,价格翻倍):输入 ¥{input}/M · 输出 ¥{output}/M",
|
|
480
|
+
"peak.tooltip.offpeak": "当前为谷时(其余时段,半价):输入 ¥{input}/M · 输出 ¥{output}/M",
|
|
481
|
+
"sessionNote": "基于最近活跃会话:",
|
|
519
482
|
"hint": "价格为估算值,实际以官方账单为准。",
|
|
520
483
|
"topUp": "去充值",
|
|
521
484
|
"close": "关闭"
|
|
@@ -528,21 +491,25 @@ window.__ModuleLoader__.load({
|
|
|
528
491
|
"refresh.title": "Refresh balance and costs",
|
|
529
492
|
"balance": "Account balance",
|
|
530
493
|
"lastPrompt": "Last prompt",
|
|
531
|
-
"
|
|
532
|
-
"todayWs": "Today · this
|
|
533
|
-
"todayAll": "Today · all",
|
|
494
|
+
"todaySession": "Today · this session",
|
|
495
|
+
"todayWs": "Today · this workspace",
|
|
496
|
+
"todayAll": "Today · all workspaces",
|
|
534
497
|
"explain.aria": "Explain",
|
|
535
498
|
"explain.balance": "DeepSeek account balance, fetched live from the official API.",
|
|
536
499
|
"explain.lastPrompt": "Estimated cost of the latest question (incl. the AI reply) in the most recent session.",
|
|
537
|
-
"explain.
|
|
500
|
+
"explain.todaySession": "Estimated cost of today's usage in the current session.",
|
|
538
501
|
"explain.todayWs": "Estimated total cost of today's sessions in the current workspace.",
|
|
539
502
|
"explain.todayAll": "Estimated total cost of today's sessions across all workspaces.",
|
|
540
503
|
"warn.title.1": "Balance below ¥5, consider topping up",
|
|
541
504
|
"warn.title.2": "Balance below ¥1, almost exhausted!",
|
|
542
505
|
"warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
|
|
543
506
|
"warn.text.2": "⛔ Balance below ¥1, almost exhausted — top up now!",
|
|
544
|
-
"
|
|
545
|
-
"
|
|
507
|
+
"todayCostShort": "Today · all",
|
|
508
|
+
"peak.active": "Peak",
|
|
509
|
+
"peak.offpeak": "Off-peak",
|
|
510
|
+
"peak.tooltip.active": "Peak hours now (09:00-12:00, 14:00-18:00, 2× price): input ¥{input}/M · output ¥{output}/M",
|
|
511
|
+
"peak.tooltip.offpeak": "Off-peak hours now (rest of day, half price): input ¥{input}/M · output ¥{output}/M",
|
|
512
|
+
"sessionNote": "Based on most recent session:",
|
|
546
513
|
"hint": "Prices are estimates; actual billing from the provider is authoritative.",
|
|
547
514
|
"topUp": "Top up",
|
|
548
515
|
"close": "Close"
|
package/lib/index.js
CHANGED
|
@@ -246,9 +246,15 @@ const PRICING_ALIASES = {
|
|
|
246
246
|
"deepseek-official": "deepseek-v4-flash"
|
|
247
247
|
};
|
|
248
248
|
|
|
249
|
-
/**
|
|
249
|
+
/**
|
|
250
|
+
* Whether `date` falls inside a Beijing-time (UTC+8) peak window.
|
|
251
|
+
* DeepSeek's peak pricing applies to Beijing time 09:00-12:00 and 14:00-18:00
|
|
252
|
+
* regardless of the host's timezone, so the window is computed in UTC+8
|
|
253
|
+
* explicitly instead of the machine's local time.
|
|
254
|
+
*/
|
|
250
255
|
function isPeak(date) {
|
|
251
|
-
const
|
|
256
|
+
const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000);
|
|
257
|
+
const hour = beijing.getUTCHours();
|
|
252
258
|
return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
|
|
253
259
|
}
|
|
254
260
|
|
|
@@ -330,30 +336,40 @@ function extractModelBlock(html, model) {
|
|
|
330
336
|
}
|
|
331
337
|
|
|
332
338
|
/**
|
|
333
|
-
* Parse per-1M-token rates from a block of HTML text.
|
|
334
|
-
* "
|
|
335
|
-
*
|
|
339
|
+
* Parse per-1M-token rates from a block of HTML text. The pricing table lists
|
|
340
|
+
* rows per metric with "空闲时段 <off-peak> … 高峰时段 <peak>" columns (CN) or
|
|
341
|
+
* "off-peak … peak" (EN); the first model column is the one we price. Each
|
|
342
|
+
* metric is captured as an (offPeak, peak) pair from its labelled row.
|
|
343
|
+
* Returns { offPeak, peak } or null when any off-peak figure is missing
|
|
344
|
+
* (the caller then falls back to the built-in rate table rather than silently
|
|
345
|
+
* pricing cache hits at 0).
|
|
336
346
|
*/
|
|
337
347
|
function parseRates(block) {
|
|
338
348
|
// Strip tags to plain text for simpler matching.
|
|
339
349
|
const text = block.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
350
|
+
const pair = (label) => {
|
|
351
|
+
// label + short gap + "空闲时段 <n>" + gap + "高峰时段 <n>" (first model
|
|
352
|
+
// column). Also tolerate EN "off-peak … peak".
|
|
353
|
+
const re = new RegExp(
|
|
354
|
+
label + "[\\s\\S]{0,120}?(?:空闲时段|off[- ]?peak)\\s*([\\d.]+)[\\s\\S]{0,80}?(?:高峰时段|peak)\\s*([\\d.]+)",
|
|
355
|
+
"i"
|
|
356
|
+
);
|
|
357
|
+
const m = text.match(re);
|
|
358
|
+
if (!m) return null;
|
|
359
|
+
const a = parseFloat(m[1]);
|
|
360
|
+
const b = parseFloat(m[2]);
|
|
361
|
+
return Number.isFinite(a) && Number.isFinite(b) ? [a, b] : null;
|
|
345
362
|
};
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const peakOutput = (offPeakOutput ?? 0) * 2;
|
|
363
|
+
const hit = pair("缓存命中");
|
|
364
|
+
const miss = pair("缓存未命中");
|
|
365
|
+
const out = pair("百万tokens输出") ?? pair("输出");
|
|
366
|
+
if (miss === null || out === null) return null;
|
|
367
|
+
// Cache-hit row missing is acceptable only if it cannot be parsed at all —
|
|
368
|
+
// reject rather than guess zero.
|
|
369
|
+
if (hit === null) return null;
|
|
354
370
|
return {
|
|
355
|
-
|
|
356
|
-
|
|
371
|
+
offPeak: { inputMiss: miss[0], inputHit: hit[0], output: out[0] },
|
|
372
|
+
peak: { inputMiss: miss[1], inputHit: hit[1], output: out[1] }
|
|
357
373
|
};
|
|
358
374
|
}
|
|
359
375
|
|
|
@@ -461,6 +477,7 @@ function parseSessionUsages(text) {
|
|
|
461
477
|
const samples = [];
|
|
462
478
|
let currentModel = void 0;
|
|
463
479
|
let cwd = void 0;
|
|
480
|
+
let title = void 0;
|
|
464
481
|
for (const line of text.split("\n")) {
|
|
465
482
|
if (line === "") continue;
|
|
466
483
|
let event;
|
|
@@ -472,6 +489,8 @@ function parseSessionUsages(text) {
|
|
|
472
489
|
const type = event.type;
|
|
473
490
|
if (type === "session") {
|
|
474
491
|
cwd = event.cwd;
|
|
492
|
+
} else if (type === "session/title") {
|
|
493
|
+
title = event.data?.title ?? title;
|
|
475
494
|
} else if (type === "request/header") {
|
|
476
495
|
currentModel = event.data?.header?.config?.model ?? currentModel;
|
|
477
496
|
} else if (type === "assistant/message") {
|
|
@@ -486,7 +505,7 @@ function parseSessionUsages(text) {
|
|
|
486
505
|
}
|
|
487
506
|
}
|
|
488
507
|
}
|
|
489
|
-
return { samples, cwd };
|
|
508
|
+
return { samples, cwd, title };
|
|
490
509
|
}
|
|
491
510
|
|
|
492
511
|
/** Cost of the last turn (the most recent user prompt) in one session file. */
|
|
@@ -506,12 +525,16 @@ function lastTurnCost(samples) {
|
|
|
506
525
|
return { cost, inputTokens: input, outputTokens: output };
|
|
507
526
|
}
|
|
508
527
|
|
|
509
|
-
/**
|
|
510
|
-
function
|
|
528
|
+
/** Cost of one session's usage that falls on the current calendar day. */
|
|
529
|
+
function todaySessionCost(samples) {
|
|
530
|
+
const now = new Date();
|
|
531
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
532
|
+
const dayEnd = dayStart + 24 * 60 * 60 * 1000;
|
|
511
533
|
let cost = 0;
|
|
512
534
|
let input = 0;
|
|
513
535
|
let output = 0;
|
|
514
536
|
for (const s of samples) {
|
|
537
|
+
if (s.time < dayStart || s.time >= dayEnd) continue;
|
|
515
538
|
cost += priceUsage(s.usage, s.model, s.time);
|
|
516
539
|
input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
|
|
517
540
|
output += s.usage.outputTokens ?? 0;
|
|
@@ -580,7 +603,34 @@ function todayCost(sessionList, cwdFilter) {
|
|
|
580
603
|
return { cost, inputTokens: input, outputTokens: output };
|
|
581
604
|
}
|
|
582
605
|
|
|
583
|
-
/**
|
|
606
|
+
/**
|
|
607
|
+
* Decompressed session parse cache, keyed by path and validated against the
|
|
608
|
+
* file's mtime. Long-lived: an untouched file is never re-decompressed, so a
|
|
609
|
+
* cold /today-cost after the outer 30s TTL expires only re-reads files that
|
|
610
|
+
* actually changed. Cleared wholesale when it outgrows a bound.
|
|
611
|
+
*/
|
|
612
|
+
const sessionParseCache = new Map();
|
|
613
|
+
const SESSION_PARSE_CACHE_MAX = 600;
|
|
614
|
+
|
|
615
|
+
/** Decompress+parse one file, honoring the (path,mtime) cache. */
|
|
616
|
+
async function parseSessionFileCached(path, mtimeMs) {
|
|
617
|
+
const hit = sessionParseCache.get(path);
|
|
618
|
+
if (hit !== void 0 && hit.mtimeMs === mtimeMs) return hit.parsed;
|
|
619
|
+
const result = await decompressSession(path);
|
|
620
|
+
if (!result.ok) return null;
|
|
621
|
+
const parsed = parseSessionUsages(result.text);
|
|
622
|
+
if (sessionParseCache.size >= SESSION_PARSE_CACHE_MAX) sessionParseCache.clear();
|
|
623
|
+
sessionParseCache.set(path, { mtimeMs, parsed });
|
|
624
|
+
return parsed;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Walk every session file under the sessions root and parse the ones that can
|
|
629
|
+
* contribute to "today" numbers. Only files whose mtime falls on the current
|
|
630
|
+
* local calendar day can contain today's samples, so older files are skipped
|
|
631
|
+
* entirely (avoids decompressing the whole ~/.dsh/sessions tree on every cold
|
|
632
|
+
* call). Parsing runs in parallel; results are cached by (path, mtime).
|
|
633
|
+
*/
|
|
584
634
|
async function collectAllSessionSamples() {
|
|
585
635
|
const root = sessionsRoot();
|
|
586
636
|
const fs = (await awaitImportFs());
|
|
@@ -597,14 +647,17 @@ async function collectAllSessionSamples() {
|
|
|
597
647
|
} catch {
|
|
598
648
|
return [];
|
|
599
649
|
}
|
|
600
|
-
const
|
|
650
|
+
const now = new Date();
|
|
651
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
652
|
+
const candidates = [];
|
|
601
653
|
for (const file of files) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
654
|
+
try {
|
|
655
|
+
const mtimeMs = fs.statSync(file).mtimeMs;
|
|
656
|
+
if (mtimeMs >= dayStart) candidates.push({ path: file, mtimeMs });
|
|
657
|
+
} catch { /* file vanished mid-walk */ }
|
|
606
658
|
}
|
|
607
|
-
|
|
659
|
+
const parsed = await Promise.all(candidates.map((c) => parseSessionFileCached(c.path, c.mtimeMs)));
|
|
660
|
+
return parsed.filter((p) => p !== null);
|
|
608
661
|
}
|
|
609
662
|
|
|
610
663
|
/** Simple in-memory TTL cache for cost computations (30s). */
|
|
@@ -695,24 +748,36 @@ function makeRoutes(ctx, config) {
|
|
|
695
748
|
path: API.todayCost,
|
|
696
749
|
handler: async (req, res) => {
|
|
697
750
|
if (!guard(req, res, "GET")) return;
|
|
698
|
-
const
|
|
751
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
752
|
+
const sessionId = url.searchParams.get("session") ?? "";
|
|
753
|
+
const key = `today:${sessionId}`;
|
|
699
754
|
const compute = async () => {
|
|
700
755
|
const sessionList = await collectAllSessionSamples();
|
|
701
756
|
const all = todayCost(sessionList);
|
|
702
|
-
// "Current workspace": the cwd of the
|
|
703
|
-
//
|
|
757
|
+
// "Current workspace": prefer the cwd of the explicitly passed
|
|
758
|
+
// session (the client's currently selected session). Fall back
|
|
759
|
+
// to the session with the most samples today when absent.
|
|
704
760
|
let activeCwd = void 0;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
761
|
+
if (sessionId !== "") {
|
|
762
|
+
const target = await resolveSessionFile(sessionId);
|
|
763
|
+
if (target !== void 0) {
|
|
764
|
+
const text = await decompressSession(target);
|
|
765
|
+
if (text.ok) activeCwd = parseSessionUsages(text.text).cwd;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (activeCwd === void 0) {
|
|
769
|
+
let activeCount = 0;
|
|
770
|
+
for (const session of sessionList) {
|
|
771
|
+
if (session.cwd === void 0) continue;
|
|
772
|
+
const todaySamples = session.samples.filter((s) => {
|
|
773
|
+
const now = new Date();
|
|
774
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
775
|
+
return s.time >= dayStart && s.time < dayStart + 24 * 60 * 60 * 1000;
|
|
776
|
+
}).length;
|
|
777
|
+
if (todaySamples > activeCount) {
|
|
778
|
+
activeCount = todaySamples;
|
|
779
|
+
activeCwd = session.cwd;
|
|
780
|
+
}
|
|
716
781
|
}
|
|
717
782
|
}
|
|
718
783
|
const workspace = todayCost(sessionList, activeCwd);
|
|
@@ -732,7 +797,13 @@ function makeRoutes(ctx, config) {
|
|
|
732
797
|
path: API.activeCost,
|
|
733
798
|
handler: async (req, res) => {
|
|
734
799
|
if (!guard(req, res, "GET")) return;
|
|
735
|
-
const
|
|
800
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
801
|
+
const sessionId = url.searchParams.get("session") ?? "";
|
|
802
|
+
// Prefer the explicitly passed session (the client's currently
|
|
803
|
+
// selected one); fall back to the most recently active session.
|
|
804
|
+
const file = sessionId !== ""
|
|
805
|
+
? await resolveSessionFile(sessionId)
|
|
806
|
+
: await findMostRecentSessionFile();
|
|
736
807
|
if (file === void 0) {
|
|
737
808
|
writeJson(res, 404, { error: "no session files found", modelId: config.modelId });
|
|
738
809
|
return;
|
|
@@ -744,11 +815,36 @@ function makeRoutes(ctx, config) {
|
|
|
744
815
|
}
|
|
745
816
|
const parsed = parseSessionUsages(result.text);
|
|
746
817
|
const last = lastTurnCost(parsed.samples);
|
|
747
|
-
const
|
|
818
|
+
const todaySession = todaySessionCost(parsed.samples);
|
|
819
|
+
// Current peak/off-peak state + price tier for the configured model.
|
|
820
|
+
const now = new Date();
|
|
821
|
+
const peakActive = isPeak(now);
|
|
822
|
+
const table = peakActive
|
|
823
|
+
? pricingTable[pricingKey(config.modelId)].peak
|
|
824
|
+
: pricingTable[pricingKey(config.modelId)].offPeak;
|
|
825
|
+
// Short workspace name from the cwd path (last segment).
|
|
826
|
+
let workspaceName = "未知工作区";
|
|
827
|
+
if (typeof parsed.cwd === "string" && parsed.cwd !== "") {
|
|
828
|
+
const segments = parsed.cwd.split("/").filter((s) => s !== "");
|
|
829
|
+
workspaceName = segments.length > 0 ? segments[segments.length - 1] : parsed.cwd;
|
|
830
|
+
}
|
|
831
|
+
// Session id from the file path (…/session-<id>/session.jsonl.zstd).
|
|
832
|
+
let fileSessionId = void 0;
|
|
833
|
+
const sidMatch = file.match(/session-([0-9a-f-]+)/);
|
|
834
|
+
if (sidMatch !== null) fileSessionId = sidMatch[1];
|
|
748
835
|
writeJson(res, 200, {
|
|
749
836
|
lastPrompt: last,
|
|
750
|
-
|
|
837
|
+
todaySession,
|
|
751
838
|
cwd: parsed.cwd,
|
|
839
|
+
title: parsed.title,
|
|
840
|
+
workspaceName,
|
|
841
|
+
sessionId: fileSessionId,
|
|
842
|
+
peak: {
|
|
843
|
+
active: peakActive,
|
|
844
|
+
inputMiss: table.inputMiss,
|
|
845
|
+
inputHit: table.inputHit,
|
|
846
|
+
output: table.output
|
|
847
|
+
},
|
|
752
848
|
modelId: config.modelId,
|
|
753
849
|
pricingSource,
|
|
754
850
|
pricingSyncedAt
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-balance-widget",
|
|
3
|
-
"description": "Balance & cost widget for the dsh web GUI: a sidebar footer card shows the DeepSeek account balance
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Balance & cost widget for the dsh web GUI: a sidebar footer card shows the DeepSeek account balance and today's costs; clicking opens a popover with a five-tier cost breakdown (last prompt with session name / today-this-session / today-this-workspace / today-all-workspaces), peak/off-peak status tags, term explanations, and a one-click top-up link. Zero external dependencies — works on Node 24.",
|
|
4
|
+
"version": "0.5.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"files": [
|