← 返回列表
⚠ 装前注意
DSHDeepSeek Harness模型 Token 用量监测插件:侧边栏小窗口显示当日 token、花费与余额 +…
基本兼容但装前注意:未发布到 npm registry,仅可从源码安装 · 最近上游提交 2026/9/17 · 已提供中文文档
DSH(DeepSeek Harness)模型 Token 用量监测插件:侧边栏小窗口显示当日 token、花费与余额 + 每小时峰谷直方图,另带可按区间/按对话查询的用量日志弹窗。零依赖,数据取自会话日志
综合分
29.4
GitHub 分
29.4
用户评分
—
★ Stars
0
周下载量
—
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add hipigod/dsh-token-monitor未发布到 npm registry,仅可从源码安装,改用 GitHub 源安装
信任档位:已验证本站已于 1 天前真实安装成功
- 是什么
- dsh 原生插件 · other
- 装得上吗
- 本站已真实安装成功(非静态推断)
- 安全吗
- 本站尚未对该插件做风险分级(暂未覆盖,不等同于无风险)
- 还在维护吗
- 活跃:最近一次提交在 8 天前
档位由下列信号合成:本站实装验证(真实安装,当前最高到 L4)· 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。下方各区块是它的证据明细。 验证判据与等级说明 →
🟢实装验证通过· 2026/9/25
由本站实装验证器在真实 dsh 环境安装成功,非静态推断。
数据截至 2026/9/22(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
安装兼容性检查⚠ 装前注意
以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。
✗npm 包@local/dsh-token-monitor(未发布到 npm,仅可源码安装)
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明
未发布到 npm registry,仅可从源码安装
验证方式:npm registry 存在性 + package.json 静态校验 · 最后验证 2026/9/22 19:19:41
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动
README
由 DeepSeek 最新模型翻译生成dsh-token-monitor · 模型 Token 用量监测
DSH(DeepSeek Harness)Web 插件。在左侧边栏「会话列表下方、页脚(设置按钮)上方」放一个可拖动、可最小化的浮窗,显示当前模型的当日 token 总量和每小时直方图(输入命中缓存 / 输入未命中缓存 / 输出),右上角一个【日志】按钮打开完整的用量报告弹窗。
数据全部来自本机会话日志($DSH_HOME/sessions/*/session.v3.jsonl.zstd),不额外埋点、不改动 DSH 源码、不依赖任何第三方包。
一、装上之后长什么样
浮窗(侧边栏会话列表下方、页脚上方)
挂载点是 shell.overlay(ui-layout 声明的全框浮层,可叠加、默认点击穿透)。
┌─────────────────────────────┐
│ deepseek-flash 日志 — │ ← 头部可拖动;右上角【日志】【最小化】
│ 2亿 今日总量 · 1.3k 次请求 │
│ ¥14.35 今日花费 ¥35.42 余额 │
│ ▓▓░░▓▓▓░░░░▓▓▓░░░░░░░░░░░░ │ ← 峰谷时段带(深=高峰 / 浅=空闲)
│ ▁▃█▅▂ ▁▁▁▁ ▁▁▁▁ ▁▁▁▁▁▁▁▁▁▁ │ ← 24 小时堆叠直方图
│ 00:00 12:00 23:00 │
│ ● 输入(命中缓存) ● 未命中 ● 输出 │
└─────────────────────────────┘
[ ⚙ Cordis Plugin ... ]
[ ⚙ Settings ]
- 右上角【—】最小化 → 收成一个小胶囊(总量 + 花费),点胶囊还原;状态记在 localStorage
- 拖动头部可移动位置,双击头部回到默认锚点;位置也记在 localStorage
- 位置不是写死的:实时量侧边栏与页脚容器的矩形(ResizeObserver + resize),
所以侧栏宽度变化、收起成窄栏、页脚条目增减都会自动跟随
为什么不用 sidebar.footer.action:那个槽位只能表达"页脚内的一行",
而需求是「工作区 → 会话列表 → 本插件 → 页脚」的次序,落在列表与页脚之间——
只有浮层能做到,且不会被其它插件(如 Cordis Plugin 区)挤占。
小窗口(旧形态,已改为浮窗)
┌────────────────────────────────────────┐
│ deepseek-official/deepseek-flash 日志 │ ← 模型名 + 【日志】按钮
│ 1.1亿 今日总量 · 858 次请求 │ ← 当日总量
│ ▁▃█▅▂▁▁▁ ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ │ ← 24 小时堆叠直方图
│ 00:00 12:00 23:00 │
│ ● 输入(命中缓存) 1.1亿 │ ← 图例(跟随悬浮小时)
│ ● 输入(未命中缓存) 908k │
│ ● 输出 605k │
│ 悬停柱子看该小时明细 │
└────────────────────────────────────────┘
[ ⚙ 设置 ] ← 本插件在它上方
- 鼠标悬停任意柱子 → 图例与提示行切换成那一小时的明细。
- 每 30 秒自动刷新一次,窗口重新聚焦时立刻补一次。
日志弹窗(点【日志】)
- 区间:今日 / 近一周 / 近一月 / 自定义(起止日期选择器,按上海日界,最多 400 天)
- 视图:按对话 / 按模型(可翻页)
- 总量卡片:总用量、输入(命中缓存)、输入(未命中缓存)、输出
- 时间分布:区间 ≤2 天按小时、更长按天,堆叠柱图 + 悬浮读数
- 模型分布:每个模型的用量占比条(同样按三桶堆叠)
- 明细表:按对话(标题 / 工作区 / 最后活动时间 / 涉及模型)或按模型
- 健康度:缓存命中率、活跃小时、活跃小时均值、日均、峰值小时、最费对话、覆盖会话数
- 页脚:写明统计口径与采样时刻;若有个别日志读取异常,在这里如实列出(不静默吞掉)
二、统计口径(重要,决定你看到的数字是什么意思)
每一条用量都来自会话日志里 assistant/message 事件的 data.usage,即 provider 自己回报的用量:
| 界面上的桶 | 日志字段 | 说明 |
|---|---|---|
| 输入(命中缓存) | cacheReadTokens | 命中 prefix cache 的输入 |
| 输入(未命中缓存) | inputTokens + cacheWriteTokens | DeepSeek 侧 inputTokens 不含缓存读取;cacheWriteTokens 恒为 0,存在时并入此桶 |
| 输出 | outputTokens | 已包含 reasoning tokens,不重复计算 |
- 模型归属:按事件顺序取「最近一次 request/context」作为该次请求生效的路由(provider/model)。DSH 只在路由/上下文变化时才追加 request/context,所以用游标取最近值是正确的归因方式;连 request/context 都没有的极端情况会记为 unknown 而不是丢弃。
- 时区:固定 Asia/Shanghai(UTC+8),日界 = 本地 00:00。近一周 = 含今日共 7 天,近一月 = 含今日共 30 天。
- 会话标题:优先取 DSH 投影缓存(storages/session_projcache)里的标题(即侧边栏显示的那个),取不到则退回日志里的 session/title,再退回首条用户消息(截断 80 字)。
- 不含费用估算:按你的要求,只报 token,不做任何价格折算。
三、安装 / 卸载
从 GitHub 直接装(推荐)
cd /opt/deepseek-harness && DSH_HOME=~/.dsh pnpm dsh plugin --profile web add github:hipigod/dsh-token-monitor
或者先克隆到本地再以本地路径装(改代码即时生效,不用重装)
git clone https://github.com/hipigod/dsh-token-monitor.git /root/apps/dsh-plugin-token-monitor
cd /opt/deepseek-harness && DSH_HOME=~/.dsh pnpm dsh plugin --profile web add /root/apps/dsh-plugin-token-monitor
生效(宿主路由与客户端 bundle 都需要重启进程)
systemd-run --on-active=2 --unit="dsh-restart-$(date +%s)" systemctl restart dsh-web.service
然后用浏览器刷新 http://127.0.0.1:3080
⚠️ 不要在当前会话里直接 systemctl restart dsh-web.service:agent 自己是这个服务的子进程,直接重启会把当前对话打断(已实测踩过两次)。用上面的 systemd-run --on-active 定时重启,命令会先返回,再由 systemd 去重启。
卸载
cd /opt/deepseek-harness && DSH_HOME=/root/.dsh pnpm dsh plugin --profile web remove @local/dsh-token-monitor
回滚
好版本快照(每次 preflight 通过后建议复制一份)
ls -t /root/archive/dsh-token-monitor-backups/
cp -a /root/archive/dsh-token-monitor-backups/client.js.good- /root/apps/dsh-plugin-token-monitor/client.js
或整体回滚 profile
cp -a /root/.dsh/archive/profile-web-backup-20260915-163321/. /root/.dsh/profiles/web/
systemd-run --on-active=2 --unit="dsh-rollback-$(date +%s)" systemctl restart dsh-web.service
改完源码后:宿主侧改动(index.js)要重启;客户端改动(client.js)只需在浏览器刷新页面(reg 里的 bundle 是实时从磁盘读的,实测 curl 已能取到新内容)。宿主侧 client-hmr 还会 stat-poll 每个 bundle,内容一变就推 rebuilt 帧把插件热换掉(实测改完 client.js 约 1 秒后浏览器里的浮窗会重挂一次)。
四、自检(不用看界面就能判断好坏)
插件自带一个诊断端点:
curl -s http://127.0.0.1:3080/plugin-api/token-monitor/doctor | jq
它会如实报告三件事:
1. host.sessionsRoot —— 宿主实际读的日志目录;
2. client.available / client.rows —— 浏览器模块表里登记了哪些插件(这是「界面上会不会出现」的唯一权威依据,宿主路由 200 不代表客户端会被加载);
3. client.ours —— 本插件那一行,含 bundle 绝对路径;null 就说明 dsh.client 声明或 ./client 导出有问题。
另外两个只读路由:
GET /plugin-api/token-monitor/overview 今日总览(小窗口数据)
GET /plugin-api/token-monitor/log?period=…&view=… 日志报告
period = today | week | month | custom | all
start / end = YYYY-MM-DD(period=custom 时生效)
view = total | conversation
口径一致性是硬约束:totals、models 求和、buckets 求和、conversations 求和在任一次响应里必须相等(三者都来自同一批已过滤记录)。对不上就是 bug。
五、已修缺陷(都是真浏览器/真数据抓到的,不是"看起来对")
| # | 症状 | 根因 | 修法 |
|---|---|---|---|
| 1 | 整个 Web 应用不挂载:页面只剩 Failed to load plugins,侧栏/会话全没了;正在对话的 agent 也被连带打断(2026-09-15 16:33–17:06 的真实事故) | client.js 里 __ModuleLoader__.load({ id }) 写的是 dsh-token-monitor,而模块表行 id 是包名 @local/dsh-token-monitor。client-modules 的 arrive() 以行 id 为键等待 bundle 自注册,不匹配即抛 bundle … loaded without registering "" via __ModuleLoader__.load | 两处统一为包名;加回归断言(tests/client.test.mjs)+ 真浏览器 boot 审计(tests/e2e-browser.mjs)+ 放行闸门 preflight.sh |
| 2 | 弹窗内容被裁成 380px 宽、右侧全部看不见 | ui-primitives 的 Modal 卡片写死 width: min(380px, 100%) + overflow: hidden。只改内层 contentClassName 无效——外层卡片才是裁剪者 | 改用 headless: true 自绘标题栏,并通过 Modal 的 className(唯一透传到卡片的入口)覆盖宽度 |
| 3 | 侧栏模型名压到【日志】按钮上(文字重叠) | flex 子项默认不收缩到可用宽度,只写 text-overflow: ellipsis 不会生效 | 给标题显式 max-width: 96px + min-width: 0;模型名只显示末段,完整 provider/model 放进 title |
| 4 | 内层盒子比卡片宽 40px(贴边/图例被裁) | 内层宽度算式与卡片内边距不匹配 | 内层改 box-sizing: border-box + width: 100%,图例 flex-wrap + 右对齐 |
| 5 | 用户截图里弹窗下边内容被裁(记录deepseek… 那行看不见) | 高度按 84vh 算,而笔记本 + 浏览器工具栏后可用高度只有 ~550px,内容超出卡片被 overflow: hidden 吃掉 | 改成 flex 链:卡片 max-height: calc(100vh - 48px),内层 flex: 1 1 auto + min-height: 0 + overflow-y: auto,滚动交给内层 |
| 6b | 弹窗顶部「花费」卡显示 ¥0.0 / 未启用计价,而同一弹窗的模型分布显示 ¥13.09(自相矛盾) | 宿主 logPayload 的返回对象漏了顶层 cost/currency 两个字段(模型分布读的是 models[].cost,所以显示正常) | 客户端加兜底:顶层缺 cost 时用 totals.cost 重建、币种从余额推断;宿主侧也补上字段(下次重启生效) |
| 7 | 浮窗完全拖不动(位置永远不变) | React 的 onPointerDown 在无头 Chromium 下不触发;另外我以为 jsdom 支持 pointer 事件 | 改用 mouse 事件(onMouseDown + window 上的 move/up,指针滑出头部也不丢事件),并把「拖动后坐标真的变了」变成端到端硬断言 |
| 6d | 侧栏底部位置被其它插件(Cordis Plugin 区)占用 | 原来注册在 sidebar.footer.action,只能落在页脚内 | 改挂 shell.overlay 做浮窗,位置由实时测量的锚点决定 |
| 6c | 小窗口的花费/余额/峰谷带完全不出现,但样式与辅助函数都在 | 那次 patch 的锚点在文件里已不匹配,替换静默未生效,而脚本仍打印成功;我误信了输出 | 重写并就地 grep 自证;preflight 新增「关键代码块是否真的写进文件」静态核对(1.5/4) |
| 6 | 窄视口下弹窗有横向超出风险 | 宽度用 94vw,而 Modal 的 .root 自带 24px 内边距(可用宽度 = 100vw - 48px),且卡片是 flex item(min-width: auto 不收缩) | 改 min(1080px, calc(100vw - 48px)) + min-width: 0;表格包一层 overflow-x: auto 的容器 |
| 8 | 浮窗偶尔变成没有皮肤的裸 div,跑到侧边栏左上角压住会话列表(用户 2026-09-16 截图);点【日志】打开再关掉就恢复正常 | 皮肤 只写了 data-plugin-css、没写 data-plugin。DSH 的 client-modules 在每个模块 materialize 时都会 claimStyles(id),把所有 style:not([data-plugin]) 认领给那个模块;这张表被别的模块认领后,client-hmr 重建那个模块时会 removeOwnedStyles(id) 把它一起删掉。插件的 fiber 毫发无伤、React 树照常渲染,只是 CSS 没了 → 浮窗落回 shell.overlay 的正常流(左上角 0,0)。而重新注入的唯一路径,恰好是打开日志弹窗时 LogDialog 顺带跑的那次 ensureStyle()——这就是"开关一次日志就好了"的原因 | ① 样式表补上 data-plugin = 包名(与官方 tsdown.client.ts 的注入器一致,别人 claim 不走);② apply() 里注入皮肤并挂 MutationObserver(document.head),表一消失就补回;③ position:fixed/z-index 同时走 inline,样式表缺失的那一瞬间也不会掉进正常流;④ ensureStyle() 发现这张表归属被别人改写过时把归属抢回来(removeOwnedStyles 是按 data-plugin 属性比对的,抢回来就等于销掉别人的账),内容被清空也补回。回归:tests/client.test.mjs(归属 + 抢回 + 自愈 + 降级 + head 缺失兜底)、tests/render.test.mjs(inline 定位)、tests/e2e-browser.mjs(真浏览器里删表 → 自动补回、位置不跳)、scripts/hmr-probe.mjs(下面「真 HMR 回归」) |
| 9 | 峰谷时段带在跨零点后的当天把 9:00–12:00 的高峰涂成空闲(2026-09-17 00:55 由 e2e 抓到:峰 0 谷 24;此前 20:55 跑是绿的,被旧断言放过去了) | PeakBand 对没有用量的小时(宿主给 band = 0,含"今天还没到的小时")一律回落到 bandOf(Date.now()) —— 用当前小时的颜色涂遍整条带。当前小时是空闲时,一整天的高峰全被涂成"便宜" | 空格子按它自己那个小时判定:先解析桶 key(bandOfKey('2026-09-17T09')),解析不出再退到「上海今天第 i 个小时」(新增 shanghaiDayStartMs())。旧断言只查「峰谷都有」,改成金标准向量:测试与 e2e 都独立算一遍 24 格应有的峰谷,逐格比对,与运行时刻无关 |
这两个(5、6)是我最初 e2e 只跑 1400×900 一个尺寸漏掉的——测试视口不等于用户视口。
现在 tests/e2e-browser.mjs 里加了 1280×560 矮视口回归:断言「卡片不越视口 + 内层可滚 + 滚到底最后一段可达」。
配色
统一用 DeepSeek 自己的蓝色系(--dsw-static-deepseek-),三桶靠明度区分,而不是靠色相:
| 桶 | token | 值 |
|---|---|---|
| 输入(命中缓存) | --dsw-static-deepseek-400 | rgb(103, 158, 254) |
| 输入(未命中缓存) | --dsw-static-deepseek-500 | rgb(65, 118, 230) |
| 输出 | --dsw-static-deepseek-450 | rgb(86, 134, 254) |
明度顺序也表达语义:命中缓存是“省下来的”(偏浅),输出是“真花出去的”(最亮)。
六、花费与余额(2026-09-15 新增)
小窗口在「今日总量」下方显示 今日花费 + 账户余额,日志弹窗里另有「花费」「账户余额」卡与明细表的「花费」列。
单价(官方价目表,元 / 百万 tokens)
来源 DeepSeek 官方定价页(2026-09-10 12:00 起生效)。价格分峰谷,这是计费正确性的核心:
| 模型 | 输入·命中缓存 | 输入·未命中 | 输出 |
|---|---|---|---|
| deepseek-flash(V4.1-Flash) | 0.02 / 0.04 | 1 / 2 | 4 / 8 |
| deepseek-v4-pro | 0.15 / 0.30 | 4.5 / 9.0 | 13.5 / 27.0 |
「空闲 / 高峰」——空闲时段价格正好是高峰的一半。高峰时段 = 北京时间周一至周五 9:00–12:00、14:00–18:00,其余(含周末全天)为空闲。
因此:花费不能拿区间总量乘单一单价,必须逐条按该条记录的时间取价再累加。aggregate / overview 都是这么算的,并且额外出一个峰谷分解(今天有多少钱花在高峰时段),直接回答“要不要把活挪到空闲时段跑”。
配置
单价可能再变,所以写在插件配置里,改 ~/.dsh/profiles/web/cordis.patch.yml 即可,不必改代码:
- id: token-monitor
config:
pricing:
currency: CNY
unit: 1000000
peakMultiplier: 2 # 高峰 = 空闲 × 2
defaultModel: { cacheRead: 0.02, uncached: 1, output: 4 } # 未登记模型的兜底价
models:
deepseek-flash: { cacheRead: 0.02, uncached: 1, output: 4 }
deepseek-v4-pro: { cacheRead: 0.15, uncached: 4.5, output: 13.5 }
apiKeyEnv: DEEPSEEK_API_KEY # 余额查询用;也可直接 apiKey: sk-...
未登记单价的模型不会静默算成 0(那会被读成“不花钱”),而是用 defaultModel 估算并在弹窗里点名提示。
余额
调 DeepSeek 官方 GET /user/balance(只读、免费)。凭据解析顺序:插件配置 apiKey → 环境变量 apiKeyEnv → $DSH_HOME/.credentials.yaml 的 refs 段。结果缓存 120 秒,接口超时 6 秒即降级。
取不到时显示「—」并在悬浮里写明原因(no-key / timeout / network / http-4xx),绝不用 0 冒充——0 会被读成“没钱了”,这是误导。
峰谷时段带(直方图上方)
直方图上方一条 24 格色带,同一 DeepSeek 蓝的明度差表达价格差异:高峰 deepseek-500(深,贵)、空闲 deepseek-200(浅,半价)。不用红/绿是因为峰谷只是价格差,不是对错。
七、放行闸门(改完必跑)
bash /root/apps/dsh-plugin-token-monitor/preflight.sh # 退出码 0 才允许说“完成”
它依次跑:客户端契约(含 id == 包名)→ 宿主逻辑 → 元素树 → profile 挂载核对 → 真浏览器 boot 审计(全新 context,零缓存;断言「无 pageerror / 无 4xx / 无 Failed to load plugins / 侧栏已渲染 / 硬刷新后仍正常」)。
为什么需要它:事故当时 host/client/render 三层全绿,界面上却是整个应用打不开——单元测试无法发现“注册 id 与模块表行 id 不匹配”这类跨进程契约错误,只有真的 boot 一次才知道。
八、验证方式(五层,越往下越接近真相)
从 GitHub 克隆下来后,直接在克隆目录里跑(测试不依赖绝对路径)
git clone https://github.com/hipigod/dsh-token-monitor.git && cd dsh-token-monitor
node tests/host.test.mjs # 51 项:宿主纯逻辑 + 真实 HTTP 路由(自建 server,不依赖宿主)
node tests/client.test.mjs # 23 项:格式化/桶标签/峰谷判定/日期/注册契约与皮肤归属契约
node tests/render.test.mjs # 27 项:用假 React 直接调用组件函数,断言真实元素树
TOKEN=$(grep -oE 'token=[A-Za-z0-9_-]+' /var/log/dsh-web.log | tail -1)
node tests/e2e-browser.mjs "$TOKEN" # 32 项:无头 Chromium 打开真 GUI,断言真 DOM 几何
第四层是唯一能发现其中大多数缺陷的一层(1、3、5、6、7、8、9 都是它或下面的
「真 HMR 回归」抓到的):前三层全绿的时候,界面上依然是空的。
tests/e2e-browser.mjs 直接断言「模型名与【日志】按钮的矩形不相交」「弹窗卡片宽度 > 900px」「内层不溢出卡片」这类几何事实,而不是截图给人看。
真 HMR 回归(缺陷 8 的机制级复现)
tests/e2e-browser.mjs 里删 是手工模拟。要证明「宿主自己的重建路径也不会弄丢皮肤」,
Use scripts/hmr-probe.mjs in the workspace (temporarily change the content of client.js once → the host's 500ms stat-poll pushes a rebuilt frame →
the browser hot-rebuilds the plugin, then restore the file byte-for-byte and verify sha256):
TOKEN=$(grep -oE 'token=[A-Za-z0-9_-]+' /var/log/dsh-web.log | tail -1)
node /root/dsh-workspace/2026-09-token-monitor-workspace/scripts/hmr-probe.mjs "$TOKEN"
It asserts three things: an already-open page switches to the new bundle without refreshing (it really fetches the new rev, and the skin table is a new DOM node),
the skin and coordinates are lossless after the rebuild, and when the skin table is claimed by another module and then rebuilt, ownership is snatched back. As of 2026-09-17, 7/7 pass in real testing.
There is also a host-side self-check endpoint (no token needed, no browser needed):
curl -s http://127.0.0.1:3080/plugin-api/token-monitor/doctor | jq
8.5. Local deployment locations (the actual situation on this machine)
| Path | What it is |
|---|---|
| /root/apps/dsh-plugin-token-monitor/ | Plugin source code. Do not move it: the profile depends on it via a link: dependency, and moving it will prevent the entire GUI from mounting |
| /root/dsh-workspace/2026-09-token-monitor-workspace/ | The workspace for this round of development (notes, screenshots, scripts/hmr-probe.mjs, with the source symlink pointing to the source code; the old name dsh-token-monitor-workspace has been renamed) |
| /root/archive/dsh-token-monitor-backups/ | Snapshots of good versions and backups of incident versions |
| /root/archive/profile-web-backup-20260915-163321/ | Backup of the web profile before installing the plugin (for a full rollback) |
| https://github.com/hipigod/dsh-token-monitor | The public repository that has been pushed |
The complete record of that round of conversation (implementation details, postmortems of the two incidents, recovery manual) is in this workspace's
notes/token-monitor-plugin.md.
9. Implementation points (why it is done this way)
- zstd multi-frame: DSH's session logs are multi-frame concatenated zstd streams, each frame ~1.8KB (measured 1.38MB / 750 frames). zlib.zstdDecompressSync(buffer) only decompresses the first frame—you get "1 line of log"; a streaming decompressor instead reports Unknown frame descriptor at the second frame boundary. Therefore, split by frame magic number, decode frame by frame, and add finishFlush: Z_SYNC_FLUSH to the decoder: without it, bad data containing only a frame header will silently return an empty buffer, masquerading as "this session has no usage".
- Truncation detection: zstd is block-compressed, and missing a few bytes at the end often still decompresses, so "whether it can be decompressed" is not enough to determine truncation. The approach is: if the last frame cannot be decompressed and content has already been decompressed before it → mark it as truncated and continue; otherwise report an error. At the same time, use hasIncompleteTail() (no newline at the end of the plaintext) to catch the case where "the residual line happens to still be JSON.parse-able".
- Scan cache: do file-level caching by (mtimeMs, size), so the 30-second polling does not repeatedly decode historical logs; single-flight within the process (only one scan runs at a time), and clean up cache entries for deleted sessions.
- Zero cross-plugin dependencies: the host side only uses node:zlib + node:fs; the client side only requires react / react/jsx-runtime / @deepseek-ai/dsh-client-ui-primitives (all three are DSH's PLATFORM_MODULES baseline externals), and does not import any other plugin.
- The registration id must equal the package name: client-modules' arrive() waits for the bundle to self-register using the "module table row id" as the key. Writing another name in __ModuleLoader__.load({id}) will throw directly during browser boot, manifesting as "the plugin does not show" with no trace at all in the host logs (this is the first pit this plugin fell into).
- Mount point: register into the shell.overlay declared by ui-layout (a full-frame overlay, list slot, stackable, click-through by default), and use ctx.slots.inject(...) to wait for that slot to be declared before registering—directly register-ing into a slot declared by someone else will error during loading. The position is not hard-coded: it measures the anchor points of the sidebar and footer rectangles in real time (bottom positioning). Historically sidebar.footer.action was used, but that slot can only express "a row within the footer" and cannot land between the session list and the footer (see defect 6d).
- The skin must "register itself": writes both data-plugin (= package name, ownership) and data-plugin-css (deduplication key), and injects them in apply() + self-heals with MutationObserver(document.head); if ensureStyle() finds that this table's ownership has been rewritten by someone else, it will snatch ownership back (removeOwnedStyles compares character by character by the data-plugin attribute, and snatching it back cancels the other party's account). A table without data-plugin will be claimed by client-modules' claimStyles for someone else, and then deleted by removeOwnedStyles when that module hot-rebuilds, while this plugin receives no notification at all (see defect 8). position:fixed/z-index additionally fall back to inline: even if the skin is temporarily missing, the floating window is merely "unstyled" and will not fall into the overlay's normal flow to cover the sidebar.
- Peaks and valleys are calculated by "that cell's own hour": the host only attaches band to hours that have usage, and empty cells (including hours that have not yet arrived today) are filled in by the client. When filling in, it must use the hour corresponding to that cell (bucket key → bandOfKey, falling back to shanghaiDayStartMs(now) + i hours), not Date.now()—that would paint the entire band with the color of "the current hour", and after crossing midnight the colors of peak periods would be wrong (defect 9).
四层测试都是对抗性用例:跨日界、跨月、非法日期(2026-99-99、2026-02-30 必须被拒而不是被 Date.parse 归一)、坏帧、截断帧、越界 hover、空数据、NaN 传播、定时器泄漏、口径一致性(totals == models 求和 == buckets 求和 == conversations 求和)。
十、已知边界
- 第三方客户端插件是“全应用级”风险:一个注册 id 写错的 bundle 会让整个 GUI 不挂载(不是只坏自己)。所以改动客户端代码后必须跑 preflight.sh,别只看单元测试。
- 只统计写进会话日志的用量。如果某个 provider 不回报 usage,那部分不会出现在这里(也无法从日志推断)。
- 「按对话」里同时包含 subagent 会话(如果有的话),会按会话 id 分开列出。
- 历史日志格式升级(当前硬编码 session.v3.jsonl.zstd)后需要同步改路径;doctor 会在模块表层面先暴露异常。
- 日志目录默认取 $DSH_HOME(未设则 ~/.dsh),与 DSH 自身一致。