DSH Usage Stats(dsh-usage-stats)
English | 中文
DSH 用量统计插件:在 设置 中新增「使用统计」页 —— 累计/峰值、连续天数、近一年热力图、各模型按天曲线、各模型按月堆叠柱状图。API 请求次数与 token 合并呈现,不再需要切换指标。web profile 与 desktop(DeepSeek Harness 桌面端)profile 通用。
功能
token 与请求次数并列,不做指标切换
token 是主指标:统计条的数值、热力图的着色、两张图表的纵轴都用它。
请求次数作为并列信息出现在图表 tooltip 中(1.2万 tokens · 16 次),
在具体日期与模型的上下文里精确对照,不占据顶部版面。
这样划分的原因:token 表达「消耗了多少」,请求次数表达「调用了几次」,后者更适合以精确数字
呈现在具体上下文里;把两者都做成可切换的主视图会让人对「当前看的是哪套口径」产生误判。
统计条
顶部是一条单卡统计条:竖向发丝线把五个数据位分开,大数值在上、灰标签在下,
一眼读完全部规模与节奏。token 数值用中文数量级(万 / 亿)缩写:
| 数据位 | 数值 |
|---|
| 累计 Token 数 | 累计 token |
| 峰值 Token 数 | 单日最高 token |
| 今日 Token 数 | 今日 token |
| 当前连续天数 | 天 |
| 最长连续天数 | 天 |
请求次数不再占据卡片副行——精确数字退到图表 tooltip,在具体上下文里才更有意义;
顶部只留一眼可读的规模与节奏。
近一年热力图(GitHub 贡献图风格)
- 着色用 DSH 主题里的 DeepSeek 蓝色阶(
--dsw-static-deepseek-100/300/450/500),
与品牌蓝同源而不是自造色;空档用主题分层底色 --dsw-alias-bg-layer-2
- 格子尺寸放大(15 设计单位 + 4 间距),一年一屏呈现且不出现横向滚动
- 横轴显示月份标签(每月起始位置,自动防重叠)
- 悬停方块显示「日期 · token · 请求次数」tooltip(与 DSH 原生 Tooltip 视觉一致)
明暗适配挂在 body[data-ds-dark-theme] 上(DSH 用该属性开关暗色,而不是
prefers-color-scheme 媒体查询),因此应用内切换明暗时热力图会正确跟随,
即使应用主题与系统主题不一致。
各模型曲线图(按天,近 7 / 30 / 90 天)
- 每个模型一条平滑曲线(按 token;单调三次样条插值,曲线过每个数据点且不超调);数据点节点仅悬停时显示(对齐悬停日参考线与 tooltip)
- 悬停显示当日分模型明细 + 合计,每行同时给出 tokens 与请求次数
各模型堆叠柱状图(按月,近 6 / 12 / 24 个月)
- 每根柱子一个自然月,柱内按模型堆叠(按 token);悬停显示「月份 + 分模型 tokens · 次数 + 合计 + 环比」
- 图例显示每个模型在窗口内的 token 总量与占比,点击图例可临时隐藏/显示某个模型
- 月度概览卡:窗口累计、本月(含环比与请求次数)、单月峰值、活跃月均值
- 月份轴定长连续,无数据的月份留空但不塌陷,柱宽随窗口自适应
明暗主题自适应 · 遵循 reduced-motion
安装
desktop(DeepSeek Harness 桌面端)
# 方式一:官方 CLI
dsh plugin --profile desktop add @kindred7/dsh-usage-stats -w
# 方式二:从本仓库直接链接(开发用,改代码即生效)
node scripts/install-profile.mjs desktop
web
dsh plugin --profile web add @kindred7/dsh-usage-stats -w
dsh web # 重启生效
从 GitHub 安装指定版本:
dsh plugin --profile desktop add github:kindred-7/dsh-usage-stats#v0.4.7 -w
卸载:
dsh plugin --profile desktop remove @kindred7/dsh-usage-stats -w
安装后重启 DSH(桌面端直接退出重开;web 端 Ctrl+F5 硬刷新一次)加载新插件脚本。
桌面端排查:装好了却在设置里看不到入口
DSH 0.2.0 起对 profile bundle 增加了兼容性闸门:插件的 package.json 里声明的
peerDependencies(仅看 @deepseek-ai/dsh / @deepseek-ai/dsh-*)若不满足当前运行时版本,
宿主会把整个 bundle 静默跳过 —— 它的 cordis.patch.yml 永远不会被套用,
于是「设置」里连入口都不出现(不报错、不弹窗,只有启动 stderr 会打印一行
skipping profile bundle ...)。
本项目把范围写成 >=0.1.1-rc.2 <0.3.0,同时覆盖 0.1.1-rc.2(web)与 0.2.0-rc.2(桌面端)。
自检脚本会用打包版 app.asar 里的真实闸门代码跑一遍判定:
# 1) 解出打包版 app-boot 及其依赖(只需一次)
node scripts/asar-extract.cjs "$env:TEMP\asar-pkg" @deepseek-ai/dsh-app-boot
# 2) 判定 desktop profile 到底加不加载本插件
node scripts/verify-desktop-load.mjs "$env:TEMP\asar-pkg"
判定项包括:兼容性闸门通过、resolveBundleDir 能定位到插件、patch 用的是 insert: 形式、
dsh.client.platform === "web"、bundle 出现在已加载层级中而不在 skippedBundles 里,
以及组合后的 entry 列表确实包含 ui-usage-stats。
历史接口的版本兼容
当前宿主(0.2.x)的会话历史 RPC 是 session/page,payload 为
{ args: { request: { address: { kind: "session", sessionId }, throughSeq, beforeSeq?, maxMessages? } } },
结果字段为 records[].event。注意 throughSeq: -1 语义是「空页」而非「最新」,
必须先调 session/projections 取 asOfSeq 作为翻页游标。
更早的宿主分别是 session-controller.history(0.2.0-rc 命名,payload { address, throughSeq, ... },结果 records[].event)
与 session.history(payload { sessionId, beforeSeq? },结果 events[])。
插件在首次对账时按「新 → 旧」探测可用契约并缓存,各版本都能正确折叠用量(见 tests/history-rpc.cjs)。
数据口径与隐私
- Token 数 = 每条 assistant 回复的 provider 上报用量(input + output + cache 读 + cache 写;reasoning 已含在 output 内,不重复计)
- 请求次数 = 一条带 usage 的 assistant 回复计 1 次,即一次 provider 请求。与 token 在同一次折叠中累加(同一事件、同一水位线),因此两者不可能互相漂移;无 usage 的事件只推进水位线,不计请求
- 模型归属取该请求日志记录的
source.model,同会话中途切换模型时按各自请求分别计入
- 月度视图按本地自然月折叠日账本:月份边界即日志所在自然日所属的月份,不做额外时区换算
- 数据仅存于本机浏览器 localStorage,不上传任何服务器
- 账本按会话水位线增量累计:打开设置页时枚举所有会话并从日志逐个对账回填(含历史)
升级说明(v0.3 → v0.4)
v0.3 的账本没有分模型请求数,且原存储键与新结构不兼容。v0.4 改用新键
dsh-usage-stats/ledger/v3:旧键原样保留不被读写(便于回滚),新账本从零开始,
由会话历史 RPC 对账自动重新折叠历史用量。升级后首次打开设置页时数字会从 0 逐步回填,
不会丢失日志里的历史。
为什么「没有数据」
用量折叠自每个会话日志里的 assistant 回复(provider 上报的 usage),不是从空中来的。
没有数据时分三种情况,页面底部会直接告诉你是哪一种:
| 诊断显示 | 含义 | 处理 |
|---|
| 会话列表 N 个 / 已扫描 N 个,但 tokens = 0 | 扫描成功,但这些会话确实没有带 usage 的回复 | 正常,还没产生过用量 |
失败:HTTP 404 | 历史 RPC 端点不被当前宿主接受(见下方「两个宿主契约」) | 用与宿主匹配的端点分段 |
失败:HTTP 401 | 采集请求被鉴权拒绝 | 重启应用重新建立会话 |
日志游标不可用:… | session/projections 没返回 asOfSeq,session/page 只能翻出空页 | 这是读不到,不是没有用量 |
| 一直停在「正在后台枚举会话…」 | 列表 / 回填还在跑 | 点「重新扫描」 |
关键点一:过去版本只对账当前打开的会话,历史会话的用量不会被累计,除非逐个打开它们 ——
这正是「面板一直是空的」的原因。现在打开设置页会后台枚举并扫描所有会话,
按水位线去重,重复打开不会重算。
关键点二(0.2.0 之后的真实故障):插件曾只试 session.history /
session-controller.history 这类单段端点,而当前宿主的 gateway 通道只接受
命名空间/方法 两段,单段一律 404 —— 于是每一次翻页都失败,面板永远是空的。
工作原理
| 槽位 | 作用 |
|---|
conversation.composer.dock | 隐形采集器(渲染 null),探测会话快照变化并触发日志对账 |
settings.section | 「使用统计」设置页(打开时触发全量回填) |
数据来自两条互补路径:
- 全量回填(打开设置页):会话列表 RPC 分页枚举所有会话,逐个
reconcileSession,
把历史会话的用量一次性折叠进账本。
- 增量对账(会话运行时):composer 采集器探测到快照里出现超过水位线的 assistant
消息时,防抖触发该会话的日志对账,把新回复折叠进来。
对账通过会话历史 RPC 向后分页拉取完整日志,逐条折叠 assistant/message 事件
(usage / 时间 / source.model),以事件 seq 为水位线去重。日志不可读时留下诊断信息,
等下次触发再试。
会话历史 RPC 的两个宿主契约
插件按「新 → 旧」顺序探测一次并按会话缓存。两者的差异不只是方法名:
| 当前宿主(Typert gateway) | 旧宿主(api-proxy) |
|---|
| 端点 | session/list、session/page(命名空间/方法 用 / 分段) | session.list、session.history(用 . 分段) |
| 请求体 | { args: { request: … } },键名是形参名 | 请求对象本身 |
| 列表参数键 | _request | — |
| 结果字段 | { records: [{ event }] } | { events: [event] } |
端点分段是这里最容易踩空的地方:gateway 的 /api 通道只接受恰好两段
命名空间/方法,单段方法名(例如 session.history)直接 404,于是所有候选都失败、
面板永远是空的。
session/page 还要求 throughSeq 是日志里真实存在的 seq,传 -1 会被当成空日志、
翻出一页空记录。插件因此先用 session/projections 取 asOfSeq 作为游标(每个会话只取一次)。
取不到游标时,页面底部会显示「日志游标不可用:…」,因为那是读不到,不是没有用量。
session/list 也会列出子代理会话,而 session/page 对它们要求持久化父地址
{ kind: 'subagent', parentSessionId, childSessionId, mode };只传普通会话地址会被
session/agent-busy 拒绝。mode 填 "unknown"(调用方只知道父子身份、不知道委托模式),
填具体模式反而会被 subagent/unauthorized 拒绝。
月度统计不新增采集链路:computeMonthlyUsage(months) 直接把 ledger.days["YYYY-MM-DD"].byModel
累加进 YYYY-MM 桶,因此日/月两个视图的口径必然一致(测试里对此有断言)。
开发
node --check lib/client.js # 语法检查
npm test # 全部测试
node tests/harness.cjs # 日账本折叠 + 月度聚合一致性
node tests/monthly-harness.cjs # 月度聚合专项(月份轴/窗口/其他归并/环比)
node tests/client-smoke.cjs # 槽位注册 + 渲染冒烟(含堆叠段不重叠)
node tests/history-rpc.cjs # 历史 RPC 的双宿主契约 / 游标诊断 / 子代理地址 / 全量回填
node tests/resolve-check.cjs # 各 profile 包名解析
node tests/import-check.mjs # 宿主半边可导入
lib/client.js 是浏览器端插件主体;lib/index.js 是宿主半边(空实现,仅让 client-modules 扫描到 dsh.client 声明);cordis.patch.yml 声明宿主侧装配;scripts/install-profile.mjs 负责把插件链接进指定 profile。profile 里的安装位是指向本目录的 Junction,改代码即生效。
License
MIT