dsh-balance-widget 0.5.0 → 0.5.2
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 +55 -9
- package/README.md +55 -9
- package/docs/screenshot-corner.png +0 -0
- package/docs/screenshot-popover.png +0 -0
- package/lib/client.js +8 -89
- package/lib/index.js +181 -93
- package/package.json +8 -3
package/README.en.md
CHANGED
|
@@ -24,8 +24,8 @@ 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
|
-
| **
|
|
28
|
-
| **Peak/off-peak pricing** | ✅ Built-in official 2026-
|
|
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
|
+
| **Peak/off-peak pricing** | ✅ Built-in official 2026-09-10 rate table, re-synced from the official page at startup and every 12h | Partial support |
|
|
29
29
|
| **Security** | ✅ API key stays in the host process; loopback-only guard | Varies |
|
|
30
30
|
|
|
31
31
|
**In one line**: *The zero-dependency, Node 24-ready balance/cost widget that never breaks `dsh web` boot.*
|
|
@@ -40,11 +40,10 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
|
|
|
40
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).
|
|
41
41
|
- **Token usage** — Also shows the session's input (incl. cache hits) / output tokens.
|
|
42
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.
|
|
43
|
-
- **Sidebar card** — a persistent card at the sidebar footer shows balance and today's cost; globally visible, refreshes every 60s.
|
|
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
44
|
- **Balance color warning** — the balance number is tinted in three tiers: default (healthy) → amber (below `lowThreshold`) → red (below `criticalThreshold`).
|
|
45
45
|
- **Official price auto-sync** — fetches the DeepSeek official pricing page on startup and every 12h; falls back to built-in rates on failure.
|
|
46
46
|
- **Agent tool** — a `deepseek_billing` tool lets the model answer "how much balance do I have / how much did today cost".
|
|
47
|
-
- **On-demand refresh** — No polling, no background requests; endpoints are only hit when you click the icon. Costs zero tokens to use.
|
|
48
47
|
|
|
49
48
|
## Why this plugin
|
|
50
49
|
|
|
@@ -137,23 +136,68 @@ If you mainly use DeepSeek-V4-Pro, point the pricing model at it for a more accu
|
|
|
137
136
|
modelId: deepseek-v4-pro
|
|
138
137
|
```
|
|
139
138
|
|
|
139
|
+
**Note**: the provider has announced that V4-Pro is being retired — from 2026-09-14 12:00 CST, V4-Pro requests are routed to V4.1-Flash and billed at Flash rates. Keeping `deepseek-v4-pro` past that date will over-estimate costs, so the default Flash tier is the better choice.
|
|
140
|
+
|
|
140
141
|
**Note**: `cordis.patch.yml` may already contain lines for other plugins — append new lines without touching existing ones.
|
|
141
142
|
|
|
142
143
|
## Pricing
|
|
143
144
|
|
|
144
|
-
Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective 2026-
|
|
145
|
+
Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective 2026-09-10. Peak windows are Beijing time 09:00–12:00 and 14:00–18:00; prices are double the off-peak rates:
|
|
145
146
|
|
|
146
147
|
| Model | Window | Cache hit (input) | Cache miss (input) | Output |
|
|
147
148
|
| --- | --- | --- | --- | --- |
|
|
148
|
-
| V4-Flash | Off-peak | 0.
|
|
149
|
-
| V4-Flash | Peak | 0.
|
|
149
|
+
| V4.1-Flash | Off-peak | 0.02 | 1.0 | 4.0 |
|
|
150
|
+
| V4.1-Flash | Peak | 0.04 | 2.0 | 8.0 |
|
|
150
151
|
| V4-Pro | Off-peak | 0.15 | 4.5 | 13.5 |
|
|
151
152
|
| V4-Pro | Peak | 0.30 | 9.0 | 27.0 |
|
|
152
153
|
|
|
153
|
-
`deepseek-
|
|
154
|
+
The official page currently lists only `deepseek-flash` and `deepseek-v4-pro`; older names (`deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`, `deepseek-chat`) still work and are billed at the Flash rate. The plugin re-parses the page at startup and every 12h, and falls back to the table above only when parsing fails. Costs are **estimates**; the provider's bill is authoritative.
|
|
155
|
+
|
|
156
|
+
## Security & permission boundaries
|
|
157
|
+
|
|
158
|
+
This section is for the DSH Store / plugin audit: dependencies, runtime permissions, external services, and failure bounds.
|
|
159
|
+
|
|
160
|
+
**Dependencies & compatibility**
|
|
161
|
+
- Zero runtime dependencies: imports no `@deepseek-ai/*` packages; no third-party host deps
|
|
162
|
+
- `peerDependencies["@deepseek-ai/dsh"]`: `>=0.1.2-rc.1 <0.2.0` (DSH compatibility range)
|
|
163
|
+
- `engines.node`: `^22.19.0 || >=24.0.0`
|
|
164
|
+
- `peerDependencies["react"]`: `^18.2.0` (browser rendering only)
|
|
165
|
+
|
|
166
|
+
**Runtime permissions**
|
|
167
|
+
- `files`: reads only `~/.dsh/sessions/` session JSONL (cost stats); never writes or mutates any session file
|
|
168
|
+
- `network`: only the DeepSeek official endpoints — `api.deepseek.com` (`GET /user/balance`) and `api-docs.deepseek.com` pricing page (fetched every 12h); no third-party proxy
|
|
169
|
+
- `commands`: spawns `zstd -d -c` to decompress session files (macOS needs `brew install zstd`); no other commands
|
|
170
|
+
- `credentials`: reads `DEEPSEEK_API_KEY` (resolved via the host credentials service), used only in the host process behind a loopback-only route guard; the browser never sees the key
|
|
171
|
+
- All host routes are bound to the loopback address and unreachable externally
|
|
172
|
+
|
|
173
|
+
**External services**
|
|
174
|
+
- DeepSeek official balance endpoint `GET /user/balance` (on click / 60s refresh)
|
|
175
|
+
- DeepSeek official pricing page (on startup + every 12h, for peak/off-peak rates)
|
|
176
|
+
|
|
177
|
+
**Failure bounds**
|
|
178
|
+
- Balance fetch failure: the panel shows the error and keeps the last successful snapshot (no interruption)
|
|
179
|
+
- Pricing fetch failure: falls back to the built-in 2026-09-10 rate table, `pricingSource` marked `default` (`synced` once parsing succeeds)
|
|
180
|
+
- Missing `zstd`: returns an actionable error (points to the install command) instead of failing silently
|
|
181
|
+
- Missing/corrupt session files: that session is skipped; other sessions are unaffected
|
|
182
|
+
- All costs are estimates; the provider's bill is authoritative
|
|
154
183
|
|
|
155
184
|
## Changelog
|
|
156
185
|
|
|
186
|
+
### v0.5.2 — DSH 0.1.5: new session format & new pricing page
|
|
187
|
+
- 🐛 **Fixed**: DSH 0.1.5 writes session logs under a generation-tagged name (`session.v3.jsonl.zstd`), but the plugin only matched `session.jsonl.zstd`, so every session created after the upgrade was invisible — viewing one produced red error text in the popover, and today's cost was understated (measured ~36% low). Logs are now found by taking the highest generation per session directory; a migrated session's older file is a subset of the new one, so reading only the newest avoids double-counting the session
|
|
188
|
+
- 🐛 **Fixed**: the official pricing page renamed the Flash column to `deepseek-flash` and cut its rates. The parser's model-id anchor landed on a page footnote, so the sync reported success while silently keeping the stale, higher rates — overstating costs by ~1.7x. The parser now anchors on the table's row labels and applies that column to every Flash-family name
|
|
189
|
+
- 💰 **Rates**: the built-in fallback table now carries V4.1-Flash pricing (off-peak 0.02 / 1.0 / 4.0, peak 0.04 / 2.0 / 8.0 CNY per 1M tokens); the V4-Pro tier is unchanged
|
|
190
|
+
- 📄 **Docs**: corrected the `pricingSource` values to `default` / `synced` (previously misdocumented as `builtin`)
|
|
191
|
+
|
|
192
|
+
### v0.5.1 — Cold-start speedup & cleanup
|
|
193
|
+
- 🚀 **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
|
|
194
|
+
- ⏰ **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)
|
|
195
|
+
- 🌏 **Timezone fix**: peak/off-peak windows are now computed in Beijing time (UTC+8) explicitly instead of the host's local timezone
|
|
196
|
+
- 🧹 **Cleanup**: removed a dead client-side pricing stack (PRICING/priceSession) that was never called; all pricing now goes through the host
|
|
197
|
+
- 📐 **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
|
|
198
|
+
- 🏷️ **Card label**: the card footer now reads "Today · all" to make clear it is the global (all-workspaces) figure
|
|
199
|
+
- 🎨 **Visual refresh**: SVG wallet icon replaces 💰, press/pop micro-animations, muted secondary tiers in the popover
|
|
200
|
+
|
|
157
201
|
### v0.5.0 — Five-tier costs & peak/off-peak status
|
|
158
202
|
- ✨ **Added**: the cost breakdown is now five tiers — balance / last prompt / today-this-session / today-this-workspace / today-all-workspaces
|
|
159
203
|
- The last-prompt row labels the **session name** underneath (based on the most recent session)
|
|
@@ -176,7 +220,9 @@ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective
|
|
|
176
220
|
|
|
177
221
|
- Config tree: `dsh --profile web --dump-config` should show a `balance-widget` entry.
|
|
178
222
|
- Balance route: after restarting dsh web, `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` should return `{ ok, balance_infos, modelId }`.
|
|
179
|
-
-
|
|
223
|
+
- 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.
|
|
224
|
+
- Today costs: `curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` should return `{ workspace: { cost, ..., cwd }, all: { cost, ... }, modelId }`.
|
|
225
|
+
- 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 }`.
|
|
180
226
|
|
|
181
227
|
## License
|
|
182
228
|
|
package/README.md
CHANGED
|
@@ -24,8 +24,8 @@ 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
|
-
|
|
|
28
|
-
| **峰谷定价** | ✅ 内置官方 2026-
|
|
27
|
+
| **常驻自动刷新** | ✅ 侧边栏卡片每 60s 自动刷新余额与成本,弹框点击即强刷 | 有的常驻徽章定时刷新 |
|
|
28
|
+
| **峰谷定价** | ✅ 内置官方 2026-09-10 峰谷价表,启动时与每 12h 自动从官方页刷新 | 部分支持 |
|
|
29
29
|
| **安全性** | ✅ API key 仅在宿主进程,loopback-only 守卫 | 参差不齐 |
|
|
30
30
|
|
|
31
31
|
**一句话**:*零依赖、Node 24 就绪、永不拖垮 dsh web 启动的余额/成本小部件。*
|
|
@@ -40,11 +40,10 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
|
|
|
40
40
|
- **峰谷状态标签** — 卡片与弹框边框按当前时段着色(峰时橙色 / 谷时绿色),弹框标题旁显示「峰时/谷时」标签,悬停可查看当前价格档位(输入/输出单价)。
|
|
41
41
|
- **Token 用量** — 同时展示输入(含缓存命中)/ 输出 token 数。
|
|
42
42
|
- **一键充值** — 弹层底部「去充值」链接直达 DeepSeek 官方充值页(platform.deepseek.com/top_up),新窗口打开。
|
|
43
|
-
- **侧边栏常驻卡片** — 侧边栏底部(设置上方)显示余额 +
|
|
43
|
+
- **侧边栏常驻卡片** — 侧边栏底部(设置上方)显示余额 + 今日花费,全局可见;每 60 秒自动刷新(余额走官方接口、成本为本地会话解析),打开弹框时也会立即刷新一次,无需手动操作。
|
|
44
44
|
- **余额数字变色预警** — 余额数字按三档着色:充足(默认色)→ 琥珀(低于 `lowThreshold`)→ 红色(低于 `criticalThreshold`),一眼判断余额健康度。
|
|
45
45
|
- **官方价格自动同步** — 启动时 + 每 12 小时抓取 DeepSeek 官方定价页,改价自动跟进;失败回退内置价目表。
|
|
46
46
|
- **模型工具查询** — 新增 `deepseek_billing` 工具,可直接问模型"余额多少/今天花了多少"。
|
|
47
|
-
- **按需刷新** — 无轮询、无后台请求;只有点击图标时才发起查询,不消耗任何 token。
|
|
48
47
|
|
|
49
48
|
## 架构
|
|
50
49
|
|
|
@@ -134,23 +133,68 @@ DSH 的插件配置统一放在这个文件里:
|
|
|
134
133
|
modelId: deepseek-v4-pro
|
|
135
134
|
```
|
|
136
135
|
|
|
136
|
+
**注意**:官方已公告 V4-Pro 将于北京时间 2026-09-14 12:00 下线——此后 V4-Pro 请求会被路由到 V4.1-Flash 并按 Flash 计费。若在这之后仍配置 `deepseek-v4-pro`,估算会偏高,建议使用默认的 Flash 档。
|
|
137
|
+
|
|
137
138
|
**提示**:`cordis.patch.yml` 可能有其他插件的配置行,追加时注意**不要改动已有的行**,只加新内容。
|
|
138
139
|
|
|
139
140
|
## 定价说明
|
|
140
141
|
|
|
141
|
-
内置 DeepSeek 官方 2026-
|
|
142
|
+
内置 DeepSeek 官方 2026-09-10 峰谷定价(元 / 百万 tokens),高峰时段为北京时间 09:00–12:00、14:00–18:00,价格为空闲时段两倍:
|
|
142
143
|
|
|
143
144
|
| 模型 | 时段 | 缓存命中(输入) | 缓存未命中(输入) | 输出 |
|
|
144
145
|
| --- | --- | --- | --- | --- |
|
|
145
|
-
| V4-Flash | 空闲 | 0.
|
|
146
|
-
| V4-Flash | 高峰 | 0.
|
|
146
|
+
| V4.1-Flash | 空闲 | 0.02 | 1.0 | 4.0 |
|
|
147
|
+
| V4.1-Flash | 高峰 | 0.04 | 2.0 | 8.0 |
|
|
147
148
|
| V4-Pro | 空闲 | 0.15 | 4.5 | 13.5 |
|
|
148
149
|
| V4-Pro | 高峰 | 0.30 | 9.0 | 27.0 |
|
|
149
150
|
|
|
150
|
-
`deepseek-
|
|
151
|
+
官方页当前只列 `deepseek-flash` 与 `deepseek-v4-pro` 两列;`deepseek-v4-flash`、`deepseek-v4-flash-vision-exp`、`deepseek-chat` 等旧名仍可调用,按 Flash 价计费。插件在启动时与每 12h 从官方页重新解析,只有解析失败才回退上表。成本为**估算值**,实际以官方账单为准。
|
|
152
|
+
|
|
153
|
+
## 安全与权限边界
|
|
154
|
+
|
|
155
|
+
本节面向 DSH Store / 插件审计,列出依赖、运行时权限、外部服务与失败边界。
|
|
156
|
+
|
|
157
|
+
**依赖与兼容**
|
|
158
|
+
- 零运行时依赖:不 import 任何 `@deepseek-ai/*` 包,宿主端无第三方依赖
|
|
159
|
+
- `peerDependencies["@deepseek-ai/dsh"]`: `>=0.1.2-rc.1 <0.2.0`(DSH 兼容范围)
|
|
160
|
+
- `engines.node`: `^22.19.0 || >=24.0.0`
|
|
161
|
+
- `peerDependencies["react"]`: `^18.2.0`(仅浏览器端渲染)
|
|
162
|
+
|
|
163
|
+
**运行时权限**
|
|
164
|
+
- `files`:只读 `~/.dsh/sessions/` 下的会话 JSONL(成本统计);不写入、不修改任何会话文件
|
|
165
|
+
- `network`:仅请求 DeepSeek 官方端点——`api.deepseek.com`(`GET /user/balance` 余额)与 `api-docs.deepseek.com` 定价页(每 12h 抓取);不走任何第三方代理
|
|
166
|
+
- `commands`:spawn `zstd -d -c` 解压会话文件(macOS 需 `brew install zstd`);不执行其他命令
|
|
167
|
+
- `credentials`:读取 `DEEPSEEK_API_KEY`(经宿主凭据服务解析),仅宿主进程使用、loopback-only 路由守卫;浏览器不接触密钥
|
|
168
|
+
- 所有 host 路由均绑定 load 回环地址,外部不可达
|
|
169
|
+
|
|
170
|
+
**外部服务**
|
|
171
|
+
- DeepSeek 官方余额接口 `GET /user/balance`(点击/60s 刷新时调用)
|
|
172
|
+
- DeepSeek 官方定价页(启动时 + 每 12h 抓取,用于峰谷单价)
|
|
173
|
+
|
|
174
|
+
**失败边界**
|
|
175
|
+
- 余额接口失败:面板提示失败信息,保留上次成功快照(不中断)
|
|
176
|
+
- 定价页抓取失败:回退内置 2026-09-10 价目表,`pricingSource` 标记为 `default`(解析成功则为 `synced`)
|
|
177
|
+
- `zstd` 缺失:返回可读错误提示(指引安装),而非静默失败
|
|
178
|
+
- 会话文件缺失/损坏:跳过该会话,不影响其他会话统计
|
|
179
|
+
- 所有成本为估算值,实际以官方账单为准
|
|
151
180
|
|
|
152
181
|
## 版本历史
|
|
153
182
|
|
|
183
|
+
### v0.5.2 — 适配 DSH 0.1.5:新会话格式与新定价页
|
|
184
|
+
- 🐛 **修复**:DSH 0.1.5 将会话日志改为代数命名 `session.v3.jsonl.zstd`,插件此前只认 `session.jsonl.zstd`,导致升级后新建的会话全部不可见——查看这类会话时弹层出现红色报错,今日成本也漏算(实测少约 36%)。现按「每个会话目录取最高代」发现日志;迁移会话保留的旧文件是新代的子集,因此只读最高代,避免整个会话被重复计算
|
|
185
|
+
- 🐛 **修复**:官方定价页将 Flash 列改名为 `deepseek-flash` 并下调价格,插件按模型名定位价格块的锚点因此落到页面脚注,同步"成功"却静默沿用旧高价,成本被高估约 1.7 倍。现改为按价格表行标签锚定,并把该列价格套用到全部 Flash 系名称
|
|
186
|
+
- 💰 **价目**:内置兜底价目表更新为 V4.1-Flash 价(空闲 0.02 / 1.0 / 4.0,高峰 0.04 / 2.0 / 8.0,元/百万 tokens);V4-Pro 档位保留
|
|
187
|
+
- 📄 **文档**:更正 `pricingSource` 取值为 `default` / `synced`(此前误写为 `builtin`)
|
|
188
|
+
|
|
189
|
+
### v0.5.1 — 冷启动提速与代码清理
|
|
190
|
+
- 🚀 **性能**:今日成本冷启动从 ~5.4s 降至 ~0.01s——只解压当天活跃的会话文件(按 mtime 过滤)+ 解压结果按 (path, mtime) 长期缓存 + 并行解析
|
|
191
|
+
- ⏰ **轮询口径**:README 与代码统一——常驻卡片每 60s 自动刷新(此前文档"无轮询"表述自相矛盾,已更正)
|
|
192
|
+
- 🌏 **峰谷时区修正**:峰/谷判断改为显式按北京时间(UTC+8)计算,不再依赖宿主机时区
|
|
193
|
+
- 🧹 **清理**:移除客户端一套从未调用的死计价代码(PRICING/priceSession),计价统一走宿主
|
|
194
|
+
- 📐 **定价解析加固**:峰价改为从官方页面显式解析(不再硬编码"谷价 ×2");解析不到缓存命中价时整表回退内置价目表,避免静默按 0 计价
|
|
195
|
+
- 🏷️ **卡片语义**:卡片底部"今日"改为"今日·全部",明确是全局(所有工作区)口径
|
|
196
|
+
- 🎨 **外观**:💰 换 SVG 钱包图标、弹框/卡片微交互动画、层级弱化(详情见 v0.5.1 diff)
|
|
197
|
+
|
|
154
198
|
### v0.5.0 — 五层级成本与峰谷状态
|
|
155
199
|
- ✨ **新增**:成本明细改为五层级——余额 / 最近一次提问 / 今日·本会话 / 今日·本工作区 / 今日·所有工作区
|
|
156
200
|
- 最近一次提问下方标注**会话名**(基于最近活跃会话)
|
|
@@ -173,7 +217,9 @@ DSH 的插件配置统一放在这个文件里:
|
|
|
173
217
|
|
|
174
218
|
- 配置树:`dsh --profile web --dump-config` 应出现 `balance-widget` 条目
|
|
175
219
|
- 余额路由:重启 dsh web 后 `curl -s http://127.0.0.1:3080/api/dsh-balance/balance` 应返回 `{ ok, balance_infos, modelId }`
|
|
176
|
-
-
|
|
220
|
+
- 会话成本:`curl -s http://127.0.0.1:3080/api/dsh-balance/active-cost` 应返回 `{ lastPrompt, todaySession, title, sessionId, peak, workspaceName, ... }`;加 `?session=SESSION_ID` 可指定会话
|
|
221
|
+
- 今日成本:`curl -s http://127.0.0.1:3080/api/dsh-balance/today-cost` 应返回 `{ workspace: { cost, ..., cwd }, all: { cost, ... }, modelId }`
|
|
222
|
+
- 兼容旧路由:`curl -s "http://127.0.0.1:3080/api/dsh-balance/last-cost?session=SESSION_ID"` 仍可用,返回 `{ cost, inputTokens, outputTokens, modelId }`
|
|
177
223
|
|
|
178
224
|
## License
|
|
179
225
|
|
|
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) backwards}.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,18 +18,10 @@ 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",
|
|
@@ -47,85 +39,12 @@ window.__ModuleLoader__.load({
|
|
|
47
39
|
"sessionName": "dshbw_sessionName"
|
|
48
40
|
};
|
|
49
41
|
//#endregion
|
|
50
|
-
//#region lib/types/client/pricing.js
|
|
51
|
-
/**
|
|
52
|
-
* DeepSeek official pricing, in CNY per 1M tokens, effective 2026-08-17
|
|
53
|
-
* (peak/off-peak two-tier billing). Peak windows are Beijing time:
|
|
54
|
-
* 09:00-12:00 and 14:00-18:00; off-peak is half the peak price.
|
|
55
|
-
* Prices are hard-coded from the official announcement and are estimates
|
|
56
|
-
* for display — actual billing is authoritative from the provider.
|
|
57
|
-
*/
|
|
58
|
-
const PRICING = {
|
|
59
|
-
"deepseek-v4-flash": {
|
|
60
|
-
peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 },
|
|
61
|
-
offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 }
|
|
62
|
-
},
|
|
63
|
-
"deepseek-v4-pro": {
|
|
64
|
-
peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 },
|
|
65
|
-
offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 }
|
|
66
|
-
},
|
|
67
|
-
"deepseek-chat": {
|
|
68
|
-
peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 },
|
|
69
|
-
offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 }
|
|
70
|
-
},
|
|
71
|
-
"deepseek-reasoner": {
|
|
72
|
-
peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 },
|
|
73
|
-
offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 }
|
|
74
|
-
}
|
|
75
|
-
};
|
|
76
|
-
/** Fall back to Flash pricing for unknown model ids. */
|
|
77
|
-
function pricingFor(modelId) {
|
|
78
|
-
return PRICING[modelId] ?? PRICING["deepseek-v4-flash"];
|
|
79
|
-
}
|
|
80
|
-
/**
|
|
81
|
-
* Whether `date` falls inside a Beijing-time peak window.
|
|
82
|
-
* The host's local timezone is used; the widget is for a CN deployment.
|
|
83
|
-
*/
|
|
84
|
-
function isPeak(date) {
|
|
85
|
-
const hour = date.getHours();
|
|
86
|
-
return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Price one session's token usage into a CNY estimate.
|
|
90
|
-
* @param usage - tokenUsage projection value.
|
|
91
|
-
* @param modelId - provider model id used for pricing.
|
|
92
|
-
* @returns { input, output, total } in CNY, or null when usage is absent.
|
|
93
|
-
*/
|
|
94
|
-
function priceSession(usage, modelId) {
|
|
95
|
-
if (usage === void 0 || usage === null) return null;
|
|
96
|
-
const input = usage.uncachedInputTokens ?? 0;
|
|
97
|
-
const hit = usage.cacheReadTokens ?? 0;
|
|
98
|
-
const write = usage.cacheWriteTokens ?? 0;
|
|
99
|
-
const output = usage.outputTokens ?? 0;
|
|
100
|
-
if (input + hit + write + output <= 0) return null;
|
|
101
|
-
const table = isPeak(new Date()) ? pricingFor(modelId).peak : pricingFor(modelId).offPeak;
|
|
102
|
-
const inputCost = (input * table.inputMiss + (hit + write) * table.inputHit) / 1e6;
|
|
103
|
-
const outputCost = (output * table.output) / 1e6;
|
|
104
|
-
return {
|
|
105
|
-
input: inputCost,
|
|
106
|
-
output: outputCost,
|
|
107
|
-
total: inputCost + outputCost
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
42
|
/** Format a CNY amount: ¥ + 3 significant decimals (drop trailing zeros). */
|
|
111
43
|
function formatCny(value) {
|
|
112
44
|
if (value === void 0 || value === null || !Number.isFinite(value)) return "—";
|
|
113
45
|
const rounded = Math.round(value * 1e4) / 1e4;
|
|
114
46
|
return `¥${rounded.toLocaleString("zh-CN", { minimumFractionDigits: 0, maximumFractionDigits: 4 })}`;
|
|
115
47
|
}
|
|
116
|
-
/** Format token counts compactly: 1234 -> 1.2K. */
|
|
117
|
-
function formatTokens(value) {
|
|
118
|
-
if (value === void 0 || value === null) return "—";
|
|
119
|
-
if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
|
|
120
|
-
if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
121
|
-
return String(value);
|
|
122
|
-
}
|
|
123
|
-
/** Format an epoch ms as HH:MM local time. */
|
|
124
|
-
function formatTime(ms) {
|
|
125
|
-
if (ms === void 0 || ms === null) return "—";
|
|
126
|
-
const d = new Date(ms);
|
|
127
|
-
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
128
|
-
}
|
|
129
48
|
//#endregion
|
|
130
49
|
//#region lib/types/client/balance-api.js
|
|
131
50
|
/** Same-origin balance route (host half registers it over ctx.webServer). */
|
|
@@ -354,8 +273,8 @@ window.__ModuleLoader__.load({
|
|
|
354
273
|
children: [(0, react_jsx_runtime.jsx)("div", {
|
|
355
274
|
className: styles.sideTop,
|
|
356
275
|
children: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, {
|
|
357
|
-
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
358
|
-
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" })] })
|
|
359
278
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
360
279
|
className: styles.sideAmount,
|
|
361
280
|
"data-level": level > 0 ? level : undefined,
|
|
@@ -438,7 +357,7 @@ window.__ModuleLoader__.load({
|
|
|
438
357
|
})]
|
|
439
358
|
})
|
|
440
359
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
441
|
-
className: styles.val,
|
|
360
|
+
className: styles.val, "data-tier": "ter",
|
|
442
361
|
children: loading ? "…" : lastPrompt !== null ? formatCny(lastPrompt) : "—"
|
|
443
362
|
})]
|
|
444
363
|
})
|
|
@@ -481,7 +400,7 @@ window.__ModuleLoader__.load({
|
|
|
481
400
|
})]
|
|
482
401
|
})
|
|
483
402
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
484
|
-
className: styles.val,
|
|
403
|
+
className: styles.val, "data-tier": "sec",
|
|
485
404
|
children: loading ? "…" : todayWs !== null ? formatCny(todayWs) : "—"
|
|
486
405
|
})]
|
|
487
406
|
})
|
|
@@ -497,7 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
497
416
|
})]
|
|
498
417
|
})
|
|
499
418
|
}), (0, react_jsx_runtime.jsx)("span", {
|
|
500
|
-
className: styles.val,
|
|
419
|
+
className: styles.val, "data-tier": "sec",
|
|
501
420
|
children: loading ? "…" : todayAll !== null ? formatCny(todayAll) : "—"
|
|
502
421
|
})]
|
|
503
422
|
})
|
|
@@ -554,7 +473,7 @@ window.__ModuleLoader__.load({
|
|
|
554
473
|
"warn.title.2": "余额低于 ¥1,即将耗尽!",
|
|
555
474
|
"warn.text.1": "⚠️ 余额不足 ¥5,建议及时充值。",
|
|
556
475
|
"warn.text.2": "⛔ 余额低于 ¥1,即将耗尽,请立即充值!",
|
|
557
|
-
"todayCostShort": "
|
|
476
|
+
"todayCostShort": "今日·全部",
|
|
558
477
|
"peak.active": "峰时",
|
|
559
478
|
"peak.offpeak": "谷时",
|
|
560
479
|
"peak.tooltip.active": "当前为峰时(09:00-12:00、14:00-18:00,价格翻倍):输入 ¥{input}/M · 输出 ¥{output}/M",
|
|
@@ -585,7 +504,7 @@ window.__ModuleLoader__.load({
|
|
|
585
504
|
"warn.title.2": "Balance below ¥1, almost exhausted!",
|
|
586
505
|
"warn.text.1": "⚠️ Balance below ¥5, consider topping up.",
|
|
587
506
|
"warn.text.2": "⛔ Balance below ¥1, almost exhausted — top up now!",
|
|
588
|
-
"todayCostShort": "Today",
|
|
507
|
+
"todayCostShort": "Today · all",
|
|
589
508
|
"peak.active": "Peak",
|
|
590
509
|
"peak.offpeak": "Off-peak",
|
|
591
510
|
"peak.tooltip.active": "Peak hours now (09:00-12:00, 14:00-18:00, 2× price): input ¥{input}/M · output ¥{output}/M",
|
package/lib/index.js
CHANGED
|
@@ -220,16 +220,31 @@ async function fetchBalance(ctx, config) {
|
|
|
220
220
|
}
|
|
221
221
|
|
|
222
222
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
223
|
+
* Built-in pricing table (CNY per 1M tokens). Used before the first successful
|
|
224
|
+
* sync and as the fallback for any model the official page cannot be parsed
|
|
225
|
+
* for, so it must be kept current by hand: V4.1-Flash rates as of 2026-09-10.
|
|
226
|
+
* Mutable: syncOfficialPricing() replaces entries with freshly parsed rates.
|
|
227
|
+
*/
|
|
228
|
+
const FLASH_RATES = {
|
|
229
|
+
peak: { inputMiss: 2.0, inputHit: 0.04, output: 8.0 },
|
|
230
|
+
offPeak: { inputMiss: 1.0, inputHit: 0.02, output: 4.0 }
|
|
231
|
+
};
|
|
232
|
+
const PRO_RATES = {
|
|
233
|
+
peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 },
|
|
234
|
+
offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 }
|
|
235
|
+
};
|
|
236
|
+
/**
|
|
237
|
+
* "deepseek-flash" is the current model name; the older names below are
|
|
238
|
+
* retired but still accepted by the API and billed at the Flash rate
|
|
239
|
+
* (V4-Pro requests are routed to V4.1-Flash from 2026-09-14 12:00 CST).
|
|
227
240
|
*/
|
|
228
241
|
const DEFAULT_PRICING = {
|
|
229
|
-
"deepseek-
|
|
230
|
-
"deepseek-v4-
|
|
231
|
-
"deepseek-
|
|
232
|
-
"deepseek-
|
|
242
|
+
"deepseek-flash": FLASH_RATES,
|
|
243
|
+
"deepseek-v4-flash": FLASH_RATES,
|
|
244
|
+
"deepseek-v4-flash-vision-exp": FLASH_RATES,
|
|
245
|
+
"deepseek-v4-pro": PRO_RATES,
|
|
246
|
+
"deepseek-chat": FLASH_RATES,
|
|
247
|
+
"deepseek-reasoner": PRO_RATES
|
|
233
248
|
};
|
|
234
249
|
let pricingTable = { ...DEFAULT_PRICING };
|
|
235
250
|
/** Source of the current pricing: "default" | "synced". */
|
|
@@ -246,9 +261,15 @@ const PRICING_ALIASES = {
|
|
|
246
261
|
"deepseek-official": "deepseek-v4-flash"
|
|
247
262
|
};
|
|
248
263
|
|
|
249
|
-
/**
|
|
264
|
+
/**
|
|
265
|
+
* Whether `date` falls inside a Beijing-time (UTC+8) peak window.
|
|
266
|
+
* DeepSeek's peak pricing applies to Beijing time 09:00-12:00 and 14:00-18:00
|
|
267
|
+
* regardless of the host's timezone, so the window is computed in UTC+8
|
|
268
|
+
* explicitly instead of the machine's local time.
|
|
269
|
+
*/
|
|
250
270
|
function isPeak(date) {
|
|
251
|
-
const
|
|
271
|
+
const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000);
|
|
272
|
+
const hour = beijing.getUTCHours();
|
|
252
273
|
return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
|
|
253
274
|
}
|
|
254
275
|
|
|
@@ -303,57 +324,74 @@ async function syncOfficialPricing() {
|
|
|
303
324
|
}
|
|
304
325
|
|
|
305
326
|
/**
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
|
|
309
|
-
|
|
327
|
+
* Pricing keys served by the page's leftmost (Flash) model column. Retired
|
|
328
|
+
* names are still billed at the Flash rate, so they share its entries.
|
|
329
|
+
*/
|
|
330
|
+
const FLASH_PRICING_KEYS = ["deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp", "deepseek-chat"];
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Best-effort parse of the official pricing page HTML. Only the leftmost model
|
|
334
|
+
* column is readable by {@link parseRates}, i.e. the Flash-tier column, so its
|
|
335
|
+
* rates are applied to every flash-family pricing key. Heuristic: returns a
|
|
336
|
+
* partial table rather than failing the whole sync.
|
|
310
337
|
*/
|
|
311
338
|
function parsePricingPage(html) {
|
|
339
|
+
const block = extractPricingTable(html);
|
|
340
|
+
if (block === null) return {};
|
|
341
|
+
const rates = parseRates(block);
|
|
342
|
+
if (rates === null) return {};
|
|
312
343
|
const out = {};
|
|
313
|
-
const
|
|
314
|
-
for (const model of modelIds) {
|
|
315
|
-
const block = extractModelBlock(html, model);
|
|
316
|
-
if (block === null) continue;
|
|
317
|
-
const rates = parseRates(block);
|
|
318
|
-
if (rates !== null) out[model] = rates;
|
|
319
|
-
}
|
|
344
|
+
for (const key of FLASH_PRICING_KEYS) out[key] = rates;
|
|
320
345
|
return out;
|
|
321
346
|
}
|
|
322
347
|
|
|
323
|
-
/**
|
|
324
|
-
|
|
325
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Extract the pricing-table region: a window around the first per-metric row
|
|
350
|
+
* label. Anchoring on the row label rather than on a model id keeps the sync
|
|
351
|
+
* alive across DeepSeek's model renames — anchoring on "deepseek-v4-flash"
|
|
352
|
+
* silently broke once that id survived only in a footnote *after* the table.
|
|
353
|
+
*/
|
|
354
|
+
function extractPricingTable(html) {
|
|
355
|
+
const idx = html.indexOf("缓存命中");
|
|
326
356
|
if (idx < 0) return null;
|
|
327
|
-
|
|
328
|
-
const end = Math.min(html.length, idx + 3000);
|
|
329
|
-
return html.slice(start, end);
|
|
357
|
+
return html.slice(Math.max(0, idx - 200), Math.min(html.length, idx + 1600));
|
|
330
358
|
}
|
|
331
359
|
|
|
332
360
|
/**
|
|
333
|
-
* Parse per-1M-token rates from a block of HTML text.
|
|
334
|
-
* "
|
|
335
|
-
*
|
|
361
|
+
* Parse per-1M-token rates from a block of HTML text. The pricing table lists
|
|
362
|
+
* rows per metric with "空闲时段 <off-peak> … 高峰时段 <peak>" columns (CN) or
|
|
363
|
+
* "off-peak … peak" (EN); the first model column is the one we price. Each
|
|
364
|
+
* metric is captured as an (offPeak, peak) pair from its labelled row.
|
|
365
|
+
* Returns { offPeak, peak } or null when any off-peak figure is missing
|
|
366
|
+
* (the caller then falls back to the built-in rate table rather than silently
|
|
367
|
+
* pricing cache hits at 0).
|
|
336
368
|
*/
|
|
337
369
|
function parseRates(block) {
|
|
338
370
|
// Strip tags to plain text for simpler matching.
|
|
339
371
|
const text = block.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
372
|
+
const pair = (label) => {
|
|
373
|
+
// label + short gap + "空闲时段 <n>" + gap + "高峰时段 <n>" (first model
|
|
374
|
+
// column). Also tolerate EN "off-peak … peak".
|
|
375
|
+
const re = new RegExp(
|
|
376
|
+
label + "[\\s\\S]{0,120}?(?:空闲时段|off[- ]?peak)\\s*([\\d.]+)[\\s\\S]{0,80}?(?:高峰时段|peak)\\s*([\\d.]+)",
|
|
377
|
+
"i"
|
|
378
|
+
);
|
|
379
|
+
const m = text.match(re);
|
|
380
|
+
if (!m) return null;
|
|
381
|
+
const a = parseFloat(m[1]);
|
|
382
|
+
const b = parseFloat(m[2]);
|
|
383
|
+
return Number.isFinite(a) && Number.isFinite(b) ? [a, b] : null;
|
|
345
384
|
};
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const peakOutput = (offPeakOutput ?? 0) * 2;
|
|
385
|
+
const hit = pair("缓存命中");
|
|
386
|
+
const miss = pair("缓存未命中");
|
|
387
|
+
const out = pair("百万tokens输出") ?? pair("输出");
|
|
388
|
+
if (miss === null || out === null) return null;
|
|
389
|
+
// Cache-hit row missing is acceptable only if it cannot be parsed at all —
|
|
390
|
+
// reject rather than guess zero.
|
|
391
|
+
if (hit === null) return null;
|
|
354
392
|
return {
|
|
355
|
-
|
|
356
|
-
|
|
393
|
+
offPeak: { inputMiss: miss[0], inputHit: hit[0], output: out[0] },
|
|
394
|
+
peak: { inputMiss: miss[1], inputHit: hit[1], output: out[1] }
|
|
357
395
|
};
|
|
358
396
|
}
|
|
359
397
|
|
|
@@ -381,19 +419,61 @@ function normalizeSessionId(sessionId) {
|
|
|
381
419
|
return sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
|
|
382
420
|
}
|
|
383
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Canonical session log basenames (DSH's @deepseek-ai/dsh-session-format):
|
|
424
|
+
* generation 0 is "session.jsonl.zstd"; later generations carry a lowercase
|
|
425
|
+
* ".vN" tag, e.g. "session.v3.jsonl.zstd" (Session format v3, DSH 0.1.5+).
|
|
426
|
+
*/
|
|
427
|
+
const SESSION_LOG_RE = /^session(?:\.v([1-9][0-9]*))?\.jsonl\.zstd$/;
|
|
428
|
+
|
|
429
|
+
/** Session format generation of a canonical log name, or undefined if not one. */
|
|
430
|
+
function sessionLogVersion(filename) {
|
|
431
|
+
const match = SESSION_LOG_RE.exec(filename);
|
|
432
|
+
if (match === null) return void 0;
|
|
433
|
+
return match[1] === void 0 ? 0 : Number(match[1]);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Collect the newest-generation log of every session directory, walking the
|
|
438
|
+
* sessions tree. A session that migrated formats keeps its older artifact on
|
|
439
|
+
* disk, and the newer generation is a full rewrite that already contains those
|
|
440
|
+
* rows — so only the highest generation per directory may be read. Reading
|
|
441
|
+
* both would double-count the whole session.
|
|
442
|
+
*/
|
|
443
|
+
function collectNewestSessionLogs(fs) {
|
|
444
|
+
const out = [];
|
|
445
|
+
const walk = (dir) => {
|
|
446
|
+
let entries;
|
|
447
|
+
try {
|
|
448
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
449
|
+
} catch {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
let newest;
|
|
453
|
+
let newestVersion = -1;
|
|
454
|
+
for (const entry of entries) {
|
|
455
|
+
if (entry.isDirectory()) {
|
|
456
|
+
walk(`${dir}/${entry.name}`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
const version = sessionLogVersion(entry.name);
|
|
460
|
+
if (version === void 0 || version <= newestVersion) continue;
|
|
461
|
+
newestVersion = version;
|
|
462
|
+
newest = `${dir}/${entry.name}`;
|
|
463
|
+
}
|
|
464
|
+
if (newest !== void 0) out.push(newest);
|
|
465
|
+
};
|
|
466
|
+
walk(sessionsRoot());
|
|
467
|
+
return out;
|
|
468
|
+
}
|
|
469
|
+
|
|
384
470
|
/** Resolve the current session's JSONL path by walking the sessions tree. */
|
|
385
471
|
async function resolveSessionFile(sessionId) {
|
|
386
472
|
if (sessionId === void 0 || sessionId === "") return void 0;
|
|
387
|
-
const
|
|
388
|
-
const root = sessionsRoot();
|
|
473
|
+
const marker = `/session-${normalizeSessionId(sessionId)}/`;
|
|
389
474
|
try {
|
|
390
475
|
const fs = await awaitImportFs();
|
|
391
|
-
|
|
392
|
-
for (const dir of dirs) {
|
|
393
|
-
if (!dir.isDirectory()) continue;
|
|
394
|
-
const candidate = `${root}/${dir.name}/session-${bareId}/session.jsonl.zstd`;
|
|
395
|
-
if (fs.existsSync(candidate)) return candidate;
|
|
396
|
-
}
|
|
476
|
+
return collectNewestSessionLogs(fs).find((path) => path.includes(marker));
|
|
397
477
|
} catch {
|
|
398
478
|
/* ignore filesystem errors */
|
|
399
479
|
}
|
|
@@ -531,31 +611,22 @@ function todaySessionCost(samples) {
|
|
|
531
611
|
* Walks the sessions tree and picks by mtime; returns the path or undefined.
|
|
532
612
|
*/
|
|
533
613
|
async function findMostRecentSessionFile() {
|
|
534
|
-
const root = sessionsRoot();
|
|
535
614
|
try {
|
|
536
615
|
const fs = await awaitImportFs();
|
|
537
616
|
const { statSync } = fs;
|
|
538
617
|
let best = void 0;
|
|
539
618
|
let bestMtime = -1;
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
if (
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
const mtime = statSync(full).mtimeMs;
|
|
548
|
-
if (mtime > bestMtime) {
|
|
549
|
-
bestMtime = mtime;
|
|
550
|
-
best = full;
|
|
551
|
-
}
|
|
552
|
-
} catch {
|
|
553
|
-
/* ignore unreadable files */
|
|
554
|
-
}
|
|
619
|
+
for (const file of collectNewestSessionLogs(fs)) {
|
|
620
|
+
try {
|
|
621
|
+
const mtime = statSync(file).mtimeMs;
|
|
622
|
+
if (mtime > bestMtime) {
|
|
623
|
+
bestMtime = mtime;
|
|
624
|
+
best = file;
|
|
555
625
|
}
|
|
626
|
+
} catch {
|
|
627
|
+
/* ignore unreadable files */
|
|
556
628
|
}
|
|
557
|
-
}
|
|
558
|
-
walk(root);
|
|
629
|
+
}
|
|
559
630
|
return best;
|
|
560
631
|
} catch {
|
|
561
632
|
return void 0;
|
|
@@ -587,31 +658,48 @@ function todayCost(sessionList, cwdFilter) {
|
|
|
587
658
|
return { cost, inputTokens: input, outputTokens: output };
|
|
588
659
|
}
|
|
589
660
|
|
|
590
|
-
/**
|
|
661
|
+
/**
|
|
662
|
+
* Decompressed session parse cache, keyed by path and validated against the
|
|
663
|
+
* file's mtime. Long-lived: an untouched file is never re-decompressed, so a
|
|
664
|
+
* cold /today-cost after the outer 30s TTL expires only re-reads files that
|
|
665
|
+
* actually changed. Cleared wholesale when it outgrows a bound.
|
|
666
|
+
*/
|
|
667
|
+
const sessionParseCache = new Map();
|
|
668
|
+
const SESSION_PARSE_CACHE_MAX = 600;
|
|
669
|
+
|
|
670
|
+
/** Decompress+parse one file, honoring the (path,mtime) cache. */
|
|
671
|
+
async function parseSessionFileCached(path, mtimeMs) {
|
|
672
|
+
const hit = sessionParseCache.get(path);
|
|
673
|
+
if (hit !== void 0 && hit.mtimeMs === mtimeMs) return hit.parsed;
|
|
674
|
+
const result = await decompressSession(path);
|
|
675
|
+
if (!result.ok) return null;
|
|
676
|
+
const parsed = parseSessionUsages(result.text);
|
|
677
|
+
if (sessionParseCache.size >= SESSION_PARSE_CACHE_MAX) sessionParseCache.clear();
|
|
678
|
+
sessionParseCache.set(path, { mtimeMs, parsed });
|
|
679
|
+
return parsed;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* Walk every session file under the sessions root and parse the ones that can
|
|
684
|
+
* contribute to "today" numbers. Only files whose mtime falls on the current
|
|
685
|
+
* local calendar day can contain today's samples, so older files are skipped
|
|
686
|
+
* entirely (avoids decompressing the whole ~/.dsh/sessions tree on every cold
|
|
687
|
+
* call). Parsing runs in parallel; results are cached by (path, mtime).
|
|
688
|
+
*/
|
|
591
689
|
async function collectAllSessionSamples() {
|
|
592
|
-
const root = sessionsRoot();
|
|
593
690
|
const fs = (await awaitImportFs());
|
|
594
|
-
const files =
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
const full = `${dir}/${entry.name}`;
|
|
599
|
-
if (entry.isDirectory()) walk(full);
|
|
600
|
-
else if (entry.name === "session.jsonl.zstd") files.push(full);
|
|
601
|
-
}
|
|
602
|
-
};
|
|
603
|
-
walk(root);
|
|
604
|
-
} catch {
|
|
605
|
-
return [];
|
|
606
|
-
}
|
|
607
|
-
const perSession = [];
|
|
691
|
+
const files = collectNewestSessionLogs(fs);
|
|
692
|
+
const now = new Date();
|
|
693
|
+
const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
|
694
|
+
const candidates = [];
|
|
608
695
|
for (const file of files) {
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
696
|
+
try {
|
|
697
|
+
const mtimeMs = fs.statSync(file).mtimeMs;
|
|
698
|
+
if (mtimeMs >= dayStart) candidates.push({ path: file, mtimeMs });
|
|
699
|
+
} catch { /* file vanished mid-walk */ }
|
|
613
700
|
}
|
|
614
|
-
|
|
701
|
+
const parsed = await Promise.all(candidates.map((c) => parseSessionFileCached(c.path, c.mtimeMs)));
|
|
702
|
+
return parsed.filter((p) => p !== null);
|
|
615
703
|
}
|
|
616
704
|
|
|
617
705
|
/** Simple in-memory TTL cache for cost computations (30s). */
|
|
@@ -782,7 +870,7 @@ function makeRoutes(ctx, config) {
|
|
|
782
870
|
const segments = parsed.cwd.split("/").filter((s) => s !== "");
|
|
783
871
|
workspaceName = segments.length > 0 ? segments[segments.length - 1] : parsed.cwd;
|
|
784
872
|
}
|
|
785
|
-
// Session id from the file path (…/session-<id>/session.jsonl.zstd).
|
|
873
|
+
// Session id from the file path (…/session-<id>/session[.vN].jsonl.zstd).
|
|
786
874
|
let fileSessionId = void 0;
|
|
787
875
|
const sidMatch = file.match(/session-([0-9a-f-]+)/);
|
|
788
876
|
if (sidMatch !== null) fileSessionId = sidMatch[1];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-balance-widget",
|
|
3
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.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"files": [
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"react": "^18.2.0"
|
|
36
|
+
"react": "^18.2.0",
|
|
37
|
+
"@deepseek-ai/dsh": ">=0.1.2-rc.1 <0.2.0"
|
|
37
38
|
},
|
|
38
39
|
"keywords": [
|
|
39
40
|
"deepseek",
|
|
@@ -49,5 +50,9 @@
|
|
|
49
50
|
"repository": {
|
|
50
51
|
"type": "git",
|
|
51
52
|
"url": "git+https://github.com/LL-cmyk-so/dsh-balance-widget.git"
|
|
52
|
-
}
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
56
|
+
},
|
|
57
|
+
"packageManager": "pnpm@11.7.0"
|
|
53
58
|
}
|