dsh-plugin-cost · DSH 花费显示
在 DeepSeek Harness(DSH)的对话界面里直接看到每轮对话花了多少钱、整个会话累计花了多少钱,以及账户余额。
… 助手回答正文 …
👍 ⑂ · 12.3k tokens · 8.2s · 本轮 2 · ¥0.0873 ← 本插件接在同行末尾
↑ 点这里看明细
┌─────────────────────────────────────────────┐
│ 输入框 │
│ 本会话累计 ¥2.49 · 余额 ¥42.50 │ ← 本插件加在输入框下方
└─────────────────────────────────────────────┘
- 每轮花费:直接接在该轮助手回答的收尾动作行上,就在 token 用量、耗时之后。 折叠态只显示金额;点击弹出明细,含模型路由、模型调用次数与 token 拆分。
- 会话累计:显示在输入框下方,默认每 3 秒刷新。点击后是一个精简面板: 轮次 / 模型调用 / 已用 token / 当前模型(含该模型此刻的单价与所处时段)/ 价格来源, 右下角有「⚙ 设置」入口。
- 账户余额:
/user/balance的实时读数,每 3 次会话刷新读一次(约 9 秒),仅作参照。 显示币种与会话累计一致:你改币种,余额跟着换算,不会一个美元一个人民币。 - 多币种:金额按你选的币种显示,可再并排显示第二个币种。
- 按模型判断高峰/低谷:有低谷价的模型显示时段与实时单价,全天同价的模型只显示单价。
- 自动更新:模型、版本、价格、高峰时段、汇率全部会自己刷新。
安装
这个包没有任何运行时依赖,装法有三种:
# 1) 从 GitHub 装
dsh plugin --profile web add github:xiaml666/dsh-plugin-cost
# 2) 从 npm 装(若已发布)
dsh plugin --profile web add dsh-plugin-cost
# 3) 本地开发:直接 link 一个检出
dsh plugin --profile web add link:/absolute/path/to/dsh-plugin-cost
GitHub 装法的一个坑:pnpm 默认会拦截 git 托管包的
prepare构建脚本,首次安装可能失败。 dsh 在失败时会打印 pnpm 给出的确切 key,把它加到该 profile 目录下pnpm-workspace.yaml的allowBuilds里再重跑即可。本包本身并不需要构建(lib/与client/都是直接执行的普通 JS), 纯粹是 pnpm 对 git 来源包的一刀切拦截。
装完必须重启 dsh web:客户端 bundle 名册只在进程启动时组装一次,之后光刷新页面不够。
本包自带一个无人值守的重启脚本(会杀掉当前监听该端口的进程,请不要从正在使用该服务的会话里调用):
node scripts/restart-web.mjs
高峰 / 低谷时段
金额按 provider 上报的 token 精确计算,并按官方的高峰/低谷时段自动切换单价。
时段不是写死的:每次刷新价格页时,插件会同时解析官方那句时段说明, 按页面自己声明的时区计价。中英文两版官方页面写的是同一段时间的两种说法:
| 页面 | 原文 |
|---|---|
| 英文 | Peak hours are 01:00 - 04:00 and 06:00 - 10:00 UTC, Monday through Friday |
| 中文 | 高峰时段为北京时间周一至周五 9:00 - 12:00、14:00 - 18:00 |
两者等价(北京时间 = UTC+8),插件对任意时刻的判定完全一致。
官网改了时段,插件会在下一次刷新时自动跟上,不需要更新插件。
界面上会给出你所在时区的高峰时段,以及它的补集——空闲时段(两行,各带时区名与 UTC 偏移),并标出此刻是高峰还是低谷、以及下次切换的时刻——「显示的时间不对」绝大多数时候 是时区没对上,而不是规则算错。行与行的差别见下文「当前模型」一节。
空闲时段官网只写成一句话——中文页「其余为空闲时段」、英文页 all other hours are off-peak——从不列出具体钟点,所以插件是按高峰区间取补集反推的:星期一到星期五的高峰
之外,再加上周六、周日整天。反推出来的几段和高峰一样标在你自己的时区上,
"什么时候便宜"看这一行就够了。
需要的话也可以在设置面板里固定成自定义时段(自定义时区、星期、区间、低谷倍率), 适合高峰规则与官网不一致的第三方渠道。
不是每个模型都有高峰/低谷
高峰/低谷是模型自己的属性,不是全局开关。插件按当前模型的费率行来判断:
- 费率行带低谷价(DeepSeek 官方表就是这种,
cacheMissOffPeak等)→ 这个模型按时段浮动, 面板会给出当前是高峰还是低谷、时段、下次切换时刻,以及此刻实际生效的单价; - 费率行没有低谷价(多数非 DeepSeek 供应商)→ 这个模型全天同价,面板只给出单价, 并明确标注"无高峰/低谷,全天同价"——给它显示一条高峰时段是在误导。
判定读的就是计价用的那一行,所以显示与实际扣费口径永远一致,不会出现 "界面说有低谷、算钱时却没打折"的情况。
当前模型
会话详情面板和设置面板的顶部都有一块当前模型读数,内容随模型而变。
每一项都单独占一行,而且每一行只要写了时间,就一并写出它用的是哪个时区——
只给一个光秃秃的 UTC 配一串时间,等于让人无法核对:
模型 deepseek-official/deepseek-flash
时段 当前 低谷(半价)
高峰时段 周一至周五 09:00-12:00、14:00-18:00(Asia/Shanghai UTC+08:00)
空闲时段 周日 00:00-24:00;周一至周五 00:00-09:00、12:00-14:00、18:00-24:00;周六 00:00-24:00(Asia/Shanghai UTC+08:00)
下次切换 周一 09:00(Asia/Shanghai UTC+08:00)
现价(每百万 tokens)
未命中输入 ¥1.06
缓存命中 ¥0.0213
输出 ¥4.26
模型 anthropic/claude-sonnet-5
时段 无高峰/低谷,全天同价
单价(每百万 tokens)
未命中输入 ¥14.20
输出 ¥71.00
时段为什么按这个顺序排
高峰时段用你自己的时区,排在最前:这才是你"什么时候全价"要看的那一行。 官方页面写的是 UTC,你在东八区看到的就是 09:00-12:00、14:00-18:00。空闲时段是高峰的补集,同样用你自己的时区:官网只说"其余为空闲时段",不给钟点, 所以这一行由插件按高峰区间反推。星期一到星期五的高峰之外,周六、周日整天都算在内; 你就是 UTC 时也照样显示——它不会和上面那行重复。- 官网原文里的钟点不能直接拿来当空闲时段:官方那句话给的
01:00-04:00、06:00-10:00 UTC是高峰钟点,把它标成空闲时段就等于把贵的时段说成便宜的。出处仍然保留—— 设置面板的「官网原文」一行原样给出整句话。 下次切换带星期几:只写09:00会被读成"今天早上 9 点", 而它很可能是下周一。星期一到星期五的高峰,周五下午的下一次切换其实是周一。- 跨日会被算对:时区偏移会把窗口挪到前一天/后一天,甚至跨过当地午夜。
太平洋时间下的
01:00-04:00 UTC是周日 18:00-21:00,06:00-10:00 UTC会拆成 周日 23:00-24:00 + 周一 00:00-03:00——按天分列,而不是印一个需要猜的22:00-02:00。 - 半小时时区(如 +05:30)按半小时平移,不四舍五入到整点。
当前模型取本会话最新一轮所用的路由(中途换过模型时以最后一次为准); 费率按当前时段与所选币种实时换算。没有任何价格时显示**"未配置价格"**, 而不是一个看起来像真数字的 0。
这块读数由浏览器端自己算:宿主半也会在会话载荷里给出同样的描述,但宿主模块被 Node 的 ESM 缓存按 URL 缓存住,插件就地热更新时可能"浏览器半已经是新版、宿主半还是旧版"。 所以面板会在缺少宿主字段时,用价格表 + 会话轮次自己推导,读数不会因此变空。
币种
currency 选主币种(内置 30 种常用币种的符号与名称,实际可选任意三字母代码),
secondaryCurrency 可再并排显示一个,例如主显示人民币、括号里显示美元:
本会话累计 ¥2.49 ($0.35)
默认是「跟随账户」:币种取余额接口自己报的那个(官方账户是 CNY), 所以会话累计与账户余额天生同币种。余额读不到时退回官方币种 CNY。 在下拉里选一个具体代码就是固定币种;选回「跟随账户」即可恢复默认。
改币种时余额一起变:余额接口报的是账户自己的币种,面板显示的是你选的币种, 插件会按 per-USD 汇率表把余额换算过去——否则会出现「累计已经是美元、余额还写着 ¥」 的分裂状态。换算前会先看两者是否同币,同币时原样显示接口给的数字, 不做多余的乘除;余额那种汇率表里没有的币种则保留原币种金额与代码, 宁可标着原币种,也不给一个瞎换的数字。
汇率来自公开的 per-USD 汇率表(无需 API key,覆盖其提供的全部币种),
按 fxRefreshHours 刷新;取不到时退回配置里的 cnyPerUsd 兜底值,
不会把界面刷成错误状态。
模型与价格(含第三方渠道)
这一节讲的是配置文件里的能力。图形面板不提供费率编辑器(原因见上文), 这些字段直接写在
$DSH_HOME/plugin-cost.json里,或写进价格目录。
价格表按 provider/model 组织,支持三层通配:
| 键 | 作用 |
|---|---|
deepseek-official/deepseek-flash |
精确路由 |
deepseek-official/* |
该供应商下所有模型 |
*/deepseek-flash |
所有供应商下同名的模型 |
实际用到的路由如果不在表里,界面会明确显示**"未配置价格"**,而不是当成 0 ——宁可说不知道,也不给一个看起来像真数字的少算结果。 (同理,只公布了输入/输出单价、没公布缓存单价的模型,在发生缓存读取时也会标为未配置价格。)
倍率套餐
第三方渠道常见 某某模型 (1.20x) 这种倍率命名。在 routes 里把它指到一个已知路由上并
填倍率即可(provider/model 要按实际会话日志里出现的样子写,含空格与括号):
"routes": {
"my-relay/DeepSeek-V4.1-Flash (1.20x)": {
"use": "deepseek-official/deepseek-flash",
"multiplier": 1.2
}
}
也可以改用 models 直接写死单价(缓存命中 / 未命中 / 输出),优先级最高:
"models": {
"my-relay/Some-Model": { "cacheHit": 0.1, "cacheMiss": 1.25, "output": 10 }
}
自动更新
三条通道各自独立按间隔刷新,各自失败时保留上一次成功的数据:
| 通道 | 默认间隔 | 更新什么 | 来源 |
|---|---|---|---|
| 官方价格页 | 12 小时 | 模型列表、各桶单价、高峰时段 | api-docs.deepseek.com/quick_start/pricing |
| 远程价格目录 | 12 小时 | 新增供应商、新增模型、价格变动 | catalogUrl(见下) |
| 汇率 | 6 小时 | 各币种对美元汇率 | open.er-api.com/v6/latest/USD |
价格目录:改文件即可更新,不必发版
catalog/prices.json 是插件的带外更新通道:
{
"providers": {
"openai": {
"label": { "zh": "OpenAI", "en": "OpenAI" },
"docs": "https://platform.openai.com/docs/pricing",
"models": {
"gpt-某版本": { "cacheHit": 0.1, "cacheMiss": 1.25, "output": 10 }
}
}
},
"models": { "某供应商/某模型": { "cacheMiss": 2, "output": 4 } }
}
- 包内自带一份,离线也有价格;
- 把
catalogUrl指向网络上的同结构 JSON,插件就会按间隔自动拉取; - 默认值由
package.json的repository推导,fork 出来的仓库自动就是自己的更新通道, 源码里不写死任何用户名。
所以:官方新增模型、新增版本、调价——只需要更新这个 JSON,已安装的插件会自动跟上。
仓库自带的目录里,
openai与$DSH_HOME/plugin-cost.json的models/routes里直接填(设置面板不提供费率编辑器,原因见上文)。
模型下线交接
目录行可以写 retiredAt 与 billAs,到点自动改用目标路由的费率:
"deepseek-v4-pro": {
"cacheMiss": 1.32, "output": 3.96,
"retiredAt": "2026-09-14T04:00:00Z",
"billAs": "deepseek-official/deepseek-flash"
}
内置目录已按官方公告写好这条:V4 Pro 自北京时间 2026-09-14 12:00 起 由 V4.1 Flash 承接并按 Flash 价格计费,到点自动切换。
设置面板
点开会话累计 → 面板右下角「⚙ 设置」。分五块:
| 分区 | 内容 |
|---|---|
| 当前模型 | 只读:本会话在用哪个模型、它有没有高峰/低谷、此刻生效的单价(每项一行) |
| 币种 | 主币种(含「跟随账户」)、换算显示币种、自定义代码 |
| 高峰/低谷 | 跟随官网,或自定义时区/星期/区间/低谷倍率;显示官网原文与当前生效值(生效日 / 时区 / 高峰区间 / 本地时段各一行,都带时区) |
| 显示 | 账户余额、token 明细、高峰说明 |
| 自动更新 | 目录地址、三条通道的刷新间隔、立即刷新、各来源的新鲜度与错误(每条通道一行) |
写入走白名单校验:未知字段、越界数值、类型不符会被拒绝并给出原因, 不会写进配置文件。
面板不再提供逐路由的费率编辑器。那张表需要把整份费率回传才能保存, 一旦与手写的
routes/models冲突就会把它们覆盖掉;现在它是纯配置文件功能 (见下文),面板保存时完全不发送这两个字段,所以手写的表不会被碰。
配置
配置文件在首次启动时自动生成($DSH_HOME/plugin-cost.json,
Windows 上通常是 C:\Users\<你>\.dsh\plugin-cost.json)。
面板里能改的都在这里,手改文件同样生效:
{
"provider": "deepseek-official", // 默认跟踪哪家官方页
"currency": "", // 主币种;"" = 跟随账户(余额接口报的币种),否则填三字母代码
"secondaryCurrency": "", // 换算显示币种,"" 表示不显示
"schedule": null, // null = 跟随官网;对象 = 固定自定义时段
"offPeakRatio": 0.5, // 兜底低谷倍率
"cnyPerUsd": 7.1, // 汇率兜底值
"priceRefreshHours": 12, // 官方价格页刷新间隔
"catalogRefreshHours": 12, // 远程目录刷新间隔
"fxRefreshHours": 6, // 汇率刷新间隔
"catalogUrl": "", // 留空则只读包内目录;也可指向任意同结构 JSON
"balanceTtlSeconds": 3, // 余额缓存时长
"showBalance": true, // 关掉就不再查余额
"showTokens": true, // 是否显示 token 明细
"showPeakNote": true, // 是否显示高峰说明
"routes": null, // 路由定价映射(见上)
"models": null, // 直接指定的费率,优先级最高
"fallback": null // 未配置价格的模型:null = 显示"未配置价格"
}
刷新节奏
| 项 | 节奏 | 为什么 |
|---|---|---|
| 会话累计 | 3 秒 | 一轮结束后,provider 的用量一落盘金额就会出现 |
| 会话累计(日志已安静超过 45s) | 退避到 25 秒 | 没有新轮次时没什么可变的,避免空转 |
| 余额 | 每 3 次会话刷新读一次(≈9 秒) | 官方没有公开的余额接口频控,但通用 429 仍可能触发;9 秒一次约 6.7 次/分钟,属保守范围 |
| 页面切到后台 | 全部暂停 | 不可见时不做任何请求,切回来立刻刷一次 |
余额读取失败时会保留上一次的读数并退避 5 秒重试,不会把界面刷成错误状态。
为什么每轮花费不按"余额相减"算
余额只能给出时间点快照,而一轮对话往往是几十次并发/串行的模型调用:
- 快照落后于实际扣费,并发会话、子智能体、标题/压缩等辅助调用会混进同一次差值里;
- 一轮里的多次调用无法从两次快照中拆分出来,做不出"每轮";
- 缓存命中与未命中单价相差 50 倍,只有 token 分桶才能算准。
所以花费一律按 provider 上报的 token 精确计算:未命中输入、缓存命中、缓存写入、输出四个互不重叠的桶,各自乘对应单价,并按时段自动切换。余额仍然显示,但只当参考。
宿主接口
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /cost/session?id=<sessionId> |
整个会话日志折叠成每轮 token 与花费(含已滚出屏幕的历史轮次) |
| GET | /cost/balance |
代理官方余额接口,API Key 不下发到浏览器 |
| GET | /cost/prices?refresh=1 |
当前生效费率表、供应商、币种、时段、来源与新鲜度 |
| GET | /cost/config |
设置面板读取的配置与可选项 |
| POST | /cost/config |
保存设置(白名单校验) |
| POST | /cost/refresh |
立即刷新全部数据源 |
所有路由都走与 Host API 相同的签名 Cookie 鉴权(ctx.connection.requestRejection)。
读日志的策略:优先取进程内的活动会话(最新、无磁盘开销),否则通过 sessionPersistence 读该会话的持久日志,并只提交已落盘的轮次;正在进行的轮次在界面上标记"进行中",落盘后同一位置自动变成实际金额。
工作原理
lib/config.js 配置读写 + 校验边界 + 种子费率 + 路由映射展开
lib/cost-core.js 纯函数:时段解析、日志折叠成每轮用量、计价、价格表解析、币种换算
lib/index.js 宿主半:六条只读/写入路由、官方页/目录/汇率三条刷新通道
client/client.js 浏览器半:两个插槽座位,共用一个按会话共享的轮询源
catalog/prices.json 带外价格目录(可远程覆盖)
cordis.patch.yml 把插件行插入 DSH 组合层(自包含,装完即生效)
每轮金额是怎么定位到那一行的:收尾动作行只拿到一个 messageId,所以宿主在折叠时把每一轮的 assistant/message 消息 id 记进该轮(turn.messageIds),浏览器端再据此反查自己属于哪一轮。
开发
npm run check # 四个文件的语法检查
npm test # 全部:语法 + 离线 smoke + 宿主路由 smoke
npm run smoke # 计价/解析/目录/配置 + bundle 渲染
npm run smoke:host # 宿主半:五条路由注册、设置读写往返、会话折叠、余额降级
npm run verify:live # 核对线上实例是否加载了本插件并返回数据
测试是确定性的:夹具 scripts/fixtures/session-events.json 是手工编写的合成日志,
不需要本机跑过 DSH 就能完整通过。如果你本机有真实会话日志,smoke.mjs 会额外只读
折叠一次做交叉验证:
node scripts/smoke.mjs "C:\Users\...\session.v3.jsonl.zstd"
⚠️ 不要把真实会话日志提交进仓库。
scripts/fixtures/session-events.json是合成数据。 早期版本曾用最新真实日志覆盖它,等于把操作者的完整对话(系统提示词、提问、推理、 工具参数)写进了待提交文件;该行为已移除,测试脚本现在不写入任何文件,host-smoke.mjs也把$DSH_HOME指向临时目录。
verify-live.mjs 需要会话 id 才能核对会话路由:
node scripts/verify-live.mjs http://127.0.0.1:3080 <sessionId>
已知限制
- 官方价格页靠页面解析:官方改版会导致抓不到;此时保留上一张成功的表 / 包内目录, 界面会显示价格来源,不会静默算错。高峰时段同理,解析不到就退回内置时段。
- 只统计 provider 上报用量的请求:一轮里没有 usage 的调用(极少)不计费;
compaction/ 标题等辅助调用若不写进本会话日志,则不计入本会话。 - 进行中的轮次显示进行中:provider 的用量只在调用结束后上报,本轮金额在收尾时出现。
- 部分供应商价格需自行补齐:见上文"价格目录"——拿不到官方价的供应商不会猜。
- 第三方渠道的倍率套餐需要配一条路由映射,否则显示"未配置价格"。
发布到 GitHub
这个包是零依赖的纯 JS,仓库里没有构建产物需要生成。推到
https://github.com/xiaml666/dsh-plugin-cost 之后:
package.json的repository/bugs/homepage三处已经指向该地址;- 远程价格目录的默认地址会自动从
repository推导成https://raw.githubusercontent.com/xiaml666/dsh-plugin-cost/main/catalog/prices.json——所以以后要加供应商、加模型、调价,只改并推送catalog/prices.json即可, 已安装的插件会在下次刷新时自动跟上,不需要重新发版。
git init -b main
git add .
git commit -m "dsh-plugin-cost 1.2.5"
git remote add origin https://github.com/xiaml666/dsh-plugin-cost.git
git push -u origin main
仓库名必须是
dsh-plugin-cost,且默认分支为main,上面的默认目录地址才成立; 若用别的分支,请把catalogUrl一并改掉。
仓库自带的 GitHub Actions(.github/workflows/ci.yml)会在 push 与 PR 上跑
npm run check / npm run smoke / npm run smoke:host,不需要任何 secrets,
也不联网。
License
MIT
No comments yet. Be the first to write one.