dsh-cost-meter 1.7.20 → 1.7.22
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 +8 -5
- package/README.md +8 -5
- package/docs/provider-pricing.json +18 -1
- package/lib/backfill.js +48 -45
- package/lib/client.js +5 -6
- package/lib/index.js +31 -5
- package/lib/native-search-billing.js +273 -0
- package/lib/native-search-events.js +11 -0
- package/lib/native-search-history.js +79 -0
- package/lib/pricing.js +26 -7
- package/lib/repair-sessions-cli.js +51 -0
- package/lib/session-log-files.js +30 -0
- package/lib/session-log-repair.js +175 -0
- package/lib/session-tree.js +117 -0
- package/lib/store.js +15 -2
- package/lib/typert.host.js +41 -1
- package/package.json +2 -1
package/README.en.md
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
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
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
|
+
|
|
11
|
+
**v1.7.22** fixes session reload failures after native search and missed history in versioned session logs. Search usage is persisted separately. See the [recovery guide](docs/session-history-recovery.md#english) for existing affected sessions and historical imports.
|
|
12
|
+
|
|
10
13
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
14
|
[](LICENSE)
|
|
12
15
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -258,22 +261,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
258
261
|
dsh plugin --profile web add dsh-cost-meter
|
|
259
262
|
```
|
|
260
263
|
|
|
261
|
-
**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.
|
|
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.22`** — review the script before running):
|
|
262
265
|
|
|
263
266
|
```powershell
|
|
264
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.
|
|
267
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.22/install.ps1 | iex
|
|
265
268
|
```
|
|
266
269
|
|
|
267
270
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
268
271
|
|
|
269
272
|
```sh
|
|
270
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.
|
|
273
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.22
|
|
271
274
|
```
|
|
272
275
|
|
|
273
276
|
Without git, use the GitHub tag archive:
|
|
274
277
|
|
|
275
278
|
```sh
|
|
276
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.
|
|
279
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.22.tar.gz
|
|
277
280
|
```
|
|
278
281
|
|
|
279
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,7 +6,10 @@
|
|
|
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
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
|
+
|
|
11
|
+
**v1.7.22**:修复原生搜索后会话重启无法加载,以及带版本号日志的历史导入遗漏。搜索明细改为独立存储;已受影响的会话可按 [恢复说明](docs/session-history-recovery.md) 备份修复。
|
|
12
|
+
|
|
10
13
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
14
|
[](LICENSE)
|
|
12
15
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -260,22 +263,22 @@
|
|
|
260
263
|
dsh plugin --profile web add dsh-cost-meter
|
|
261
264
|
```
|
|
262
265
|
|
|
263
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.7.
|
|
266
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.7.22`**,建议先下载审阅再运行):
|
|
264
267
|
|
|
265
268
|
```powershell
|
|
266
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.
|
|
269
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.7.22/install.ps1 | iex
|
|
267
270
|
```
|
|
268
271
|
|
|
269
272
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
270
273
|
|
|
271
274
|
```sh
|
|
272
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.
|
|
275
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.7.22
|
|
273
276
|
```
|
|
274
277
|
|
|
275
278
|
没有 git 时可用 GitHub tag 打包直链:
|
|
276
279
|
|
|
277
280
|
```sh
|
|
278
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.
|
|
281
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.7.22.tar.gz
|
|
279
282
|
```
|
|
280
283
|
|
|
281
284
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-09-
|
|
2
|
+
"generatedAt": "2026-09-11",
|
|
3
3
|
"note": "Auto-generated from lib/pricing.js DEFAULT_PROVIDER_PRICE_TABLE. USD per 1M tokens; unpriced entries have no verified official price.",
|
|
4
4
|
"providers": {
|
|
5
5
|
"openai": {
|
|
6
6
|
"models": {
|
|
7
|
+
"gpt-6-astra": {
|
|
8
|
+
"input": 10,
|
|
9
|
+
"cachedInput": 1,
|
|
10
|
+
"cacheWrite": 12.5,
|
|
11
|
+
"output": 50,
|
|
12
|
+
"longContext": {
|
|
13
|
+
"aboveInputTokens": 272000,
|
|
14
|
+
"cacheMiss": 20,
|
|
15
|
+
"cacheHit": 2,
|
|
16
|
+
"cacheWrite": 25,
|
|
17
|
+
"output": 75
|
|
18
|
+
},
|
|
19
|
+
"billingMode": "flat",
|
|
20
|
+
"sourceUrl": "https://developers.openai.com/api/docs/models/gpt-6-astra",
|
|
21
|
+
"checkedAt": "2026-09-11",
|
|
22
|
+
"notes": "标准 API 价,OpenCode Zen 同价;完整输入(含缓存读写)超过 272K 时整次请求按长上下文档计费;不含 Batch/Flex/Fast 服务档折扣或倍率"
|
|
23
|
+
},
|
|
7
24
|
"gpt-5.6-sol": {
|
|
8
25
|
"input": 2,
|
|
9
26
|
"cachedInput": 0.2,
|
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,14 +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'
|
|
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'
|
|
21
22
|
|
|
22
23
|
const ZSTD_MAGIC = 4247762216
|
|
23
24
|
/** 打包行(文本/推理/工具调用增量游程)不含 header 与 usage,回放时跳过。 */
|
|
@@ -244,45 +245,38 @@ export async function readSessionRecordsAsync(path, options = {}) {
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
/**
|
|
247
|
-
*
|
|
248
|
+
* 枚举每个会话最高数字代际的日志路径(session[.vN].jsonl[.zstd])。
|
|
248
249
|
* @param root - 会话根目录。
|
|
249
250
|
* @param onlySessionIds - 可选:仅枚举这些会话目录(Set<会话 id>)。纯标题/时间戳
|
|
250
251
|
* 补齐时按缺失会话定向,避免为补齐一两个标题全量读取全部会话日志(其中可能
|
|
251
252
|
* 含解压后数百 MB 的大文件)。null = 全部。
|
|
252
253
|
*/
|
|
253
254
|
export function listSessionLogs(root, onlySessionIds = null) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
continue
|
|
268
|
-
}
|
|
269
|
-
for (const session of sessions) {
|
|
270
|
-
if (!session.isDirectory()) continue
|
|
271
|
-
if (onlySessionIds !== null && !onlySessionIds.has(session.name)) continue
|
|
272
|
-
for (const name of ['session.jsonl.zstd', 'session.jsonl']) {
|
|
273
|
-
const path = join(root, project.name, session.name, name)
|
|
274
|
-
try {
|
|
275
|
-
if (statSync(path).isFile()) {
|
|
276
|
-
paths.push(path)
|
|
277
|
-
break // 同一会话两种编码互斥,取先命中者
|
|
278
|
-
}
|
|
279
|
-
} catch {
|
|
280
|
-
// 不存在:继续尝试另一后缀。
|
|
281
|
-
}
|
|
282
|
-
}
|
|
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. */ }
|
|
283
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]
|
|
284
279
|
}
|
|
285
|
-
return paths
|
|
286
280
|
}
|
|
287
281
|
|
|
288
282
|
/**
|
|
@@ -371,6 +365,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
371
365
|
}
|
|
372
366
|
// 本回放已计入的改挂样本(fp + 时刻):包装层重复分发(窗口内同指纹两次)只计一次。
|
|
373
367
|
let remappedAdmitted = []
|
|
368
|
+
const nativeSearchIds = new Set()
|
|
374
369
|
for (const event of records) {
|
|
375
370
|
if (event === null || typeof event !== 'object') continue
|
|
376
371
|
if (event.type === 'session' && typeof event.id === 'string') {
|
|
@@ -407,6 +402,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
407
402
|
// model,两代宿主形态 data.message.source.{provider,model} 与
|
|
408
403
|
// data.{provider,model} 都兼容),仅作用于本样本;去重键独立于循环步。
|
|
409
404
|
let isCompaction = false
|
|
405
|
+
const isNativeSearch = isNativeSearchUsageEvent(event)
|
|
410
406
|
// 判空与去重键缺省值与 index.js 投影完全同构(usage null 不入账;turn/step
|
|
411
407
|
// 缺省 0,保证实时投影与回放对同一事件产生同一 (turn,step) 去重键)。
|
|
412
408
|
if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'usage' && event.data.chunk.usage != null) {
|
|
@@ -417,13 +413,17 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
417
413
|
usage = event.data.usage
|
|
418
414
|
turn = event.data.turn ?? 0
|
|
419
415
|
step = event.data.step ?? 0
|
|
416
|
+
} else if (isNativeSearch && event.data?.usage != null && typeof event.data.requestId === 'string') {
|
|
417
|
+
if (nativeSearchIds.has(event.data.requestId)) continue
|
|
418
|
+
nativeSearchIds.add(event.data.requestId)
|
|
419
|
+
usage = event.data.usage
|
|
420
420
|
} else if (event.type === 'compaction/summary' && event.data?.usage != null) {
|
|
421
421
|
usage = event.data.usage
|
|
422
422
|
isCompaction = true
|
|
423
423
|
} else {
|
|
424
424
|
continue
|
|
425
425
|
}
|
|
426
|
-
const atMs = eventMs
|
|
426
|
+
const atMs = isNativeSearch ? Number(event.data.startedAtMs) : eventMs
|
|
427
427
|
if (!Number.isFinite(atMs) || atMs <= 0) continue
|
|
428
428
|
const date = localDayKey(atMs)
|
|
429
429
|
if (wantDates !== null && !wantDates.has(date)) continue
|
|
@@ -437,7 +437,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
437
437
|
// 摘要样本的事件自带路由(header 状态不受影响,后续循环步仍按其 header 计费)。
|
|
438
438
|
let sampleProvider = provider
|
|
439
439
|
let sampleModel = model
|
|
440
|
-
if (isCompaction) {
|
|
440
|
+
if (isCompaction || isNativeSearch) {
|
|
441
441
|
const source = event.data?.message?.source ?? {}
|
|
442
442
|
if (typeof source.provider === 'string' && source.provider.length > 0) sampleProvider = source.provider
|
|
443
443
|
else if (typeof event.data.provider === 'string' && event.data.provider.length > 0) sampleProvider = event.data.provider
|
|
@@ -454,7 +454,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
454
454
|
let effectiveProvider = sampleProvider
|
|
455
455
|
if (isWrapperProviderId(sampleProvider)) {
|
|
456
456
|
effectiveProvider = wrapperUpstreamProvider(sampleProvider) ?? sampleProvider
|
|
457
|
-
if (!isCompaction) {
|
|
457
|
+
if (!isCompaction && !isNativeSearch) {
|
|
458
458
|
const fp = usageFingerprint(sampleModel, buckets)
|
|
459
459
|
remappedAdmitted = remappedAdmitted.filter(entry => atMs - entry.at <= USAGE_DEDUP_WINDOW_MS)
|
|
460
460
|
const plainTimes = plainFingerprints.get(fp)
|
|
@@ -463,7 +463,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
463
463
|
remappedAdmitted.push({ fp, at: atMs })
|
|
464
464
|
}
|
|
465
465
|
}
|
|
466
|
-
const key = isCompaction
|
|
466
|
+
const key = isNativeSearch ? `search:${event.data.requestId}` : isCompaction
|
|
467
467
|
? `compaction:${Number.isFinite(Number(event.seq)) && Number(event.seq) >= 0 ? Number(event.seq) : 't' + String(event.time ?? '')}`
|
|
468
468
|
: `${turn}:${step}`
|
|
469
469
|
// 去重与旧版完全一致:单一 (turn,step) 状态机跨种子/own 段连续生效;
|
|
@@ -501,7 +501,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
501
501
|
if (prev !== null) shift(prev, -1, prev.seed ? seedDays : days)
|
|
502
502
|
const sample = { key, date, providerKey, buckets, cost, apiCost, seed: isSeed }
|
|
503
503
|
shift(sample, 1, isSeed ? seedDays : days)
|
|
504
|
-
last = sample
|
|
504
|
+
if (!isNativeSearch) last = sample
|
|
505
505
|
}
|
|
506
506
|
return { sessionId, title, createdAt, days, seedDays }
|
|
507
507
|
}
|
|
@@ -563,14 +563,15 @@ export async function backfillLegacyLedger(ledger, sessionsRoot) {
|
|
|
563
563
|
}
|
|
564
564
|
}
|
|
565
565
|
}
|
|
566
|
-
for (const
|
|
566
|
+
for await (const records of sessionBillingRecords(ledger, sessionsRoot, onlySessionIds)) {
|
|
567
567
|
// 会话日志多时逐份解压会长时间占住事件循环:每 8 份让出一次,不卡宿主 UI;
|
|
568
568
|
// 单文件内部亦为流式读取(v1.7.7,issue #88:解压期间周期性让出,大日志不再卡死宿主)。
|
|
569
569
|
if ((scannedCount += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
|
|
570
570
|
result.scanned += 1
|
|
571
571
|
let replayed
|
|
572
572
|
try {
|
|
573
|
-
|
|
573
|
+
if (records === null) continue
|
|
574
|
+
replayed = replaySessionRecords(records, ledger.config, needDates.size > 0 ? needDates : new Set(['-']))
|
|
574
575
|
} catch {
|
|
575
576
|
continue // 单文件损坏(含解压预算超限)不阻断整体回填。
|
|
576
577
|
}
|
|
@@ -804,12 +805,13 @@ export async function importLegacyHistory(ledger, sessionsRoot) {
|
|
|
804
805
|
const result = { days: 0, sessions: 0, scanned: 0 }
|
|
805
806
|
const bySession = new Map()
|
|
806
807
|
let scannedCount = 0
|
|
807
|
-
for (const
|
|
808
|
+
for await (const records of sessionBillingRecords(ledger, sessionsRoot)) {
|
|
808
809
|
if ((scannedCount += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
|
|
809
810
|
result.scanned += 1
|
|
810
811
|
let replayed
|
|
811
812
|
try {
|
|
812
|
-
|
|
813
|
+
if (records === null) continue
|
|
814
|
+
replayed = replaySessionRecords(records, ledger.config, null)
|
|
813
815
|
} catch {
|
|
814
816
|
continue // 单文件损坏(含解压预算超限)不阻断整体导入。
|
|
815
817
|
}
|
|
@@ -899,11 +901,12 @@ export async function importLegacyHistory(ledger, sessionsRoot) {
|
|
|
899
901
|
export async function recomputeLedgerPricingBasis(ledger, sessionsRoot, includeBucket) {
|
|
900
902
|
const result = { scanned: 0, recostedSessions: 0, skippedSessions: 0, recostedDays: 0 }
|
|
901
903
|
const bySession = new Map()
|
|
902
|
-
for (const
|
|
904
|
+
for await (const records of sessionBillingRecords(ledger, sessionsRoot)) {
|
|
903
905
|
if ((result.scanned += 1) % 8 === 0) await new Promise(resolve => setImmediate(resolve))
|
|
904
906
|
let replayed
|
|
905
907
|
try {
|
|
906
|
-
|
|
908
|
+
if (records === null) continue
|
|
909
|
+
replayed = replaySessionRecords(records, ledger.config, null)
|
|
907
910
|
} catch {
|
|
908
911
|
continue // 单文件损坏(含解压预算超限)不阻断整体重算。
|
|
909
912
|
}
|