dsh-cost
DSH(DeepSeek Harness)对话底部的费用显示插件。在对话下方的统计行基础上,展示实时累计费用,并把费用按每笔请求的实际发生时间与当时的模型分批准确计费,支持高峰 / 空闲 / 历史分时段、模型归属、读图金额与账户余额。
功能
- 按笔准确计费:逐请求按
发生时间 × 当时模型单价累加,而不是拿当前价套全程。 - 分时段计价(三段时间轴):
- 北京时间 2026-09-10 12:00 之前 —— 归入「历史」,按当时旧价(8-17 前 flat、8-17~9-10 旧峰谷价)计费。
- 2026-09-10 12:00 起 —— flash 系列降价:空闲时段 输入缓存命中 0.02 / 未命中 1.0 / 输出 4.0 元/M,高峰时段为空闲的 2 倍(0.04 / 2.0 / 8.0)。这一时段再分「高峰 / 低谷」。
- 高峰 = 工作日 9:00–12:00 / 14:00–18:00(北京时间),其余为空闲。周末一律空闲。
- 注:V4 Pro 请求后续会路由到 V4.1 Flash 并按 V4.1 Flash 单价计费,本插件在 2026-09-10 12:00 起对其亦按新 flash 价处理。
- 分时段明细卡(悬停/点开药丸查看):高峰 / 低谷 / 历史 三档,当前档加粗,底部按金额占比分段小条。
- 官方同款「药丸」外观:底部那一行沿用 DSH 自带统计行(
dsh-client-ui-chat的 StatsPills)的规格, 图标 14px、border-radius:24px、padding:1px 8px、gap:6px、13px 三级文字色、tabular-nums, hover 反色背景。药丸内容为 费用 · 账户余额 · 当前时段(高峰/低谷/历史),与官方统计药丸不重复 (官方那条给轮次/用时/TPS 与 token 总量/缓存命中率,本插件不再重复这些)。 参考取值:
图标是自绘的 ¥ 硬币(官方图标集/* 官方 packages/client/ui-chat/src/client/chat/StatsPills.module.css(哈希类名已换成 dsh-cost- 前缀) */ .root{max-width:var(--dsh-chat-content-width);width:100%;padding:4px calc(var(--dsh-composer-side-clearance) + 16px) 0; font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px)); justify-content:center;gap:12px;margin:0 auto;display:flex} .pill{border-radius:24px;padding:1px 8px;gap:6px;display:inline-flex;align-items:center;color:var(--dsw-alias-label-tertiary); font:inherit;font-variant-numeric:tabular-nums;background:0 0;border:none} .pill svg{flex:none;width:14px;height:14px} button.pill:hover,button.pill[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary)} .sep{color:var(--dsw-alias-separator-primary);margin:0 6px}ui-primitives里没有钱币/费用类图形,只有 gauge、database、clock、api 等), 按官方同一绘图规格:16 viewBox、纯描边、strokeWidth 1.25、currentColor、显示尺寸 14px。 官方自带的统计药丸继续保留,本插件只补一条费用药丸,不重复统计。 - 模型归属:按模型拆分调用次数与金额,多模型时自动分列。
- 读图金额:把会话里读图(视觉输入)的 token 单独记账。图片 token 按 DeepSeek 官方规则估算(进模型前自动缩放,单张上限 384 token,按官方计算器实测口径线性拟合,平均误差 ~5%)。
- 账户余额:药丸中段显示「余额 ¥X」,进会话先拉一次、之后每 5 分钟自动刷新,来源为官方
GET /user/balance。 点击药丸立即刷新:同时重取余额与费用明细(跳过 5 分钟定时与 15 秒节流),点击时硬币图标转一圈 (0.7s 线性、prefers-reduced-motion: reduce下自动关闭;每次点击用 React key 重新挂载 svg,保证动画重播), 刷新中带aria-busy="true"与title="刷新中…",明细卡里「余额」行会标注本次刷新时间(如「(02:45:28 刷新)」)。
安装
插件分为宿主半(Node 服务端)与客户端半(浏览器)。本包自带 bundle patch
(package.json 的 dsh.bundle.patch → cordis.patch.yml),不需要手写 profile 补丁:
dsh plugin --profile web add github:david0702/dsh-cost
然后重启 dsh 即可(客户端改动刷新页面即生效;宿主改动需要重启进程,宿主插件不会热载宿主代码)。
上一步在做什么:dsh plugin add 跑完 pnpm 后会自动核对已安装依赖——凡是声明了
dsh.bundle.patch 的包会被加进 profile 的 dsh.profile.bundles,成为一层 bundle patch;
本包的 patch 只插入一条插件自有条目:
# cordis.patch.yml(本包自带,无需手工复制)
- insert:
- id: dsh-cost
name: '@david0702/dsh-cost'
三个名字必须一致(最容易踩的坑)
DSH 的 client-modules 按包名(node_modules/<pkg>/ 的 <pkg>)查找客户端工厂,
而 lib/client.js 是自注册脚本 window.__ModuleLoader__.load({ id: "@david0702/dsh-cost", … })。
因此下面三者必须同名,本包已自洽为 @david0702/dsh-cost:
package.json的name- 安装目录名(
node_modules/<pkg>/) lib/client.js里__ModuleLoader__.load({ id })
不一致时会报 bundle … loaded without registering "@david0702/dsh-cost",
并连累整个初始批次(整页 “Failed to load plugins”)。不要把本包放进官方
@deepseek-ai/* 命名空间,也不要改 id 去迁就目录名——第三方插件只应使用自有 scope。
验证是否进组合(浏览器控制台):
__DSH_BOOT__.entries.map((e) => e.id).filter((id) => /cost/i.test(id))
// 期望输出 ["@david0702/dsh-cost"];空数组说明条目没进组合
开发
# 把工作区仓库直接链进 profile(pnpm link,改仓库文件即改插件,不产生副本)
dsh plugin --profile web add /path/to/dsh-cost
链入后仓库就是 profile 的实体,不会再出现"profile 副本与仓库漂移"。
改动生效范围(实测口径):
| 改动 | 生效方式 |
|---|---|
lib/client.js |
保存即热更:dsh-client-modules 监视客户端 bundle,内容变了就重算哈希、换 rev,前端重新拉取执行;刷新页面即可见,不需要重启 dsh web |
lib/index.js |
必须重启 dsh web(宿主插件不会热载宿主代码) |
package.json 的 dsh 段(patch / client / 兼容表) |
重启(profile 组合在启动时合成) |
README / docs / 版本号 |
只随提交走,不影响运行 |
日常循环:
- 在仓库里改代码(不要再往 profile 目录里改,也不要留
client.js.bak-*之类手工备份)。 - 自检:
npm test(槽位时序 + 上架契约),必要时node --check lib/index.js。 - 看效果:客户端改动刷新页面;宿主改动重启
dsh web。 - 版本与兼容:用户可见行为变了就提
version;只有真的跑过某个官方 DSH 版本的一次性 Profile,才把dsh.compatibility.dshReleases里那一项改成compatible,并在docs/store-conformance.md记录环境与步骤。 - 提交并
git push origin master(DSH STORE 每 8 小时自动复检,不需要在 Issue 里回复)。
不打扰日常实例的做法:日常 web profile 从 GitHub 固定提交安装,另建开发 profile 指向工作区
dsh --profile dev --from-default-profile web # 首启初始化
dsh plugin --profile dev add /path/to/dsh-cost
dsh --profile dev --port 3099 --no-open # 与日常端口并存
配置
无必填配置。API Key 走 DSH 的 credentials(ctx.credentials.resolve("DEEPSEEK_API_KEY")),用于拉取余额。
兼容性
- 依赖 DSH 的具体版本与约定:
- 客户端使用
conversation.composer.dock槽位、props.useProjection("tokenUsage")、props.useProjection("sessionStats")、props.modelDirectories。 - 宿主使用
ctx.webServer.register、ctx.credentials.resolve("DEEPSEEK_API_KEY")、Node 全局fetch。
- 客户端使用
- 逐版本兼容声明在
package.json的dsh.compatibility.dshReleases(对应官方发布逐项compatible/incompatible/unknown), 实测口径与证据记录见docs/store-conformance.md:只有真实跑过一次性 Profile 安装/启动/卸载的版本才标compatible(当前为0.1.5-rc.1),未验证的一律unknown。 宽泛范围(dsh字段)只作参考,不构成"可安装证据"。 - 槽位注册必须走
ctx.slots.inject(slot, cb)(DSH 0.1.5+):conversation.composer.dock由client-ui-conversation的 composer-bar 条目在其 children 表里声明, 只在该条目挂载期间存在;直接调ctx.slots.register会抛slot "conversation.composer.dock" is not declared (a parent entry's children table must declare it), 且该异常会顶着宿主 loader 条目名上报(表现为整页 “Failed to load plugins” 的报错块)。slots.inject会把注册推迟到声明提交之后,声明消失时自动注销、恢复后重注册。 离线自检:npm test(scripts/slot-inject-check.mjs,模拟“先 apply、后声明”的时序)。 lib/client.js里__ModuleLoader__.load({ id })必须与 loader 条目解析出的模块 id 一致 (即 profile 里node_modules/<pkg>/的目录名)。对不上时 client-modules 会报bundle ... loaded without registering "<pkg>",且会连累整个初始批次,表现为整页 “Failed to load plugins”。 本包三处名字统一为@david0702/dsh-cost(见上节),不要为了让目录名迁就官方 scope 而改id。- 定价与高峰时段常量写死在
lib/index.js的RATES/PEAK_EPOCH_UTC,按官方发布更新。 - 实际 token 数以模型接口返回为准;
imageTokensOf与卡片金额为 DeepSeek 估算口径(官方说明:估算值,以接口返回为准)。
License
MIT
No comments yet. Be the first to write one.