dsh-balance-widget 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +27 -15
- package/README.md +27 -16
- package/docs/screenshot-corner.png +0 -0
- package/docs/screenshot-popover.png +0 -0
- package/lib/client.js +90 -42
- package/lib/index.js +70 -20
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
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
|
|
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
|
-
|
|
|
16
|
+
| Sidebar card | Five-tier cost popover |
|
|
17
17
|
| --- | --- |
|
|
18
|
-
|  |  |
|
|
19
19
|
|
|
20
20
|
## How it differs from similar plugins
|
|
21
21
|
|
|
@@ -32,14 +32,16 @@ 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, refreshes every 60s.
|
|
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
47
|
- **On-demand refresh** — No polling, no background requests; endpoints are only hit when you click the icon. Costs zero tokens to use.
|
|
@@ -54,14 +56,16 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
|
|
|
54
56
|
```
|
|
55
57
|
host half (lib/index.js)
|
|
56
58
|
ctx.webServer.register:
|
|
57
|
-
GET /api/dsh-balance/balance
|
|
58
|
-
GET /api/dsh-balance/
|
|
59
|
-
GET /api/dsh-balance/today-cost
|
|
59
|
+
GET /api/dsh-balance/balance → official /user/balance (loopback-only guard)
|
|
60
|
+
GET /api/dsh-balance/active-cost → last prompt + today-this-session (with session name, most recent session)
|
|
61
|
+
GET /api/dsh-balance/today-cost → today's costs (dual: current workspace + all workspaces)
|
|
62
|
+
Zero @deepseek-ai/* imports; loads from any profile layout.
|
|
63
|
+
Also registers a deepseek_billing tool for model-driven queries.
|
|
60
64
|
|
|
61
65
|
client half (lib/client.js)
|
|
62
|
-
ctx.slots.inject("
|
|
63
|
-
→
|
|
64
|
-
→ click
|
|
66
|
+
ctx.slots.inject("sidebar.footer.action")
|
|
67
|
+
→ persistent sidebar footer card (balance + today's cost, peak-tinted border)
|
|
68
|
+
→ click opens five-tier cost popover + peak tag + ⓘ term explanations
|
|
65
69
|
```
|
|
66
70
|
|
|
67
71
|
## Installation
|
|
@@ -150,6 +154,14 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
|
|
|
150
154
|
|
|
151
155
|
## Changelog
|
|
152
156
|
|
|
157
|
+
### v0.5.0 — Five-tier costs & peak/off-peak status
|
|
158
|
+
- ✨ **Added**: the cost breakdown is now five tiers — balance / last prompt / today-this-session / today-this-workspace / today-all-workspaces
|
|
159
|
+
- The last-prompt row labels the **session name** underneath (based on the most recent session)
|
|
160
|
+
- "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
|
|
161
|
+
- ✨ **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
|
|
162
|
+
- 🎨 **Changed**: removed the remaining-ratio bar; the balance number is now color-coded directly by threshold (healthy / amber / red)
|
|
163
|
+
- 🗑️ **Removed**: the "This session" row (all-time session total) from the popover
|
|
164
|
+
|
|
153
165
|
### v0.2.0 — Last prompt & today total cost
|
|
154
166
|
- ✨ **Added**: popover now shows "last prompt cost" and "today total cost"
|
|
155
167
|
- Last prompt: prices the current session's last turn from the session file
|
package/README.md
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
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
|
-
|  |  |
|
|
19
19
|
|
|
20
20
|
## 与同类插件的区别
|
|
21
21
|
|
|
@@ -32,14 +32,16 @@ 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
47
|
- **按需刷新** — 无轮询、无后台请求;只有点击图标时才发起查询,不消耗任何 token。
|
|
@@ -49,15 +51,16 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:在会话头部右
|
|
|
49
51
|
```
|
|
50
52
|
host 半区 (lib/index.js)
|
|
51
53
|
ctx.webServer.register:
|
|
52
|
-
GET /api/dsh-balance/balance
|
|
53
|
-
GET /api/dsh-balance/
|
|
54
|
-
GET /api/dsh-balance/today-cost
|
|
54
|
+
GET /api/dsh-balance/balance → 官方 /user/balance(loopback-only 守卫)
|
|
55
|
+
GET /api/dsh-balance/active-cost → 最近活跃会话的最近提问 + 今日·本会话(含会话名)
|
|
56
|
+
GET /api/dsh-balance/today-cost → 今日成本(双值:当前工作区 + 所有工作区)
|
|
55
57
|
依赖:零外部 @deepseek-ai/* import,任何 profile 布局均可解析
|
|
58
|
+
另有 deepseek_billing 工具供模型直接查询余额/成本
|
|
56
59
|
|
|
57
60
|
client 半区 (lib/client.js)
|
|
58
|
-
ctx.slots.inject("
|
|
59
|
-
→
|
|
60
|
-
→
|
|
61
|
+
ctx.slots.inject("sidebar.footer.action")
|
|
62
|
+
→ 侧边栏底部常驻卡片(余额 + 今日花费,峰/谷时段描边着色)
|
|
63
|
+
→ 点击弹出五层级成本明细 + 峰谷标签 + ⓘ 名词解释
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
## 安装
|
|
@@ -148,6 +151,14 @@ DSH 的插件配置统一放在这个文件里:
|
|
|
148
151
|
|
|
149
152
|
## 版本历史
|
|
150
153
|
|
|
154
|
+
### v0.5.0 — 五层级成本与峰谷状态
|
|
155
|
+
- ✨ **新增**:成本明细改为五层级——余额 / 最近一次提问 / 今日·本会话 / 今日·本工作区 / 今日·所有工作区
|
|
156
|
+
- 最近一次提问下方标注**会话名**(基于最近活跃会话)
|
|
157
|
+
- 「今日·本会话」= 当前会话今天产生的费用;「今日·本工作区」= 当前工作区今天所有会话合计(由当前会话锚定工作区);「今日·所有工作区」= 全部工作区今天合计
|
|
158
|
+
- ✨ **新增**:峰/谷时段状态可视化——卡片与弹框边框按时段着色(峰时橙色 / 谷时绿色),弹框标题旁显示「峰时/谷时」标签,悬停查看当前价格档位
|
|
159
|
+
- 🎨 **调整**:移除余额剩余比例条,改为余额数字按阈值直接变色(充足 / 黄 / 红)
|
|
160
|
+
- 🗑️ **移除**:弹框中的「本会话成本」(会话全程累计)行
|
|
161
|
+
|
|
151
162
|
### v0.2.0 — 最近一次提问与今日总成本
|
|
152
163
|
- ✨ **新增**:弹层增加「最近一次提问成本」与「今天总成本」两项
|
|
153
164
|
- 最近一次提问:解析当前会话最后一个 turn 的 token 用量 × 单价
|
|
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 = ".dshbw_wrap{position:relative;display:inline-flex;align-items:center}.dshbw_btn{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit}.dshbw_btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_btn[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_btn[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_pop{position:absolute;top:calc(100% + 6px);right:0;z-index:60;min-width:230px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.dshbw_head{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}.dshbw_title{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_refresh{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:24px;height:24px;border-radius:6px;font-size:13px;line-height:1;padding:0;font-family:inherit}.dshbw_refresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_refresh[data-spinning=true]{animation:dshbw-spin .8s linear infinite}.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-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:11px;line-height:16px;margin-top:6px}.dshbw_hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin-top:6px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px}.dshbw_foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}.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}@keyframes dshbw-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.dshbw_side{display:flex;flex-direction:column;gap:4px;padding:6px 8px;margin:0 8px 4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sideTop{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;line-height:18px}.dshbw_sideAmount{font-weight:
|
|
11
|
+
const css = ".dshbw_wrap{position:relative;display:inline-flex;align-items:center}.dshbw_btn{background:none;border:none;cursor:pointer;color:var(--dsw-alias-label-secondary);display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:6px;font-size:12px;line-height:18px;font-family:inherit}.dshbw_btn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_btn[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary)}.dshbw_btn[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_pop{position:absolute;top:calc(100% + 6px);right:0;z-index:60;min-width:230px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.dshbw_head{display:flex;align-items:center;justify-content:space-between;margin-bottom:4px}.dshbw_title{font-weight:600;color:var(--dsw-alias-label-primary)}.dshbw_refresh{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:24px;height:24px;border-radius:6px;font-size:13px;line-height:1;padding:0;font-family:inherit}.dshbw_refresh:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3);background:var(--dsw-alias-interactive-bg-hover)}.dshbw_refresh[data-spinning=true]{animation:dshbw-spin .8s linear infinite}.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-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:11px;line-height:16px;margin-top:6px}.dshbw_hint{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin-top:6px;border-top:1px solid var(--dsw-alias-border-l1);padding-top:6px}.dshbw_foot{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}.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}@keyframes dshbw-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.dshbw_side{display:flex;flex-direction:column;gap:4px;padding:6px 8px;margin:0 8px 4px;border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.dshbw_side:hover{background:var(--dsw-alias-interactive-bg-hover)}.dshbw_sideTop{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;line-height:18px}.dshbw_sideAmount{font-weight:700}.dshbw_sideAmount[data-level=\"1\"]{color:var(--dsw-alias-state-warn-primary);font-weight:700}.dshbw_sideAmount[data-level=\"2\"]{color:var(--dsw-alias-state-error-primary)}.dshbw_sideFoot{display:flex;justify-content:space-between;font-size:10px;line-height:14px;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:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;text-align:left}.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:1px solid var(--dsw-alias-border-l3);border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.18);padding:12px 14px;font-size:12px;line-height:20px;color:var(--dsw-alias-label-primary)}.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);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, var(--dsw-alias-bg-layer-2))}.dshbw_side[data-peak=\"false\"]{border:1px solid var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary) 8%, var(--dsw-alias-bg-layer-2))}.dshbw_sidePop[data-peak=\"true\"]{border-color:var(--dsw-alias-state-warn-primary)}.dshbw_sidePop[data-peak=\"false\"]{border-color:var(--dsw-alias-state-success-primary)}.dshbw_peakTag{display:inline-flex;align-items:center;gap:4px;font-size:11px;line-height:16px;padding:1px 6px;border-radius:999px}.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:11px;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}";
|
|
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");
|
|
@@ -35,15 +35,16 @@ window.__ModuleLoader__.load({
|
|
|
35
35
|
"sideWrap": "dshbw_sideWrap",
|
|
36
36
|
"sideTop": "dshbw_sideTop",
|
|
37
37
|
"sideAmount": "dshbw_sideAmount",
|
|
38
|
-
"bar": "dshbw_bar",
|
|
39
|
-
"barFill": "dshbw_barFill",
|
|
40
38
|
"sideFoot": "dshbw_sideFoot",
|
|
41
39
|
"sidePop": "dshbw_sidePop",
|
|
42
40
|
"sidePopHead": "dshbw_sidePopHead",
|
|
43
41
|
"sidePopTitle": "dshbw_sidePopTitle",
|
|
44
42
|
"sidePopClose": "dshbw_sidePopClose",
|
|
45
43
|
"sidePopFoot": "dshbw_sidePopFoot",
|
|
46
|
-
"sidePopRefresh": "dshbw_sidePopRefresh"
|
|
44
|
+
"sidePopRefresh": "dshbw_sidePopRefresh",
|
|
45
|
+
"peakTag": "dshbw_peakTag",
|
|
46
|
+
"sessionNote": "dshbw_sessionNote",
|
|
47
|
+
"sessionName": "dshbw_sessionName"
|
|
47
48
|
};
|
|
48
49
|
//#endregion
|
|
49
50
|
//#region lib/types/client/pricing.js
|
|
@@ -136,15 +137,29 @@ window.__ModuleLoader__.load({
|
|
|
136
137
|
/** Official DeepSeek platform top-up page (opens in a new tab). */
|
|
137
138
|
const TOP_UP_URL = "https://platform.deepseek.com/top_up";
|
|
138
139
|
/**
|
|
139
|
-
* Fetch the active session's costs (last prompt + session total).
|
|
140
|
-
* @
|
|
140
|
+
* Fetch the active session's costs (last prompt + today's session total).
|
|
141
|
+
* @param {string} [sessionId] - the currently selected session; when omitted
|
|
142
|
+
* the host falls back to its most recently active session.
|
|
143
|
+
* @returns { ok, lastPrompt?, todaySession?, sessionTitle?, workspaceName?, peak?, error? }
|
|
141
144
|
*/
|
|
142
|
-
async function fetchActiveCost() {
|
|
145
|
+
async function fetchActiveCost(sessionId) {
|
|
143
146
|
try {
|
|
144
|
-
const
|
|
147
|
+
const url = sessionId === void 0 || sessionId === ""
|
|
148
|
+
? ACTIVE_COST_ROUTE
|
|
149
|
+
: `${ACTIVE_COST_ROUTE}?session=${encodeURIComponent(sessionId)}`;
|
|
150
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
145
151
|
const payload = await response.json().catch(() => ({}));
|
|
146
152
|
if (!response.ok) return { ok: false, error: payload.error ?? `active-cost responded ${response.status}` };
|
|
147
|
-
return {
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
lastPrompt: payload.lastPrompt?.cost ?? 0,
|
|
156
|
+
todaySession: payload.todaySession?.cost ?? 0,
|
|
157
|
+
sessionTitle: payload.title,
|
|
158
|
+
workspaceName: payload.workspaceName,
|
|
159
|
+
cwd: payload.cwd,
|
|
160
|
+
sessionId: payload.sessionId,
|
|
161
|
+
peak: payload.peak
|
|
162
|
+
};
|
|
148
163
|
} catch (error) {
|
|
149
164
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
150
165
|
}
|
|
@@ -238,20 +253,30 @@ window.__ModuleLoader__.load({
|
|
|
238
253
|
* cost, top-up, refresh) using only global data.
|
|
239
254
|
* @param {Object} props - wide (sidebar expanded), t (locale).
|
|
240
255
|
*/
|
|
241
|
-
function SidebarBalanceCard({ wide, t }) {
|
|
256
|
+
function SidebarBalanceCard({ wide, t, useSessions }) {
|
|
242
257
|
const [open, setOpen] = (0, react.useState)(false);
|
|
243
258
|
const [balance, setBalance] = (0, react.useState)(null);
|
|
244
259
|
const [lastPrompt, setLastPrompt] = (0, react.useState)(null);
|
|
245
|
-
const [
|
|
260
|
+
const [todaySession, setTodaySession] = (0, react.useState)(null);
|
|
261
|
+
const [sessionTitle, setSessionTitle] = (0, react.useState)(null);
|
|
246
262
|
const [todayWs, setTodayWs] = (0, react.useState)(null);
|
|
247
263
|
const [todayAll, setTodayAll] = (0, react.useState)(null);
|
|
248
264
|
const [lowThreshold, setLowThreshold] = (0, react.useState)(5);
|
|
249
265
|
const [criticalThreshold, setCriticalThreshold] = (0, react.useState)(1);
|
|
250
266
|
const [error, setError] = (0, react.useState)(null);
|
|
251
267
|
const [loading, setLoading] = (0, react.useState)(false);
|
|
268
|
+
const [peak, setPeak] = (0, react.useState)(null);
|
|
269
|
+
// The currently selected session id (the session the user is viewing).
|
|
270
|
+
// This anchors "today · this workspace" to the actual current workspace.
|
|
271
|
+
const currentSessionId = typeof useSessions === "function"
|
|
272
|
+
? (0, useSessions)((s) => s.current)
|
|
273
|
+
: void 0;
|
|
252
274
|
/** Fetch today's costs (workspace + all) from the dual-value route. */
|
|
253
275
|
const fetchTodayCosts = async () => {
|
|
254
|
-
const
|
|
276
|
+
const url = currentSessionId !== void 0 && currentSessionId !== ""
|
|
277
|
+
? `${TODAY_COST_ROUTE}?session=${encodeURIComponent(currentSessionId)}`
|
|
278
|
+
: TODAY_COST_ROUTE;
|
|
279
|
+
const response = await fetch(url, { headers: { accept: "application/json" } });
|
|
255
280
|
const payload = await response.json().catch(() => ({}));
|
|
256
281
|
if (!response.ok) return { ok: false, error: payload.error ?? `today-cost responded ${response.status}` };
|
|
257
282
|
return {
|
|
@@ -265,7 +290,7 @@ window.__ModuleLoader__.load({
|
|
|
265
290
|
setError(null);
|
|
266
291
|
const [bResult, aResult, tResult] = await Promise.all([
|
|
267
292
|
fetchBalance(),
|
|
268
|
-
fetchActiveCost(),
|
|
293
|
+
fetchActiveCost(currentSessionId),
|
|
269
294
|
fetchTodayCosts()
|
|
270
295
|
]);
|
|
271
296
|
const failures = [];
|
|
@@ -278,7 +303,9 @@ window.__ModuleLoader__.load({
|
|
|
278
303
|
}
|
|
279
304
|
if (aResult.ok) {
|
|
280
305
|
setLastPrompt(aResult.lastPrompt);
|
|
281
|
-
|
|
306
|
+
setTodaySession(aResult.todaySession);
|
|
307
|
+
setSessionTitle(aResult.sessionTitle ?? null);
|
|
308
|
+
setPeak(aResult.peak ?? null);
|
|
282
309
|
} else {
|
|
283
310
|
failures.push(`会话: ${aResult.error}`);
|
|
284
311
|
}
|
|
@@ -295,7 +322,7 @@ window.__ModuleLoader__.load({
|
|
|
295
322
|
refresh(false);
|
|
296
323
|
const timer = setInterval(() => refresh(false), 60000);
|
|
297
324
|
return () => clearInterval(timer);
|
|
298
|
-
}, []);
|
|
325
|
+
}, [currentSessionId]);
|
|
299
326
|
// Close the popover when clicking outside the card.
|
|
300
327
|
const wrapRef = (0, react.useRef)(null);
|
|
301
328
|
(0, react.useEffect)(() => {
|
|
@@ -308,11 +335,6 @@ window.__ModuleLoader__.load({
|
|
|
308
335
|
return () => document.removeEventListener("mousedown", onDocClick);
|
|
309
336
|
}, [open]);
|
|
310
337
|
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
338
|
const toggle = async () => {
|
|
317
339
|
const next = !open;
|
|
318
340
|
setOpen(next);
|
|
@@ -325,6 +347,7 @@ window.__ModuleLoader__.load({
|
|
|
325
347
|
children: [(0, react_jsx_runtime.jsx)("button", {
|
|
326
348
|
type: "button",
|
|
327
349
|
className: styles.side,
|
|
350
|
+
"data-peak": peak !== null ? String(peak.active) : undefined,
|
|
328
351
|
title: t("title"),
|
|
329
352
|
onClick: toggle,
|
|
330
353
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
@@ -339,14 +362,6 @@ window.__ModuleLoader__.load({
|
|
|
339
362
|
children: balance !== null ? `${balance.symbol}${balance.total.toFixed(2)}` : "—"
|
|
340
363
|
})]
|
|
341
364
|
})
|
|
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
365
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
351
366
|
className: styles.sideFoot,
|
|
352
367
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
@@ -360,6 +375,7 @@ window.__ModuleLoader__.load({
|
|
|
360
375
|
})
|
|
361
376
|
}), open && (0, react_jsx_runtime.jsx)("div", {
|
|
362
377
|
className: styles.sidePop,
|
|
378
|
+
"data-peak": peak !== null ? String(peak.active) : undefined,
|
|
363
379
|
role: "dialog",
|
|
364
380
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
365
381
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
@@ -367,7 +383,20 @@ window.__ModuleLoader__.load({
|
|
|
367
383
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
368
384
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
369
385
|
className: styles.sidePopTitle,
|
|
370
|
-
children:
|
|
386
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
387
|
+
children: [t("title"), peak !== null && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
|
|
388
|
+
label: peak.active
|
|
389
|
+
? t("peak.tooltip.active", { input: peak.inputMiss, output: peak.output })
|
|
390
|
+
: t("peak.tooltip.offpeak", { input: peak.inputMiss, output: peak.output }),
|
|
391
|
+
side: "top",
|
|
392
|
+
delayMs: 200,
|
|
393
|
+
children: (0, react_jsx_runtime.jsx)("span", {
|
|
394
|
+
className: styles.peakTag,
|
|
395
|
+
"data-peak": String(peak.active),
|
|
396
|
+
children: peak.active ? t("peak.active") : t("peak.offpeak")
|
|
397
|
+
})
|
|
398
|
+
})]
|
|
399
|
+
})
|
|
371
400
|
}), (0, react_jsx_runtime.jsx)("button", {
|
|
372
401
|
type: "button",
|
|
373
402
|
className: styles.sidePopClose,
|
|
@@ -413,20 +442,31 @@ window.__ModuleLoader__.load({
|
|
|
413
442
|
children: loading ? "…" : lastPrompt !== null ? formatCny(lastPrompt) : "—"
|
|
414
443
|
})]
|
|
415
444
|
})
|
|
445
|
+
}), sessionTitle !== null && sessionTitle !== "" && (0, react_jsx_runtime.jsx)("div", {
|
|
446
|
+
className: styles.sessionNote,
|
|
447
|
+
title: sessionTitle,
|
|
448
|
+
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
449
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
450
|
+
children: t("sessionNote")
|
|
451
|
+
}), (0, react_jsx_runtime.jsx)("span", {
|
|
452
|
+
className: styles.sessionName,
|
|
453
|
+
children: sessionTitle
|
|
454
|
+
})]
|
|
455
|
+
})
|
|
416
456
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
417
457
|
className: styles.row,
|
|
418
458
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
419
459
|
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
420
460
|
className: styles.label,
|
|
421
461
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
422
|
-
children: [t("
|
|
423
|
-
label: t("explain.
|
|
462
|
+
children: [t("todaySession"), (0, react_jsx_runtime.jsx)(ExplainIcon, {
|
|
463
|
+
label: t("explain.todaySession"),
|
|
424
464
|
t
|
|
425
465
|
})]
|
|
426
466
|
})
|
|
427
467
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
428
468
|
className: styles.val,
|
|
429
|
-
children: loading ? "…" :
|
|
469
|
+
children: loading ? "…" : todaySession !== null ? formatCny(todaySession) : "—"
|
|
430
470
|
})]
|
|
431
471
|
})
|
|
432
472
|
}), (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -501,21 +541,25 @@ window.__ModuleLoader__.load({
|
|
|
501
541
|
"refresh.title": "刷新余额与成本",
|
|
502
542
|
"balance": "账户余额",
|
|
503
543
|
"lastPrompt": "最近一次提问",
|
|
504
|
-
"
|
|
505
|
-
"todayWs": "
|
|
506
|
-
"todayAll": "
|
|
544
|
+
"todaySession": "今日·本会话",
|
|
545
|
+
"todayWs": "今日·本工作区",
|
|
546
|
+
"todayAll": "今日·所有工作区",
|
|
507
547
|
"explain.aria": "解释",
|
|
508
548
|
"explain.balance": "DeepSeek 账户可用余额,实时查询官方接口。",
|
|
509
549
|
"explain.lastPrompt": "最近活跃会话的最后一个问题(含 AI 回复)的费用估算。",
|
|
510
|
-
"explain.
|
|
550
|
+
"explain.todaySession": "当前会话今天产生的费用估算(从会话开始累计到今天)。",
|
|
511
551
|
"explain.todayWs": "当前工作区今天所有会话的费用合计估算。",
|
|
512
552
|
"explain.todayAll": "所有工作区今天所有会话的费用合计估算(含其他项目)。",
|
|
513
553
|
"warn.title.1": "余额不足 ¥5,建议及时充值",
|
|
514
554
|
"warn.title.2": "余额低于 ¥1,即将耗尽!",
|
|
515
555
|
"warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
|
|
516
556
|
"warn.text.2": "⛔ 余额低于 ¥1,即将耗尽,请立即充值!",
|
|
517
|
-
"side.bar": "余额剩余比例",
|
|
518
557
|
"todayCostShort": "今日",
|
|
558
|
+
"peak.active": "峰时",
|
|
559
|
+
"peak.offpeak": "谷时",
|
|
560
|
+
"peak.tooltip.active": "当前为峰时(09:00-12:00、14:00-18:00,价格翻倍):输入 ¥{input}/M · 输出 ¥{output}/M",
|
|
561
|
+
"peak.tooltip.offpeak": "当前为谷时(其余时段,半价):输入 ¥{input}/M · 输出 ¥{output}/M",
|
|
562
|
+
"sessionNote": "基于最近活跃会话:",
|
|
519
563
|
"hint": "价格为估算值,实际以官方账单为准。",
|
|
520
564
|
"topUp": "去充值",
|
|
521
565
|
"close": "关闭"
|
|
@@ -528,21 +572,25 @@ window.__ModuleLoader__.load({
|
|
|
528
572
|
"refresh.title": "Refresh balance and costs",
|
|
529
573
|
"balance": "Account balance",
|
|
530
574
|
"lastPrompt": "Last prompt",
|
|
531
|
-
"
|
|
532
|
-
"todayWs": "Today · this
|
|
533
|
-
"todayAll": "Today · all",
|
|
575
|
+
"todaySession": "Today · this session",
|
|
576
|
+
"todayWs": "Today · this workspace",
|
|
577
|
+
"todayAll": "Today · all workspaces",
|
|
534
578
|
"explain.aria": "Explain",
|
|
535
579
|
"explain.balance": "DeepSeek account balance, fetched live from the official API.",
|
|
536
580
|
"explain.lastPrompt": "Estimated cost of the latest question (incl. the AI reply) in the most recent session.",
|
|
537
|
-
"explain.
|
|
581
|
+
"explain.todaySession": "Estimated cost of today's usage in the current session.",
|
|
538
582
|
"explain.todayWs": "Estimated total cost of today's sessions in the current workspace.",
|
|
539
583
|
"explain.todayAll": "Estimated total cost of today's sessions across all workspaces.",
|
|
540
584
|
"warn.title.1": "Balance below ¥5, consider topping up",
|
|
541
585
|
"warn.title.2": "Balance below ¥1, almost exhausted!",
|
|
542
586
|
"warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
|
|
543
587
|
"warn.text.2": "⛔ Balance below ¥1, almost exhausted — top up now!",
|
|
544
|
-
"side.bar": "Balance remaining ratio",
|
|
545
588
|
"todayCostShort": "Today",
|
|
589
|
+
"peak.active": "Peak",
|
|
590
|
+
"peak.offpeak": "Off-peak",
|
|
591
|
+
"peak.tooltip.active": "Peak hours now (09:00-12:00, 14:00-18:00, 2× price): input ¥{input}/M · output ¥{output}/M",
|
|
592
|
+
"peak.tooltip.offpeak": "Off-peak hours now (rest of day, half price): input ¥{input}/M · output ¥{output}/M",
|
|
593
|
+
"sessionNote": "Based on most recent session:",
|
|
546
594
|
"hint": "Prices are estimates; actual billing from the provider is authoritative.",
|
|
547
595
|
"topUp": "Top up",
|
|
548
596
|
"close": "Close"
|
package/lib/index.js
CHANGED
|
@@ -461,6 +461,7 @@ function parseSessionUsages(text) {
|
|
|
461
461
|
const samples = [];
|
|
462
462
|
let currentModel = void 0;
|
|
463
463
|
let cwd = void 0;
|
|
464
|
+
let title = void 0;
|
|
464
465
|
for (const line of text.split("\n")) {
|
|
465
466
|
if (line === "") continue;
|
|
466
467
|
let event;
|
|
@@ -472,6 +473,8 @@ function parseSessionUsages(text) {
|
|
|
472
473
|
const type = event.type;
|
|
473
474
|
if (type === "session") {
|
|
474
475
|
cwd = event.cwd;
|
|
476
|
+
} else if (type === "session/title") {
|
|
477
|
+
title = event.data?.title ?? title;
|
|
475
478
|
} else if (type === "request/header") {
|
|
476
479
|
currentModel = event.data?.header?.config?.model ?? currentModel;
|
|
477
480
|
} else if (type === "assistant/message") {
|
|
@@ -486,7 +489,7 @@ function parseSessionUsages(text) {
|
|
|
486
489
|
}
|
|
487
490
|
}
|
|
488
491
|
}
|
|
489
|
-
return { samples, cwd };
|
|
492
|
+
return { samples, cwd, title };
|
|
490
493
|
}
|
|
491
494
|
|
|
492
495
|
/** Cost of the last turn (the most recent user prompt) in one session file. */
|
|
@@ -506,12 +509,16 @@ function lastTurnCost(samples) {
|
|
|
506
509
|
return { cost, inputTokens: input, outputTokens: output };
|
|
507
510
|
}
|
|
508
511
|
|
|
509
|
-
/**
|
|
510
|
-
function
|
|
512
|
+
/** Cost of one session's usage that falls on the current calendar day. */
|
|
513
|
+
function todaySessionCost(samples) {
|
|
514
|
+
const now = new Date();
|
|
515
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
516
|
+
const dayEnd = dayStart + 24 * 60 * 60 * 1000;
|
|
511
517
|
let cost = 0;
|
|
512
518
|
let input = 0;
|
|
513
519
|
let output = 0;
|
|
514
520
|
for (const s of samples) {
|
|
521
|
+
if (s.time < dayStart || s.time >= dayEnd) continue;
|
|
515
522
|
cost += priceUsage(s.usage, s.model, s.time);
|
|
516
523
|
input += (s.usage.inputTokens ?? s.usage.uncachedInputTokens ?? 0) + (s.usage.cacheReadTokens ?? 0);
|
|
517
524
|
output += s.usage.outputTokens ?? 0;
|
|
@@ -695,24 +702,36 @@ function makeRoutes(ctx, config) {
|
|
|
695
702
|
path: API.todayCost,
|
|
696
703
|
handler: async (req, res) => {
|
|
697
704
|
if (!guard(req, res, "GET")) return;
|
|
698
|
-
const
|
|
705
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
706
|
+
const sessionId = url.searchParams.get("session") ?? "";
|
|
707
|
+
const key = `today:${sessionId}`;
|
|
699
708
|
const compute = async () => {
|
|
700
709
|
const sessionList = await collectAllSessionSamples();
|
|
701
710
|
const all = todayCost(sessionList);
|
|
702
|
-
// "Current workspace": the cwd of the
|
|
703
|
-
//
|
|
711
|
+
// "Current workspace": prefer the cwd of the explicitly passed
|
|
712
|
+
// session (the client's currently selected session). Fall back
|
|
713
|
+
// to the session with the most samples today when absent.
|
|
704
714
|
let activeCwd = void 0;
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
if (
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
715
|
+
if (sessionId !== "") {
|
|
716
|
+
const target = await resolveSessionFile(sessionId);
|
|
717
|
+
if (target !== void 0) {
|
|
718
|
+
const text = await decompressSession(target);
|
|
719
|
+
if (text.ok) activeCwd = parseSessionUsages(text.text).cwd;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (activeCwd === void 0) {
|
|
723
|
+
let activeCount = 0;
|
|
724
|
+
for (const session of sessionList) {
|
|
725
|
+
if (session.cwd === void 0) continue;
|
|
726
|
+
const todaySamples = session.samples.filter((s) => {
|
|
727
|
+
const now = new Date();
|
|
728
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
729
|
+
return s.time >= dayStart && s.time < dayStart + 24 * 60 * 60 * 1000;
|
|
730
|
+
}).length;
|
|
731
|
+
if (todaySamples > activeCount) {
|
|
732
|
+
activeCount = todaySamples;
|
|
733
|
+
activeCwd = session.cwd;
|
|
734
|
+
}
|
|
716
735
|
}
|
|
717
736
|
}
|
|
718
737
|
const workspace = todayCost(sessionList, activeCwd);
|
|
@@ -732,7 +751,13 @@ function makeRoutes(ctx, config) {
|
|
|
732
751
|
path: API.activeCost,
|
|
733
752
|
handler: async (req, res) => {
|
|
734
753
|
if (!guard(req, res, "GET")) return;
|
|
735
|
-
const
|
|
754
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
755
|
+
const sessionId = url.searchParams.get("session") ?? "";
|
|
756
|
+
// Prefer the explicitly passed session (the client's currently
|
|
757
|
+
// selected one); fall back to the most recently active session.
|
|
758
|
+
const file = sessionId !== ""
|
|
759
|
+
? await resolveSessionFile(sessionId)
|
|
760
|
+
: await findMostRecentSessionFile();
|
|
736
761
|
if (file === void 0) {
|
|
737
762
|
writeJson(res, 404, { error: "no session files found", modelId: config.modelId });
|
|
738
763
|
return;
|
|
@@ -744,11 +769,36 @@ function makeRoutes(ctx, config) {
|
|
|
744
769
|
}
|
|
745
770
|
const parsed = parseSessionUsages(result.text);
|
|
746
771
|
const last = lastTurnCost(parsed.samples);
|
|
747
|
-
const
|
|
772
|
+
const todaySession = todaySessionCost(parsed.samples);
|
|
773
|
+
// Current peak/off-peak state + price tier for the configured model.
|
|
774
|
+
const now = new Date();
|
|
775
|
+
const peakActive = isPeak(now);
|
|
776
|
+
const table = peakActive
|
|
777
|
+
? pricingTable[pricingKey(config.modelId)].peak
|
|
778
|
+
: pricingTable[pricingKey(config.modelId)].offPeak;
|
|
779
|
+
// Short workspace name from the cwd path (last segment).
|
|
780
|
+
let workspaceName = "未知工作区";
|
|
781
|
+
if (typeof parsed.cwd === "string" && parsed.cwd !== "") {
|
|
782
|
+
const segments = parsed.cwd.split("/").filter((s) => s !== "");
|
|
783
|
+
workspaceName = segments.length > 0 ? segments[segments.length - 1] : parsed.cwd;
|
|
784
|
+
}
|
|
785
|
+
// Session id from the file path (…/session-<id>/session.jsonl.zstd).
|
|
786
|
+
let fileSessionId = void 0;
|
|
787
|
+
const sidMatch = file.match(/session-([0-9a-f-]+)/);
|
|
788
|
+
if (sidMatch !== null) fileSessionId = sidMatch[1];
|
|
748
789
|
writeJson(res, 200, {
|
|
749
790
|
lastPrompt: last,
|
|
750
|
-
|
|
791
|
+
todaySession,
|
|
751
792
|
cwd: parsed.cwd,
|
|
793
|
+
title: parsed.title,
|
|
794
|
+
workspaceName,
|
|
795
|
+
sessionId: fileSessionId,
|
|
796
|
+
peak: {
|
|
797
|
+
active: peakActive,
|
|
798
|
+
inputMiss: table.inputMiss,
|
|
799
|
+
inputHit: table.inputHit,
|
|
800
|
+
output: table.output
|
|
801
|
+
},
|
|
752
802
|
modelId: config.modelId,
|
|
753
803
|
pricingSource,
|
|
754
804
|
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.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"files": [
|