Skip to content
dsh.fish
Bundle

dsh-usage-dock

Per-session CNY cost pill in the composer dock: rides the shipped stats row and expands into the token buckets and both tariff bounds.

Source
aliensweety
License
MIT
Updated
Updated 14 hours ago

Readme

# 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 控制台执行(对所有已计价模型生效,均填高峰价):

```js
localStorage.setItem('dsh-usage-dock.pricing', JSON.stringify({ miss: 2, hit: 0.04, out: 8, write: 0 }))
```

删除该 key 即恢复内置价格。

## 数据来源

只读 DSH 内置的会话投影 `tokenUsage` 与 `modelSelection`,**不读会话日志文件、不发网络请求、不碰任何凭证**。
`tokenUsage` 由 provider 上报的用量累加而成,整段会话一个值,带 seq 单调保护,所以它是权威值而非本地估算;它的线上视图是扁平四桶:

```ts
{ 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` 也限制在视口内)。

## 安装

```powershell
# 从 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 只能确认服务活着、证不了插件对不对。

1. 强刷界面(Ctrl+F5)。
2. 看输入框下方那排统计药丸的**右侧同一行**是否出现 `¥0.xx`(空白会话不渲染)。
3. 点开明细,用 DSH 自己的投影缓存核对四个桶与费用:

   ```powershell
   $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 / cacheWriteTokens
   ```

4. DevTools → 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`。

## 卸载 / 回滚

```powershell
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 注册路径:

```powershell
node .verify\render-check.cjs     # 期望 ALL GREEN (pass=86 fail=0)
```

覆盖:注册契约、纯金额收起态、展开面板、provider 守卫、价格覆盖(含坏 JSON)、按模型匹配价格、命中率取整护栏、空/缺字段/无 hook 的退化输入、**portal 目标识别与退化(含官方那排消失时的回退)**、中英双语文案。

Install

dsh plugin --profile web add github:aliensweety/dsh-usage-dock

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source