dsh-balance-widget 0.6.0 → 0.6.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
@@ -32,7 +32,7 @@ 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 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.
35
+ - **Account balance** — The balance comes from the host account service `deepseekAccount`, provided by `@deepseek-ai/dsh-deepseek-account-platform` — the official "Settings → Account & balance" page's own source, so both surfaces show the same number and **no API key is needed**; unsigned hosts and older runtimes (0.1.x) fall back to DeepSeek's official `GET /user/balance`. The `¥` balance 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
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
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
38
  - **Today · this workspace cost (estimate)** — Sums today's token usage × price across every session in the current workspace (anchored by the current session).
@@ -144,7 +144,7 @@ If you mainly use DeepSeek-V4-Pro, point the pricing model at it for a more accu
144
144
 
145
145
  ## Pricing
146
146
 
147
- 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:
147
+ Built-in DeepSeek official peak/off-peak pricing (CNY per 1M tokens), effective 2026-09-10. Peak windows are Beijing time **Mon–Fri (China's statutory holidays excluded)** 09:00–12:00 and 14:00–18:00; **everything else is off-peak**, weekends (including 调休 make-up workdays) and statutory holidays all day included. Peak prices are double the off-peak rates:
148
148
 
149
149
  | Model | Window | Cache hit (input) | Cache miss (input) | Output |
150
150
  | --- | --- | --- | --- | --- |
@@ -167,19 +167,20 @@ This section is for the DSH Store / plugin audit: dependencies, runtime permissi
167
167
 
168
168
  **Dependencies & compatibility**
169
169
  - Zero runtime dependencies: imports no `@deepseek-ai/*` packages; no third-party host deps
170
- - `peerDependencies["@deepseek-ai/dsh"]`: `>=0.1.2-rc.1 <0.2.0` (DSH compatibility range)
170
+ - `peerDependencies["@deepseek-ai/dsh"]`: `>=0.1.2-rc.1 <0.3.0` (DSH compatibility range; 0.1.5 / 0.1.7 / 0.2.0 verified)
171
171
  - `engines.node`: `^22.19.0 || >=24.0.0`
172
172
  - `peerDependencies["react"]`: `^18.2.0` (browser rendering only)
173
173
 
174
174
  **Runtime permissions**
175
175
  - `files`: reads only `~/.dsh/sessions/` session JSONL (cost stats); never writes or mutates any session file
176
- - `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
176
+ - `network`: only the DeepSeek official endpoints — `api.deepseek.com` (`GET /user/balance`, used only when unsigned or on an older host; a signed-in runtime reads the host's `deepseekAccount` service instead, so no plugin request leaves the process) and `api-docs.deepseek.com` pricing page (fetched every 12h); no third-party proxy
177
177
  - `commands`: **none**. Session logs are decompressed with Node's built-in zstd (`node:zlib`, needs Node >=22.15), so no `zstd` binary and no `PATH` dependency
178
- - `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
178
+ - `credentials`: reads `DEEPSEEK_API_KEY` on the fallback path only (resolved via the host credentials service), used only in the host process behind a loopback-only route guard; the browser never sees the key. A signed-in account reads no key at all
179
179
  - All host routes are bound to the loopback address and unreachable externally
180
180
 
181
181
  **External services**
182
- - DeepSeek official balance endpoint `GET /user/balance` (on click / 60s refresh)
182
+ - Host account service `deepseekAccount` (DSH 0.2.0+ with a signed-in account; the balance is the official page's own value, truncated to cents the way that page displays it)
183
+ - DeepSeek official balance endpoint `GET /user/balance` (fallback: unsigned or older host; on click / 60s refresh)
183
184
  - DeepSeek official pricing page (on startup + every 12h, for peak/off-peak rates)
184
185
 
185
186
  **Failure bounds**
@@ -191,6 +192,31 @@ This section is for the DSH Store / plugin audit: dependencies, runtime permissi
191
192
 
192
193
  ## Changelog
193
194
 
195
+ ### v0.6.3 — China's statutory holidays are now priced off-peak all day
196
+ - 🐛 **Fixed**: the peak/off-peak check excluded weekends but **not China's statutory holidays**. The official rule is "Beijing time **Mon–Fri (China's statutory holidays excluded)** 09:00–12:00 and 14:00–18:00 are peak; everything else — weekends and statutory holidays all day — is off-peak", so a statutory holiday falling on a weekday was **priced at 2×**. Measured against the built-in 2026 arrangement: Spring Festival 2/18, Dragon Boat 6/19, Mid-Autumn 9/25 and National Day 10/1–10/7 were all mispriced as peak, doubling their cost
197
+ - 📅 **The 2026 statutory holiday table is built in** ([国办发明电〔2025〕7号](https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm); **33 days** across New Year, Spring Festival, Qingming, Labour Day, Dragon Boat, Mid-Autumn and National Day — **19 of them on weekdays**, exactly the ones that used to be priced at 2×), matched on the Beijing calendar day. Make-up workdays that fall on a Saturday or Sunday were already off-peak (the official rule lists weekends outright); they are now pinned by tests too
198
+ - 🧪 **Differential verification**: 14 fixture sessions (each with its own fake `$HOME`, driven through the real route, real zstd frames and real pricing) assert ¥1.00 all day on holidays, ¥2.00 inside a normal weekday peak window, and the first working day after a holiday plus the lunch/evening boundaries — **14/14 pass**. Running the same cases against the pre-fix code fails all six holiday cases (¥2.00)
199
+ - ⚠️ **Maintenance note**: the table needs an annual refresh (the State Council publishes the next arrangement, usually in the previous November); years it does not cover fall back to the old weekend-only rule
200
+ - 📄 **Docs**: the pricing section and the peak/off-peak tooltips (both languages) now state "statutory holidays excluded / make-up workdays included" precisely
201
+
202
+ ### v0.6.2 — the balance now prefers the official account service (same source as the settings page, no API key needed)
203
+ - ✨ **The balance is read from the host account service `deepseekAccount` (provided by `@deepseek-ai/dsh-deepseek-account-platform`)** whenever DSH >= 0.2.0 ships it **and a DeepSeek account is signed in**. That service is the official "Settings → Account & balance" page's own source, so the card and the settings page show the same number. Amounts follow the official rule — **positive values are truncated to cents** (`55.6787307800000000` → `55.67`, matching the page) — with the purchased wallet from `value[]`, the granted wallet from `bonusWallets[]`, their sum as the total, and one entry per currency
204
+ - 🚪 **Every fallback is unchanged**: when signed out, when the host has no such service (DSH 0.1.x), or when the account read fails (expired token and friends), the balance comes from the original `GET /user/balance` + `DEEPSEEK_API_KEY`, exactly as in 0.6.1; a failed account read logs one warning saying why
205
+ - 🔑 **Signed in, no key is read at all**: the balance no longer hard-depends on `DEEPSEEK_API_KEY`, so an account-only install still shows a balance
206
+ - 🔎 **Auditable**: the balance response carries a `balanceSource` field (`deepseekAccount` or the fallback path), so it is always clear where a number came from
207
+ - 🧪 **Verified**: a fake cordis ctx drives the **real routes** through 13 assertions — account mapping, multi-currency and bonus summation, cent truncation, signed-out fallback, missing-service fallback, throwing-account fallback with a warning, account winning over an API key, the agent billing tool reading the same source, and a guard that the credential key's spelling is never mistaken for the service name
208
+ - 📏 **Measured same-source** (2026-09-29, after a real restart): the official `account/getBalance` returned `¥55.0685054200000000` while the card showed `¥55.06` — identical to the cent
209
+
210
+ > **Why this release, said out loud.**
211
+ > After desktop 0.2.0 shipped, the official app put a signed-in balance page under Settings → Account & balance. **That means the platform now recognises the pain point** — people need to see their balance. It just takes several clicks to reach, which for everyday use still loses to a card that sits in the sidebar and reads at a glance.
212
+ >
213
+ > So this release adapts anyway: the balance is read from the official account service, identical to the settings page to the cent, and no API key is needed. But we are not pretending otherwise — now that the platform has taken the need over, **this plugin will be replaced sooner or later**. A little wistful, yet a tool's job is to make itself unnecessary: **we are entering the farewell period.** From here we only keep compatibility maintenance in step with DSH, with no new features. Thanks for letting it watch your balance all this time.
214
+
215
+ ### v0.6.1 — compatibility range widened to DSH 0.2.0 (upgrading would otherwise drop the plugin silently)
216
+ - 🐛 **Fixed**: DSH 0.2.0 adds a plugin compatibility gate (`evaluatePluginCompatibility` in `dsh-app-boot`). It matches every declared `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` peer range against the running version — **prereleases included** (`semver.satisfies(..., { includePrerelease: true })`) — and a mismatching, non-exempted bundle is **skipped silently at startup** (`loadProfileDirectory` files it under `skippedBundles`: no error, no manifest change). The old declaration `>=0.1.2-rc.1 <0.2.0` happened to cover `0.2.0-rc.1` (which is why the widget works on the current 0.2.0-rc.1 desktop, verified live) but **not 0.2.0 final** — so the first stable-desktop upgrade would have made the sidebar card vanish, with the plugin manager demanding a manual `dsh plugin allow-version` exemption. The range is now `<0.3.0` (verified to cover 0.2.0 / 0.2.1 / 0.3.0-rc.1)
217
+ - ✅ **Live 0.2.0 compatibility check** (against the 0.2.0-rc.1 desktop app, driven in a real browser): all five host routes answer; session logs are still `SESSION_FORMAT_VERSION = 4` and `totalTokens = inputTokens + outputTokens + cacheReadTokens` holds for all 394 usage events (pricing math unchanged); the `sidebar.footer.action` slot, the `dsh.client.platform === "web"` loading gate and the `IconRefreshOutlineRegular` name are all unchanged; every one of the 19 `--dsw-*` tokens the plugin uses still exists (15 of them redefined under `body[data-ds-dark-theme]`, and the popover portalled into `document.body` still inherits the theme); React is still 18.3.1; the 36×36 collapsed-rail button and the popover anchoring behave as before
218
+ - 📦 **Scope**: the compatibility range in `package.json` plus documentation; no runtime code changed
219
+
194
220
  ### v0.6.0 — desktop app support, and a UI that matches the desktop shell
195
221
  - 🖥️ **Desktop app support (DSH 0.1.7 / DeepSeek Harness.app)**
196
222
  - **Session logs are no longer decompressed by the `zstd` CLI** — Node's built-in zstd (`node:zlib`) does it instead. The desktop app is launched by the GUI, so its PATH is only `/usr/bin:/bin:/usr/sbin:/sbin` and the Homebrew `zstd` is invisible: every cost tier (today's spend, last prompt) failed outright there
package/README.md CHANGED
@@ -32,7 +32,7 @@ DeepSeek Harness (DSH) **Web GUI 与桌面版**通用的余额与成本小部件
32
32
 
33
33
  ## 功能
34
34
 
35
- - **账户余额** — 点击图标时经宿主代理查询 DeepSeek 官方 `GET /user/balance`,展示 `¥` 余额;余额数字按阈值自动变色(充足 / 低于 `lowThreshold` 变黄 / 低于 `criticalThreshold` 变红);API key 只在宿主进程内读取(凭据服务),浏览器不接触密钥。
35
+ - **账户余额** — 余额优先取自宿主账号服务 `deepseekAccount`(由 `@deepseek-ai/dsh-deepseek-account-platform` 提供,即官方「设置 → 账号与余额」页的同一来源,因此两处显示同一个数字,且**不需要 API Key**);未登录或旧版宿主(0.1.x)时回退到 DeepSeek 官方 `GET /user/balance`。展示 `¥` 余额,按阈值自动变色(充足 / 低于 `lowThreshold` 变黄 / 低于 `criticalThreshold` 变红);API key 只在宿主进程内读取(凭据服务),浏览器不接触密钥。
36
36
  - **最近一次提问成本(估算)** — 从最近活跃会话文件解析最后一个 turn 的 token 用量 × 单价,回答"刚才那条提问花了多少";下方标注该会话的**会话名**。
37
37
  - **今日·本会话成本(估算)** — 当前会话今天(自然日)产生的 token 用量 × DeepSeek 官方峰谷定价表计算,随当前会话模型(默认 `deepseek-v4-flash`,可在配置中改为 `deepseek-v4-pro`)与北京时间高峰/空闲时段自动切换。
38
38
  - **今日·本工作区成本(估算)** — 遍历当前工作区(由当前会话锚定)下的所有会话,累加今天的 token 用量 × 单价。
@@ -141,7 +141,7 @@ DSH 的插件配置统一放在这个文件里:
141
141
 
142
142
  ## 定价说明
143
143
 
144
- 内置 DeepSeek 官方 2026-09-10 峰谷定价(元 / 百万 tokens),高峰时段为北京时间**周一至周五** 09:00–12:00、14:00–18:00(周末全天为空闲时段),价格为空闲时段两倍:
144
+ 内置 DeepSeek 官方 2026-09-10 峰谷定价(元 / 百万 tokens),高峰时段为北京时间**周一至周五(不含中国法定节假日)** 09:00–12:00、14:00–18:00;**其余时段全部为空闲时段**,包括周末(含调休上班的周六/周日)与中国法定节假日全天,价格为空闲时段两倍。
145
145
 
146
146
  | 模型 | 时段 | 缓存命中(输入) | 缓存未命中(输入) | 输出 |
147
147
  | --- | --- | --- | --- | --- |
@@ -164,19 +164,20 @@ DSH 的插件配置统一放在这个文件里:
164
164
 
165
165
  **依赖与兼容**
166
166
  - 零运行时依赖:不 import 任何 `@deepseek-ai/*` 包,宿主端无第三方依赖
167
- - `peerDependencies["@deepseek-ai/dsh"]`: `>=0.1.2-rc.1 <0.2.0`(DSH 兼容范围)
167
+ - `peerDependencies["@deepseek-ai/dsh"]`: `>=0.1.2-rc.1 <0.3.0`(DSH 兼容范围;0.1.5 / 0.1.7 / 0.2.0 实测通过)
168
168
  - `engines.node`: `^22.19.0 || >=24.0.0`
169
169
  - `peerDependencies["react"]`: `^18.2.0`(仅浏览器端渲染)
170
170
 
171
171
  **运行时权限**
172
172
  - `files`:只读 `~/.dsh/sessions/` 下的会话 JSONL(成本统计);不写入、不修改任何会话文件
173
- - `network`:仅请求 DeepSeek 官方端点——`api.deepseek.com`(`GET /user/balance` 余额)与 `api-docs.deepseek.com` 定价页(每 12h 抓取);不走任何第三方代理
173
+ - `network`:仅请求 DeepSeek 官方端点——`api.deepseek.com`(`GET /user/balance`,仅在未登录/旧版宿主时使用;已登录时余额走宿主的 `deepseekAccount` 服务,不产生本插件的网络请求)与 `api-docs.deepseek.com` 定价页(每 12h 抓取);不走任何第三方代理
174
174
  - `commands`:**不执行任何命令**。会话解压改用 Node 内置 zstd(`node:zlib`,需 Node ≥22.15),不再依赖 `zstd` 可执行文件与 `PATH`
175
- - `credentials`:读取 `DEEPSEEK_API_KEY`(经宿主凭据服务解析),仅宿主进程使用、loopback-only 路由守卫;浏览器不接触密钥
175
+ - `credentials`:仅在回退路径上读取 `DEEPSEEK_API_KEY`(经宿主凭据服务解析),仅宿主进程使用、loopback-only 路由守卫;浏览器不接触密钥。已登录账号时余额不读任何密钥
176
176
  - 所有 host 路由均绑定 load 回环地址,外部不可达
177
177
 
178
178
  **外部服务**
179
- - DeepSeek 官方余额接口 `GET /user/balance`(点击/60s 刷新时调用)
179
+ - 宿主账号服务 `deepseekAccount`(DSH 0.2.0+ 且已登录账号时;余额与官方账号页同源,金额按官方规则截断到分)
180
+ - DeepSeek 官方余额接口 `GET /user/balance`(回退路径:未登录或旧版宿主;点击/60s 刷新时调用)
180
181
  - DeepSeek 官方定价页(启动时 + 每 12h 抓取,用于峰谷单价)
181
182
 
182
183
  **失败边界**
@@ -188,6 +189,31 @@ DSH 的插件配置统一放在这个文件里:
188
189
 
189
190
  ## 版本历史
190
191
 
192
+ ### v0.6.3 — 中国法定节假日全天按空闲时段计价
193
+ - 🐛 **修复**:峰/谷判断只排除了周末,**没有排除中国法定节假日**。官方规则是「北京时间**周一至周五(不含中国法定节假日)** 09:00–12:00、14:00–18:00 为高峰时段;其余时段,包括周末及中国法定节假日全天均为空闲时段」——所以落在工作日的法定假日,此前会被**按 2× 计价**。以内置 2026 年安排实测:春节 2/18、端午 6/19、中秋 9/25、国庆 10/1–10/7 全部被误判为峰时,成本翻倍
194
+ - 📅 **内置 2026 年法定节假日表**([国办发明电〔2025〕7号](https://www.gov.cn/zhengce/zhengceku/202511/content_7047091.htm),含元旦/春节/清明/劳动/端午/中秋/国庆共 **33 天**,其中 **19 天落在工作日**——正是此前会被 2× 计价的那部分),按北京时间日历日匹配;调休上班的周六/周日**本来就已按周末计**(官方明确按空闲时段),本次一并写入测试用例防回归
195
+ - 🧪 **差分验证**:新建 14 个夹具会话(每个自带假 `$HOME`,走真实路由 + 真实 zstd 帧 + 真实定价),断言节假日全天 ¥1.00、普通工作日峰窗 ¥2.00、节后首个工作日与午休/晚间边界均正确 —— **14/14 通过**;用修复前的代码跑同一组用例,6 个节假日用例全部失败(¥2.00)
196
+ - ⚠️ **维护提示**:节假日表需每年更新(国务院办公厅通常在上一年 11 月发布下一年安排),未覆盖的年份会退回"仅排除周末"的旧行为
197
+ - 📄 **文档**:定价说明与峰谷 tooltip(中英)补上"不含法定节假日 / 含调休上班日"的准确表述
198
+
199
+ ### v0.6.2 — 余额优先读官方账号服务(与设置页同源,不再需要 API Key)
200
+ - ✨ **余额优先取自宿主账号服务 `deepseekAccount`**:当 DSH ≥ 0.2.0 **且已登录 DeepSeek 账号**时,余额改读这个服务——它就是官方「设置 → 账号与余额」页的数据来源,所以卡片与设置页显示同一个数字。金额按官方规则处理:**正金额截断至分**(`55.6787307800000000` → `55.67`,与设置页一致),充值余额 = 官方 `value[]`、赠金余额 = `bonusWallets[]`,总额为两者之和,多币种各自成组
201
+ - 🚪 **降级路径完全不变**:未登录、宿主无该服务(DSH 0.1.x)、或账号读取失败(token 过期等)时,一律回退到原来的 `GET /user/balance` + `DEEPSEEK_API_KEY`,行为与 0.6.1 相同;账号读取失败会记一条 warn 说明原因
202
+ - 🔑 **登录态下余额不读任何密钥**:不再强制依赖 `DEEPSEEK_API_KEY`,纯账号模式(不配 API Key)也能显示余额
203
+ - 🔎 **可审计**:余额响应新增 `balanceSource` 字段(`deepseekAccount` 或回退路径),便于确认这一次的数字从哪来
204
+ - 🧪 **验证**:用假 cordis ctx 驱动**真实路由**跑 13 项断言全通过——账号映射 / 多币种与赠金求和 / 截断到分、未登录回退、服务缺失回退、账号抛错回退并告警、账号优先于 API Key、agent 计费工具同源,以及"凭据 key 的拼写不能被误当成服务名"这条防回归
205
+ - 📏 **实测同源**(2026-09-29,真机重启后):官方 `account/getBalance` 返回 `¥55.0685054200000000`,插件卡片显示 `¥55.06` —— 逐分一致
206
+
207
+ > **关于这个版本的背景,也说给我们自己听。**
208
+ > 桌面版 0.2.0 上线后,官方在「设置 → 账号与余额」里放出了登录后的余额页。**这说明官方已经承认"用户需要随时知道余额"这个痛点**——只是它要多点几下才能看到,日常用起来仍然不如侧边栏常驻、一眼可读。
209
+ >
210
+ > 所以这一版还是做了适配:余额改读官方账号服务,与设置页逐分一致,并且不再需要 API Key。但我们心里清楚,官方既然已经把这个需求接过去了,**这个插件被替代只是时间问题**。有点不舍,不过工具的价值本来就应该由平台自己长出来——**接下来进入告别期**:后续只跟随 DSH 版本做必要的兼容性维护,不再扩展新功能。谢谢一路用它看住余额的每一天。
211
+
212
+ ### v0.6.1 — 兼容范围放宽到 DSH 0.2.0(否则升级后插件会被静默摘掉)
213
+ - 🐛 **修复**:DSH 0.2.0 新增了插件兼容性闸门(`dsh-app-boot` 的 `evaluatePluginCompatibility`),它把 `peerDependencies` 里每个 `@deepseek-ai/dsh` / `@deepseek-ai/dsh-*` 范围与运行时版本比对,**且预发布版本参与范围匹配**(`semver.satisfies(..., { includePrerelease: true })`);不匹配且未被豁免的 bundle 在启动时被**静默跳过**(`loadProfileDirectory` 收进 `skippedBundles`,不报错、也不改 manifest)。旧声明 `>=0.1.2-rc.1 <0.2.0` 恰好覆盖 `0.2.0-rc.1`(所以在 0.2.0-rc.1 桌面上一切正常,实测确认),但**不覆盖 0.2.0 正式版**——桌面版一升级,侧边栏卡片就会直接消失,插件管理器还会要求 `dsh plugin allow-version` 手动豁免。现放宽为 `<0.3.0`(实测覆盖 0.2.0 / 0.2.1 / 0.3.0-rc.1)
214
+ - ✅ **0.2.0 兼容性实测**(在 0.2.0-rc.1 桌面版 + 真实浏览器上运行):宿主五条路由全部正常;会话日志仍为 `SESSION_FORMAT_VERSION = 4`,`totalTokens = inputTokens + outputTokens + cacheReadTokens` 在 394 条 usage 事件上全部成立(计价公式未变);`sidebar.footer.action` 槽位、`dsh.client.platform === "web"` 加载闸门、`IconRefreshOutlineRegular` 图标名均未变;插件用到的 19 个 `--dsw-*` token 全部存在(其中 15 个在 `body[data-ds-dark-theme]` 下重定义,弹层 portal 到 `document.body` 仍能继承主题);React 仍为 18.3.1;收起态 36×36 图标按钮与弹层定位照常
215
+ - 📦 **范围**:仅 `package.json` 的兼容范围声明与文档,运行代码无变化
216
+
191
217
  ### v0.6.0 — 适配 DSH 桌面版,界面与桌面端视觉对齐
192
218
  - 🖥️ **适配桌面版(DSH 0.1.7 / DeepSeek Harness.app)**
193
219
  - **会话解压不再依赖 `zstd` 命令行**,改用 Node 内置 zstd(`node:zlib`)。桌面版由 GUI 启动,进程 PATH 只有 `/usr/bin:/bin:/usr/sbin:/sbin`,找不到 Homebrew 的 zstd——此前在桌面端「今日花费 / 最近一次提问」等所有成本档位都会直接报错
package/lib/client.js CHANGED
@@ -564,8 +564,8 @@ window.__ModuleLoader__.load({
564
564
  "todayCostShort": "今日·全部",
565
565
  "peak.active": "峰时",
566
566
  "peak.offpeak": "谷时",
567
- "peak.tooltip.active": "当前为峰时(周一至周五 09:00-12:00、14:00-18:00,价格翻倍):输入 ¥{input}/M · 输出 ¥{output}/M",
568
- "peak.tooltip.offpeak": "当前为谷时(其余时段含周末全天,半价):输入 ¥{input}/M · 输出 ¥{output}/M",
567
+ "peak.tooltip.active": "当前为峰时(周一至周五 09:00-12:00、14:00-18:00,不含法定节假日,价格翻倍):输入 ¥{input}/M · 输出 ¥{output}/M",
568
+ "peak.tooltip.offpeak": "当前为谷时(其余时段:周末含调休上班日、法定节假日全天,半价):输入 ¥{input}/M · 输出 ¥{output}/M",
569
569
  "sessionNote": "基于最近活跃会话:",
570
570
  "hint": "价格为估算值,实际以官方账单为准。",
571
571
  "topUp": "去充值",
@@ -595,8 +595,8 @@ window.__ModuleLoader__.load({
595
595
  "todayCostShort": "Today · all",
596
596
  "peak.active": "Peak",
597
597
  "peak.offpeak": "Off-peak",
598
- "peak.tooltip.active": "Peak hours now (Mon-Fri 09:00-12:00, 14:00-18:00, 2× price): input ¥{input}/M · output ¥{output}/M",
599
- "peak.tooltip.offpeak": "Off-peak hours now (all other times incl. weekends, half price): input ¥{input}/M · output ¥{output}/M",
598
+ "peak.tooltip.active": "Peak hours now (Mon-Fri 09:00-12:00, 14:00-18:00, statutory holidays excluded, 2× price): input ¥{input}/M · output ¥{output}/M",
599
+ "peak.tooltip.offpeak": "Off-peak hours now (everything else: weekends incl. make-up workdays, statutory holidays all day, half price): input ¥{input}/M · output ¥{output}/M",
600
600
  "sessionNote": "Based on most recent session:",
601
601
  "hint": "Prices are estimates; actual billing from the provider is authoritative.",
602
602
  "topUp": "Top up",
package/lib/index.js CHANGED
@@ -180,10 +180,107 @@ async function resolveApiKey(ctx, config) {
180
180
  }
181
181
 
182
182
  /**
183
- * Query the official DeepSeek /user/balance endpoint.
183
+ * The host account service behind the official "Settings → Account & balance"
184
+ * page. `@deepseek-ai/dsh-deepseek-account-platform` registers it as
185
+ * `deepseekAccount` (its `super(ctx, "deepseekAccount")`, and the account
186
+ * controller's `inject = ["deepseekAccount", ...]`), shipped from DSH 0.2.0 on;
187
+ * on older hosts and profiles without an account backend ctx.get() returns
188
+ * undefined. The package's credential key happens to be spelled differently
189
+ * (`deepseek-account-platform`) and is deliberately not used here.
190
+ */
191
+ const ACCOUNT_SERVICE = "deepseekAccount";
192
+
193
+ /**
194
+ * A platform wallet amount as integer cents, truncated below a cent — the same
195
+ * rule the official page uses for positive amounts ("正金额截断至分").
196
+ * @returns BigInt cents, or null when the value is not a decimal string.
197
+ */
198
+ function centsOf(value) {
199
+ const match = /^(-?)(\d+)(?:\.(\d+))?$/.exec(String(value ?? "").trim());
200
+ if (match === null) return null;
201
+ const cents = BigInt(match[2]) * 100n + BigInt((match[3] ?? "").slice(0, 2).padEnd(2, "0"));
202
+ return match[1] === "-" ? -cents : cents;
203
+ }
204
+
205
+ /** Render integer cents as a two-decimal string. */
206
+ function formatCents(cents) {
207
+ const negative = cents < 0n;
208
+ const absolute = negative ? -cents : cents;
209
+ const text = `${absolute / 100n}.${String(absolute % 100n).padStart(2, "0")}`;
210
+ return negative ? `-${text}` : text;
211
+ }
212
+
213
+ /**
214
+ * Reshape an account balance view into the /user/balance payload the client
215
+ * already reads (purchased + granted wallets per currency).
216
+ */
217
+ function accountBalancePayload(view) {
218
+ const wallets = Array.isArray(view.value) ? view.value : [];
219
+ const bonuses = Array.isArray(view.bonusWallets) ? view.bonusWallets : [];
220
+ const currencies = [];
221
+ for (const wallet of [...wallets, ...bonuses]) {
222
+ if (typeof wallet?.currency === "string" && !currencies.includes(wallet.currency)) currencies.push(wallet.currency);
223
+ }
224
+ const sumCents = (list, currency) => list.reduce((total, wallet) => {
225
+ if (wallet?.currency !== currency) return total;
226
+ const cents = centsOf(wallet.balance);
227
+ return cents === null ? total : total + cents;
228
+ }, 0n);
229
+ const balance_infos = currencies.map((currency) => {
230
+ const toppedUp = sumCents(wallets, currency);
231
+ const granted = sumCents(bonuses, currency);
232
+ return {
233
+ currency,
234
+ total_balance: formatCents(toppedUp + granted),
235
+ granted_balance: formatCents(granted),
236
+ topped_up_balance: formatCents(toppedUp)
237
+ };
238
+ });
239
+ return {
240
+ is_available: balance_infos.some((info) => Number(info.total_balance) > 0),
241
+ balance_infos,
242
+ balanceSource: ACCOUNT_SERVICE
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Read the balance from the host account service — the official page's own
248
+ * source, so both surfaces show the same number, and no API key is involved.
249
+ * @returns the /user/balance-shaped payload, or undefined when the service is
250
+ * absent, the user is not signed in, or the read fails (caller falls back).
251
+ */
252
+ async function fetchAccountBalance(ctx) {
253
+ let account;
254
+ try {
255
+ account = ctx.get(ACCOUNT_SERVICE);
256
+ } catch {
257
+ return void 0;
258
+ }
259
+ if (account === void 0 || typeof account.getBalance !== "function") return void 0;
260
+ try {
261
+ const view = await account.getBalance({
262
+ version: "dsh-balance-widget",
263
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
264
+ timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60
265
+ });
266
+ if (view?.status !== "ready") return void 0;
267
+ const payload = accountBalancePayload(view);
268
+ return payload.balance_infos.length === 0 ? void 0 : payload;
269
+ } catch (error) {
270
+ ctx.logger?.warn?.("[dsh-balance-widget] account balance read failed: %s", error instanceof Error ? error.message : String(error));
271
+ return void 0;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Query the account balance: the host's account service when the user signed in
277
+ * with a DeepSeek account, else the official /user/balance endpoint with the API
278
+ * key.
184
279
  * @returns { ok: true, ...payload } | { ok: false, error }
185
280
  */
186
281
  async function fetchBalance(ctx, config) {
282
+ const account = await fetchAccountBalance(ctx);
283
+ if (account !== void 0) return { ok: true, ...account };
187
284
  let apiKey;
188
285
  try {
189
286
  apiKey = await resolveApiKey(ctx, config);
@@ -261,17 +358,56 @@ const PRICING_ALIASES = {
261
358
  "deepseek-official": "deepseek-v4-flash"
262
359
  };
263
360
 
361
+ /**
362
+ * Statutory holiday days (Beijing calendar dates) of the 2026 arrangement,
363
+ * per 国办发明电〔2025〕7号. DeepSeek bills Beijing time 周一至周五(**不含中国
364
+ * 法定节假日**)09:00-12:00 / 14:00-18:00 as peak; everything else — weekends
365
+ * (including 调休 workdays) and statutory holidays all day — is off-peak, so a
366
+ * weekday holiday must never be priced as peak. Weekend days are listed too:
367
+ * harmless here, and it keeps the table a verbatim copy of the notice.
368
+ * Refresh once a year, when the State Council publishes the next arrangement
369
+ * (usually the previous November).
370
+ */
371
+ const CN_HOLIDAYS = new Set([
372
+ // 元旦:1月1日(周四)至3日(周六)
373
+ "2026-01-01", "2026-01-02", "2026-01-03",
374
+ // 春节:2月15日(周日)至23日(周一)
375
+ "2026-02-15", "2026-02-16", "2026-02-17", "2026-02-18", "2026-02-19",
376
+ "2026-02-20", "2026-02-21", "2026-02-22", "2026-02-23",
377
+ // 清明节:4月4日(周六)至6日(周一)
378
+ "2026-04-04", "2026-04-05", "2026-04-06",
379
+ // 劳动节:5月1日(周五)至5日(周二)
380
+ "2026-05-01", "2026-05-02", "2026-05-03", "2026-05-04", "2026-05-05",
381
+ // 端午节:6月19日(周五)至21日(周日)
382
+ "2026-06-19", "2026-06-20", "2026-06-21",
383
+ // 中秋节:9月25日(周五)至27日(周日)
384
+ "2026-09-25", "2026-09-26", "2026-09-27",
385
+ // 国庆节:10月1日(周四)至7日(周三)
386
+ "2026-10-01", "2026-10-02", "2026-10-03", "2026-10-04", "2026-10-05",
387
+ "2026-10-06", "2026-10-07"
388
+ ]);
389
+
390
+ /** The Beijing (UTC+8) calendar day of an instant, as "YYYY-MM-DD". */
391
+ function beijingDay(date) {
392
+ const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000);
393
+ const month = String(beijing.getUTCMonth() + 1).padStart(2, "0");
394
+ const day = String(beijing.getUTCDate()).padStart(2, "0");
395
+ return `${beijing.getUTCFullYear()}-${month}-${day}`;
396
+ }
397
+
264
398
  /**
265
399
  * Whether `date` falls inside a Beijing-time (UTC+8) peak window.
266
400
  * DeepSeek's peak pricing applies to Beijing time 09:00-12:00 and 14:00-18:00
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.
401
+ * on weekdays only, and explicitly excludes China's statutory holidays; the
402
+ * whole weekend counts as off-peak even when a 调休 turns it into a workday.
403
+ * The window is computed in UTC+8 explicitly instead of the machine's local
404
+ * time, which also supplies the Beijing weekday and calendar day.
270
405
  */
271
406
  function isPeak(date) {
272
407
  const beijing = new Date(date.getTime() + 8 * 60 * 60 * 1000);
273
408
  const day = beijing.getUTCDay(); // 0 = Sunday, 6 = Saturday
274
409
  if (day === 0 || day === 6) return false;
410
+ if (CN_HOLIDAYS.has(beijingDay(date))) return false;
275
411
  const hour = beijing.getUTCHours();
276
412
  return (hour >= 9 && hour < 12) || (hour >= 14 && hour < 18);
277
413
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-balance-widget",
3
3
  "description": "Balance & cost widget for the DeepSeek Harness GUI (web and desktop app): 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 tag, term explanations, and a one-click top-up link. Zero external dependencies — works on Node 24.",
4
- "version": "0.6.0",
4
+ "version": "0.6.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "files": [
@@ -31,7 +31,7 @@
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": "^18.2.0",
34
- "@deepseek-ai/dsh": ">=0.1.2-rc.1 <0.2.0"
34
+ "@deepseek-ai/dsh": ">=0.1.2-rc.1 <0.3.0"
35
35
  },
36
36
  "keywords": [
37
37
  "deepseek",