- Inicio
- Plugins
- Uso y facturación
- dsh-usage-card
dsh-usage-card
icstick/dsh-usage-card
Sidebar card that prices this session's tokens in RMB at official peak/off-peak rates and splits context share by source, with per-turn pricing and a Markdown/CSV history report.
Instalar
dsh plugin --profile web add github:icstick/dsh-usage-cardREADME
dsh-usage-card
侧栏「设置」按钮正上方的一张常驻小卡片:本会话的 token 用量、费用(RMB)、以及按来源拆分的上下文占比。
为 DeepSeek Harness(DSH)Web 界面设计。
默认(收起)——头部与四桶常显,占比明细收起:
┌─ 本会话用量 ─────────── 7.1M tok ¥0.55 ▸ ─┐
│ 输入未命中 96,266 输出 67,618 │
│ 缓存命中 6,955,648 命中率 99.0% │
├────────────────────────────────────────────┤
│ 上下文占比 · 221k [估算 ▸] │
├────────────────────────────────────────────┤
│ subagent 3 个 · 5,500 tok · ¥0.02 [实测] │
└────────────────────────────────────────────┘
展开(点右上角 ▸ 或占比标题行)——逐项明细与收口两行:
│ 上下文占比 · 221k [估算] │
│ ██████████████████████████████████████████ │
│ ● 系统提示 14.2% ¥0.04│
│ ● 工具 schema 0.2% <¥0.01│
│ ● 用户消息 0.1% <¥0.01│
│ ● 环境注入 17.0% ¥0.04│
│ ● 工具结果 33.3% ¥0.08│
│ ● AI 历史回复 35.2% ¥0.09│
│ 输出 ¥0.09│
│ 合计 ¥0.55│
三段折叠:头部与四桶常显,占比明细默认收起(收起时仍给出占比总量与展开入口,不丢信息)。
展开状态记在浏览器 localStorage(dsh-usage-card.detailOpen),不占宿主设置,换浏览器各记各的。
侧栏收成 rail 时(wide: false)降级成 34px 的环:外圈缓存命中率 + 环下极小金额。
它解决什么
DSH 内核已经内置了「Token 用量」对话框(四个桶)和轨迹视图。本插件补的是内核没有的三件事:
- 金额 —— 内核里没有任何「钱」的概念。本插件按 DeepSeek 官方价目(含峰谷与缓存命中/未命中差价)把 token 折算成人民币。
- 按来源的归因 —— 上下文里到底是谁占的:系统提示、工具定义、你打的字、宿主注入、工具返回、还是 AI 自己之前的回复。
- 常驻可见 —— 不用点开对话框,侧栏一直看得到;切换会话自动跟随。
装
dsh plugin --profile web add github:Icstick/dsh-usage-card
装完重启 dsh web,刷新页面。卡片出现在左侧栏「设置」正上方。
不需要构建步骤:lib/client.js 随包发布(用 npm pack --dry-run 校验过产物里确实含有它)。
多机安装
在每台目标机上跑同一条命令即可:
dsh plugin --profile web add github:Icstick/dsh-usage-card
要在多台机器上批量装,用 scripts/install-usage-card.ps1(装 → 校验 → 提示重启)。它既能就地运行,也能让已有的 run-on.ps1 经 ssh 推到目标机执行:
pwsh -File run-on.ps1 -Script .\install-usage-card.ps1 -Target b-server
脚本会校验三件事——包在 dependencies、在 bundles、lib/client.js 存在。"装完了但其实没生效"比装失败更难查,所以这一步不能省。
本插件的账本是每台机器各管自己:各自读自己的会话日志,不跨机合并。
本地开发 / 离线安装:
node scripts/install-local.mjs # 链接进 web profile(不走 pnpm)
node scripts/install-local.mjs --uninstall # 可逆
设置页(插件设置 tab)
设置页里「用量卡片」是一项独立 tab:
| 项 | 说明 |
|---|---|
| 汇率(USD → CNY) | 可手填,也可点「同步」拉实时价(三个源,取第一个合理值)。本机实测市场价 ≈ 6.72,旧的硬编码 7.2 会把金额高估约 7%。手填的值不会被自动同步冲掉(见下) |
| 拉起 dsh 时自动同步汇率 | 默认开。每次启动 dsh 拉一次实时价写进上面的汇率;拉不到就保留现值,绝不把金额算成 0 |
| 显示金额 / 显示上下文占比 | 两个显示开关;关掉金额可与其它显示金额的插件并存 |
| 费用价目 | 显示当前生效单价(谷/峰 × 命中/未命中/输出),「同步官方价目」直接从官方定价页抓取 |
| 导出报告 | 会话列表(带标题,默认隐藏子代理会话,可勾选「包含子代理」)+ 两个导出按钮 |
价目同步的自我约束:解析结果必须先过合理性校验(数值范围 + 与内置价目比量级),不合理就拒绝落盘继续用旧价——宁可慢一拍,也不拿半截价目算钱。
逐轮账本:把「当时算出来的钱」冻结下来
卡片和报告算钱的方式不同,这不是不一致,是分工:
| 取数 | 计价 | 结果的性质 | |
|---|---|---|---|
| 卡片 | 内存逐轮账本 + 落盘账本 | 每轮写入时定价,此后不重算 | 当时的事实 |
| 报告 | 直接折叠会话日志 | 一律按当前价表重算 | 今天的价钱 |
为什么要有落盘账本(三条,多一条都不做):
- 重启不再重走日志。 会话第一次被看到时读一份小文件;账本已覆盖到会话末尾时,日志一次都不读。
- 冻结价格版本。 官方调价(我们踩过 V4-Pro 少算 3.3 倍的坑)只影响之后的轮次,历史金额不跟着漂。
- 历史可查(按会话 / 按天 / 按模型的明细不必现场折叠日志)。
文件:<DSH_HOME>/storages/dsh-usage-card/ledger/2026-09.<pid>.jsonl,按轮次发生的月份(UTC)分文件,
一行一个轮次,追加写、防抖 300 ms。文件名里的 <pid> 是写它的那个 dsh 进程 ——
同一个 home 可能被多个进程共用(web / worker / headless 各起一份),各写各的文件,行与行就不会互相插进去
(拼接出来的行解析失败会被当坏行丢掉,那是静默少算钱)。同一轮被两个进程各记一次也没关系:读取时按
(sessionId, seq) 去重。一行大致长这样:
{ "sessionId": "session-beb41318-…", "seq": 1234, "time": 1789963101317, "model": "deepseek-flash",
"tier": "offpeak", "buckets": { "uncachedInputTokens": 96266, "cacheReadTokens": 6955648, "cacheWriteTokens": 0, "outputTokens": 67618 },
"usdInput": 0.05, "usdOutput": 0.0319, "priceVersion": "2026-09-18", "pluginVersion": "0.13.0" }
三条纪律:
- 幂等键
(sessionId, seq),同键后写覆盖 —— 重放、回填、实时事件交叠都不会重复计数(金额翻倍是这里最容易出的错)。 - 投影仍是权威:账本只影响「逐轮归属」。账本没覆盖的部分照旧按当前档位线性估算,
coverage如实标注 —— 账本只减少「未覆盖」,不粉饰它。 - 坏了不带走卡片:坏行跳过(崩在写入中途留下的半行只坏它自己那一行,还会补一个换行把它封死)、文件读不出就整月放弃,任何 IO 异常只记一条 warn。最坏情况是回到「走日志回填」。
回滚:删掉 storages/dsh-usage-card/ledger/ 目录即回到没有账本时的行为,口径不变。
报告里的对照:报告主体按当前价重算,末尾另给一张「账本口径对照」表(账本口径 / 当前价重算 / 差异), 差异就是官方调价的量。那张表只在账本有数据时出现,没数据时不会多一行噪音。
汇率的来路
自动同步的规则就三条,每条都挡一个真实会出现的结果:
- 拉起 dsh 时同步一次(fire-and-forget,不等网络)。启动关键路径上不能卡在 HTTP 上,失败也只留一条日志。
- 手填的值优先。判据是「当前汇率 ≠ 上次同步写进去的那个值」—— 不相等就说明同步之后有人手改过,自动同步让位,直到你点一次「同步」或改回去。 注意这条判据看的是值是否相等,不是"谁写的":你要是手填了一个正好等于同步值的数,它会被当成同步来的值。这一条不能省:否则每次重启都会把记账用的汇率冲掉。 全新安装是例外:从未同步过(没有时间戳)时自动同步照跑,否则默认的 7.2 会被误判成手动值,自动更新永远起不来。
- 距上次成功同步不足 10 分钟就跳过,挡住崩溃循环/反复重启把同一件事重复做很多遍。
三个源依次试(open.er-api.com → exchangerate-api.com → frankfurter.app),取第一个落在合理区间的值(> 1 且 < 20); 全部失败就什么都不写,沿用现值。自动同步永远不覆盖写失败:设置只读、网络断、源给离谱数,最坏结果都是"汇率没变"。
设置页里的「上次同步 … · 来源 · 汇率」只在当前汇率确实就是上次同步写进去的那个值时才显示——同步之后被你手改过,它就不吭声了,不会拿一个同步时间冒充你手填的数。
导出报告
卡片面板底部有两个入口(也可直接开 URL):
/usage-card/report?format=md—— Markdown:总览 / 按天 / 按模型 / 按会话 / 计价说明/usage-card/report?format=csv—— CSV:一行一个「天 × 模型」
两个可选参数:
?sessions=<id1,id2>—— 只导出这些会话(逗号分隔;缺省或全选 = 全部)?subagents=1—— 把子代理会话也算进明细(默认不算,与卡片的「独立会话」口径一致)
报告直接读本机会话日志折叠生成(不落账本库),因此覆盖全部历史会话。本机实测:50 个日志、3242 轮、1.2 秒、0 坏行
(这是本机一次性实测、不是 CI 断言:换台机器、换批日志数字都会不同,脚本 test/coverage-check.mjs 随时可复跑)。
折叠口径与卡片一致:每个轮次按它自己的时刻判峰谷,模型取该轮时点上生效的请求头;未定价轮次单列且不计入金额。
口径纪律(本插件最在意的部分)
- 实测与估算永不相加。 四桶来自 provider 上报(实测);占比是按 surface 逐节点定价的估算。两者在接口里就是两组字段,界面分区显示、各带标签。
- 未定价返回
null,不是 0,也不回退默认价。 - 占比与金额解耦。 占比是 token 比例,与定价无关 —— 算不出钱不该把占比一起抹掉。
- 金额变量名带币种后缀,换算只在记账时发生一次。
- 峰谷按 UTC + ISO 星期判定,禁用本机时区方法。
- 价目只由官方页面驱动,不按「预期下线/预期调价」提前改价。
- 降级要说出来。 投影不可用、会话未加载、价表退回内核三元……都走明确原因码,不静默显示 0 或别人的数字。
- 渲染期不抛。 卡片挂在宿主侧栏里,抛出去会把别人的界面一起带下水。所以两道闸:
ok:true的 payload 先过形状闸(不认识就报SCHEMA_MISMATCH,不硬按老字段取),过了还抛的由渲染边界兜成一行提示(用量卡片渲染失败,已隔离,悬停看原因)。原因码一律安全转字符串——reason是对象时不参与拼接。
数据来源
| 数据 | 来源 |
|---|---|
| 四桶用量 | ctx.sessionProjections.snapshot(s).values.tokenUsage |
| 上下文三元 | ...values.contextBreakdown |
| 本会话模型 | ...values.modelSelection.lastUsed.model |
| 逐节点定价 | ctx.tokenMeter.measure(session).nodes[] |
| 节点分类 | session.eventAt(node.seq) → 事件类型 |
| 子代理 | session.header.parentSession + 子会话自己的四桶 |
两个实测踩出来的坑,写在这里省得别人再踩:真人消息在
agent/inbox/spliced(source.kind === 'user'),不在user/message(后者绝大多数是宿主注入);工具内容散在多种事件里且有重叠,按事件体积直接相加会重复计数。
开发
node scripts/build-client.mjs # 改了 client/index.js 必须重建 lib/client.js 并提交
node test/m0-check.mjs # 纯函数与 payload 验收
node test/wiring-check.mjs # 接线验证(假 ctx 跑 apply)
node test/subagent-check.mjs # 子代理归集(实测四桶 + 已释放的诚实处理)
node test/report-check.mjs # 报告:多帧解码、逐轮折叠、聚合与渲染
node test/isolation-check.mjs # M5-b 崩溃隔离(敌意 URL/坏日志 + 迷你渲染器灌敌意 payload)
node test/fx-check.mjs # M6 汇率自动更新(多源/手改优先/节流/拉起即同步)
node test/ledger-check.mjs # M6 逐轮账本(幂等/崩溃截断/价格冻结/重启不重走日志)
npm test # 以上全部(不含需要真实日志的 coverage-check)
node scripts/verify.mjs # 起服务后自检路由与 payload
node test/coverage-check.mjs # M5-a 老会话覆盖率实测(读本机 DSH_HOME/sessions 真实日志,非 CI)
coverage-check 的输出就是「老会话到底能算出多少」:扫描会话数、轮次、可定价/未定价、坏行、耗时,以及未定价都落在哪些模型上(决定覆盖率是价表缺口还是历史脏数据)。本机 W 机实测:74 个日志 / 16 个用户会话 + 58 个子代理 / 3,363 轮 / 覆盖率 100.00% / 0 坏行 / 1.75 s。
宿主半改动要重启 dsh web;客户端改动要重建 + 刷新页面。
本地路由的安全姿态
/usage-card/current.json 走两级栅栏:优先用宿主的 connection.requestRejection(Host/Origin + 浏览器 cookie 认证);该服务在当前上下文不可达时(插件挂在 bundle 作用域)退到本地栅栏 —— 对端必须是回环(挡局域网/外网直连)且 Host 必须是回环或 localhost(挡 DNS rebinding)。每个响应带 x-usage-card-fence 说明走的哪一级。
本地兜底弱于宿主完整栅栏(没有 cookie 认证),且不返回跨源可读的 CORS 头,因此网页无法读取响应体;数据本身是本机 token 与费用数字。
已知限制
- 归因是估算,启发式会低估 CJK 与 JSON schema
- 金额按轮计价(每轮用它自己的时刻判峰谷)。既没被实时看到、也没有账本行的轮次(账本启用之前的历史)按当前档位线性估算,卡片上打「含估算」并给出覆盖率;新会话从第一轮起、以及账本启用后的每一轮都是精确且金额冻结的
- 「环境注入」与「用户消息」的区分依赖事件形态,宿主改了格式可能要跟
- 汇率由实时源维护(拉起 dsh 时自动同步,也可手动同步或手填);同步只写汇率本身,不动其它设置
路线图
| 期 | 内容 | 状态 |
|---|---|---|
| M0 | 骨架 + 路由 + 卡片 + 四桶计价 | ✅ |
| M1 | 六类归因 + 会话跟随 + 模型解析 | ✅ |
| M2 | 六类归因(已并入 M1) | ✅ |
| M3 | subagent 归集(子会话四桶,实测) | ✅ |
| M4 | 设置页(汇率 / 显示开关) | ✅ |
| M5 | 导出报告(日志折叠,Markdown/CSV) | ✅ |
| M6 | 逐轮账本落盘 + 汇率自动更新 + A/B 机验证 | 🔄 汇率自动更新 ✅ · 逐轮账本 ✅(2026-09-21);B 机验证 ✅;A 机待开机 |
许可
MIT
Plugins relacionados
DeepSeek-Balance-Whale-Widget
meteornox/deepseek-balance-whale-widget
dsh-context
bowenliang123/dsh-context
dsh-cost-meter
han-1413141/dsh-cost-meter
TokenLedger
zh667/tokenledger