# dsh-token-bill [English](README.md) | 中文 **DeepSeek Harness(DSH)的实时 token 账单插件**:实时账户余额、当日真实消费、按小时归集的 token 用量与金额柱状图,以及可归档的 Markdown / HTML / JSON 账单。 两个面向模型的工具,不需要任何界面,也不需要额外常驻进程: - **`token_status`** — 实时余额、今日账本实扣、今日 token 分桶。 - **`token_bill`** — 按小时柱状图、峰谷拆分、分时段明细表,并写出账单文件。 ``` 📊 2026-09-28 token 账单 余额 ¥52.9100 · 今日消耗 ¥4.6900(账本实扣) Token 合计 79.65M(输入 1.02M · 缓存命中 78.14M · 输出 492.7K) · 缓存命中率 98.1% · 716 次调用 最忙时段 15:00(29.00M tokens) · 最贵时段 15:00(¥2.3380) 按小时 token 消耗(6 个时段有用量) 00:00 │█████████▃ │ 9.89M 01:00 │███████▂ │ 7.72M 11:00 │██▆ │ 2.92M 12:00 │██████████████▇ │ 15.80M 13:00 │█████████████▄ │ 14.33M 15:00 │██████████████████████████│ 29.00M 按小时金额(已按账本实扣校准) 00:00 │██████▃ │ 0.5607 01:00 │██▅ │ 0.2259 ... ``` ## 为什么需要这个插件 DSH 有意把 token 数当作**度量**而不是**账单**:`ctx.tokenMeter` 只提供请求压力快照,整个 harness 里没有任何一处把 token 换算成钱,也**完全不内置价目表**。而你真想要的三个数字散落在三个互不相通的地方: | 数据 | 位置 | 权威性 | | --- | --- | --- | | 账户余额 | `GET https://api.deepseek.com/user/balance`(API key 鉴权) | 实时、权威 | | 当日真实消费 | `$DSH_HOME/.dshw-usage.json` 余额记账本 | 已观测、权威 | | 各时段 token 用量 | `$DSH_HOME/sessions/**/session.v*.jsonl.zstd` | 服务商上报、精确 | 本插件用一条规则把它们对齐:**钱以观测为准,token 以计数为准。** 当日金额取自账户自身的余额变动,价目表只用来把这份已观测的总额分摊到各小时。 ## 特性 - 💰 **实时余额**,来自官方余额接口;网络失败时回落到账本的最后一次观测,并明确标注「非实时」。 - 📊 **按小时的真实 token 用量**,读自持久化会话日志 —— 服务商上报的四个互不重叠的桶(`inputTokens`、`cacheReadTokens`、`cacheWriteTokens`、`outputTokens`),不是按字符数的启发式估算。 - 🧾 **落盘账单**:自包含 HTML(内联 SVG 柱状图)、可归档的 Markdown、可脚本化处理的原始 JSON。 - ⛰️ **峰谷感知**:北京时间高峰时段、周末与法定节假日全天谷价,报告中给出峰谷拆分。 - 🎯 **精确金额运算**:金额全程以整数 1e-8 单位承载,因此 `sum(小时金额) === 当日金额` 是恒等式,而不是四舍五入的巧合。 - 🔒 **只读**:插件不写入你的账本、会话日志或凭据。 ## 安装 ```bash # 已发布到 registry 后 dsh plugin --profile web add dsh-token-bill # 从本地目录安装 dsh plugin --profile web add link:/绝对路径/dsh-token-bill ``` 安装后,新会话即可让模型使用上面两个工具。若你的 profile 不会自动重载组合,请随后重启 DSH web 服务。 **环境要求**:Node ≥ 22.12(插件通过 `require(esm)` 解析 DSH 的 peer 依赖),以及挂载了 `tools` 服务的 DSH profile(任意标准 Web profile 均可)。 ## 余额记账本 当日**真实**消费来自 `$DSH_HOME/.dshw-usage.json`。这个文件由优秀的 [`dsh-whale-widget`](https://github.com/MeteorNOX/DeepSeek-Balance-Whale-Widget) 余额挂件写入,**不是**本插件写的。每个日期行记录期初余额、最后观测余额,以及累计的借(消费)与贷(充值): ```json { "openingUnits": 760000000, "lastUnits": 5525000000, "debitUnits": 388000000, "creditUnits": 5000000000, "firstAt": 1790529962822, "lastAt": 1790580387568 } ``` 金额是整数 1e-8 元单位。已观测消费只统计余额**下降**,所以充值只累加到 `creditUnits`,永远不会虚增你的消费。如果该行同时**能对上账**(`期初 − 消费 + 充值 = 期末`),充值会作为说明信息给出;如果对不上,说明有账本无法归类的余额变动,插件会直接说明,而不是悄悄报一个错数字。 **如果你没有装那个挂件**,账本不存在,插件会干净地退化为按价目表估算并明确标注为估算 —— 余额仍然来自实时接口。 ## 工具 ### `token_status` | 参数 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | `includeLedger` | boolean | `true` | 附上账本路径、更新新鲜度与事件条数。 | | `includePricing` | boolean | `false` | 附上峰谷价目表。 | 返回 `day`、`balance`、`currency`、`balanceSource`(`api` 或 `ledger`)、`balanceStale`、`todayObserved`、`todayTokenEstimate`、`totalTokens`、`inputTokens`、`cacheReadTokens`、`outputTokens`、`calls`、`ledgerPath`、`text`。 ### `token_bill` | 参数 | 类型 | 默认 | 说明 | | --- | --- | --- | --- | | `day` | string | 今天 | 账期,`YYYY-MM-DD`,北京时间。 | | `days` | integer | `1` | 同时给出最近 N 天的按日汇总。 | | `costMode` | `ledger` \| `token` \| `both` | `ledger` | 见[金额口径](#金额口径)。 | | `write` | boolean | `true` | 是否把账单写到磁盘。 | | `formats` | `html` \| `md` \| `json` 数组 | `["html","md"]` | 写出哪些账单文件。 | 返回 `day`、`amount`、`amountSource`(`ledger` 或 `token-estimate`)、`currency`、`balance`、`totalTokens`、`calls`、`activeHourCount`、`peakTokens`、`valleyTokens`、`files`,以及完整的 `text` 文本账单。 账单写出到 `$DSH_HOME/token-bill/`,文件名为 `token-bill-.{html,md,json}`。宿主进程的工作目录就是 DSH 安装目录本身,所以默认值刻意**不取** `process.cwd()`;可用 `DSH_TOKEN_BILL_DIR` 环境变量或配置项 `outDir` 覆盖。 ## 数据来源 ``` api.deepseek.com/user/balance ──► 实时余额 ──┐ ├──► 报告 ──► 柱状图 + 账单 $DSH_HOME/.dshw-usage.json ─────► 已观测 ¥ ──┤ $DSH_HOME/sessions/**/*.zstd ───► 精确 token ─┘ ``` **余额** —— `GET https://api.deepseek.com/user/balance`,请求头 `Authorization: Bearer `,返回 `balance_infos[]`,每币种含 `total_balance` / `granted_balance` / `topped_up_balance`。这不需要浏览器登录态。API key 依次从 `DEEPSEEK_API_KEY`(以及 `DSH_DEEPSEEK_API_KEY`、`DEEPSEEK_KEY`)环境变量,或 `$DSH_HOME/.credentials.yaml` 里的 `refs.DEEPSEEK_API_KEY` 读取。 **token 用量** —— 每条持久化的 `assistant/message` 事件都同时带着适配器上报的 `usage` 和毫秒级 `time`,后者正是按小时归集得以成立的前提。四个桶严格按服务商口径互不重叠: - `inputTokens` —— 未命中缓存的输入(cache miss) - `cacheReadTokens` —— 命中缓存的输入(cache hit) - `cacheWriteTokens` —— 写入缓存的输入 - `outputTokens` —— 输出,reasoning 已包含在内 会话日志位于 `$DSH_HOME/sessions///`,文件名 `session.v.jsonl.zstd`,首行是 header,之后每次追加写成一个独立的校验 Zstandard 帧。更早的未压缩 `session.v.jsonl` 同样可读。 ## 金额口径 `costMode` 决定金额数字的来源: | 模式 | 当日金额 | 小时金额 | `token 估算` 列 | | --- | --- | --- | --- | | `ledger`(默认) | 账本观测到的实扣 | 按 token 分摊并校准到该总额 | 显示 | | `token` | 价目表估算 | 价目表估算 | 不适用 | | `both` | 账本观测到的实扣 | 按 token 分摊并校准到该总额 | 显示 | `ledger` 模式下,小时金额采用**按账单自身精度的最大余额法分摊**,因此账单打印出来的各小时金额之和,永远等于它打印出来的当日总额。当账本当天没有观测时,插件退化为价目表估算并如实标注 —— 它绝不会把估算当作实扣呈现。 ### 峰谷定价 北京时间。高峰(标准价)适用于**周一至周五 09:00–12:00、14:00–18:00**。其余所有小时,加上全部周末与法定节假日,按谷价(高峰价的一半)计费。价格为「元 / 百万 token」,下表为 `谷价 / 峰价`: | 模型 | 缓存命中 | 缓存未命中 | 输出 | | --- | --- | --- | --- | | `deepseek-flash`、`deepseek-v4-flash`、`deepseek-v4-flash-vision-exp`、`deepseek-chat` | 0.02 / 0.04 | 1 / 2 | 4 / 8 | | `deepseek-pro`、`deepseek-v4-pro`、`deepseek-reasoner` | 0.15 / 0.3 | 4.5 / 9 | 13.5 / 27 | 无法识别的模型名会回落到 Flash 系列计价,这样新模型上线时仍能给出可用的估算。法定节假日表(`HOLIDAYS_2026`,34 天)在 `lib/pricing.mjs`,新一年国务院放假安排公布后可通过配置扩充。 ## 配置 在 profile 的 `cordis.patch.yml` 里给插件那一行加 `config`: ```yaml - id: token-bill name: dsh-token-bill config: costMode: ledger # ledger | token | both decimals: 4 # 打印金额的小数位 chartWidth: 26 # 终端柱状图宽度 historyDays: 1 # 按日汇总包含的天数 writeFiles: true # 设为 false 则永不写账单文件 timeoutMs: 10000 # 实时余额请求超时 outDir: !!js dshHomePath('token-bill') sessionsRoot: !!js dshHomePath('sessions') # 覆盖或扩充价目表(元 / 百万 token): # models: # my-model: { hit: { off: 0.01, peak: 0.02 }, miss: { off: 1, peak: 2 }, out: { off: 4, peak: 8 } } # 追加法定节假日(全天谷价): # holidays: ['2027-01-01'] ``` | 字段 | 默认 | 说明 | | --- | --- | --- | | `dshHome` | `$DSH_HOME` 或 `~/.dsh` | DSH 主目录。 | | `profile` | `$DSH_PROFILE` 或 `web` | 同时检查哪个 profile 目录下的账本副本。 | | `sessionsRoot` | `/sessions` | 会话日志根目录。 | | `outDir` | `$DSH_TOKEN_BILL_DIR` 或 `/token-bill` | 账单输出目录。 | | `decimals` | `4` | 打印金额的小数位。 | | `chartWidth` | `26` | 终端柱状图宽度。 | | `costMode` | `ledger` | 金额来源,见上。 | | `historyDays` | `1` | 按日汇总包含的天数。 | | `maxBytes` | `134217728`(128 MB) | 超过此大小的单个会话日志会被跳过。 | | `timeoutMs` | `10000` | 余额请求超时。 | | `writeFiles` | `true` | 是否允许写出账单文件。 | | `models` | 内置价目表 | 价目表覆盖。 | | `holidays` | `HOLIDAYS_2026` | 全天按谷价计费的法定节假日日期。 | ## 架构 ``` lib/index.js 插件入口:name / inject / Config / apply,注册两个工具 lib/service.mjs 编排:解析配置 → 采集 → 折叠 → 生成报告 → 落盘 lib/account.mjs API key 解析、余额接口、实时到账本的降级策略 lib/ledger.mjs 只读读取 $DSH_HOME/.dshw-usage.json lib/logs.mjs 会话枚举、多帧 zstd 解码、按小时/按日折叠 lib/pricing.mjs 峰谷时段判定与 DeepSeek 价目表 lib/money.mjs 精确十进制金额(整数 1e-8 单位) lib/report.mjs 报告装配、终端柱状图、HTML 账单 lib/markdown.mjs Markdown 账单 lib/chart.mjs 终端柱状图 ``` 几个值得说明的实现决定: - **金额全程不经过二进制浮点。** 每个金额都是整数个 1e-8 单位。模型单价极小,且要在几千次调用上累加 —— 这恰恰是浮点漂移最容易出现的场景,也正是分摊恒等式能够成立的原因。 - **会话日志是多帧 Zstandard。** Node 的 `zstdDecompressSync` 只解第一帧且不返回消耗字节数,直接读会把日志悄悄截断成一行 header。`decodeZstdFrames()` 按帧魔数定位并逐片验证解码;末尾被截断的那一帧直接结束扫描 —— 对于一份正在被写入的日志,这正是想要的行为。 - **账本是读,不是轮询。** 余额挂件已经在按定时器观测你的账户;复用它的记录可以避免两个写入方争抢同一个文件,而且只有余额**显示**会发起网络请求。 - **只打开近期改动过的日志**(按 mtime 过滤),一天的账单不会为整段历史付出解压代价。 ## 隐私 全部数据留在本地。唯一的网络请求是向 `api.deepseek.com` 查询余额,用你自己的 API key 鉴权,由 DSH 宿主进程直接发出。账单写在 `$DSH_HOME/token-bill/` 下。不会上传任何数据给插件作者,插件也不含任何遥测。 ## 测试 ```bash node test/unit.mjs # 21 项纯函数测试:金额精度、峰谷时段、zstd 多帧、账本行 node test/report.mjs # 8 项报告层测试:分摊恒等式、精度网格、HTML 转义 node test/smoke.mjs # 对真实账本 / 余额接口 / 会话日志跑端到端 node test/verify.mjs # 走真实 execute(),校验输出 schema 与 HTML 结构 ``` `npm test` 只跑三个不依赖外部数据的单测集(`docs`、`unit`、`report`)。`smoke` 与 `verify` 会读取你真实的 DSH 数据,并把账单写到包内的 `.dsh-token-bill/`(已 gitignore),因此需要一个可用的 DSH 主目录,且建议在源码检出处运行 —— `test/` 不在发布包内(`files` 只发布 `lib/`、patch、两份 README 与许可证)。 ## 已知边界 - **账本依赖余额挂件。** 没有它时,金额列变成价目表估算而非观测实扣;余额本身始终是实时的。 - **会话日志是追加落盘的,最新一轮可能尚未写入。** `token_status` 的新鲜度取决于日志与账本,而非瞬时值。 - **小时金额假设当天使用同一套价目表。** 在服务商调价当天,按小时拆分是观测总额的比例分摊,不是逐笔对账。 - **节假日表需要人工维护。** 未列入的法定假日会按高峰价计费。 - **目前只支持 DeepSeek。** 余额来源与价目表都是 DeepSeek 专有的;接入其它提供方需要自带余额来源,并补一条 `models` 配置。 ## 许可 [MIT](LICENSE)。