dsh-cost-meter 1.7.21 → 1.7.23

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,9 +6,9 @@
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.7.21-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.7.23-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
 
11
- **v1.7.21** fixes session costs lost after restart. Settings → Cost → Display adds optional subagent cost aggregation and Codex quota display (both off by default). Gateway cards support account switching. See [native DeepSeek search accounting](docs/native-search-billing.md) and [GPT-6 Astra pricing](docs/gpt-astra-pricing.md).
11
+ **v1.7.23** right-aligns custom balances in the sidebar. Long names use an ellipsis, with the full name available on hover. See the [recovery guide](docs/session-history-recovery.md#english) for sessions affected by the earlier native-search log issue.
12
12
 
13
13
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
14
14
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
@@ -261,22 +261,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
261
261
  dsh plugin --profile web add dsh-cost-meter
262
262
  ```
263
263
 
264
- **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.7.21`** — review the script before running):
264
+ **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.7.23`** — review the script before running):
265
265
 
266
266
  ```powershell
267
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.21/install.ps1 | iex
267
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.23/install.ps1 | iex
268
268
  ```
269
269
 
270
270
  **Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
271
271
 
272
272
  ```sh
273
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.21
273
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.23
274
274
  ```
275
275
 
276
276
  Without git, use the GitHub tag archive:
277
277
 
278
278
  ```sh
279
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.21.tar.gz
279
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.23.tar.gz
280
280
  ```
281
281
 
282
282
  After installing, **restart** `dsh web` (plugin rows, the Typert manifest and the client bundle are all scanned at startup):
package/README.md CHANGED
@@ -6,9 +6,9 @@
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.7.21-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
9
+ [![version](https://img.shields.io/badge/version-1.7.23-4176E6)](https://github.com/Han-1413141/dsh-cost-meter)
10
10
 
11
- **v1.7.21**:修复重启后会话费用少算;「设置 → 费用 → 显示」可开启子代理费用合并及 Codex 额度(均默认关闭)。网关额度支持多账号切换。新增 [DeepSeek 原生搜索计量](docs/native-search-billing.md) 与 [GPT-6 Astra 完整计价](docs/gpt-astra-pricing.md)。
11
+ **v1.7.23**:侧栏自定义余额靠右对齐,长名称自动省略、悬停可看全名。此前受原生搜索日志问题影响的会话,仍可按 [恢复说明](docs/session-history-recovery.md) 备份修复。
12
12
 
13
13
  [![npm](https://img.shields.io/npm/v/dsh-cost-meter?label=npm)](https://www.npmjs.com/package/dsh-cost-meter)
14
14
  [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
@@ -263,22 +263,22 @@
263
263
  dsh plugin --profile web add dsh-cost-meter
264
264
  ```
265
265
 
266
- **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.7.21`**,建议先下载审阅再运行):
266
+ **PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.7.23`**,建议先下载审阅再运行):
267
267
 
268
268
  ```powershell
269
- irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.21/install.ps1 | iex
269
+ irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.23/install.ps1 | iex
270
270
  ```
271
271
 
272
272
  **或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
273
273
 
274
274
  ```sh
275
- dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.21
275
+ dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.23
276
276
  ```
277
277
 
278
278
  没有 git 时可用 GitHub tag 打包直链:
279
279
 
280
280
  ```sh
281
- dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.21.tar.gz
281
+ dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.23.tar.gz
282
282
  ```
283
283
 
284
284
  安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
package/lib/backfill.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * 历史账本按模型回填:按模型统计(byProviderModel)上线之前的账本只有
3
3
  * 每日/会话合计,没有 provider:model 拆分。本模块回放宿主会话日志
4
- * ($DSH_HOME/sessions/<项目>/<会话>/session.jsonl[.zstd]),按与 costUsage
4
+ * ($DSH_HOME/sessions/<项目>/<会话>/session[.vN].jsonl[.zstd]),按与 costUsage
5
5
  * 投影一致的逻辑逐次重建用量,并按事件时刻的档位(峰谷时代前按
6
6
  * legacyBase 历史价)计算费用,回填到账本中 byProviderModel 为空的
7
7
  * 日期与会话条目。
@@ -10,15 +10,15 @@
10
10
  * 避免与实时计费重复计数。会话日志是宿主的只读数据,本模块从不写入。
11
11
  */
12
12
 
13
- import { readdirSync, statSync } from 'node:fs'
14
13
  import { open } from 'node:fs/promises'
15
- import { join } from 'node:path'
16
14
  import * as zlib from 'node:zlib'
17
15
  import { costOf, providerPriceEntryFor, usdFromCost, isWrapperProviderId, wrapperUpstreamProvider } from './pricing.js'
18
16
  import { localDayKey, zeroDay, splitLedgerApiCost } from './store.js'
19
17
  import { billingClassOf, enabledPlanSetOf } from './plan-billing.js'
20
18
  import { USAGE_DEDUP_WINDOW_MS, usageFingerprint } from './usage-dedup.js'
21
- import { isNativeSearchUsageEvent } from './native-search-billing.js'
19
+ import { isNativeSearchUsageEvent } from './native-search-events.js'
20
+ import { listSessionLogFiles } from './session-log-files.js'
21
+ import { listNativeSearchHistory, readNativeSearchHistory, nativeSearchRecordsFor } from './native-search-history.js'
22
22
 
23
23
  const ZSTD_MAGIC = 4247762216
24
24
  /** 打包行(文本/推理/工具调用增量游程)不含 header 与 usage,回放时跳过。 */
@@ -245,45 +245,38 @@ export async function readSessionRecordsAsync(path, options = {}) {
245
245
  }
246
246
 
247
247
  /**
248
- * 枚举会话根目录下全部会话日志路径(<root>/<项目>/<会话>/session.jsonl[.zstd])。
248
+ * 枚举每个会话最高数字代际的日志路径(session[.vN].jsonl[.zstd])。
249
249
  * @param root - 会话根目录。
250
250
  * @param onlySessionIds - 可选:仅枚举这些会话目录(Set<会话 id>)。纯标题/时间戳
251
251
  * 补齐时按缺失会话定向,避免为补齐一两个标题全量读取全部会话日志(其中可能
252
252
  * 含解压后数百 MB 的大文件)。null = 全部。
253
253
  */
254
254
  export function listSessionLogs(root, onlySessionIds = null) {
255
- const paths = []
256
- let projects
257
- try {
258
- projects = readdirSync(root, { withFileTypes: true })
259
- } catch {
260
- return paths
261
- }
262
- for (const project of projects) {
263
- if (!project.isDirectory()) continue
264
- let sessions
265
- try {
266
- sessions = readdirSync(join(root, project.name), { withFileTypes: true })
267
- } catch {
268
- continue
269
- }
270
- for (const session of sessions) {
271
- if (!session.isDirectory()) continue
272
- if (onlySessionIds !== null && !onlySessionIds.has(session.name)) continue
273
- for (const name of ['session.jsonl.zstd', 'session.jsonl']) {
274
- const path = join(root, project.name, session.name, name)
275
- try {
276
- if (statSync(path).isFile()) {
277
- paths.push(path)
278
- break // 同一会话两种编码互斥,取先命中者
279
- }
280
- } catch {
281
- // 不存在:继续尝试另一后缀。
282
- }
283
- }
255
+ return listSessionLogFiles(root, onlySessionIds)
256
+ }
257
+
258
+ /** Merge the plugin journal before replay, so UUID dedup also covers repaired v1.7.21 logs. */
259
+ async function* sessionBillingRecords(ledger, sessionsRoot, onlySessionIds = null) {
260
+ const seen = new Set()
261
+ for (const path of listSessionLogs(sessionsRoot, onlySessionIds)) {
262
+ let records
263
+ try { records = await readSessionRecordsAsync(path) } catch { yield null; continue }
264
+ const id = records.find(record => record.type === 'session')?.id
265
+ if (typeof id === 'string') {
266
+ seen.add(id)
267
+ try { for (const record of await nativeSearchRecordsFor(ledger.path, id)) records.push(record) } catch { /* Keep the readable host prefix. */ }
284
268
  }
269
+ yield records
270
+ }
271
+ // A deleted/missing host log must not discard the plugin's surviving search history.
272
+ for (const path of listNativeSearchHistory(ledger.path, seen, onlySessionIds)) {
273
+ let history
274
+ try { history = await readNativeSearchHistory(path) } catch { continue }
275
+ const id = history.sessionId
276
+ if (!id || seen.has(id) || (onlySessionIds !== null && !onlySessionIds.has(id))) continue
277
+ seen.add(id)
278
+ yield [{ type: 'session', id, createdAt: 0 }, ...history.records]
285
279
  }
286
- return paths
287
280
  }
288
281
 
289
282
  /**
@@ -570,14 +563,15 @@ export async function backfillLegacyLedger(ledger, sessionsRoot) {
570
563
  }
571
564
  }
572
565
  }
573
- for (const path of listSessionLogs(sessionsRoot, onlySessionIds)) {
566
+ for await (const records of sessionBillingRecords(ledger, sessionsRoot, onlySessionIds)) {
574
567
  // 会话日志多时逐份解压会长时间占住事件循环:每 8 份让出一次,不卡宿主 UI;
575
568
  // 单文件内部亦为流式读取(v1.7.7,issue #88:解压期间周期性让出,大日志不再卡死宿主)。
576
569
  if ((scannedCount += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
577
570
  result.scanned += 1
578
571
  let replayed
579
572
  try {
580
- replayed = replaySessionRecords(await readSessionRecordsAsync(path), ledger.config, needDates.size > 0 ? needDates : new Set(['-']))
573
+ if (records === null) continue
574
+ replayed = replaySessionRecords(records, ledger.config, needDates.size > 0 ? needDates : new Set(['-']))
581
575
  } catch {
582
576
  continue // 单文件损坏(含解压预算超限)不阻断整体回填。
583
577
  }
@@ -811,12 +805,13 @@ export async function importLegacyHistory(ledger, sessionsRoot) {
811
805
  const result = { days: 0, sessions: 0, scanned: 0 }
812
806
  const bySession = new Map()
813
807
  let scannedCount = 0
814
- for (const path of listSessionLogs(sessionsRoot)) {
808
+ for await (const records of sessionBillingRecords(ledger, sessionsRoot)) {
815
809
  if ((scannedCount += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
816
810
  result.scanned += 1
817
811
  let replayed
818
812
  try {
819
- replayed = replaySessionRecords(await readSessionRecordsAsync(path), ledger.config, null)
813
+ if (records === null) continue
814
+ replayed = replaySessionRecords(records, ledger.config, null)
820
815
  } catch {
821
816
  continue // 单文件损坏(含解压预算超限)不阻断整体导入。
822
817
  }
@@ -906,11 +901,12 @@ export async function importLegacyHistory(ledger, sessionsRoot) {
906
901
  export async function recomputeLedgerPricingBasis(ledger, sessionsRoot, includeBucket) {
907
902
  const result = { scanned: 0, recostedSessions: 0, skippedSessions: 0, recostedDays: 0 }
908
903
  const bySession = new Map()
909
- for (const path of listSessionLogs(sessionsRoot)) {
904
+ for await (const records of sessionBillingRecords(ledger, sessionsRoot)) {
910
905
  if ((result.scanned += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
911
906
  let replayed
912
907
  try {
913
- replayed = replaySessionRecords(await readSessionRecordsAsync(path), ledger.config, null)
908
+ if (records === null) continue
909
+ replayed = replaySessionRecords(records, ledger.config, null)
914
910
  } catch {
915
911
  continue // 单文件损坏(含解压预算超限)不阻断整体重算。
916
912
  }