dsh-token-usage-dashboard
v1.3.2
Published
Token 用量统计看板:扫描 $DSH_HOME/sessions 下的持久化会话日志(token-meter 的同一份用量记录),聚合出 时间×模型 的输入/输出/缓存用量,生成自包含 HTML 仪表盘
Maintainers
Readme
dsh-token-usage-dashboard
DeepSeek Harness(dsh)插件:Token 用量统计看板。
扫描 $DSH_HOME/sessions 下的持久化会话日志(与 dsh-token-meter 计量的同一份
assistant/message.usage 记录),按「日 × 模型」聚合输入 / 缓存读 / 缓存写 / 输出 /
推理 token,渲染一份自包含、可离线打开的 HTML 仪表盘(KPI 卡 + 每日堆积条 +
日×模型热力图 + 明细表),并在 dsh web 侧边栏底部加入口按钮。
- 许可:MIT(Copyright (c) 2026 skjie)
- 分支:
plugin-token-usage(本仓库采用「分支即插件」组织,本分支即插件本体)
✨ 功能
| 功能 | 说明 |
|---|---|
| HTML 看板 | 自包含单文件,可离线打开;排版对齐 dsh web 自带 /token-usage.html 页面(深色主题、四张 stat 卡、热力图 / 按天堆叠 / 饼图三块 canvas、每日明细表格、tooltip);页头范围按钮(今日 / 三天 / 七天 / 半月 / 一月 / 三月,默认今日),点击在浏览器端重算视图;今日 = 按每小时展示(柱状图 / 热力图 / 明细表均为小时粒度) |
| 数据口径 | 与 dsh-token-meter 投影一致:同一 (turn, step) 只保留最后一条 assistant/message 用量(重试 / 改写不重复计数);reasoningTokens 独立成列;inputTokens 为未缓存输入,计费输入 = input + cacheRead + cacheWrite |
| 模型归因 | 取事件之前最新的 request/context(缺失回退 request/header 的 config),两者皆无记 unknown |
| 自动刷新 | 启动即生成;监听 session/flush 防抖重生成(默认 5s),文件打开时聚焦自动刷新 |
| 容错 | 损坏尾部按「连续前缀」语义截断,坏日志计入 warnings 跳过,不影响整体;zstd 用 Node 内置 node:zlib,零额外依赖 |
| 防注入 | 模型名等日志派生字符串经 HTML 转义后渲染;内联 JSON 数据区的 < 序列化为 \u003c,杜绝 </script> 逃逸 |
| 增量重扫 | mtime+size 解析缓存:session/flush 触发的重生成只重读有变化的日志(热扫描约为全量的 1/200) |
| 侧边栏入口 | 在 dsh web 侧边栏底部(sidebar.footer.action 槽位)注册「📊 Token 用量」按钮,点击打开看板 |
🚀 一键安装
前提:已安装
dshCLI 与pnpm(dsh plugin内部调用 pnpm;没有 pnpm 可corepack enable pnpm启用),以及 Node ≥ 22.15(依赖node:zlib内置的 zstd 解压,旧版本会在模块加载期直接报错)。
# git 地址直装(dist 已提交,安装时无需 prepare 构建,不触发 allowBuilds 拦截)
dsh plugin --profile web add "git+https://cnb.cool/skjli/dsh-plugin.git#plugin-token-usage"
# 重启 dsh web 生效
dsh web
dsh plugin装完会自动 reconcile:包声明了dsh.bundle.patch,会自动加入dsh.profile.bundles层栈,无需手改配置。
其他安装源(任选其一):
# npm registry(若已发布)
dsh plugin --profile web add dsh-token-usage-dashboard
# 本地路径(开发联调,pnpm link 语义)
dsh plugin --profile web add .验证安装
dsh --profile web --dump-config # 应看到 id: token-usage-dashboard 的插件行完整操作流(安装 → 验证 → 重启 → 确认生效):
# 1. 安装(若报 EPERM 锁文件错误,先停 web 再重跑)
dsh plugin --profile web add "git+https://cnb.cool/skjli/dsh-plugin.git#plugin-token-usage"
# 2. 验证三处已写入
$pkg = Get-Content "$env:USERPROFILE\.dsh\profiles\web\package.json" -Raw | ConvertFrom-Json
$pkg.dependencies # ① 应有 git URL
$pkg.dsh.profile.bundles # ② 应含 dsh-token-usage-dashboard
Test-Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-token-usage-dashboard" # ③ True
# 3. 重启 web 生效(运行中的进程不会感知新装包,必须重启)
Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -match 'dsh.*bin.js.*web' } | Stop-Process -Force
Start-Process "C:\Program Files\nodejs\node.exe" -ArgumentList '"C:\Users\skyli\.dsh\profiles\node_modules\@deepseek-ai\dsh\lib\bin.js"','web' -WindowStyle Hidden
Start-Sleep -Seconds 12
Invoke-WebRequest -Uri "http://127.0.0.1:3080/" -UseBasicParsing | Select-Object StatusCode # 200
# 4. 浏览器打开 http://127.0.0.1:3080/ ,侧边栏底部应出现「📊 Token 用量」按钮一键卸载
dsh plugin --profile web remove dsh-token-usage-dashboard
dsh web # 重启生效完整操作流(卸载 → 验证干净 → 重启):
# 1. 停 web(避免 pnpm 写锁文件时 EPERM)
Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Where-Object { $_.CommandLine -match 'dsh.*bin.js.*web' } | Stop-Process -Force
# 2. 官方卸载命令(预期输出 dependencies: - dsh-token-usage-dashboard git+...)
dsh plugin --profile web remove dsh-token-usage-dashboard
# 3. 验证四处全部干净
$pkg = Get-Content "$env:USERPROFILE\.dsh\profiles\web\package.json" -Raw | ConvertFrom-Json
$pkg.dependencies # ① 应为空 / 无该键
$pkg.dsh.profile.bundles # ② 应只剩 dsh-base、dsh-web-app
Test-Path "$env:USERPROFILE\.dsh\profiles\web\node_modules\dsh-token-usage-dashboard" # ③ False
Select-String -Path "$env:USERPROFILE\.dsh\profiles\web\pnpm-lock.yaml" -Pattern 'token-usage' # ④ 无输出
# 4. 重启 web
Start-Process "C:\Program Files\nodejs\node.exe" -ArgumentList '"C:\Users\skyli\.dsh\profiles\node_modules\@deepseek-ai\dsh\lib\bin.js"','web' -WindowStyle Hidden
Start-Sleep -Seconds 12
Invoke-WebRequest -Uri "http://127.0.0.1:3080/" -UseBasicParsing | Select-Object StatusCode # 200卸载会同时移除
dependencies与dsh.profile.bundles中的条目,node_modules 无残留。
⬆️ 升级与版本感知
dsh 没有第三方插件的自动更新(pnpm 锁定安装时的 commit / 版本,dsh plugin 只是
pnpm 透传),本插件内置轻量版本探测,帮助已装用户感知新版本:
| 感知渠道 | 说明 |
|---|---|
| 侧边栏角标 + 升级面板 | web 启动时探测远端最新版本,有新版时「Token 用量」按钮右上角出现与 dsh 自身更新提示同款的圆点;点击弹主题化升级面板(对齐 dsh 自带「检查更新」面板的样式语言):开始更新 / 复制命令 / 直接打开看板 |
| 看板页 footer | /token-usage.html 页脚显示「版本 x.y.z · 🔄 有新版 a.b.c(升级命令)」 |
| JSON 接口 | GET /token-usage-version.json → {current, latest, hasUpdate, canSelfUpdate, checkedAt},供其他工具消费 |
「开始更新」由 host 端执行固定命令:正式安装在 profile 目录跑
pnpm update dsh-token-usage-dashboard;link/junction 开发形态在仓库根跑
git pull --ff-only(dist 已提交)。更新在下次 dsh web 启动时自然生效
(开机自启 / 手动启动),不做进程内重启。动作路由仅接受同源请求且命令固定,
不接受任意输入。
探测机制(lib/version.js,全程静默容错,失败只显示当前版本、绝不阻塞启动):
- 配置了
updateUrl时优先请求该 JSON(取.version字段)——包发布到 npm 后可填https://registry.npmjs.org/dsh-token-usage-dashboard/latest; - 否则
git ls-remote --tags匿名读取远端仓库的 semver 标签取最大者 (git 协议对公开仓库匿名开放,无需平台 REST API / 登录令牌)。
发布新版本的配套动作(仓库维护者:打标签即发布信号):
git tag v1.2.0 && git push origin v1.2.0用户升级:
dsh plugin --profile web update dsh-token-usage-dashboard # git 源重新解析到最新 commit
dsh web # 重启生效⚠️ profile 的供应链策略(
minimumReleaseAge)可能拦截刚发布的版本:把新版本号加进 profilepnpm-workspace.yaml的minimumReleaseAgeExclude列表,或等待冷却期。
⚠️ 装 / 卸后必须重启 dsh web 才生效。DSH 的热加载只覆盖「已装载插件的配置 变化」(如改
cordis.patch.yml),不覆盖安装 / 卸载 npm 包:运行中的进程 启动时已读取 bundles 与插件 roster,新装包不会被感知(/plugins/<id>/client.js会 404)。Windows 下若 pnpm 报EPERM: rename ... pnpm-lock.yaml,先停 web 再执行(锁文件被占用,偶发,重试即可)。
⚙️ 配置
插件行 config 键(cordis.patch.yml 或 profile 用户层的 id 覆盖),均为默认值:
| key | 默认 | 说明 |
|---|---|---|
| root | '' | 会话根目录;空 = dshHomePath('sessions') |
| scope | 'all' | 'all' | 'cwd'(仅当前工作目录)| 目录数组 |
| days | 0 | 时间窗天数;0 = 全部 |
| outFile | 'token-usage.html' | 输出路径(相对 cwd 解析,可绝对路径) |
| generateOnBoot | true | 启动即生成 |
| watch | true | 监听 session/flush 防抖重生成 |
| watchDebounceMs | 5000 | 防抖毫秒 |
| excludeOrigin | [] | 排除的会话 origin(如 ['subagent']) |
| updateCheck | true | 启动时探测远端最新版本(看板 footer 提示 + 侧边栏角标) |
| updateUrl | '' | 自定义版本 JSON 源(取 .version,如 npm 的 /latest);空 = git ls-remote --tags |
| updateRepo | '' | 版本标签所在 git 仓库;空 = 本插件官方仓库 |
⚠️ outFile 的 cwd 陷阱:web 常以「SYSTEM 调度任务 / 隐藏窗口」方式启动 (如
start-dsh-web.vbs),此时 node 的process.cwd()是C:\Windows\System32而非工作区,相对outFile会把看板写进系统目录。建议在 profile 用户层cordis.patch.yml里覆盖为绝对路径或!!js dshHomePath('token-usage.html')(用户层热重载,改完无需重启 dsh)。
覆盖示例(profile 用户层 cordis.patch.yml)
- id: token-usage-dashboard
config:
scope: cwd
days: 7
outFile: !!js dshHomePath('token-usage.html') # 或绝对路径,如 /home/user/token-usage.html🔧 开发
结构
lib/index.js 服务端(Cordis Service:generateOnBoot + watch + 服务方法)
lib/usage.js 数据读取与聚合(纯模块,无 Cordis 依赖)
lib/render.js HTML 看板渲染
lib/format.js 展示辅助(日期键 / 千分位)
src/client.js browser 端:侧边栏按钮(esbuild 打包 → dist/client.js)
dist/client.js 已提交的构建产物(一键安装免构建)
cordis.patch.yml 插件 patch 层(插入插件行)
scripts/build-client.mjs esbuild 构建脚本
test/ 自测脚本(run.mjs / diagnostics.mjs)
DESIGN.md 详细设计说明数据源:为什么读持久化日志,而不是 ctx.tokenMeter
dsh-token-meter 的 ctx.tokenMeter.measure(session) 是实时回放感知的:
返回「最新请求的压力 + 当前表层」,不是历史累计账本。历史账单藏在会话日志的
累积方式里,token-meter 自己也是回放这份日志做锚定计价的。因此本插件直接读取
这份持久日志,即使用了与 token-meter 完全相同的事件事实。
磁盘形态:
<root>/<projectKey(cwd)>/<encodeSegment(sessionId)>/session.jsonl[.zstd]- 首行 = header 记录
{ type:'session', version, id, createdAt, cwd, ... }; - 其余行 = 存储记录,经
decodeStorageRecord(@deepseek-ai/dsh-session官方导出) 还原为SessionEvent[]; .jsonl.zstd是多帧容器(每次 flush 追加一个独立帧),用 Node 内置node:zlib的zstdDecompressSync逐帧解压,尾部不完整帧跳过。
聚合语义
- 模型归因:事件之前最新的
request/context(provider/model),缺失回退request/header的 config,两者皆无记unknown; - 去重:同一
(turn, step)只保留最后一条assistant/message的用量; - 时间窗:按事件
time(Unix ms)归本地日YYYY-MM-DD,days > 0时过滤 早于now - days*86400e3的样本。
构建
node scripts/build-client.mjs # esbuild 打包 src/client.js → dist/client.js自测
# 需要先让 @deepseek-ai/dsh-session 可解析:
# 在仓库根目录建一个指向 profile 扁平 node_modules 的 junction(Windows):
# cmd /c mklink /J node_modules %USERPROFILE%\.dsh\profiles\node_modules
node test/run.mjs [会话根目录] # 默认 $USERPROFILE\.dsh\sessionstest/run.mjs 输出聚合摘要的 JSON 并写出 test-out.html 供人工抽查。
node test/xss-check.mjs # 渲染防注入回归(转义 / \u003c 序列化 / esc 插入点)
node test/cache-check.mjs # mtime 解析缓存回归(命中 / 失效 / 清理)客户端槽位机制(重要)
侧边栏按钮注册在 sidebar.footer.action 槽位。该槽位由
@deepseek-ai/dsh-client-ui-sidebar 的 sidebar entry 声明,而 client 插件的
激活顺序不受 dsh.client.inject 边约束(那是信息性元数据,非装载顺序),
因此不能直接 ctx.slots.register——那会在本插件先于 sidebar 激活时抛
slot "sidebar.footer.action" is not declared。官方约定(同 ui-workspace)是:
ctx.slots.inject("sidebar.footer.action", () =>
ctx.slots.register({ name: "sidebar.footer.action", id: "dsh-token-usage-dashboard-action", order: 100, label: "Token 用量" }, component)
);inject 在声明存在时同步注册、声明折叠时自动 dispose、重新声明时再次注册。
⚠️ 槽位组件不得使用 React hooks(1.2.0 的真实事故,1.2.1 修复):client bundle 经 esbuild 自带一份 React 拷贝,而槽位组件由 SPA 自身的 React 渲染器渲染; 普通元素因
$$typeof走Symbol.for('react.element')跨拷贝安全,但 hooks 的 内部 dispatcher 是每份 React 的模块级私有状态,跨拷贝调用useState/useEffect会抛 "Invalid hook call",槽位渲染失败、按钮整体消失。需要"状态"的行为改用 DOM 属性 + CSS 选择器实现(升级角标的body[data-token-usage-update]方案)。
📦 发布
npm pack --dry-run # 预览包内容(files 白名单:lib/dist/src/scripts/cordis.patch.yml/README/LICENSE)
npm publish # prepack 自动构建 dist/client.js🗂 相关文档
- DeepSeek Harness 官方仓库:deepseek-ai/deepseek-harness
dshCLI 使用说明(profile / bundle / patch 层机制):@deepseek-ai/dsh
