dsh-usage-dock
在输入框下方那排统计药丸(官方 Session 信息 + Token 用量)后面并排追加一颗只显示金额的药丸,样式与官方药丸一致、三粒作为一组居中:
[ Session 信息 ] [ Token 用量 ] [¥ 0.47]
点开是明细:未缓存输入 / 缓存读取 / 缓存写入 / 输出 / 总计 / 高峰价与空闲价两档费用 / 缓存命中率。
官方那排已经在显示 Session 与 Token 用量了,所以这颗药丸不再重复这些数字:收起态只有一个金额,其余信息放在 tooltip 与展开面板里。
费用估算口径
- 价格来源:DeepSeek 官方定价页(人民币/百万 tokens,高峰价)——
deepseek-flash输入未命中 ¥2、缓存命中 ¥0.04、输出 ¥8;deepseek-v4-pro未命中 ¥9、缓存命中 ¥0.30、输出 ¥27。空闲档一律为高峰价的一半。 - 模型匹配:读
modelSelection投影的lastUsed.model,id 含pro按 v4-pro 计价,其余按 flash 计价。 - 只对
deepseek-official路由计价:lastUsed.provider是别的路由时整颗药丸不渲染。用 DeepSeek 价目表去算 Claude/GPT 的 token 会得出一个"很确定的错数字",宁可沉默。 - 药丸上的金额:按查看时刻所处时段自动取高峰或空闲档;面板里两档始终并列展示,所以跨时段的会话其真实费用介于两者之间。
- 缓存写入不计价:官方价格表没有单独的缓存写入价(本机数据里 cacheWrite 也恒为 0),字段保留、默认 0。
价格与时段(已核对,截至 2026-09-12)
lib/client.js 里的 PRICING / isPeakNow() 与官方口径一致:
| 模型 | 档位 | 缓存命中 | 未命中 | 输出 |
|---|---|---|---|---|
deepseek-v4-pro |
高峰 | 0.30 | 9 | 27 |
deepseek-v4-pro |
空闲 | 0.15 | 4.5 | 13.5 |
deepseek-v4.1-flash(含 deepseek-flash) |
高峰 | 0.04 | 2 | 8 |
deepseek-v4.1-flash |
空闲 | 0.02 | 1 | 4 |
单位:元/百万 tokens。峰谷时段:北京时间工作日 9:00–12:00、14:00–18:00 为高峰,其余(含夜间与周末)为空闲、按高峰价的一半计。
关键点:V4 Pro 独立计费——它继续调用 V4 Pro 本身,不会路由到 V4.1 Flash(原定 2026-09-14 12:00 的路由计划已于 09-11 取消),所以两者的价目表不会互相套用。这也是本插件按 modelSelection.lastUsed.model 取价、而不是按"当前默认模型"取价的原因。
价格调整后无需改代码,在 Web 控制台执行(对所有已计价模型生效,均填高峰价):
localStorage.setItem('dsh-usage-dock.pricing', JSON.stringify({ miss: 2, hit: 0.04, out: 8, write: 0 }))
删除该 key 即恢复内置价格。
数据来源
只读 DSH 内置的会话投影 tokenUsage 与 modelSelection,不读会话日志文件、不发网络请求、不碰任何凭证。
tokenUsage 由 provider 上报的用量累加而成,整段会话一个值,带 seq 单调保护,所以它是权威值而非本地估算;它的线上视图是扁平四桶:
{ uncachedInputTokens, outputTokens, cacheReadTokens, cacheWriteTokens }
样式、位置与"明细放哪"
样式:与官方药丸逐条对齐——padding:1px 8px、border-radius:24px、gap:6px、--dsw-alias-label-tertiary 文字色与 --dsw-alias-interactive-bg-hover 悬停底色、font:inherit / line-height:inherit;宽屏带一颗 14px 图标,窄屏(≤640px)图标让位给一个 ¥ 字符,把宽度留给三个药丸。
位置:composer 下方那个 dock 是一个 column flex 容器(flex-direction:column; align-items:center),slot outlet 自己带 display:contents,所以第二个条目天然会另起一行;slot 契约里也没有"加入官方那一行"的入口(官方那颗 StatsPills 是自带排版的完整条目,也不声明子 slot)。
所以本插件用 ReactDOM.createPortal 把药丸直接渲染进官方那一排的 DOM 里(官方药丸的浮层也是用同一个 API):
- 官方那排是
display:flex; gap:12px; justify-content:center,药丸进去后自动拿到同样的 12px 间距、并和它们一起居中——不需要平移、不需要改官方任何元素、不需要测量像素; - 靠一个
display:none的宿主节点留在 outlet 里,只用来找那一排(data-composer-stats标记);每次提交都重新解析,composer 重挂载换了节点、或那一排消失时会自动跟上; - 退化行为:找不到那一排(或宿主没提供
react-dom)时,药丸退回"自己一行居中"的原状。 - 药丸带
flex:none:窄屏上被压缩的是官方那两粒(它们本来就会 ellipsis),本插件这一粒金额始终完整可读。
明细放在官方 token 用量卡里:点官方那粒 21.1M tok · Cache hit 99% 打开的卡片,本来就已经列出四个 token 桶;本插件把那两行费用(高峰价 / 空闲价)和一个估算说明追加进那张卡的同一个 <dl>:
- 那张卡的行是
dt/dd对,而它的样式是元素选择器(.xxx_details dt/.xxx_details dd{display:grid;…text-align:right}),所以追加进去的<dt>/<dd>自动拿到和官方行完全一致的排版与配色,不需要写任何上游类名; - 检测方式:那一排里最后一颗
button[aria-haspopup="dialog"]就是官方 token 用量药丸,它aria-expanded="true"时出现的div[role="dialog"]就是它自己的卡(官方一次只开一张卡);MutationObserver盯document.body的childList,卡片一出现就写入,卡片还开着时数值变化就地更新; - 点本插件这颗药丸 = 直接打开那张官方卡片(没有自己的浮层,所以窄屏不会再溢出);
- 写入失败(上游改了结构、宿主没给
react-dom、或那一排/那粒药丸不存在)时:药丸的金额照常显示,明细退回自带的兜底面板(该面板只在对齐到屏幕外时才翻转贴边,max-width也限制在视口内)。
安装
# 从 GitHub 安装(推荐锁定 tag 或 commit)
dsh plugin --profile web add github:aliensweety/dsh-usage-dock#v0.4.0
# 本地开发(link 安装,改完即生效)
dsh plugin --profile web add D:\Ai\dsh\dsh-usage-dock
dsh plugin add 会把该包写进 profile 的 dependencies 与 dsh.profile.bundles(CLI 内含 bundles 与已安装依赖的对账逻辑),无需手工编辑 package.json。
profile 的 patchReload 是 live,通常热挂载;若没生效,重启 dsh web 并强刷浏览器。
验证
必须在上装的浏览器里验证:DSH Web 有信任围栏,从别的 shell 直接请求 /、/api/*、/plugins/* 会拿到 401(或鉴权后的 404),curl 只能确认服务活着、证不了插件对不对。
强刷界面(Ctrl+F5)。
看输入框下方那排统计药丸的右侧同一行是否出现
¥0.xx(空白会话不渲染)。点开明细,用 DSH 自己的投影缓存核对四个桶与费用:
$s = Get-Content "$env:USERPROFILE\.dsh\storages\session_projcache\sessions\<session-id>.json" -Raw | ConvertFrom-Json $s.record.rows.tokenUsage.val.totals # uncachedInputTokens / outputTokens / cacheReadTokens / cacheWriteTokensDevTools → Network:应看到
/plugins/dsh-usage-dock/client.js为200。 Console 若出现[dsh-usage-dock] slots service unavailable,说明没拿到 slot 服务。
排查顺序:dsh --profile web --dump-config 看 dsh-usage-dock 是否在层栈里 → 不在就重跑 dsh plugin --profile web add。
卸载 / 回滚
dsh plugin --profile web remove dsh-usage-dock
设计要点与已知限制
- 失败安全:
useProjection缺席、投影未到达、四桶全为 0(空白会话)、或路由不是deepseek-official时渲染null,不留空壳、不报错。 - 不改官方元素:药丸是 portal 进官方那排的,插件不移动、不重设官方那排的位置或样式,也不碰它的数据;找不到那排时退回自己一行,卸载即消失。
- 它是估算值:会话中途换过模型、或跨高峰/空闲时段时,真实账单介于面板两档之间;药丸只按当前时段显示其中一档。
- 档位不会自己刷新:金额在重渲染时计算(新用量到达即重算),没有定时器,所以闲置会话跨过时段边界后药丸会短暂停留在旧档位。
- 金额不谎报零:
fmtMoney自动加小数位,费用非零时绝不显示¥0.00。 - 药丸上的数字不带货币符号:
¥只出现在那颗 14px 图标上(官方药丸也是"图标 + 数字"的排版);面板与 tooltip 里仍带¥,因为那里没有图标可依赖。 - 命中率口径:
cacheRead / (uncachedInput + cacheRead + cacheWrite),与官方cacheHitPercent同一口径(分母是提示侧计费输入,不含输出);整数四舍五入到 100% 时会自动加精度,绝不把未命中说成 100%。 - 文案按浏览器语言(
zh*→ 中文,否则英文),没有走 DSH 的 locale seat。 - 只报消耗,不报余额:本地不存在剩余额度数据,会话投影里也没有。
- 数字口径:
总计= 四桶之和,含 cacheRead,所以它明显大于"计费输入"——看成本时别拿它乘输入价。 - 无 React 依赖打包:客户端 bundle 走宿主注入的虚拟
react(require('react'))。
开发自检
.verify/render-check.cjs 是一个零依赖的自检脚本(自带最小 React 垫片、DOM 桩与内联 fixture——一串真实会话投影值),会加载真实客户端 bundle、走真实 slot 注册路径:
node .verify\render-check.cjs # 期望 ALL GREEN (pass=86 fail=0)
覆盖:注册契约、纯金额收起态、展开面板、provider 守卫、价格覆盖(含坏 JSON)、按模型匹配价格、命中率取整护栏、空/缺字段/无 hook 的退化输入、portal 目标识别与退化(含官方那排消失时的回退)、中英双语文案。
No comments yet. Be the first to write one.