dsh-cost-meter 1.5.32 → 1.5.34
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 +5 -5
- package/README.md +5 -5
- package/lib/backfill.js +109 -8
- package/lib/client.js +13 -1
- package/lib/index.js +101 -15
- package/package.json +1 -1
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) · 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) · 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
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -228,22 +228,22 @@ Real captures from an actual DSH sidebar of the period strip and collapsed verti
|
|
|
228
228
|
dsh plugin --profile web add dsh-cost-meter
|
|
229
229
|
```
|
|
230
230
|
|
|
231
|
-
**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.5.
|
|
231
|
+
**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.5.34`** — review the script before running):
|
|
232
232
|
|
|
233
233
|
```powershell
|
|
234
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
234
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.34/install.ps1 | iex
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
**Or a plain command line** (the machine must already have pnpm and git; also pinned to the tag):
|
|
238
238
|
|
|
239
239
|
```sh
|
|
240
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
240
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.34
|
|
241
241
|
```
|
|
242
242
|
|
|
243
243
|
Without git, use the GitHub tag archive:
|
|
244
244
|
|
|
245
245
|
```sh
|
|
246
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
246
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.34.tar.gz
|
|
247
247
|
```
|
|
248
248
|
|
|
249
249
|
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,7 @@
|
|
|
6
6
|
|
|
7
7
|
本会话费用 · 当日费用 · OpenCode Go 订阅额度显示 · 预算与已用百分比 · 官方账户余额 · 自定义 Provider 余额查询(可配任意 HTTP 端点) · 余额三段进度条 · 历史记录 · 峰谷计价时段显示(UTC 01:00–04:00、06:00–10:00 为峰时段) · 峰/谷切换前弹窗与系统通知提醒(位置/提前量/提醒类型可配) · 官方价格一键同步 · 类 Codex Token 用量热图 · 多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配) · 主流 Coding Plan 额度查询与显示(Anthropic / Z.ai / MiniMax / Kimi / OpenRouter / SiliconFlow / CommandCode / SCNet 八家) · 输入框上方额度横条(预算/Go/Coding Plan 用量一条横排显示,可开关)
|
|
8
8
|
|
|
9
|
-
[](https://github.com/Han-1413141/dsh-cost-meter)
|
|
10
10
|
[](https://www.npmjs.com/package/dsh-cost-meter)
|
|
11
11
|
[](LICENSE)
|
|
12
12
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
@@ -230,22 +230,22 @@
|
|
|
230
230
|
dsh plugin --profile web add dsh-cost-meter
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.
|
|
233
|
+
**PowerShell 一键脚本**(复制整行粘贴回车;自动补齐 pnpm、自动探测 git,无需克隆仓库;安装链**固定到发布 tag `v1.5.34`**,建议先下载审阅再运行):
|
|
234
234
|
|
|
235
235
|
```powershell
|
|
236
|
-
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.
|
|
236
|
+
irm https://raw.githubusercontent.com/Han-1413141/dsh-cost-meter/v1.5.34/install.ps1 | iex
|
|
237
237
|
```
|
|
238
238
|
|
|
239
239
|
**或直接命令行**(机器上需已有 pnpm 与 git;同样固定到 tag):
|
|
240
240
|
|
|
241
241
|
```sh
|
|
242
|
-
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.
|
|
242
|
+
dsh plugin --profile web add github:Han-1413141/dsh-cost-meter#v1.5.34
|
|
243
243
|
```
|
|
244
244
|
|
|
245
245
|
没有 git 时可用 GitHub tag 打包直链:
|
|
246
246
|
|
|
247
247
|
```sh
|
|
248
|
-
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.
|
|
248
|
+
dsh plugin --profile web add https://github.com/Han-1413141/dsh-cost-meter/archive/refs/tags/v1.5.34.tar.gz
|
|
249
249
|
```
|
|
250
250
|
|
|
251
251
|
安装后**重启** `dsh web`(插件行、Typert 清单与客户端 bundle 均在启动时扫描):
|
package/lib/backfill.js
CHANGED
|
@@ -138,10 +138,23 @@ export function listSessionLogs(root) {
|
|
|
138
138
|
* 回放单个会话的事件流,重建逐次用量。与 costUsage 投影同规则:
|
|
139
139
|
* request/header 切换当前 provider/model;usage 块按 (turn, step) 去重,
|
|
140
140
|
* 同键最终样本替换流式样本(先减后加);按事件时刻计价。
|
|
141
|
+
*
|
|
142
|
+
* fork 种子分段(issue #38):DSH 的 fork 把父会话事件流整段拷贝进子会话
|
|
143
|
+
* 日志(header 带 parentSession / seedLength),拷贝事件的时间戳早于
|
|
144
|
+
* header.createdAt。回放时把 time < createdAt 的种子事件单独聚合进
|
|
145
|
+
* seedDays(供一次性清洗扣账),不再计入 days——父会话已计过,重复计费
|
|
146
|
+
* 即虚高。非 fork 会话 createdAt 早于全部事件,判定不生效,无回归影响。
|
|
147
|
+
*
|
|
148
|
+
* 状态机与旧版逐字一致(header 一律切换 provider/model、单一 (turn,step)
|
|
149
|
+
* 去重跨种子/own 段连续生效),仅聚合目标按事件分段路由:旧版把种子段
|
|
150
|
+
* 写进账本的量与新算的 seedDays 逐字段相等,清洗扣账才不偏不漏;若 own
|
|
151
|
+
* 段样本复用了种子段最后的 (turn,step) 键,先减项路由回 seedDays,days
|
|
152
|
+
* 只含 own 部分——同样与旧版账本内容对齐。
|
|
153
|
+
*
|
|
141
154
|
* @param records - readSessionRecords 的输出。
|
|
142
155
|
* @param config - 账本配置(prices / peak* / priceMatch / priceOverrides)。
|
|
143
156
|
* @param wantDates - 只统计这些日期键(YYYY-MM-DD)内的调用;null = 全部。
|
|
144
|
-
* @returns { sessionId, title, createdAt, days
|
|
157
|
+
* @returns { sessionId, title, createdAt, days, seedDays }。
|
|
145
158
|
*/
|
|
146
159
|
export function replaySessionRecords(records, config, wantDates = null) {
|
|
147
160
|
const zeroBuckets = () => ({ input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, calls: 0, cost: 0 })
|
|
@@ -156,8 +169,9 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
156
169
|
let model = 'default'
|
|
157
170
|
let last = null
|
|
158
171
|
const days = {}
|
|
159
|
-
const
|
|
160
|
-
|
|
172
|
+
const seedDays = {}
|
|
173
|
+
const shift = (sample, sign, target) => {
|
|
174
|
+
const dayMap = target[sample.date] ?? (target[sample.date] = {})
|
|
161
175
|
const current = dayMap[sample.providerKey] ?? zeroBuckets()
|
|
162
176
|
dayMap[sample.providerKey] = {
|
|
163
177
|
input: current.input + sign * sample.buckets.input,
|
|
@@ -184,7 +198,13 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
184
198
|
continue
|
|
185
199
|
}
|
|
186
200
|
if (PACKED_ROW_TYPES.has(event.type)) continue
|
|
201
|
+
const eventMs = Number(event.time)
|
|
202
|
+
// fork 种子事件判定:时间戳早于会话创建时刻 = 从父会话拷来的历史。
|
|
203
|
+
const isSeed = createdAt > 0 && Number.isFinite(eventMs) && eventMs > 0 && eventMs < createdAt
|
|
187
204
|
if (event.type === 'request/header') {
|
|
205
|
+
// header 一律更新计费口径(与旧版状态机一致):种子段用量必须按
|
|
206
|
+
// 父会话当时的模型计价,seedDays 才能与旧版写入账本的污染量对齐,
|
|
207
|
+
// 清洗扣账逐字段精确;fork 后自己首轮也几乎总带着新 header,不受影响。
|
|
188
208
|
const nextModel = event.data?.header?.config?.model
|
|
189
209
|
const nextProvider = event.data?.header?.config?.provider
|
|
190
210
|
model = typeof nextModel === 'string' && nextModel.length > 0 ? nextModel : 'default'
|
|
@@ -205,7 +225,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
205
225
|
} else {
|
|
206
226
|
continue
|
|
207
227
|
}
|
|
208
|
-
const atMs =
|
|
228
|
+
const atMs = eventMs
|
|
209
229
|
if (!Number.isFinite(atMs) || atMs <= 0) continue
|
|
210
230
|
const date = localDayKey(atMs)
|
|
211
231
|
if (wantDates !== null && !wantDates.has(date)) continue
|
|
@@ -217,6 +237,7 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
217
237
|
reasoning: num(usage.reasoningTokens),
|
|
218
238
|
}
|
|
219
239
|
const key = `${turn}:${step}`
|
|
240
|
+
// 去重与旧版完全一致:单一 (turn,step) 状态机跨种子/own 段连续生效。
|
|
220
241
|
const prev = last !== null && last.key === key ? last : null
|
|
221
242
|
if (prev !== null && prev.providerKey === `${provider}:${model}`
|
|
222
243
|
&& prev.buckets.input === buckets.input && prev.buckets.output === buckets.output
|
|
@@ -236,12 +257,14 @@ export function replaySessionRecords(records, config, wantDates = null) {
|
|
|
236
257
|
}
|
|
237
258
|
const cost = resolved.priced ? costOf(buckets, resolved.entry, atMs, peak) : 0
|
|
238
259
|
const providerKey = `${provider}:${model}`
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
shift(
|
|
260
|
+
// 聚合目标按事件分段路由;被替换的旧样本从它当时所在的段扣回(与旧版
|
|
261
|
+
// 单流先减后加的净效果一致,仅种子段的量落到 seedDays 而非 days)。
|
|
262
|
+
if (prev !== null) shift(prev, -1, prev.seed ? seedDays : days)
|
|
263
|
+
const sample = { key, date, providerKey, buckets, cost, seed: isSeed }
|
|
264
|
+
shift(sample, 1, isSeed ? seedDays : days)
|
|
242
265
|
last = sample
|
|
243
266
|
}
|
|
244
|
-
return { sessionId, title, createdAt, days }
|
|
267
|
+
return { sessionId, title, createdAt, days, seedDays }
|
|
245
268
|
}
|
|
246
269
|
|
|
247
270
|
/**
|
|
@@ -581,3 +604,81 @@ export async function importLegacyHistory(ledger, sessionsRoot) {
|
|
|
581
604
|
}
|
|
582
605
|
return result
|
|
583
606
|
}
|
|
607
|
+
|
|
608
|
+
/** 从目标桶(可变)中减去源桶,逐字段 clamp ≥ 0(清洗不产生负数)。 */
|
|
609
|
+
function subtractBucketInto(target, source) {
|
|
610
|
+
target.input = Math.max(0, (target.input ?? 0) - (source.input ?? 0))
|
|
611
|
+
target.output = Math.max(0, (target.output ?? 0) - (source.output ?? 0))
|
|
612
|
+
target.cacheRead = Math.max(0, (target.cacheRead ?? 0) - (source.cacheRead ?? 0))
|
|
613
|
+
target.cacheWrite = Math.max(0, (target.cacheWrite ?? 0) - (source.cacheWrite ?? 0))
|
|
614
|
+
target.reasoning = Math.max(0, (target.reasoning ?? 0) - (source.reasoning ?? 0))
|
|
615
|
+
target.calls = Math.max(0, (target.calls ?? 0) - (source.calls ?? 0))
|
|
616
|
+
target.cost = Math.max(0, (target.cost ?? 0) - (source.cost ?? 0))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* fork 种子重复计费一次性清洗(issue #38):旧版把 fork 会话日志里从父会话
|
|
621
|
+
* 拷贝的种子事件(time < createdAt)也计入了账本(该会话条目与所在日期合
|
|
622
|
+
* 计均虚高)。本函数扫描全部会话日志,对 header 带 parentSession 的 fork
|
|
623
|
+
* 会话重算种子聚合(seedDays),从账本对应会话条目与日合计中扣除等量污染。
|
|
624
|
+
*
|
|
625
|
+
* 幂等由调用方用账本 migrations 标记(fork-seed-dedup-v1)保证只跑一次;
|
|
626
|
+
* 减法逐字段 clamp ≥ 0,即使账本未被污染(种子量为 0)也无副作用。
|
|
627
|
+
* @param ledger - 已打开的账本。
|
|
628
|
+
* @param sessionsRoot - 宿主会话根目录($DSH_HOME/sessions)。
|
|
629
|
+
* @returns { sessions, days, scanned } 受影响的 fork 会话数、扣除的日期数与扫描文件数。
|
|
630
|
+
*/
|
|
631
|
+
export function repairForkSeed(ledger, sessionsRoot) {
|
|
632
|
+
const result = { sessions: 0, days: 0, scanned: 0 }
|
|
633
|
+
for (const path of listSessionLogs(sessionsRoot)) {
|
|
634
|
+
result.scanned += 1
|
|
635
|
+
let records
|
|
636
|
+
try {
|
|
637
|
+
records = readSessionRecords(path)
|
|
638
|
+
} catch {
|
|
639
|
+
continue // 单文件损坏不阻断整体清洗。
|
|
640
|
+
}
|
|
641
|
+
// 只处理 fork 会话(header 带 parentSession);普通会话日志无种子段。
|
|
642
|
+
const header = records.find(r => r?.type === 'session')
|
|
643
|
+
if (header === undefined || header === null || typeof header.parentSession !== 'string' || header.parentSession.length === 0) continue
|
|
644
|
+
let replayed
|
|
645
|
+
try {
|
|
646
|
+
replayed = replaySessionRecords(records, ledger.config, null)
|
|
647
|
+
} catch {
|
|
648
|
+
continue
|
|
649
|
+
}
|
|
650
|
+
if (replayed.sessionId.length === 0) continue
|
|
651
|
+
let touched = false
|
|
652
|
+
for (const [date, pm] of Object.entries(replayed.seedDays)) {
|
|
653
|
+
const totals = sumDayMap(pm)
|
|
654
|
+
if (totals.calls <= 0) continue
|
|
655
|
+
const day = ledger.days[date]
|
|
656
|
+
if (day === null || typeof day !== 'object') continue
|
|
657
|
+
const session = Array.isArray(day.sessions) ? day.sessions.find(s => s?.id === replayed.sessionId) : null
|
|
658
|
+
if (session === undefined || session === null) continue
|
|
659
|
+
// 会话条目:顶层字段与 byProviderModel 拆分同步扣除种子污染。
|
|
660
|
+
subtractBucketInto(session, totals)
|
|
661
|
+
if (session.byProviderModel !== null && typeof session.byProviderModel === 'object') {
|
|
662
|
+
for (const [providerKey, bucket] of Object.entries(pm)) {
|
|
663
|
+
const current = session.byProviderModel[providerKey]
|
|
664
|
+
if (current !== null && typeof current === 'object') subtractBucketInto(current, bucket)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
// 日合计:顶层字段与 byProviderModel 同步扣除。
|
|
668
|
+
subtractBucketInto(day, totals)
|
|
669
|
+
if (day.byProviderModel !== null && typeof day.byProviderModel === 'object') {
|
|
670
|
+
for (const [providerKey, bucket] of Object.entries(pm)) {
|
|
671
|
+
const current = day.byProviderModel[providerKey]
|
|
672
|
+
if (current !== null && typeof current === 'object') subtractBucketInto(current, bucket)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
touched = true
|
|
676
|
+
result.days += 1
|
|
677
|
+
}
|
|
678
|
+
if (touched) {
|
|
679
|
+
result.sessions += 1
|
|
680
|
+
ledger.scheduleWrite()
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return result
|
|
684
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -2751,11 +2751,22 @@ window.__ModuleLoader__.load({
|
|
|
2751
2751
|
// 更新后的提醒(issue #37):告知侧边栏余额/额度图框支持点击立即刷新;
|
|
2752
2752
|
// 「知道了」写回 balance.clickHintSeen=true 后永久消失(挂常驻 sidebar.footer.action)。
|
|
2753
2753
|
// 仅在侧边栏确有可点击图框(官方余额 / 自定义余额 / Coding Plan)时展示,避免噪音。
|
|
2754
|
+
// 已读标记双通道:配置标记(跨设备) + localStorage 本地兜底。后者防御版本错位——
|
|
2755
|
+
// 服务端/网关若运行旧版 typert schema(decode 时 zod parse 会剥离 schema 未声明的键),
|
|
2756
|
+
// 新增的配置标记在到达客户端前就被剥掉,卡片会在每次刷新后重现;localStorage 不经 RPC 链路,不受影响。
|
|
2757
|
+
const BALANCE_CLICK_HINT_LS_KEY = 'dsh-cost-meter:balance-click-hint-seen'
|
|
2758
|
+
const readBalanceClickHintLS = () => {
|
|
2759
|
+
try { return window.localStorage.getItem(BALANCE_CLICK_HINT_LS_KEY) === '1' } catch (_) { return false } // 隐私模式等场景静默
|
|
2760
|
+
}
|
|
2761
|
+
const writeBalanceClickHintLS = () => {
|
|
2762
|
+
try { window.localStorage.setItem(BALANCE_CLICK_HINT_LS_KEY, '1') } catch (_) { /* 配置标记仍是主通道 */ }
|
|
2763
|
+
}
|
|
2754
2764
|
function BalanceClickGuide(props) {
|
|
2755
2765
|
// Hook 全部前置(promptSeen 类标记翻转时会提前 return null,顺序必须稳定)。
|
|
2756
2766
|
const costStore = props.useCost ? props.useCost(s => s) : undefined
|
|
2757
2767
|
const busyRef = useRef(false)
|
|
2758
2768
|
const [dismissed, setDismissed] = useState(false)
|
|
2769
|
+
const [lsSeen] = useState(readBalanceClickHintLS)
|
|
2759
2770
|
const state = costStore?.state
|
|
2760
2771
|
if (!state) return null
|
|
2761
2772
|
const config = state.config
|
|
@@ -2766,7 +2777,7 @@ window.__ModuleLoader__.load({
|
|
|
2766
2777
|
const entry = config.codingPlans?.[r.id]
|
|
2767
2778
|
return entry?.enabled === true && (entry.display === 'sidebar' || entry.display === 'both')
|
|
2768
2779
|
})
|
|
2769
|
-
if (dismissed || config.balance?.clickHintSeen === true || (!sidebarBalanceOn && !sidebarCustomOn && !sidebarPlansOn)) return null
|
|
2780
|
+
if (dismissed || lsSeen || config.balance?.clickHintSeen === true || (!sidebarBalanceOn && !sidebarCustomOn && !sidebarPlansOn)) return null
|
|
2770
2781
|
// 串行展示:横条引导(QuotaStripGuide)与本卡同为 fixed 顶部卡片,同屏会完全重叠,
|
|
2771
2782
|
// 点掉一张露出另一张,视觉上像「点了没反应」。等横条引导处理完(promptSeen)再出现。
|
|
2772
2783
|
if (config.quotaStrip?.promptSeen !== true) return null
|
|
@@ -2776,6 +2787,7 @@ window.__ModuleLoader__.load({
|
|
|
2776
2787
|
// 乐观消失:点击立即隐藏,落盘确认失败才恢复可见(避免宿主渲染时序让卡片看起来「点不掉」)。
|
|
2777
2788
|
setDismissed(true)
|
|
2778
2789
|
busyRef.current = true
|
|
2790
|
+
writeBalanceClickHintLS() // 本地兜底同步落点,不依赖 RPC 成败
|
|
2779
2791
|
props.api.updateConfig({ balance: { ...(config.balance ?? {}), clickHintSeen: true } })
|
|
2780
2792
|
.catch(() => { busyRef.current = false; setDismissed(false) })
|
|
2781
2793
|
}
|
package/lib/index.js
CHANGED
|
@@ -19,7 +19,7 @@ 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 } from './store.js'
|
|
22
|
-
import { backfillLegacyLedger, importLegacyHistory } from './backfill.js'
|
|
22
|
+
import { backfillLegacyLedger, importLegacyHistory, repairForkSeed } from './backfill.js'
|
|
23
23
|
import { OFFICIAL_PRICING_URL, normalizePrice, parsePricingHtml, costOf, priceEntryFor, providerPriceEntryFor, buildPriceCatalog } from './pricing.js'
|
|
24
24
|
import { CODING_PLAN_PROVIDERS, CODING_PLAN_PROVIDER_IDS, queryCodingPlan, scnetTokenPlanWindows, emptyCustomBalance, queryCustomBalance } from './coding-plans.js'
|
|
25
25
|
import { fetchWithRetry } from './net.js'
|
|
@@ -153,6 +153,64 @@ const usageProjectionSchema = z.object({
|
|
|
153
153
|
})).optional(),
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
+
/** 投影内部 state 的持久化校验 schema(dsh 0.1.1-rc.1 起的 stateSchema 契约)。 */
|
|
157
|
+
const usageProjectionStateSchema = z.object({
|
|
158
|
+
provider: z.string(),
|
|
159
|
+
model: z.string(),
|
|
160
|
+
totals: z.object({
|
|
161
|
+
input: z.number(),
|
|
162
|
+
output: z.number(),
|
|
163
|
+
cacheRead: z.number(),
|
|
164
|
+
cacheWrite: z.number(),
|
|
165
|
+
reasoning: z.number(),
|
|
166
|
+
cost: z.number(),
|
|
167
|
+
}),
|
|
168
|
+
byModel: z.record(z.string(), z.object({
|
|
169
|
+
input: z.number(),
|
|
170
|
+
output: z.number(),
|
|
171
|
+
cacheRead: z.number(),
|
|
172
|
+
cacheWrite: z.number(),
|
|
173
|
+
reasoning: z.number().optional(),
|
|
174
|
+
cost: z.number(),
|
|
175
|
+
})),
|
|
176
|
+
byProviderModel: z.record(z.string(), z.object({
|
|
177
|
+
input: z.number(),
|
|
178
|
+
output: z.number(),
|
|
179
|
+
cacheRead: z.number(),
|
|
180
|
+
cacheWrite: z.number(),
|
|
181
|
+
reasoning: z.number(),
|
|
182
|
+
cost: z.number(),
|
|
183
|
+
})).optional(),
|
|
184
|
+
last: z.object({
|
|
185
|
+
key: z.string(),
|
|
186
|
+
provider: z.string(),
|
|
187
|
+
model: z.string(),
|
|
188
|
+
buckets: z.object({
|
|
189
|
+
input: z.number(),
|
|
190
|
+
output: z.number(),
|
|
191
|
+
cacheRead: z.number(),
|
|
192
|
+
cacheWrite: z.number(),
|
|
193
|
+
reasoning: z.number(),
|
|
194
|
+
}),
|
|
195
|
+
cost: z.number(),
|
|
196
|
+
}).nullable(),
|
|
197
|
+
createdAt: z.number(),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
/** state → wire payload 读侧投影(新版 wire.view 与旧版 view 共用同一实现)。 */
|
|
201
|
+
function projectionView(state) {
|
|
202
|
+
return {
|
|
203
|
+
input: state.totals.input,
|
|
204
|
+
output: state.totals.output,
|
|
205
|
+
cacheRead: state.totals.cacheRead,
|
|
206
|
+
cacheWrite: state.totals.cacheWrite,
|
|
207
|
+
reasoning: state.totals.reasoning,
|
|
208
|
+
cost: state.totals.cost,
|
|
209
|
+
byModel: state.byModel,
|
|
210
|
+
byProviderModel: state.byProviderModel,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
156
214
|
/**
|
|
157
215
|
* costUsage 会话投影工厂:闭包账本,按事件时刻(event.time)用当时的价格档位
|
|
158
216
|
* 逐次计费(峰谷时代前按 legacyBase,之后按峰谷两档),保证会话徽章历史正确。
|
|
@@ -166,17 +224,35 @@ function makeCostUsageProjection(ledger) {
|
|
|
166
224
|
})
|
|
167
225
|
return {
|
|
168
226
|
key: 'costUsage',
|
|
227
|
+
// 新旧宿主双兼容:dsh 0.1.1-rc.1 起契约为 stateSchema + wire(见下),
|
|
228
|
+
// 0.1.0 及更早版本读取 schema + view——两套字段并存,各自宿主各取所需。
|
|
169
229
|
schema: usageProjectionSchema,
|
|
170
|
-
|
|
171
|
-
|
|
230
|
+
stateSchema: usageProjectionStateSchema,
|
|
231
|
+
stateVersion: 4,
|
|
232
|
+
init: () => ({ provider: 'deepseek', model: 'default', totals: zeroBuckets(), byModel: {}, byProviderModel: {}, last: null, createdAt: 0 }),
|
|
172
233
|
apply(state, event) {
|
|
234
|
+
// fork 种子过滤(issue #38):DSH 的 fork 把父会话事件流整段拷贝进子会话
|
|
235
|
+
// 日志,time < header.createdAt 的事件是拷贝(父会话已计费),一律跳过;
|
|
236
|
+
// 非 fork 会话 createdAt 早于全部事件,判定不生效。stateVersion 3→4
|
|
237
|
+
// 触发宿主重放重建,已受污染的投影状态随之自愈。
|
|
238
|
+
if (event.type === 'session') {
|
|
239
|
+
const created = Number(event.createdAt)
|
|
240
|
+
if (!Number.isFinite(created) || created <= 0 || created === state.createdAt) return state
|
|
241
|
+
return { ...state, createdAt: created }
|
|
242
|
+
}
|
|
243
|
+
const eventMs = Number(event.time)
|
|
244
|
+
const isSeed = state.createdAt > 0 && Number.isFinite(eventMs) && eventMs > 0 && eventMs < state.createdAt
|
|
173
245
|
if (event.type === 'request/header') {
|
|
246
|
+
// header 一律更新计费口径(与回放器/旧版状态机一致):种子 header
|
|
247
|
+
// 只推进 provider/model 状态;fork 后自己首轮若未带新 header,
|
|
248
|
+
// 沿用父会话最后的模型比回退 default 更接近真实计费。
|
|
174
249
|
const model = event.data?.header?.config?.model
|
|
175
250
|
const provider = event.data?.header?.config?.provider
|
|
176
251
|
const nextModel = typeof model === 'string' && model.length > 0 ? model : 'default'
|
|
177
252
|
const nextProvider = typeof provider === 'string' && provider.length > 0 ? provider : 'deepseek'
|
|
178
253
|
return nextModel === state.model && nextProvider === state.provider ? state : { ...state, model: nextModel, provider: nextProvider }
|
|
179
254
|
}
|
|
255
|
+
if (isSeed) return state
|
|
180
256
|
let usage = null
|
|
181
257
|
let turn = 0
|
|
182
258
|
let step = 0
|
|
@@ -248,19 +324,19 @@ function makeCostUsageProjection(ledger) {
|
|
|
248
324
|
}
|
|
249
325
|
if (prev !== null) shift(prev.provider, prev.model, prev.buckets, prev.cost, -1)
|
|
250
326
|
shift(state.provider, state.model, buckets, billed, 1)
|
|
251
|
-
|
|
327
|
+
// createdAt 必须随状态携带:usage 样本更新不能丢掉 fork 过滤基准。
|
|
328
|
+
return { provider: state.provider, model: state.model, totals, byModel, byProviderModel, createdAt: state.createdAt, last: { key, provider: state.provider, model: state.model, buckets, cost: billed } }
|
|
252
329
|
},
|
|
253
|
-
view
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
330
|
+
view: projectionView,
|
|
331
|
+
// DSH 0.1.1-rc.1 起会话投影需声明 wire 才会向客户端推送(PR #39 by
|
|
332
|
+
// @aaronlei):无 wire 的投影在 snapshot/onChanged/refold 中被跳过,
|
|
333
|
+
// 客户端 useProjection('costUsage') 恒为空,输入区下方的会话费用随之
|
|
334
|
+
// 隐藏。stateSchema 同为新契约必填——持久化恢复路径 restore() 会调用
|
|
335
|
+
// stateSchema.parse(row.val) 且无 try-catch,缺省会在 checkpoint 恢复
|
|
336
|
+
// 时 TypeError。wire.view 与外层 view 复用同一实现,避免两份取值漂移。
|
|
337
|
+
wire: {
|
|
338
|
+
viewSchema: usageProjectionSchema,
|
|
339
|
+
view: projectionView,
|
|
264
340
|
},
|
|
265
341
|
}
|
|
266
342
|
}
|
|
@@ -1036,6 +1112,16 @@ export async function runStartupImports(ledger, sessionsRoot) {
|
|
|
1036
1112
|
].filter(Boolean).join(',')
|
|
1037
1113
|
console.log(`[dsh-cost-meter] 历史按模型统计回填完成:${extras}(扫描 ${filled.scanned} 份会话日志)`)
|
|
1038
1114
|
}
|
|
1115
|
+
// fork 种子重复计费一次性清洗(issue #38):账本 migrations 标记保证只跑一次;
|
|
1116
|
+
// 回放过滤(replaySessionRecords 跳过种子)已堵住新污染,此处只清历史存量。
|
|
1117
|
+
if (!ledger.migrations.includes('fork-seed-dedup-v1')) {
|
|
1118
|
+
const repaired = repairForkSeed(ledger, sessionsRoot)
|
|
1119
|
+
ledger.migrations.push('fork-seed-dedup-v1')
|
|
1120
|
+
ledger.scheduleWrite()
|
|
1121
|
+
if (repaired.sessions > 0) {
|
|
1122
|
+
console.log(`[dsh-cost-meter] fork 会话重复计费清洗完成:扣除 ${repaired.sessions} 个会话 / ${repaired.days} 天的种子污染(扫描 ${repaired.scanned} 份会话日志)`)
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1039
1125
|
if (!(Number(ledger.config?.legacyAutoImportedAt) > 0)) {
|
|
1040
1126
|
const stats = await importLegacyHistory(ledger, sessionsRoot)
|
|
1041
1127
|
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.5.
|
|
3
|
+
"version": "1.5.34",
|
|
4
4
|
"description": "DeepSeek Harness 会话费用统计插件:本会话成本、当日费用、历史记录与官方价格同步,支持多厂商多模型价格计费(内置 90+ 模型价格目录与自动匹配)、主流 Coding Plan 订阅额度查询与显示(7 家,含 SCNet Token Plan 本地 Credits 计量)、自定义 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 7 vendors (incl. local credits metering for the SCNet Token Plan), 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",
|