xinmo114514/dsh-usage-widget ↗★ 2
dsh-usage-widget
DSH web plugin: 用量统计悬浮窗(token 用量曲线/热力/圆点,窗口与圆点均可拖动;宿主半扫描会话日志并提供 /usage/api/snapshot) 适合需在 DSH 中本地查看 token 用量统计的用户。
同名パッケージの別リポジトリ
インストール
検証済み bundle がないか、互換性チェックに失敗しています。先にリポジトリの説明を読んでください。 README 全文を読む ↗
ドキュメント
README 全文を読む ↗dsh-usage-widget
DSH(DeepSeek Harness)Web 插件:Token 用量统计悬浮窗
一个常驻页面右上角的悬浮小窗:实时展示你的 DSH 会话消耗了多少 tokens —— 有可拖动的窗口、可拖动的圆点、曲线图、热力图、今日/累计大数字,全部本地聚合、不上传任何数据。
目录
- 1. 这是什么
- 2. 功能特性
- 3. 界面与交互说明
- 4. 系统架构
- 5. 数据口径与统计规则
- 6. 安装(把它装进你的 DSH)
- 7. 构建与开发
- 8. API 契约(客户端 ↔ 宿主)
- 9. 故障排查 FAQ
- 10. 卸载
- 11. 已知限制
- 12. 许可证
1. 这是什么
dsh-usage-widget 是 DSH 的持久化 Web 插件(dsh.client 双面包:宿主半运行在 Node 进程里,客户端半打包进浏览器)。
- 为什么是"持久化":它挂在 web profile 的组合配置里,随
dsh web启动自动加载 —— 不需要每次批准、不随会话/重启消失。对比"动态插件"(会话内临时定义),这是"装一次,永远用"。 - 它统计什么:所有会话日志中
assistant/message事件的data.usage(input/output/cacheRead/cacheWrite/reasoning tokens),按会话 + 按本地日聚合。 - 数据去哪了:全部留在本机进程内存里,只有浏览器页面从宿主自己的
/usage/api路由拉取聚合结果。不调用任何第三方 API、不发送任何数据。
2. 功能特性
| 特性 | 说明 |
|---|---|
| 🪟 窗口可拖动 | 整窗任意位置按住拖动(按钮除外);置顶状态下拖动自动取消置顶并跟随指针 |
| 🔴 圆点可拖动 | 最小化后变成圆点,圆点本身可自由拖动;点击圆点恢复窗口,拖动圆点只移动位置 |
| 📌 置顶 | 一键置顶到右上角固定位置(会话头部下方);再点取消 |
| 🏠 最小化 | 一键收起为圆点(圆点实时显示"今日 tokens"小数字 + 迷你走势线) |
| 🔢 总 tokens 大数字 | 窗口左下角常驻显示全部会话全时段累计总量(千分位大数字) |
| 📈 曲线图 | 近 7 天 / 2 周 / 1 月 / 全部,平滑贝塞尔曲线 + 渐变面积 + 悬停明细 |
| 🔥 热力图 | 同范围切换热力视图,按日相对强度分级着色 |
| ⚡ 实时刷新 | 每 4 秒拉取一次快照;扫描完成前显示"扫描中…" |
| 💾 状态记忆 | 模式(窗口/圆点)、置顶、位置保存在 localStorage,刷新不丢 |
| 🌗 深色模式 | 跟随页面 data-theme 自动切换深浅配色 |
| 🩺 自愈扫描 | 每 60 秒增量重扫一次,水位去重不重复计数;某会话临时不可读时自动计数并在恢复后清除提示 |
3. 界面与交互说明
3.1 窗口模式(默认)
┌────────────────────────────┐
│ 用量 [📌] [─] │ ← 标题栏(也是拖拽区;按钮区不触发拖动)
├────────────────────────────┤
│ 全部会话 [全部会话][当前] │ ← 范围切换(全部会话 / 当前会话)
│ 输入 输出 缓存命中 │ ← 当前范围小计卡片
│ 调用 N 次 · 缓存读 M │
│ [7天][2周][1月][全部] │ ← 时间范围 chips
│ [曲线] [热力] │ ← 视图切换
│ ┌──────────────────┐ │
│ │ 曲线/热力图 │ │ ← 悬停显示当日明细 tooltip
│ └──────────────────┘ │
│ 总 tokens │
│ 98,690,484 近7天 · 命中 │ ← 左下角大数字(总量) + 右侧筛选摘要
└────────────────────────────┘
- 拖动:按住窗口任意位置(按钮除外)移动,指针捕获保证拖出窗口也不断。
- 置顶:窗口右上角 📌;置顶时位置固定右上角;置顶状态下拖动超过 4px 自动取消置顶并跟随指针。
- 点击/拖动区分:按下后移动 ≤4px 视为点击(按钮正常触发),>4px 视为拖动。
3.2 圆点模式(最小化后)
- 56px 圆形,中央是今日累计 tokens(万/亿缩写),下方一条迷你走势线。
- 点击圆点 → 恢复窗口;拖动圆点 → 自由移动位置。
- 圆点与窗口共享同一个位置状态(
pos),互相切换时位置连续。
3.3 左下角"总 tokens"大数字
- 显示
data.all.usage.total(全部会话、全时段,input+output+cacheRead+cacheWrite,不含 reasoning)。 - 若个别会话日志暂不可读(例如由更新版本的 harness 写入),大数字照常显示,旁边只出现一个小徽标 "缺 N 会话",鼠标悬停可看具体原因;待这些会话可读后徽标自动消失。
4. 系统架构
┌────────────────────────── DSH 进程(Node) ──────────────────────────┐
│ dsh-usage-widget 宿主半 (lib/index.js) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 聚合存储(内存) │ │
│ │ sessions: sessionId → { daily: Map, allAgg, maxSeq } │ │
│ │ allDaily / allAgg:全部会话按日 / 累计 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ ▲ 增量折叠 (session/event) ▲ 全量扫描(并发4) │
│ │ maxSeq 水位去重 │ sessionQuery 优先 │
│ │ │ sessionPersistence 兜底 │
│ ┌────────────────────┐ ┌──────────────────────────────┐ │
│ │ ctx.on('session/event') │ │ 每 60s 自愈重扫 + 初始扫描 │ │
│ └────────────────────┘ └──────────────────────────────┘ │
│ │ │
│ ▼ POST /usage/api/snapshot ←── webServer 前缀路由 │
└──────────────────────────────────────────────────────────────────────┘
▲ HTTP (JSON)
┌──────────────────────── 浏览器页面 ──────────────────────────────────┐
│ dsh-usage-widget 客户端半 (lib/client.js,经 __ModuleLoader__ 加载) │
│ · 注册到 shell.overlay 槽位(id: uw-usage-widget) │
│ · 每 4s fetch 快照 → React 渲染窗口/圆点 │
└──────────────────────────────────────────────────────────────────────┘
关键设计:
- 宿主半零运行时第三方依赖:只用 Node 内置模块 + 通过 Cordis inject 注入的服务(
webServer/sessionQuery/sessionPersistence/timer),因此随 profile 安装时不需要额外@deepseek-ai/*依赖。 - 客户端半只 require
react:其余全部内联进 bundle;通过window.__ModuleLoader__.load({ id: 'dsh-usage-widget', factory })注册,由 DSH 的 client-modules 系统(dsh.client扫描 + 引导清单注入)加载。 - 通信:客户端 → 宿主走 HTTP
POST /usage/api/snapshot(宿主自己的前缀路由),宿主 → 客户端仅返回 lossless JSON。不使用动态插件的私有 RPC。 - 样式:CSS 以字符串内嵌,插件 apply 时注入 ``,卸载时自动移除。
5. 数据口径与统计规则
| 规则 | 说明 |
|---|---|
| 计入事件 | 仅 assistant/message 且 data.usage.inputTokens 为 number 的事件(会话标题等系统辅助 LLM 调用不计) |
| 累加字段 | input=usage.inputTokens、output=usage.outputTokens、cacheRead=usage.cacheReadTokens、cacheWrite=usage.cacheWriteTokens、reasoning=usage.reasoningTokens |
| total 口径 | total = input + output + cacheRead + cacheWrite(不含 reasoning;reasoning 仅在会话卡与序列字段中单独体现) |
| RAW 优先扫描 | 首选直接解析每个会话自己的 session.jsonl.zstd(多帧 zstd,经 zstd CLI 解压),只提取 assistant/message 的 usage —— 不受 harness 解释器的未知事件拒读影响,覆盖 100% 会话(本机审计 32/32 会话可读,与独立逐条审计结果完全一致) |
| harness 兜底 | RAW 路径失败时(如日志正在写入的最后帧不完整)回退 sessionQuery.readSession / sessionPersistence.readFrom |
| 按天分桶 | 该事件当天本地时区 0 点的 epoch ms(避免 UTC 偏差) |
| 去重 | live 事件与扫描共用会话级 maxSeq 水位,`seq ~/Code/dsh-usage-widget |
| cd ~/Code/dsh-usage-widget | |
| pnpm install | |
| pnpm build # 生成 lib/index.js(宿主)+ lib/client.js(客户端 bundle) |
**第 2 步:在 profile 中声明依赖**
编辑 `~/.dsh/profiles/web/package.json`:
```jsonc
{
"dependencies": {
// ...已有依赖
"dsh-usage-widget": "link:/home/xinmo/Code/dsh-usage-widget" // 或 file:/path
}
}
第 3 步:挂载到组合(cordis.patch.yml)
编辑 ~/.dsh/profiles/web/cordis.patch.yml:
- insert:
- id: better-sidebar # 已有行……
name: 'dsh-better-sidebar'
- id: usage-widget # ← 新增
name: 'dsh-usage-widget'
第 4 步:安装依赖并重启
cd ~/.dsh/profiles/web
CI=true pnpm install --no-frozen-lockfile # 更新锁文件并链接新包