oliblue-evan/dsh-usage-pill ↗★ 0
dsh-usage-pill
DeepSeek 用量与余额:会话 token 用量、按每笔实际发生时刻与模型计价的费用(峰谷/档位)、账户余额(账号授权优先,API Key 兜底) 适合需要免 API Key 实时查看单次会话精确花费的用户。
설치
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:oliblue-evan/dsh-usage-pilldsh-usage-pill 🧮
English quick start — A DSH plugin that shows session token usage, cost priced at each request's own time and model (peak/off-peak, per model tier), and your account balance in one pill below the composer. It needs no API key when you are signed in to a DeepSeek account (it reads the account balance through the harness's own account remote, falling back to
DEEPSEEK_API_KEY). It is a single-file client bundle with a dependency-free host half, and ships 50 assertions (npm test). MIT licensed. Install:plugin_manager(action: "install_bundle", target: "github:oliblue-evan/dsh-usage-pill"), then reload the page.
DeepSeek 用量与余额:输入框下方一枚徽标,实时显示本会话的 token 用量、按峰谷与 模型档位换算的人民币费用,以及账户余额。点开是四桶明细与计价口径。
和同类插件最不一样的一点:查余额不需要 API Key。 已登录 DeepSeek 账号的用户走宿主自己的账号授权(
ctx.remote.account.getBalance), 密钥全程不出现;只有「没登录、只有 API Key」的用户才回落到宿主路由。 据我看到的同类插件,真实余额普遍要求你自行配置 API Key。
截图
插件页 —— 标题、简介与图标按官方约定提供(package.json 的 icon + locale/*.json 的
meta.title / meta.description),栏目位置由 plugins.bundle.config 槽位决定:

弹层与胶囊 —— 输入框下方那枚胶囊,点开是明细、计价口径、峰谷倒计时与今日时段轴:

两张截图里的金额、余额、token 数均为示例值(真实数值已做打码处理);第二张底部显示 「按当前价估算」是宿主投影尚未加载时的降级提示,属预期行为,见下文「金额口径」。
🧮 108.0k tok · ¥0.05 · 余 ¥123.45
它和别家有什么不同
写之前把 GitHub 上同类插件翻了一遍(dsh-token-billing、
DSH-TOKEN-feiyong、
dsh-balance-stats、
dsh-plugin-balance、
dsh-deepseek-balance、
dsh-usage-balance 等)。
这批里功能最全的 dsh-token-billing 做得非常猛(仪表盘、预算、按路由计价、CSV 导出、
206 项测试),但它的兼容范围写的是 `dsh ,
timezoneOffsetSeconds: -new Date().getTimezoneOffset() * 60,
})
返回 `{ status:'ready', value: 充值钱包[], bonusWallets: 赠金钱包[] }`。授权 token 全程留在
宿主进程,浏览器只拿到余额数字;未登录时返回 `null`。
**兜底路径 —— 只有 API Key 的用户。** 账号返回 null 时回落到宿主路由
(`credentials.resolve('DEEPSEEK_API_KEY')` + 宿主 `fetch` 官方 `/user/balance`,
60 秒缓存、密钥不下发浏览器、错误按 `sk-` 脱敏)。
两条路径在面板里都标注来源(`账号` / `API Key`),都拿不到就如实说明原因,不编数字。
## 金额口径(这一节最重要)
显示的费用有**两种来源**,面板底部会写明当前用的是哪一种,绝不让估算冒充账单:
| 来源 | 何时使用 | 含义 |
|---|---|---|
| **实际发生额** | 宿主 `usagePillCost` 投影可用时(正常情况) | 每笔用量按**它自己那一刻**的峰谷、**它当时那个模型**计价后累加 |
| 按当前价估算 | 宿主投影不可用时(宿主未重启、投影被禁用) | 用累计四桶 × 此刻的价 × 此刻的时段反推,会在面板里标明「按当前价估算」 |
| — | 连 `tokenUsage` 都拿不到 | 显示破折号,而不是 `¥0`(`¥0` 会被读成"免费") |
**为什么不能只靠累计四桶算**:浏览器读到的 `tokenUsage` 投影只有累计值,没有"这一笔是什么时候、
什么模型花的"。用累计 × 当前价 × 当前时刻会犯两个错:
- **跨过峰谷边界后,历史花费被追溯改写** —— 高峰时段烧的那部分,到了空闲时段再看就按便宜价算了;
- **会话中途换模型后,整段历史按新档位计价** —— pro 的未命中价是 flash 的 4.5 倍。
所以宿主注册了一个 `usagePillCost` 会话投影(`lib/cost-projection.js` + `lib/cost-fold.js`),
折叠每条 `assistant/message` / `assistant/attempt`:**模型**取自它前面那条 `request/header`,
**时刻**取事件自身的 `time`。折叠口径与官方 `tokenUsage` 投影一致(同一 turn+step 后到替换先到),
并且 `llm/retry-started` 之后的重试**累加**(那是另一次真实计费调用)。
```text
费用 = 未缓存输入×miss + 缓存读×hit + 缓存写×miss + 输出×out (单位:元 / 百万 token)
- 峰谷:高峰 = 北京时间周一至周五 09:00–12:00、14:00–18:00(左闭右开), 高峰期单价 ×2;周末与中国法定节假日整天按空闲计价(官方按 UTC 判工作日, 所以调休补班的周六仍算周末)。
- 档位:模型名含
flash走 flash 价,其余走 pro 价;面板里显示真实模型 id, 识别不到时会明说「未识别到模型 · 按 pro 档计」,而不是给一句确信的档位。 - 价格政策时间线:按用量发生的时刻取段,历史回看不被新规追溯改写。
- 「缓存已省」= 命中的 token 若按未命中价计要多花的钱(按当前价算,面板里注明)。
为什么「缓存写入」对 DeepSeek 永远是 0
因为 DeepSeek 没有这个概念。 它的上下文缓存是服务端自动完成的:把前缀写进缓存
不额外收费、也从不回报,API 只给 prompt_cache_hit_tokens / prompt_cache_miss_tokens。
harness 的 TokenUsage 是provider 无关的,里面那个 cacheWriteTokens 取自
provider 适配器这一行:
const cacheWriteTokens = rawUsage.prompt_tokens_details?.cache_write_tokens || 0;
cache_write_tokens 是 OpenAI / Anthropic 那套显式缓存的字段(手动打 cache 断点、
按更高价买一次"写入"),DeepSeek 不返回它。
所以面板不写死"隐藏这一行",而是按数据判定:某个桶的 token 与金额都为 0 就隐藏
(client.js 的 visibleRows)。将来遇到真会回报缓存写入的 provider,那一行会自动回来。
四个桶全为 0 时显示「本会话暂无用量」,而不是留四行 ¥0。
界面
视觉与宿主原生弹层一致:结构与样式抄自 DSH 自带的会话统计弹层
(@deepseek-ai/dsh-client-ui-chat 的 StatsPills.module.css 与 stat-dialog.module.css),
类名用 um 前缀(避免与页面上其它插件冲突),只引用 --dsw-* token。
- 徽标:
[🌙] ¥5.00 · 余 ¥123.45—— 三段各说一件事,没有冗余:- 前置图标即时段:☀ 太阳=高峰(琥珀)、🌙 月亮=空闲(绿)。峰谷状态一眼可见, 不必再占一段文字。
- 当前花费:宿主自带的统计胶囊只报 token,不报钱。
- 账户余额:界面上其它地方都没有的数字。
token 用量刻意不放在徽标上(宿主的统计胶囊已经显示了,重复没意义),它只出现在面板里。
样式与旁边宿主的统计胶囊同一套(无边框透明胶囊、
label-tertiary、hover 上底色); 余额尚未取到时该段自动消失,徽标退化成[🌙] ¥5.00,不占空位。
- 面板(
createPortal挂 body +position:fixed,不会被输入框容器裁掉; 上方放不下自动翻到下方,滚动/缩放跟随,点外面或 Esc 收起)。 版式沿用宿主统计弹层的dt/dd两列(不另造三列表 —— 中间那列空档会把视线拉散, 还带出表格的厚重感),四组用细分隔线隔开:- 概览:
账户余额 ¥123.45(含赠送 ¥20.00)+ 一枚自绘的 ⟳ 刷新;缓存已省 ¥88.00(用 success 色,这是好消息) - 本会话花费(标题带单位
元 / 百万 tokens):四行缓存命中 / 未命中 / 写入 / 输出, 每行是金额 @单价—— 单价贴在它自己那行(¥4.00 @¥0.02),既不用三列表, 也不会像脚注那样被忽略;精确 token 数进 tooltip(正文放200,000,000 tok只会抢视线) - 时段:一行说清「现在什么价、还能用多久」:
空闲 · deepseek-flash · 10:34:31 后切换(倒计时部分自带 1 秒刷新;高峰时才追加· 高峰 ×2,空闲时不显示×1这种噪声); 下面接今日时段轴
- 概览:
- 只在异常时才解释:金额口径只有在「宿主投影不可用、金额是按当前价估算」时才占一行; 正常(逐笔实际计价)时口径说明只进总额的 tooltip。
- 中英文走 Client locale 服务;金额一分钱以上给两位小数(
¥0.85而不是¥0.8496), 不足一分才用 4 位(避免显示成¥0)。 - 计时很克制:倒计时单独成组件、只有它每秒刷新;面板其余部分 30 秒对齐一次峰谷; 页面在后台暂停。
时段判定、价目表、时段轴原先在独立的
peak-valley插件里,现已合并进本插件, 后者已删除 —— 一枚胶囊说清用量、费用、余额、当前时段与今日时段轴。
文件
| 文件 | 作用 |
|---|---|
package.json | bundle 清单(dsh.bundle.patch + dsh.client) |
cordis.patch.yml | 组合层,插入一行 usage-pill |
index.js | 宿主半边:注册 usagePillCost 投影 + 条件注册余额路由(同源校验、60 秒缓存、密钥脱敏) |
lib/pricing.js | 宿主计价内核(价目表、峰谷判定、桶映射、单笔计价、事件取用量) |
lib/cost-fold.js | 投影的纯折叠逻辑(可被普通 node 进程单测) |
lib/schema.js | 自带的最小结构校验器,替代 zod(原因见下) |
lib/cost-projection.js | 投影接线层(schema + ctx.sessionProjections.register) |
client.js | 全部界面:读投影、余额双链路、徽标与弹层 |
locale/en.json、locale/zh-CN.json | 插件页的标题与介绍(dsh-app-boot 的 readPluginMeta 读这个目录) |
assets/icon.svg | 插件图标(package.json 的 icon,相对路径、≤256 KiB) |
test/*.test.mjs | 50 项断言(npm test) |
无构建步骤,client.js 就是产物本身。 这也不是偷懒 —— 宿主把各插件的 bundle
拼接成一个 combo 脚本、以传统脚本执行(window.__ModuleLoader__ 处于 queue 模式,
只登记工厂函数),所以客户端半边必须是单文件,写 ES 相对 import 并不成立。
代价是文件较长,因此用「文件头目录 + #region 纯逻辑 标记」替代拆文件,
并由 test/structure.test.mjs 钉住这份约定。
插件页的介绍与图标
宿主插件页读 package.json + locale/,机制在 dsh-app-boot 的 readPluginMeta:
- 标题与介绍来自
locale/.json的{ meta: { title, description } }, 多语言字典合并成{ en, 'zh-cn', … }并按当前语言取用;缺字段回退到package.json。 locale/en.json是必需的:dictionariesOf只在它存在时才扫描同目录其它语言文件。- 图标是
package.json的icon:相对路径、SVG/PNG/JPEG/WebP、≤ 256 KiB、须留在包内。
这几个文件都按契约备齐了,并对解析链路做过两次实测:纯 Node 与
Electron 自带 Node(ELECTRON_RUN_AS_NODE,node 24.18.1,即宿主运行时)——
ModuleLoader.fromInternal() 可用、resolveSync('dsh-usage-pill/locale/en.json', …)
正确解析到本包,把 readPluginMeta 原样搬出来跑,输出是正确的双语标题/描述/图标。
两个坑,都记在这里
exports会把元数据挡在门外:原先只开.与./client。Node 的 ESM 解析器 在有exports时不会自动暴露package.json,未声明的子路径一律ERR_PACKAGE_PATH_NOT_EXPORTED;而readPluginMeta用的正是 Node 的解析器, 并把"不可解析"当作"资源缺失"→ 元数据整体 undefined,且不报任何错。 现在补上了./package.json、./locale/*.json、./assets/*,由test/packaging.test.mjs钉死(含iconOf的全部规则)。plugin_manager工具看不到meta:它的execute里显式把meta解构丢弃 (({ meta: _meta, ...row }) => …),所以用list_bundles无法判断宿主有没有 算出元数据 —— 我曾据此误判成"平台限制",是错的。
标题与简介只由宿主渲染一份。 元数据正常时,插件页头部显示
用量与余额 + 简介 + 图标。
中途为了在元数据不可用时也有介绍,我曾在设置卡顶部自己再渲染一份;等
exports修好、元数据恢复后,那段就变成同一句话连着出现两次,已删除。 教训:兜底内容要带"元数据可用时自动让位"的条件,否则修复真因之后它会变成新的问题。
设置
设置卡注册在 plugins.bundle.config(key 用包名),渲染在本 bundle 的插件页上、
描述与组件列表之间 —— 这是槽位文档给 bundle 配置指定的位置,官方 voice-input bundle
用的就是它。(plugins.item 是"一个官方命名空间一个伴侣包"的设置页,不是 bundle 该用的。)
卡片的开关照抄官方 @deepseek-ai/dsh-client-ui-primitives 的 Switch:它用的是
`` + 一个圆钮 span,尺寸 36×20、padding 2px、圆钮 16 + 位移 16,
关态轨道 --dsw-alias-border-l3、开态 --dsw-alias-brand-primary,圆钮用
--dsw-alias-switch-thumb / --dsw-alias-label-primary-foreground。
第一版我用的是 `` +
appearance:none,结果被宿主input[type=checkbox]的全局样式盖掉(元素+属性选择器特异性 0,1,1 > 类选择器 0,1,0), 变成"白底白钮、看不出开关状态"。官方的做法既避开了这个坑,role="switch"也才是 开关应有的无障碍角色。test/structure.test.mjs现在按官方这套数值与角色做断言。
五个开关,全部只影响展示:
| 开关 | 默认 | 效果 |
|---|---|---|
| 徽标显示时段图标 | 开 | 关掉则用通用图标代替太阳/月亮 |
| 徽标显示当前花费 | 开 | — |
| 徽标显示账户余额 | 开 | 取不到余额时该段本就自动消失 |
| 隐藏零值桶 | 开 | 如 DeepSeek 从不回报的「缓存写入」 |
| 显示「缓存已省」 | 开 | — |
三段全关会得到一个空胶囊,所以花费段有兜底:宁可无视"关掉花费",也不显示空胶囊。
为什么设置存在浏览器本地(localStorage)而不是宿主:这些都是展示偏好,不是宿主状态,
没必要走宿主;也因此宿主半边缺席时设置照常可用。值会逐项按默认值兜底、未知键丢弃
(normalizeSettings),所以旧版本残留或被手改坏的存储在下次读取时自愈。
为什么不给宿主声明 Config:官方那套 export const Config = z.object({...}) 里的 z
来自 @deepseek-ai/schemastery,而 link: 安装的插件解析不到裸模块名(见下一节)。
而且能进设置表单的是原生 schemastery schema(isNativeConfigSchema 要求
Symbol.for('schemastery') 等形状),自己糊一个只会变成 unsupported 状态。
所以本插件用同一个小节里的 slot 自持设置卡,宿主侧保持零依赖。
为什么宿主半边不带任何第三方依赖
宿主插件的裸模块名按插件的真实路径解析,而本插件是 link: 安装的
(真实路径在工作区),profile 的 autoInstallPeers 又是关闭的 —— 一旦某个裸模块
解析不到,import 会在模块加载期抛出,整个宿主半边起不来(不是降级,是硬失败)。
我最初在这里引入了 zod(沿用了社区插件的做法),后来才发现这个风险。
现在宿主半边只用相对路径 import,schema 由自带的 lib/schema.js 提供 ——
查过 @deepseek-ai/dsh-session-projection 的实现,框架对 schema 的全部要求
只是 .parse(value):不合法抛错、合法返回值。test/no-bare-imports.test.mjs 把这条钉死。
两个维护点
集中在 client.js 顶部:
CN_STATUTORY_HOLIDAYS—— 2026 年的法定节假日表(依据国办发明电〔2025〕7 号),按年扩表。PRICE_SCHEDULES—— 按生效时刻分档的价目表,官方调价时追加一条{ from: Date.UTC(...), ... }。
开发提示:改客户端代码为什么"看不到效果"
先确认宿主实际发出去的那份 bundle,而不是磁盘上的源码。 我在这里栽过三次:
改完 client.js、重启、刷新,页面纹丝不动 —— 因为 profile 里那份是物理副本。