dsh-cost-meter 1.6.6 → 1.6.7

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
@@ -6,7 +6,7 @@
6
6
 
7
7
  Per-conversation cost · daily totals · OpenCode Go subscription quota display · budget with usage percentage · official account balance · custom provider balance · balance progress bar · history · peak/off-peak pricing hours display (peak hours UTC 01:00–04:00, 06:00–10:00; from Aug 23, 2026 weekends are billed at off-peak prices all day, shown as “Weekend — all off-peak”) · pre-switch popup & system-notification alerts for peak/off-peak changes (position / lead time / alert type configurable) · one-click price sync from the official docs · Codex-style token usage heat grid · multi-vendor model pricing (built-in 90+ model price catalog with auto-matching) · mainstream Coding Plan quota queries & display (Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet) plan/API dual-track billing (subscription quota vs pay-as-you-go money separated, per-1% & full-window token/equivalent-cost estimates with daily/weekly/monthly curves) · · quota strip above the input box (budget / Go / coding-plan usage in one row, toggleable)
8
8
 
9
- [![version](https://img.shields.io/badge/version-1.6.3-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.6.7-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
11
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
12
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness)
@@ -231,22 +231,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
231
231
  dsh plugin --profile web add dsh-cost-meter
232
232
  ```
233
233
 
234
- **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.6.6`** — review the script before running):
234
+ **PowerShell one-click script** (copy the whole line, paste, press Enter; pnpm is provisioned automatically, git is auto-detected — no clone needed; the install chain is **pinned to the release tag `v1.6.7`** — review the script before running):
235
235
 
236
236
  ```powershell
237
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.6.6/install.ps1 | iex
237
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.6.7/install.ps1 | iex
238
238
  ```
239
239
 
240
240
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
241
241
 
242
242
  ```sh
243
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.6.6
243
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.6.7
244
244
  ```
245
245
 
246
246
  Without git, use the GitHub tag archive:
247
247
 
248
248
  ```sh
249
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.6.6.tar.gz
249
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.6.7.tar.gz
250
250
  ```
251
251
 
252
252
  After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
@@ -259,11 +259,11 @@ dsh web
259
259
 
260
260
  Symptom: `dsh plugin --profile web add` fails with `[ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION] N lockfile entries failed verification`.
261
261
 
262
- Cause: your environment (pnpm config or a policy bundled into the invoking installer) enforces the "minimum release age" supply-chain protection — any lockfile entry **published more recently than the threshold** is rejected. Plugin releases up to v1.6.3 declared floating ranges for their runtime dependencies, so a fresh install resolved them to whatever was newest at that moment (`^0.1.0-rc.6` was observed to float onto rc.8 published barely a week earlier), which such a policy refuses.
262
+ Cause: your environment (pnpm config or a policy bundled into the invoking installer) enforces the "minimum release age" supply-chain protection — any lockfile entry **published more recently than the threshold** is rejected. Plugin releases from before runtime dependencies were exact-pinned declared floating ranges, so a fresh install resolved them to whatever was newest at that moment (`^0.1.0-rc.6` was observed to float onto rc.8 published barely a week earlier), which such a policy refuses.
263
263
 
264
264
  Fix:
265
265
 
266
- 1. **Upgrade to v1.6.6+**: all three runtime dependencies (`@deepseek-ai/dsh-credentials`, `@deepseek-ai/dsh-home-paths`, `zod`) are now exact-pinned — a pinned version's publish date never changes, so it satisfies any age threshold and this plugin can no longer trigger the error;
266
+ 1. **Upgrade to v1.6.7+**: all three runtime dependencies (`@deepseek-ai/dsh-credentials`, `@deepseek-ai/dsh-home-paths`, `zod`) are now exact-pinned — a pinned version's publish date never changes, so it satisfies any age threshold and this plugin can no longer trigger the error;
267
267
  2. If the error is triggered by **another plugin's** dependencies instead, append an exclusion for the offending `name@version` printed in the error to the profile's `pnpm-workspace.yaml` (default `$DSH_HOME/profiles/web/pnpm-workspace.yaml`) and retry:
268
268
 
269
269
  ```yaml
@@ -298,7 +298,9 @@ dsh plugin --profile web add link:./dsh-cost-meter # symlink; edit lib/client.j
298
298
  - The ledger always stores amounts in **USD**; currency and FX rate only affect display (default 1 USD = 7.2 CNY, configurable);
299
299
  - The session badge is **billed exactly** at the moment each call is made (host-exported per-call cost), just like daily/monthly/cumulative totals and the budget;
300
300
  - Billing sources are the `usage` block of every model call (including sub-agents, compression, title generation and other auxiliary calls), matching the billable view;
301
- - **Peak/off-peak tiers follow the request-initiation moment** (v1.6.6): a streaming call can span the tier boundary hour; attributing by completion time would put a request started minutes earlier into the wrong tier;
301
+ - **Peak/off-peak tiers follow the request-initiation moment**: a streaming call can span the tier boundary hour; attributing by completion time would put a request started minutes earlier into the wrong tier;
302
+ - **Peak effective-time anchoring** (v1.6.7): the official pricing page no longer lists an effective time, and price sync no longer resets the peak effective moment to "now" — historical recomputes (session projection refolds / per-model backfill) always tier events against the 2026-08-16 16:00 UTC boundary, so peak-hour history is no longer re-costed at half price; ledgers polluted earlier are clamped back automatically on upgrade (idempotent migration);
303
+ - **Switching pricing currency re-bases the whole history** (v1.6.7): after the pricing-currency setting flips and re-syncs, historical entries are re-costed on the new price table in the background (days fully covered by session logs are replaced wholesale; sessions whose logs were cleaned keep their original basis), so history and the official bill share one basis, with a notice on completion; on upgrading to this version, existing ledgers that had switched currency and ended up with mixed bases are recomputed once automatically;
302
304
  - **Aligning with official bills**: ① minute-level lag vs live official numbers is expected — the ledger flushes on a 2s debounce, and streams still in flight at shutdown are billed server-side while their usage block is never received (the gap concentrates in cache-hit columns); ② reasoning tokens are reported separately by the API and are **not billed** — the token columns sum five buckets and will never line up with the official three columns, so **reconcile by amount**; ③ with pricing currency set to CNY, entries are recorded directly on the official CNY price list (recommended for CNY-billed accounts); with USD, displayed amounts go through a fixed FX rate and carry a small structural difference (the two lists' per-column ratios are not uniform);
303
305
  - Budget and over-budget warnings **only warn — they never block calls**.
304
306
  - **Plan/API dual-track billing** (issue #64): calls on subscription-style channels (MiniMax, manually-marked Codex, etc.) are recorded as catalog-price equivalents only; budget/today-cost/overview cards count the pay-as-you-go (API) channel exclusively.
@@ -370,7 +372,7 @@ dsh --profile web --port 3099 # real startup (watch lo
370
372
  ## Known limitations
371
373
 
372
374
  - Official-page parsing depends on the current page structure; after a redesign, “Sync prices from official docs” fails — edit the price table manually as a fallback;
373
- - The session badge is estimated at the current price tier; exact figures come from the ledger;
375
+ - The session badge's fallback estimate (used when ledger data is unavailable) prices all of a session's calls at the tier of the *current* moment, including plan-type sessions: a session spanning peak and off-peak hours gets its off-peak portion overestimated during peak hours; exact figures come from the ledger (which bills each call at its own initiation moment);
374
376
  - Price sync overwrites the same-named models listed on the official page; custom model entries are unaffected;
375
377
  - Balance lookup needs network access to api.deepseek.com and a valid API key; **the API key is only ever sent to the official domain** (if baseURL points at a non-official host, balance queries refuse to run — model requests are unaffected);
376
378
  - The OpenCode Go quota endpoint is the official opencode.ai endpoint (community-documented); if its response shape changes, the Settings page shows an error and the display can be turned off in Display settings;
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段;2026-08-23 起周末全天按谷价,显示「周末时段——全谷价」) · 峰/谷切换前弹窗与系统通知提醒(位置/提前量/提醒类型可配) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet / 火山方舟 九家,含 Volcano Ark AK/SK 签名) · Plan/API 双轨计费(订阅额度与按量金额分离统计,每 1% 额度与满窗的 token/等值金额估算及日/周/月曲线) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
8
8
 
9
- [![version](https://img.shields.io/badge/version-1.6.3-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.6.7-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
11
11
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
12
12
  [![dsh](https://img.shields.io/badge/DeepSeek%20Harness-dsh--plugin-4176E6)](https://github.com/deepseek-ai/deepseek-harness)
@@ -233,22 +233,22 @@
233
233
  dsh plugin --profile web add dsh-cost-meter
234
234
  ```
235
235
 
236
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.6.6`**,建议先下载审阅再运行):
236
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.6.7`**,建议先下载审阅再运行):
237
237
 
238
238
  ```powershell
239
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.6.6/install.ps1 | iex
239
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.6.7/install.ps1 | iex
240
240
  ```
241
241
 
242
242
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
243
243
 
244
244
  ```sh
245
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.6.6
245
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.6.7
246
246
  ```
247
247
 
248
248
  没有 git 时可用 GitHub tag 打包直链:
249
249
 
250
250
  ```sh
251
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.6.6.tar.gz
251
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.6.7.tar.gz
252
252
  ```
253
253
 
254
254
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
@@ -261,11 +261,11 @@ dsh web
261
261
 
262
262
  症状:`dsh plugin --profile web add` 阶段安装失败,pnpm 报 `[ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION] N lockfile entries failed verification`。
263
263
 
264
- 原因:你的环境(pnpm 配置或上层安装器自带策略)启用了「最小发布年龄」供应链保护——lockfile 中**发布时间距今小于阈值**的包一律拒绝。插件 v1.6.3 及更早版本的生产依赖是浮动区间,首次安装会解析到当时最新发布版(实测 `^0.1.0-rc.6` 漂到仅发布一周左右的 rc.8),在该策略下即被拒绝。
264
+ 原因:你的环境(pnpm 配置或上层安装器自带策略)启用了「最小发布年龄」供应链保护——lockfile 中**发布时间距今小于阈值**的包一律拒绝。插件引入依赖精确锁版之前的历史版本,生产依赖是浮动区间,首次安装会解析到当时最新发布版(实测 `^0.1.0-rc.6` 漂到仅发布一周左右的 rc.8),在该策略下即被拒绝。
265
265
 
266
266
  处理:
267
267
 
268
- 1. **升级到 v1.6.6+**:三个运行时依赖(`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-home-paths`、`zod`)已全部精确锁版——锁定版本的发布时间固定不变,对任意年龄阈值永远满足,本插件不再可能触发该错误;
268
+ 1. **升级到 v1.6.7+**:三个运行时依赖(`@deepseek-ai/dsh-credentials`、`@deepseek-ai/dsh-home-paths`、`zod`)已全部精确锁版——锁定版本的发布时间固定不变,对任意年龄阈值永远满足,本插件不再可能触发该错误;
269
269
  2. 若报错由**其他插件**的依赖触发,可在 profile 目录的 `pnpm-workspace.yaml`(默认 `$DSH_HOME/profiles/web/pnpm-workspace.yaml`)按报错列出的条目追加排除后重试:
270
270
 
271
271
  ```yaml
@@ -300,7 +300,9 @@ dsh plugin --profile web add link:./dsh-cost-meter # 符号链接,改 lib/clien
300
300
  - 账本金额恒以**美元**存储,币种/汇率仅影响显示(默认 1 USD = 7.2 CNY,可改);
301
301
  - 会话徽章与当日/月度/累计、预算一样,按每次调用的**实际时刻精确计费**(宿主导出的逐次成本);
302
302
  - 计费来源为每次模型调用的 usage 块(含子代理、压缩、标题等辅助调用),与账单口径一致;
303
- - **峰谷档位按请求发起时刻判定**(v1.6.6):流式调用可能跨峰谷边界整点,以完成时刻归档会把数分钟前发起的请求算进另一个峰位;
303
+ - **峰谷档位按请求发起时刻判定**:流式调用可能跨峰谷边界整点,以完成时刻归档会把数分钟前发起的请求算进另一个峰位;
304
+ - **峰谷生效时刻锚定**(v1.6.7):官方价格页已不再标注生效时间,价格同步不再把峰谷生效时刻重置为「同步时刻」——历史重算(会话投影回放 / 按模型回填)一律按 2026-08-16 16:00 UTC 分界判档,峰时历史事件不再被按谷价半价重算;此前被污染的存量账本升级时自动钳制修复(幂等迁移);
305
+ - **币种切换即全量换基准**(v1.6.7):「价格币种」切换并同步后,历史账目按新价目在后台整体重算(会话日志覆盖完整的日子整体替换,日志已清理的会话保持原口径),历史与官方账单同基准,完成后有提示;升级到本版时,此前切换过币种而新旧口径并存的存量账本自动重算一次;
304
306
  - **官方账单对齐口径**:① 与官方实时数字存在**分钟级时差**属预期——账本 2 秒防抖落盘,关停瞬间仍在途的流会被服务端照常扣费而 usage 块无人接收(缺口集中在缓存命中列);② reasoning tokens 由 API 单独上报且**不计费**,token 列为五桶合计,与官方「三列」天然对不齐,**对账请以金额为准**;③ 「价格币种」设为人民币时按官方 CNY 价目直计入账,与人民币账单币种一致(CNY 计价账号推荐);设为 USD 时显示金额经固定汇率折算,与 CNY 直计存在结构性细差(两套价目比值非均匀);
305
307
  - 预算与超支提示**仅提醒,不阻止调用**。
306
308
  - **Plan/API 双轨计费**(issue #64):订阅制渠道(MiniMax / Codex 手动标记等)的调用金额只记「等值」,预算/今日费用/概览卡片等金额展示仅统计按量计费(API)部分;
@@ -374,7 +376,7 @@ dsh --profile web --port 3099 # 真机启动(观察启
374
376
 
375
377
  - 历史按模型回填依赖宿主会话日志仍在盘:日志已被清理的早期调用无法逐模型重建,只能以「未分模型」残差行计入当日合计;
376
378
  - 官方页面解析依赖当前页面结构;改版后「从官方文档同步价格」会报错,可手动编辑价格表兜底;
377
- - 会话徽章按当前价格档位估算,精确费用以账本为准;
379
+ - 会话徽章在账本数据不可用时的回退估算按「当前时刻」的价格档位给该会话全部调用定价(含 Plan 类会话):跨峰谷时段的会话在峰时会高估其谷时部分,精确费用以账本为准(账本按每次调用的发起时刻逐笔计价);
378
380
  - 价格同步会覆盖官方页面列出的同名模型价格,自定义模型条目不受影响;
379
381
  - 余额查询需要可访问 api.deepseek.com 的网络与有效 API Key;**API Key 只会发往官方域名**(baseURL 指向非官方域名时余额查询拒绝请求,模型请求不受影响);
380
382
  - OpenCode Go 额度接口为 opencode.ai 官方端点(社区文档);接口结构变化时设置页会显示错误,可在显示设置中关闭该显示;
package/lib/backfill.js CHANGED
@@ -685,6 +685,93 @@ export async function importLegacyHistory(ledger, sessionsRoot) {
685
685
  return result
686
686
  }
687
687
 
688
+ /**
689
+ * 币种切换后的历史全量换基准(v1.6.7):回放全部会话日志,把账本中「能被回放
690
+ * 完整覆盖」的会话行按当前价格表逐事件重定价,并同步调整日期聚合。
691
+ *
692
+ * 背景(用户实测:切人民币后「误差更大」):切换价格币种只换价格表(prices.models
693
+ * + prices.currency),历史存储金额保持旧表口径;而投影 refold 用当前表重算 →
694
+ * 会话徽章(新口径)与今日卡片/历史明细(旧口径)对同一天给出两套数,与官方账单
695
+ * 对比时表现为偏差扩大。本函数把存储口径统一到当前表,消除双口径。
696
+ *
697
+ * 安全边界(纯重定价,绝不改变 token 计量):
698
+ * - 仅当回放桶与账本行键集合一致且逐键五桶+calls 相等才替换金额;不一致
699
+ * (日志缺失/新旧去重差异/数据异常)的会话保持旧口径,计入 skippedSessions;
700
+ * - 账本中不存在的会话不新增(避免与实时计费重复);回放不到的日期不动
701
+ * (日志已清理的历史保留原口径,作为残差);
702
+ * - day 顶层金额按被替换会话的差额调整;apiCost 由调用方末尾统一跑
703
+ * splitLedgerApiCost 重写(与 plan/api 分类口径对齐)。
704
+ * @param ledger - 已打开的账本(config 已是目标币种价格表)。
705
+ * @param sessionsRoot - 宿主会话根目录($DSH_HOME/sessions)。
706
+ * @returns {{ scanned, recostedSessions, skippedSessions, recostedDays }}。
707
+ */
708
+ export async function recomputeLedgerPricingBasis(ledger, sessionsRoot) {
709
+ const result = { scanned: 0, recostedSessions: 0, skippedSessions: 0, recostedDays: 0 }
710
+ const bySession = new Map()
711
+ for (const path of listSessionLogs(sessionsRoot)) {
712
+ if ((result.scanned += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
713
+ let replayed
714
+ try {
715
+ replayed = replaySessionRecords(readSessionRecords(path), ledger.config, null)
716
+ } catch {
717
+ continue // 单文件损坏不阻断整体重算。
718
+ }
719
+ if (replayed.sessionId.length === 0) continue
720
+ const existing = bySession.get(replayed.sessionId)
721
+ if (existing === undefined) {
722
+ bySession.set(replayed.sessionId, replayed.days)
723
+ continue
724
+ }
725
+ mergeDayMaps(existing.days, replayed.days)
726
+ }
727
+ const touchedDates = new Set()
728
+ const tokenFields = ['input', 'output', 'cacheRead', 'cacheWrite', 'reasoning', 'calls']
729
+ for (const [sessionId, days] of bySession) {
730
+ for (const [date, pm] of Object.entries(days)) {
731
+ const day = ledger.days?.[date]
732
+ const row = day === undefined ? undefined : (Array.isArray(day.sessions) ? day.sessions : []).find(s => s?.id === sessionId)
733
+ if (row === undefined) continue // 账本无此会话行:不新增(与实时计费互斥)。
734
+ const oldPm = row.byProviderModel !== null && typeof row.byProviderModel === 'object' ? row.byProviderModel : {}
735
+ const newKeys = Object.keys(pm)
736
+ const dayPm = day.byProviderModel !== null && typeof day.byProviderModel === 'object' ? day.byProviderModel : {}
737
+ // 前置校验(全部通过才动账):键集合一致、逐键 token/calls 相等、日期聚合
738
+ // 含全部键。任何一条不满足都保持旧口径,杜绝半改状态。
739
+ let replaceable = newKeys.length > 0
740
+ for (const key of newKeys) {
741
+ const oldBucket = oldPm[key]
742
+ const dayBucket = dayPm[key]
743
+ const newBucket = pm[key]
744
+ if (oldBucket === undefined || dayBucket === undefined) { replaceable = false; break }
745
+ for (const field of tokenFields) {
746
+ if ((Number(oldBucket[field]) || 0) !== (Number(newBucket[field]) || 0)) { replaceable = false; break }
747
+ }
748
+ if (!replaceable) break
749
+ }
750
+ if (!replaceable) {
751
+ result.skippedSessions += 1
752
+ continue
753
+ }
754
+ const totals = sumDayMap(pm)
755
+ const oldCost = Number(row.cost) || 0
756
+ const oldApi = Number(row.apiCost) || 0
757
+ row.byProviderModel = cloneDayMap(pm)
758
+ row.cost = totals.cost
759
+ row.apiCost = totals.apiCost
760
+ day.cost = (Number(day.cost) || 0) - oldCost + totals.cost
761
+ day.apiCost = (Number(day.apiCost) || 0) - oldApi + totals.apiCost
762
+ for (const key of newKeys) {
763
+ const dayBucket = dayPm[key]
764
+ dayBucket.cost = (Number(dayBucket.cost) || 0) - (Number(oldPm[key].cost) || 0) + (Number(pm[key].cost) || 0)
765
+ dayBucket.apiCost = (Number(dayBucket.apiCost) || 0) - (Number(oldPm[key].apiCost) || 0) + (Number(pm[key].apiCost) || 0)
766
+ }
767
+ result.recostedSessions += 1
768
+ touchedDates.add(date)
769
+ }
770
+ }
771
+ result.recostedDays = touchedDates.size
772
+ return result
773
+ }
774
+
688
775
  /** 从目标桶(可变)中减去源桶,逐字段 clamp ≥ 0(清洗不产生负数)。 */
689
776
  function subtractBucketInto(target, source) {
690
777
  target.input = Math.max(0, (target.input ?? 0) - (source.input ?? 0))
package/lib/index.js CHANGED
@@ -19,9 +19,9 @@ import { join } from 'node:path'
19
19
  import { credentialRef } from '@deepseek-ai/dsh-credentials'
20
20
  import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
21
21
  import { Ledger, applyConfigPatch, localDayKey, pickBalanceInfo, reconcileBalanceDelta, zeroDay, splitLedgerApiCost, repairLedgerPricing, dedupeWrapperProviderDays } from './store.js'
22
- import { backfillLegacyLedger, importLegacyHistory, repairForkSeed, repairProviderDupes } from './backfill.js'
22
+ import { backfillLegacyLedger, importLegacyHistory, repairForkSeed, repairProviderDupes, recomputeLedgerPricingBasis } from './backfill.js'
23
23
  import { createLlmStreamBilling } from './billing-stream.js'
24
- import { OFFICIAL_PRICING_URL, OFFICIAL_PRICING_URL_ZH, normalizePrice, parsePricingHtml, costOf, providerPriceEntryFor, buildPriceCatalog, usdFromCost, isWrapperProviderId } from './pricing.js'
24
+ import { OFFICIAL_PRICING_URL, OFFICIAL_PRICING_URL_ZH, LEGACY_BASE_BOUNDARY, normalizePrice, parsePricingHtml, costOf, providerPriceEntryFor, buildPriceCatalog, usdFromCost, isWrapperProviderId } from './pricing.js'
25
25
  import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, emptyCustomBalance, queryCustomBalance, normalizeVolcengineKey } from './coding-plans.js'
26
26
  import { recordSamples, buildPlanStats, canonicalWindowKey, periodStartOf, aggregateUsageSince, enabledPlanSetOf, pruneHourBuckets, suggestPlanAutoClasses } from './plan-billing.js'
27
27
  import { fetchWithRetry } from './net.js'
@@ -59,6 +59,7 @@ const SERVER_MESSAGES = {
59
59
  customBalanceQueryFailed: '自定义 Provider 余额查询失败:{message}',
60
60
  pricesSynced: '已从官方文档同步 {ids} 的价格',
61
61
  pricesSyncedFallback: '所选币种官方页同步失败({error}),已回退另一语言官方页,同步 {ids} 的价格;可稍后重试切换回目标币种',
62
+ pricesRecomputed: '价格币种已切换,历史账目按新价目重算:{days} 天 / {sessions} 个会话(日志覆盖不全的会话保持原口径)',
62
63
  priceSyncFailed: '官方价格同步失败:{error}',
63
64
  codingPlanKeyMissing: '未找到 {provider} 的凭据。请在下方填写 API Key,或配置对应环境变量/CLI 登录态;没有订阅可关闭该家的「启用」开关。',
64
65
  codingPlanUnauthorized: '{provider} 凭据无效或没有生效的订阅(接口返回 {code})。没有订阅可关闭该家的「启用」开关。',
@@ -100,6 +101,7 @@ const SERVER_MESSAGES = {
100
101
  customBalanceQueryFailed: 'Custom provider balance query failed: {message}',
101
102
  pricesSynced: 'Synced prices for {ids} from the official docs',
102
103
  pricesSyncedFallback: 'Syncing the official page for the selected currency failed ({error}); fell back to the other language page and synced prices for {ids}. You can switch back and retry later.',
104
+ pricesRecomputed: 'Pricing currency switched; history has been re-costed on the new price table: {days} day(s) / {sessions} session(s) (sessions without full log coverage keep their original basis)',
103
105
  priceSyncFailed: 'Official price sync failed: {error}',
104
106
  codingPlanKeyMissing: 'No credentials found for {provider}. Enter the API key below, or configure the matching environment variable / CLI login; turn off the Enable switch if you have no subscription.',
105
107
  codingPlanUnauthorized: '{provider} credentials are invalid or no active subscription was detected (API returned {code}). Turn off the Enable switch if you have no subscription.',
@@ -1199,6 +1201,7 @@ function createService(ctx, ledger) {
1199
1201
  },
1200
1202
 
1201
1203
  async updateConfig(patch) {
1204
+ const currencyBefore = ledger.config.prices?.currency === 'CNY' ? 'CNY' : 'USD'
1202
1205
  const { config, errors } = applyConfigPatch(ledger.config, patch)
1203
1206
  if (errors.length > 0) {
1204
1207
  const locale = patch !== null && typeof patch === 'object' && patch.locale === 'en' ? 'en' : localeOf(ledger.config)
@@ -1212,6 +1215,16 @@ function createService(ctx, ledger) {
1212
1215
  && (patch.planBilling !== undefined || patch.codingPlans !== undefined || patch.goQuota !== undefined)) {
1213
1216
  splitLedgerApiCost(ledger)
1214
1217
  }
1218
+ // 价格币种翻转(v1.6.7):全量换基准,历史按新表重定价(与 fetchPrices
1219
+ // 同款触发,防直接经配置补丁改 prices.currency 的旁路)。
1220
+ const currencyAfter = config.prices?.currency === 'CNY' ? 'CNY' : 'USD'
1221
+ if (currencyBefore !== currencyAfter) {
1222
+ const stats = await recomputeLedgerPricingBasis(ledger, join(resolveDshHome(), 'sessions'))
1223
+ if (stats.recostedSessions > 0) {
1224
+ splitLedgerApiCost(ledger)
1225
+ console.log(`[dsh-cost-meter] 价格币种切换:已按${currencyAfter === 'CNY' ? '人民币' : '美元'}价目重算历史 ${stats.recostedDays} 天 / ${stats.recostedSessions} 个会话(跳过 ${stats.skippedSessions} 个回放覆盖不全的会话)`)
1226
+ }
1227
+ }
1215
1228
  ledger.scheduleWrite()
1216
1229
  return build(false)
1217
1230
  },
@@ -1326,22 +1339,43 @@ function createService(ctx, ledger) {
1326
1339
  priceSource: 'official',
1327
1340
  fetchedAt: new Date().toISOString(),
1328
1341
  }
1329
- if (typeof parsed.effectiveAt === 'string') patch.peakEffectiveAt = parsed.effectiveAt
1330
- else patch.peakEffectiveAt = new Date().toISOString() // 页面已无生效时间:两档方案即时生效
1342
+ // 页面带生效时间才更新;页面无值时**不碰** peakEffectiveAt——它是峰谷
1343
+ // 时代的分界锚点(tierFor 据此判定历史事件档位),旧实现回落 new Date()
1344
+ // 会把「同步时刻之前」的全部峰时事件在重算路径(refold/backfill)里压到
1345
+ // base 档(base 与谷档同值,峰时事件被半价)。用户实测两次被重置。
1346
+ if (typeof parsed.effectiveAt === 'string' && !Number.isNaN(Date.parse(parsed.effectiveAt))) {
1347
+ patch.peakEffectiveAt = parsed.effectiveAt
1348
+ }
1331
1349
  if (Array.isArray(parsed.peakWindows) && parsed.peakWindows.length > 0) {
1332
1350
  patch.peakWindows = parsed.peakWindows
1333
1351
  }
1352
+ const currencyBefore = ledger.config.prices?.currency === 'CNY' ? 'CNY' : 'USD'
1334
1353
  const { config, errors } = applyConfigPatch(ledger.config, patch)
1335
1354
  if (errors.length > 0) throw new Error(errors.join(';'))
1336
1355
  ledger.config = config
1337
1356
  ledger.scheduleWrite()
1357
+ // 币种切换 = 全量换基准(v1.6.7):历史按新表逐事件重定价,消除「今日
1358
+ // 卡片/历史明细(旧口径)与会话徽章 refold(新口径)」的双口径并存
1359
+ // (用户实测:切人民币后两套数与官方账单对比,偏差反而扩大)。
1360
+ let recomputeNote = ''
1361
+ const currencyAfter = config.prices?.currency === 'CNY' ? 'CNY' : 'USD'
1362
+ if (currencyBefore !== currencyAfter) {
1363
+ const stats = await recomputeLedgerPricingBasis(ledger, join(resolveDshHome(), 'sessions'))
1364
+ if (stats.recostedSessions > 0) {
1365
+ splitLedgerApiCost(ledger)
1366
+ ledger.scheduleWrite()
1367
+ console.log(`[dsh-cost-meter] 价格币种切换:已按${currencyAfter === 'CNY' ? '人民币' : '美元'}价目重算历史 ${stats.recostedDays} 天 / ${stats.recostedSessions} 个会话(跳过 ${stats.skippedSessions} 个回放覆盖不全的会话)`)
1368
+ }
1369
+ recomputeNote = tmsg(locale, 'pricesRecomputed', { days: stats.recostedDays, sessions: stats.recostedSessions })
1370
+ }
1338
1371
  const ids = Object.keys(parsed.models)
1339
1372
  const joined = ids.join(locale === 'zh' ? '、' : ', ')
1373
+ const baseMessage = fallbackError === null
1374
+ ? tmsg(locale, 'pricesSynced', { ids: joined })
1375
+ : tmsg(locale, 'pricesSyncedFallback', { error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError), ids: joined })
1340
1376
  return {
1341
1377
  ok: true,
1342
- message: fallbackError === null
1343
- ? tmsg(locale, 'pricesSynced', { ids: joined })
1344
- : tmsg(locale, 'pricesSyncedFallback', { error: fallbackError instanceof Error ? fallbackError.message : String(fallbackError), ids: joined }),
1378
+ message: recomputeNote.length > 0 ? `${baseMessage}。${recomputeNote}` : baseMessage,
1345
1379
  state: await build(false),
1346
1380
  }
1347
1381
  } catch (error) {
@@ -1567,6 +1601,35 @@ export async function runStartupImports(ledger, sessionsRoot) {
1567
1601
  console.log(`[dsh-cost-meter] 计费口径一致性重建完成:重算 ${touched} 条记录的 apiCost`)
1568
1602
  }
1569
1603
  }
1604
+ // 峰谷生效时刻锚点修复(v1.6.7):旧实现每次价格同步在官方页无生效时间时把
1605
+ // peakEffectiveAt 重置为「同步时刻」(parsePricingHtml 恒返回 null + else 兜底
1606
+ // new Date()),分界被推到最近一次同步——重算路径(refold/backfill)对同步前的
1607
+ // 峰时事件回落 base 档(base 与谷档同值,峰时事件被半价)。tierFor 的硬编码
1608
+ // 历史分界(LEGACY_BASE_BOUNDARY)先行生效,任何 ≤ 分界的取值对全部历史判档
1609
+ // 都正确;钳到分界即安全且幂等。
1610
+ if (!ledger.migrations.includes('peak-effective-at-clamp-v1')) {
1611
+ ledger.migrations.push('peak-effective-at-clamp-v1')
1612
+ const currentMs = Date.parse(ledger.config.peakEffectiveAt ?? '')
1613
+ const boundaryMs = Date.parse(LEGACY_BASE_BOUNDARY)
1614
+ if (Number.isFinite(currentMs) && Number.isFinite(boundaryMs) && currentMs > boundaryMs) {
1615
+ ledger.config.peakEffectiveAt = LEGACY_BASE_BOUNDARY
1616
+ ledger.scheduleWrite()
1617
+ console.log('[dsh-cost-meter] 峰谷生效时刻已修复为历史分界(此前被价格同步重置为同步时刻,峰时历史事件在重算中被半价)')
1618
+ }
1619
+ }
1620
+ // 币种切换全量换基准(v1.6.7):存量「已切人民币但历史仍美元口径」的混口径
1621
+ // 账本(切换当时不重算,卡片旧口径与会话徽章 refold 新口径两套数)一次性按
1622
+ // 当前表重定价。仅 CNY 账本执行;纯 USD 账本基准确 native 无需动。此后切
1623
+ // 币种由 updateConfig/fetchPrices 实时触发,不走本迁移。
1624
+ if (!ledger.migrations.includes('currency-basis-recompute-v1') && ledger.config.prices?.currency === 'CNY') {
1625
+ ledger.migrations.push('currency-basis-recompute-v1')
1626
+ const stats = await recomputeLedgerPricingBasis(ledger, sessionsRoot)
1627
+ if (stats.recostedSessions > 0) {
1628
+ splitLedgerApiCost(ledger)
1629
+ ledger.scheduleWrite()
1630
+ console.log(`[dsh-cost-meter] 已按人民币价目重算历史 ${stats.recostedDays} 天 / ${stats.recostedSessions} 个会话(跳过 ${stats.skippedSessions} 个回放覆盖不全的会话)`)
1631
+ }
1632
+ }
1570
1633
  if (!(Number(ledger.config?.legacyAutoImportedAt) > 0)) {
1571
1634
  const stats = await importLegacyHistory(ledger, sessionsRoot)
1572
1635
  if (stats.days > 0 || stats.sessions > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cost-meter",
3
- "version": "1.6.6",
3
+ "version": "1.6.7",
4
4
  "description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(9 家,含 SCNet Token Plan 本地 Credits 计量与火山方舟 Volcano Ark Coding Plan AK/SK 签名查询)、自定义 Provider 余额查询(可配任意 HTTP 端点)与余额进度条、峰谷计价时段显示与切换前弹窗/系统通知提醒(位置/提前量/类型可配),界面中英双语。Session cost tracking plugin for DeepSeek Harness: per-conversation cost, daily totals, history and official price sync, with multi-vendor model pricing (built-in 90+ model catalog with auto-matching), Coding Plan quota queries & display for 9 vendors (incl. local credits metering for the SCNet Token Plan and Volcano Ark Coding Plan via AK/SK signature), plan/API dual-track billing with per-1% & full-window Token Plan usage estimates, custom provider balance lookup (configurable HTTP endpoint) and balance progress bar, plus peak/off-peak pricing display with pre-switch popup & system-notification alerts (position / lead time / type configurable), in a bilingual (Chinese/English) UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",