xinmo114514/dsh-usage-widget ↗★ 2
dsh-usage-widget
DSH web plugin: 用量统计悬浮窗(token 用量曲线/热力/圆点,窗口与圆点均可拖动;宿主半扫描会话日志并提供 /usage/api/snapshot) 适合需在 DSH 中本地查看 token 用量统计的用户。
Other repositories with this package name
Install
This plugin has no verified bundle, or compatibility checks failed. Read the repository notes first. Read the full README ↗
README
Read the full 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 # 更新锁文件并链接新包