Zum Hauptinhalt springen
I

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.

Installation

dsh plugin --profile web add github:icstick/dsh-usage-card

README

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 用量」对话框(四个桶)和轨迹视图。本插件补的是内核没有的三件事:

  1. 金额 —— 内核里没有任何「钱」的概念。本插件按 DeepSeek 官方价目(含峰谷缓存命中/未命中差价)把 token 折算成人民币。
  2. 按来源的归因 —— 上下文里到底是谁占的:系统提示、工具定义、你打的字、宿主注入、工具返回、还是 AI 自己之前的回复。
  3. 常驻可见 —— 不用点开对话框,侧栏一直看得到;切换会话自动跟随。

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、在 bundleslib/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
显示金额 / 显示上下文占比两个显示开关;关掉金额可与其它显示金额的插件并存
费用价目显示当前生效单价(谷/峰 × 命中/未命中/输出),「同步官方价目」直接从官方定价页抓取
导出报告会话列表(带标题,默认隐藏子代理会话,可勾选「包含子代理」)+ 两个导出按钮

价目同步的自我约束:解析结果必须先过合理性校验(数值范围 + 与内置价目比量级),不合理就拒绝落盘继续用旧价——宁可慢一拍,也不拿半截价目算钱。

逐轮账本:把「当时算出来的钱」冻结下来

卡片和报告算钱的方式不同,这不是不一致,是分工:

取数计价结果的性质
卡片内存逐轮账本 + 落盘账本每轮写入时定价,此后不重算当时的事实
报告直接折叠会话日志一律按当前价表重算今天的价钱

为什么要有落盘账本(三条,多一条都不做):

  1. 重启不再重走日志。 会话第一次被看到时读一份小文件;账本已覆盖到会话末尾时,日志一次都不读。
  2. 冻结价格版本。 官方调价(我们踩过 V4-Pro 少算 3.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/ 目录即回到没有账本时的行为,口径不变。

报告里的对照:报告主体按当前价重算,末尾另给一张「账本口径对照」表(账本口径 / 当前价重算 / 差异), 差异就是官方调价的量。那张表只在账本有数据时出现,没数据时不会多一行噪音。

汇率的来路

自动同步的规则就三条,每条都挡一个真实会出现的结果:

  1. 拉起 dsh 时同步一次(fire-and-forget,不等网络)。启动关键路径上不能卡在 HTTP 上,失败也只留一条日志。
  2. 手填的值优先。判据是「当前汇率 ≠ 上次同步写进去的那个值」—— 不相等就说明同步之后有人手改过,自动同步让位,直到你点一次「同步」或改回去。 注意这条判据看的是值是否相等,不是"谁写的":你要是手填了一个正好等于同步值的数,它会被当成同步来的值。这一条不能省:否则每次重启都会把记账用的汇率冲掉。 全新安装是例外:从未同步过(没有时间戳)时自动同步照跑,否则默认的 7.2 会被误判成手动值,自动更新永远起不来。
  3. 距上次成功同步不足 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 随时可复跑)。

折叠口径与卡片一致:每个轮次按它自己的时刻判峰谷,模型取该轮时点上生效的请求头;未定价轮次单列且不计入金额。

口径纪律(本插件最在意的部分)

  1. 实测与估算永不相加。 四桶来自 provider 上报(实测);占比是按 surface 逐节点定价的估算。两者在接口里就是两组字段,界面分区显示、各带标签。
  2. 未定价返回 null,不是 0,也不回退默认价。
  3. 占比与金额解耦。 占比是 token 比例,与定价无关 —— 算不出钱不该把占比一起抹掉。
  4. 金额变量名带币种后缀,换算只在记账时发生一次。
  5. 峰谷按 UTC + ISO 星期判定,禁用本机时区方法。
  6. 价目只由官方页面驱动,不按「预期下线/预期调价」提前改价。
  7. 降级要说出来。 投影不可用、会话未加载、价表退回内核三元……都走明确原因码,不静默显示 0 或别人的数字。
  8. 渲染期不抛。 卡片挂在宿主侧栏里,抛出去会把别人的界面一起带下水。所以两道闸: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/splicedsource.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
M3subagent 归集(子会话四桶,实测)
M4设置页(汇率 / 显示开关)
M5导出报告(日志折叠,Markdown/CSV)
M6逐轮账本落盘 + 汇率自动更新 + A/B 机验证🔄 汇率自动更新 ✅ · 逐轮账本 ✅(2026-09-21);B 机验证 ✅;A 机待开机

许可

MIT

Ähnliche Plugins