dsh-balance-widget 0.5.1 → 0.5.3

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 CHANGED
@@ -25,7 +25,7 @@ A balance & cost widget for the [DeepSeek Harness](https://github.com/deepseek-a
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
- | **Peak/off-peak pricing** | ✅ Built-in official 2026-08-17 rate table, auto-switches by window | Partial support |
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.*
@@ -136,23 +136,63 @@ If you mainly use DeepSeek-V4-Pro, point the pricing model at it for a more accu
136
136
  modelId: deepseek-v4-pro
137
137
  ```
138
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
+
139
141
  **Note**: `cordis.patch.yml` may already contain lines for other plugins — append new lines without touching existing ones.
140
142
 
141
143
  ## Pricing
142
144
 
143
- Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective 2026-08-17. Peak windows are Beijing time 09:00–12:00 and 14:00–18:00; prices are double the off-peak rates:
145
+ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective 2026-09-10. Peak windows are Beijing time **Mon–Fri** 09:00–12:00 and 14:00–18:00 (weekends are off-peak all day); prices are double the off-peak rates:
144
146
 
145
147
  | Model | Window | Cache hit (input) | Cache miss (input) | Output |
146
148
  | --- | --- | --- | --- | --- |
147
- | V4-Flash | Off-peak | 0.05 | 1.5 | 4.5 |
148
- | V4-Flash | Peak | 0.10 | 3.0 | 9.0 |
149
+ | V4.1-Flash | Off-peak | 0.02 | 1.0 | 4.0 |
150
+ | V4.1-Flash | Peak | 0.04 | 2.0 | 8.0 |
149
151
  | V4-Pro | Off-peak | 0.15 | 4.5 | 13.5 |
150
152
  | V4-Pro | Peak | 0.30 | 9.0 | 27.0 |
151
153
 
152
- `deepseek-chat` / `deepseek-reasoner` aliases map to Flash / Pro pricing respectively. Costs are **estimates**; actual billing from the provider is authoritative.
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
153
183
 
154
184
  ## Changelog
155
185
 
186
+ ### v0.5.3 — weekends no longer mispriced as peak
187
+ - 🐛 **Fixed**: the peak/off-peak check looked only at the hour and ignored the weekday, so weekends were priced as peak (2×) during 09:00–12:00 and 14:00–18:00, up to doubling "today's cost". The official rule is **Mon–Fri** 09:00–12:00 and 14:00–18:00 (everything else, weekends included, is off-peak); Saturday and Sunday are now excluded first, using the Beijing-time weekday
188
+ - 📄 **Docs**: the pricing section and the peak/off-peak tooltips now state the Mon–Fri restriction
189
+
190
+ ### v0.5.2 — DSH 0.1.5: new session format & new pricing page
191
+ - 🐛 **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
192
+ - 🐛 **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
193
+ - 💰 **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
194
+ - 📄 **Docs**: corrected the `pricingSource` values to `default` / `synced` (previously misdocumented as `builtin`)
195
+
156
196
  ### v0.5.1 — Cold-start speedup & cleanup
157
197
  - 🚀 **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
198
  - ⏰ **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)
package/README.md CHANGED
@@ -25,7 +25,7 @@ DeepSeek Harness (DSH) Web GUI 的余额与成本小部件:侧边栏底部常
25
25
  | **Node 24 兼容** | ✅ 天然兼容(零依赖设计),任何 profile 布局可加载 | ⚠️ 不少社区插件在 Node 24 下报错 |
26
26
  | **启动稳定性** | ✅ 用官方 `ctx.webServer` 注册路由,不与 apiproxy 冲突 | ⚠️ 有的自建 HTTP 服务导致 dsh web 启动崩溃 |
27
27
  | **常驻自动刷新** | ✅ 侧边栏卡片每 60s 自动刷新余额与成本,弹框点击即强刷 | 有的常驻徽章定时刷新 |
28
- | **峰谷定价** | ✅ 内置官方 2026-08-17 峰谷价表,自动按时段切换 | 部分支持 |
28
+ | **峰谷定价** | ✅ 内置官方 2026-09-10 峰谷价表,启动时与每 12h 自动从官方页刷新 | 部分支持 |
29
29
  | **安全性** | ✅ API key 仅在宿主进程,loopback-only 守卫 | 参差不齐 |
30
30
 
31
31
  **一句话**:*零依赖、Node 24 就绪、永不拖垮 dsh web 启动的余额/成本小部件。*
@@ -133,23 +133,63 @@ DSH 的插件配置统一放在这个文件里:
133
133
  modelId: deepseek-v4-pro
134
134
  ```
135
135
 
136
+ **注意**:官方已公告 V4-Pro 将于北京时间 2026-09-14 12:00 下线——此后 V4-Pro 请求会被路由到 V4.1-Flash 并按 Flash 计费。若在这之后仍配置 `deepseek-v4-pro`,估算会偏高,建议使用默认的 Flash 档。
137
+
136
138
  **提示**:`cordis.patch.yml` 可能有其他插件的配置行,追加时注意**不要改动已有的行**,只加新内容。
137
139
 
138
140
  ## 定价说明
139
141
 
140
- 内置 DeepSeek 官方 2026-08-17 峰谷定价(元 / 百万 tokens),高峰时段为北京时间 09:00–12:00、14:00–18:00,价格为空闲时段两倍:
142
+ 内置 DeepSeek 官方 2026-09-10 峰谷定价(元 / 百万 tokens),高峰时段为北京时间**周一至周五** 09:00–12:00、14:00–18:00(周末全天为空闲时段),价格为空闲时段两倍:
141
143
 
142
144
  | 模型 | 时段 | 缓存命中(输入) | 缓存未命中(输入) | 输出 |
143
145
  | --- | --- | --- | --- | --- |
144
- | V4-Flash | 空闲 | 0.05 | 1.5 | 4.5 |
145
- | V4-Flash | 高峰 | 0.10 | 3.0 | 9.0 |
146
+ | V4.1-Flash | 空闲 | 0.02 | 1.0 | 4.0 |
147
+ | V4.1-Flash | 高峰 | 0.04 | 2.0 | 8.0 |
146
148
  | V4-Pro | 空闲 | 0.15 | 4.5 | 13.5 |
147
149
  | V4-Pro | 高峰 | 0.30 | 9.0 | 27.0 |
148
150
 
149
- `deepseek-chat` / `deepseek-reasoner` 别名分别映射到 Flash / Pro 价格。成本为**估算值**,实际以官方账单为准。
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
+ - 所有成本为估算值,实际以官方账单为准
150
180
 
151
181
  ## 版本历史
152
182
 
183
+ ### v0.5.3 — 修复周末被误判为峰时
184
+ - 🐛 **修复**:峰/谷判断只看小时、不看星期,导致周末 09:00–12:00、14:00–18:00 被误判为峰时并按 2× 计价,"今日花费"最多翻倍。官方规则为**周一至周五** 09:00–12:00、14:00–18:00(其余含周末全天为空闲时段),现按北京时间的星期先行排除周六/周日
185
+ - 📄 **文档**:定价说明与峰谷 tooltip 补上"周一至周五"限定
186
+
187
+ ### v0.5.2 — 适配 DSH 0.1.5:新会话格式与新定价页
188
+ - 🐛 **修复**:DSH 0.1.5 将会话日志改为代数命名 `session.v3.jsonl.zstd`,插件此前只认 `session.jsonl.zstd`,导致升级后新建的会话全部不可见——查看这类会话时弹层出现红色报错,今日成本也漏算(实测少约 36%)。现按「每个会话目录取最高代」发现日志;迁移会话保留的旧文件是新代的子集,因此只读最高代,避免整个会话被重复计算
189
+ - 🐛 **修复**:官方定价页将 Flash 列改名为 `deepseek-flash` 并下调价格,插件按模型名定位价格块的锚点因此落到页面脚注,同步"成功"却静默沿用旧高价,成本被高估约 1.7 倍。现改为按价格表行标签锚定,并把该列价格套用到全部 Flash 系名称
190
+ - 💰 **价目**:内置兜底价目表更新为 V4.1-Flash 价(空闲 0.02 / 1.0 / 4.0,高峰 0.04 / 2.0 / 8.0,元/百万 tokens);V4-Pro 档位保留
191
+ - 📄 **文档**:更正 `pricingSource` 取值为 `default` / `synced`(此前误写为 `builtin`)
192
+
153
193
  ### v0.5.1 — 冷启动提速与代码清理
154
194
  - 🚀 **性能**:今日成本冷启动从 ~5.4s 降至 ~0.01s——只解压当天活跃的会话文件(按 mtime 过滤)+ 解压结果按 (path, mtime) 长期缓存 + 并行解析
155
195
  - ⏰ **轮询口径**:README 与代码统一——常驻卡片每 60s 自动刷新(此前文档"无轮询"表述自相矛盾,已更正)
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_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)}";
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");
@@ -476,8 +476,8 @@ window.__ModuleLoader__.load({
476
476
  "todayCostShort": "今日·全部",
477
477
  "peak.active": "峰时",
478
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",
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
481
  "sessionNote": "基于最近活跃会话:",
482
482
  "hint": "价格为估算值,实际以官方账单为准。",
483
483
  "topUp": "去充值",
@@ -507,8 +507,8 @@ window.__ModuleLoader__.load({
507
507
  "todayCostShort": "Today · all",
508
508
  "peak.active": "Peak",
509
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",
510
+ "peak.tooltip.active": "Peak hours now (Mon-Fri 09:00-12:00, 14:00-18:00, 2× price): input ¥{input}/M · output ¥{output}/M",
511
+ "peak.tooltip.offpeak": "Off-peak hours now (all other times incl. weekends, half price): input ¥{input}/M · output ¥{output}/M",
512
512
  "sessionNote": "Based on most recent session:",
513
513
  "hint": "Prices are estimates; actual billing from the provider is authoritative.",
514
514
  "topUp": "Top up",
package/lib/index.js CHANGED
@@ -220,16 +220,31 @@ async function fetchBalance(ctx, config) {
220
220
  }
221
221
 
222
222
  /**
223
- * Pricing table (CNY per 1M tokens, official 2026-08-17 peak/off-peak).
224
- * Mirrors the client half; kept here so host-computed costs use the same
225
- * numbers. Mutable: syncOfficialPricing() may replace it with freshly parsed
226
- * official rates (falling back to these defaults on failure).
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-v4-flash": { peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 }, offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 } },
230
- "deepseek-v4-pro": { peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 }, offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 } },
231
- "deepseek-chat": { peak: { inputMiss: 3.0, inputHit: 0.10, output: 9.0 }, offPeak: { inputMiss: 1.5, inputHit: 0.05, output: 4.5 } },
232
- "deepseek-reasoner": { peak: { inputMiss: 9.0, inputHit: 0.30, output: 27.0 }, offPeak: { inputMiss: 4.5, inputHit: 0.15, output: 13.5 } }
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". */
@@ -249,11 +264,14 @@ const PRICING_ALIASES = {
249
264
  /**
250
265
  * Whether `date` falls inside a Beijing-time (UTC+8) peak window.
251
266
  * 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.
267
+ * on weekdays only (Mon-Fri); the whole weekend is off-peak. The window is
268
+ * computed in UTC+8 explicitly instead of the machine's local time, which also
269
+ * supplies the Beijing weekday.
254
270
  */
255
271
  function isPeak(date) {
256
272
  const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000);
273
+ const day = beijing.getUTCDay(); // 0 = Sunday, 6 = Saturday
274
+ if (day === 0 || day === 6) return false;
257
275
  const hour = beijing.getUTCHours();
258
276
  return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
259
277
  }
@@ -309,30 +327,37 @@ async function syncOfficialPricing() {
309
327
  }
310
328
 
311
329
  /**
312
- * Best-effort parse of the official pricing page HTML. Looks for the standard
313
- * "deepseek-v4-flash" / "deepseek-v4-pro" model blocks and per-million-token
314
- * rates. This is heuristic: it tolerates missing fields and returns a partial
315
- * table rather than failing the whole sync.
330
+ * Pricing keys served by the page's leftmost (Flash) model column. Retired
331
+ * names are still billed at the Flash rate, so they share its entries.
332
+ */
333
+ const FLASH_PRICING_KEYS = ["deepseek-flash", "deepseek-v4-flash", "deepseek-v4-flash-vision-exp", "deepseek-chat"];
334
+
335
+ /**
336
+ * Best-effort parse of the official pricing page HTML. Only the leftmost model
337
+ * column is readable by {@link parseRates}, i.e. the Flash-tier column, so its
338
+ * rates are applied to every flash-family pricing key. Heuristic: returns a
339
+ * partial table rather than failing the whole sync.
316
340
  */
317
341
  function parsePricingPage(html) {
342
+ const block = extractPricingTable(html);
343
+ if (block === null) return {};
344
+ const rates = parseRates(block);
345
+ if (rates === null) return {};
318
346
  const out = {};
319
- const modelIds = ["deepseek-v4-flash", "deepseek-v4-pro"];
320
- for (const model of modelIds) {
321
- const block = extractModelBlock(html, model);
322
- if (block === null) continue;
323
- const rates = parseRates(block);
324
- if (rates !== null) out[model] = rates;
325
- }
347
+ for (const key of FLASH_PRICING_KEYS) out[key] = rates;
326
348
  return out;
327
349
  }
328
350
 
329
- /** Extract the HTML section mentioning a model id (crude but robust). */
330
- function extractModelBlock(html, model) {
331
- const idx = html.indexOf(model);
351
+ /**
352
+ * Extract the pricing-table region: a window around the first per-metric row
353
+ * label. Anchoring on the row label rather than on a model id keeps the sync
354
+ * alive across DeepSeek's model renames — anchoring on "deepseek-v4-flash"
355
+ * silently broke once that id survived only in a footnote *after* the table.
356
+ */
357
+ function extractPricingTable(html) {
358
+ const idx = html.indexOf("缓存命中");
332
359
  if (idx < 0) return null;
333
- const start = Math.max(0, idx - 400);
334
- const end = Math.min(html.length, idx + 3000);
335
- return html.slice(start, end);
360
+ return html.slice(Math.max(0, idx - 200), Math.min(html.length, idx + 1600));
336
361
  }
337
362
 
338
363
  /**
@@ -397,19 +422,61 @@ function normalizeSessionId(sessionId) {
397
422
  return sessionId.startsWith("session-") ? sessionId.slice("session-".length) : sessionId;
398
423
  }
399
424
 
425
+ /**
426
+ * Canonical session log basenames (DSH's @deepseek-ai/dsh-session-format):
427
+ * generation 0 is "session.jsonl.zstd"; later generations carry a lowercase
428
+ * ".vN" tag, e.g. "session.v3.jsonl.zstd" (Session format v3, DSH 0.1.5+).
429
+ */
430
+ const SESSION_LOG_RE = /^session(?:\.v([1-9][0-9]*))?\.jsonl\.zstd$/;
431
+
432
+ /** Session format generation of a canonical log name, or undefined if not one. */
433
+ function sessionLogVersion(filename) {
434
+ const match = SESSION_LOG_RE.exec(filename);
435
+ if (match === null) return void 0;
436
+ return match[1] === void 0 ? 0 : Number(match[1]);
437
+ }
438
+
439
+ /**
440
+ * Collect the newest-generation log of every session directory, walking the
441
+ * sessions tree. A session that migrated formats keeps its older artifact on
442
+ * disk, and the newer generation is a full rewrite that already contains those
443
+ * rows — so only the highest generation per directory may be read. Reading
444
+ * both would double-count the whole session.
445
+ */
446
+ function collectNewestSessionLogs(fs) {
447
+ const out = [];
448
+ const walk = (dir) => {
449
+ let entries;
450
+ try {
451
+ entries = fs.readdirSync(dir, { withFileTypes: true });
452
+ } catch {
453
+ return;
454
+ }
455
+ let newest;
456
+ let newestVersion = -1;
457
+ for (const entry of entries) {
458
+ if (entry.isDirectory()) {
459
+ walk(`${dir}/${entry.name}`);
460
+ continue;
461
+ }
462
+ const version = sessionLogVersion(entry.name);
463
+ if (version === void 0 || version <= newestVersion) continue;
464
+ newestVersion = version;
465
+ newest = `${dir}/${entry.name}`;
466
+ }
467
+ if (newest !== void 0) out.push(newest);
468
+ };
469
+ walk(sessionsRoot());
470
+ return out;
471
+ }
472
+
400
473
  /** Resolve the current session's JSONL path by walking the sessions tree. */
401
474
  async function resolveSessionFile(sessionId) {
402
475
  if (sessionId === void 0 || sessionId === "") return void 0;
403
- const bareId = normalizeSessionId(sessionId);
404
- const root = sessionsRoot();
476
+ const marker = `/session-${normalizeSessionId(sessionId)}/`;
405
477
  try {
406
478
  const fs = await awaitImportFs();
407
- const dirs = fs.readdirSync(root, { withFileTypes: true });
408
- for (const dir of dirs) {
409
- if (!dir.isDirectory()) continue;
410
- const candidate = `${root}/${dir.name}/session-${bareId}/session.jsonl.zstd`;
411
- if (fs.existsSync(candidate)) return candidate;
412
- }
479
+ return collectNewestSessionLogs(fs).find((path) => path.includes(marker));
413
480
  } catch {
414
481
  /* ignore filesystem errors */
415
482
  }
@@ -547,31 +614,22 @@ function todaySessionCost(samples) {
547
614
  * Walks the sessions tree and picks by mtime; returns the path or undefined.
548
615
  */
549
616
  async function findMostRecentSessionFile() {
550
- const root = sessionsRoot();
551
617
  try {
552
618
  const fs = await awaitImportFs();
553
619
  const { statSync } = fs;
554
620
  let best = void 0;
555
621
  let bestMtime = -1;
556
- const walk = (dir) => {
557
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
558
- const full = `${dir}/${entry.name}`;
559
- if (entry.isDirectory()) {
560
- walk(full);
561
- } else if (entry.name === "session.jsonl.zstd") {
562
- try {
563
- const mtime = statSync(full).mtimeMs;
564
- if (mtime > bestMtime) {
565
- bestMtime = mtime;
566
- best = full;
567
- }
568
- } catch {
569
- /* ignore unreadable files */
570
- }
622
+ for (const file of collectNewestSessionLogs(fs)) {
623
+ try {
624
+ const mtime = statSync(file).mtimeMs;
625
+ if (mtime > bestMtime) {
626
+ bestMtime = mtime;
627
+ best = file;
571
628
  }
629
+ } catch {
630
+ /* ignore unreadable files */
572
631
  }
573
- };
574
- walk(root);
632
+ }
575
633
  return best;
576
634
  } catch {
577
635
  return void 0;
@@ -632,21 +690,8 @@ async function parseSessionFileCached(path, mtimeMs) {
632
690
  * call). Parsing runs in parallel; results are cached by (path, mtime).
633
691
  */
634
692
  async function collectAllSessionSamples() {
635
- const root = sessionsRoot();
636
693
  const fs = (await awaitImportFs());
637
- const files = [];
638
- try {
639
- const walk = (dir) => {
640
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
641
- const full = `${dir}/${entry.name}`;
642
- if (entry.isDirectory()) walk(full);
643
- else if (entry.name === "session.jsonl.zstd") files.push(full);
644
- }
645
- };
646
- walk(root);
647
- } catch {
648
- return [];
649
- }
694
+ const files = collectNewestSessionLogs(fs);
650
695
  const now = new Date();
651
696
  const dayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
652
697
  const candidates = [];
@@ -828,7 +873,7 @@ function makeRoutes(ctx, config) {
828
873
  const segments = parsed.cwd.split("/").filter((s) => s !== "");
829
874
  workspaceName = segments.length > 0 ? segments[segments.length - 1] : parsed.cwd;
830
875
  }
831
- // Session id from the file path (…/session-<id>/session.jsonl.zstd).
876
+ // Session id from the file path (…/session-<id>/session[.vN].jsonl.zstd).
832
877
  let fileSessionId = void 0;
833
878
  const sidMatch = file.match(/session-([0-9a-f-]+)/);
834
879
  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.1",
4
+ "version": "0.5.3",
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
  }