dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
Session usage view: tokens, wall time and cache share per model and per turn, as a conversation view.
English | 中文
会话用量视图:会话标签页里的一个「用量」,一张会话总计加两张表。
这是一个独立安装到 profile 的插件,不修改 deepseek-harness 的任何源码(packages/ 未改动)。
| 模型用量统计 | 消息用量统计 | |
|---|---|---|
| 一行 = | 一个模型 | 一轮(不是一条消息) |
| 列 | 模型 · 轮次 · 输入 · 输出 · 用时 · 缓存命中 | 时间 · 消息 · 模型 · 输入 · 输出 · 用时 · 缓存命中 |
| 排序 | 固定,总量降序 | 点表头切换 |
| 覆盖 | 已加载窗口内的轮次 | 同左 |
一行是一轮,不是一条消息。 一条助手回复与提出它的那句提示词属于同一轮、共用同一份用量, 逐条列出来会把同一笔 token 在每轮里数两遍——这是用量表最不能犯的那个算术错误。所以行由发起 该轮的那句提示词标识。一轮里换过模型时,整轮记在产出回复的那个模型上。
会话总计那一行的三个胶囊来自 ISession.projections(宿主按整条日志算好),所以翻页、压缩
窗口都不会让它们跳动。
| 数字 | 来源 |
|---|---|
| 会话用量 / 用时 / 缓存命中 | 客户端投影 tokenUsage、sessionStats——整条日志口径 |
| 每轮用量 | 会话自己的事件窗口,交给宿主的 deriveTurnTokenUsage(@deepseek-ai/dsh-token-meter/client)折叠 |
| 每轮模型 | 该轮事件里的路由元数据,回退到 assistant/message 的 message.source |
| 每轮用时 | 该轮 turn/start 与 turn/end 两个事件自己的时刻之差(不读时钟) |
| 模型显示名 | remote.session.modelCatalog(),与输入框选择器同一写法 |
用量不自己聚合。 哪些事件算数、最终消息如何覆盖流式样本、重试如何叠加、不完整的轮次返回
空——这些规则很细,宿主已经在 deriveTurnTokenUsage 里定死了(ui-chat 建轮尾用的就是它)。
自己再实现一遍,第一次偏差会一直隐形,直到有人比较两个界面。
缓存命中率是移植,不是重写。 format.ts 里的 formatCacheHitPercent 是 ui-chat
token-format.ts 那一份的移植:同一个投影、同一份算法,所以本插件、输入框那两个胶囊与底部
状态栏印出的百分比逐字相同。宿主那条规则值得说明——部分命中接近满分时不夹紧成 99.9,
而是加小数位(99.6 / 99.95 这种形状),既真实、又明显不是满分。
① 输入 = 三个不相交桶之和。 uncachedInputTokens + cacheReadTokens + cacheWriteTokens。
宿主把 inputTokens 只算作未缓存的那部分,把它当成总量会让折叠直接拒绝整轮。
② 算不出来就留「—」,并且照常留行。 隐藏该行会让行数与覆盖度行报出的轮次数不符;而破折号 不是一个小数字,所以按任意列排序时,它在两个方向上都排最后——否则降序时一列「—」会占据 表头,读起来像"这些最贵"。
③ 并列与都未知时按 seq。 于是排序永远不会任意打乱相等的行;把它写成 seq 而不是
"输入顺序",这条规则在本插件之外也成立。
明细只能折叠已加载的事件窗口,所以覆盖度行同时报出两个数:
已统计 12 / 22 轮 · 其中 10 轮不在已加载窗口
「载入全部历史」走宿主自己的跳转加载器(binding.session.loadThrough(SessionSeq(0))):
它自带无进展守卫(空页仍然声称还有历史时结束循环,而不是空转),每页 200 条消息——是本插件
自己翻页的四倍,所以整段会话的往返次数只有四分之一。代价写在按钮的提示里:载入不回缩,
刷新前一直有效,且无法中途取消。
表格上方有一排范围按钮,默认「本会话」——也就是之前的行为,完全不变:
| 档 | 范围 |
|---|---|
| 本会话 | 没有边界:整场会话,读宿主投影 |
| 今天 | 本地 0 点 → 此刻 |
| 24 小时 | 滚动 24 小时:此刻往前一天 |
| 昨天 | 昨天 0 点 → 今天 0 点(左闭右开) |
| 3 天 | 三天前本地 0 点 → 此刻 |
| 7 天 | 七天前本地 0 点 → 此刻 |
| 自定义 | 你选的两端(本地时区;结束留空 = 此刻) |
两种形状是刻意不同的,而且都是你自己会用的说法:「今天」是一个日历日;而「最近一天」念出来的 是「昨天此刻到此刻」——不是昨天 0 点,后者会让清晨的读数少算掉大半天。
「昨天」是固定档里唯一两端都定的那个。 其余各档都延伸到此刻(「最近三天」包含今天),而「昨天」是一个 闭合的日历日——只给下界的话,它会把今天也吞进来。上界是开的:昨天 0 点到今天 0 点整,所以 今天 0 点整开始的那一轮归今天,两档在零点相接而不重叠。
「自定义」由你定两端,用的是两个原生日期时间输入,读写的都是本地时区(用 UTC 会让字段 显示的时间和你选的时间差几个小时)。结束留空 = 到此刻。未填起始时表格是空的,而不是显示全部—— 半填状态下显示全部会读成「自定义被忽略了」。它也是唯一记住具体时刻的档:其余各档都是相对此刻 算出来的,只有它存着两个瞬间。
选定范围后,顶部那三个胶囊不再是投影。没有任何投影带时间维度,所以它们改为从范围内的轮次 求和,命中率取和的比(先把缓存读取与计费输入分别求和,再套用宿主那份格式化)。和的比不是比 的平均:按轮平均会让 3 个 token 的一轮与 30 万的一轮等权。
范围是会话内的。 本插件只读当前会话的投影与事件窗,客户端够不到跨会话的日志(宿主侧的会话 查询没有对应的 remote endpoint),所以「今天」= 这个会话今天用了多少。
点一个范围不会自动载入任何东西。 窗口只装得下最近一段,所以在跑了一个月的会话上点「7 天」,
起初只会看到窗口碰巧覆盖的那部分——覆盖度行会明确写出缺口(· 更早的轮次还没载入),因为在范围内
「已统计 N / N 轮」这句话本身看不出还有更早的。
要补上缺口,由你点那个按钮(「载入范围内更早的历史」)。它是精确的:逐页往前拉(每页 50 条消息, Session Controller 自己的 pager),窗口最早一轮一早已于范围起点就停,而不是一路拖到会话开头; 上限 40 页(2000 条),超过则由旁边的「载入全部历史」一次到位。
为什么不让它自动:一页拉进来的是消息本体,而宿主无法卸载——窗口只会变大,真正的代价是 「看 7 天」所触发的载入,而不是它画出来的表。所以这个代价由选择它的人来付。
范围同样是记住的(见下),而它是五个偏好里唯一含义会移动的一个:「今天」永远是打开时的今天, 而不是选中它那天。
范围行下面有四个按钮,两两一组:前两个给数字,后两个给对话。
| 按钮 | 内容 |
|---|---|
| 统计 CSV | 与表同样的列;紧凑格式(8.2K、1分12秒),末尾一行合计 |
| 统计 JSON | 原始数字,含 seq、完整 usage 分桶、路由来源——给程序用 |
| 对话 MD | 每轮的完整提示词与回复(Markdown) |
| 对话 JSONL | 范围内每轮的原始事件,一行一个 JSON,与宿主日志同形 |
三处刻意的取舍:
8.2K 比 8214 好扫),
后者是程序读的(它没法把格式化逆回去)。缓存那一列在 CSV 里不带 %——带后缀的数字
在求和前还得先剪掉尾巴,那不是数字,是标签。导出的就是屏幕上那些:同一个范围、同一个顺序、同样的轮次——缺口也一并如实, 未载入的更早历史不会出现在文件里(与表一致)。空范围时四个按钮禁用: 一个只有表头的文件,不是比"没有什么可写"更好的回答。
CSV 的字段会按 RFC 4180 转义(含逗号、引号或换行时加引号、内部引号翻倍)—— 提示词里这些字符是常态,不转义不是"看起来乱",而是整行串列:表格会安静地读出一组 和界面不一样的数字。
五个读者偏好,都存在 localStorage(键带 dsh-session-usage. 前缀):
| 键 | 记住什么 |
|---|---|
…message-sort | 消息表的排序列与方向 |
…leaf | 停在哪个子标签 |
…scroll | 页面滚动到哪儿 |
…range | 停在哪个时间范围 |
…custom-range | 自定义区间的两端 |
两层,两层都承重:一个模块级值覆盖卸载(两个叶子是条件渲染的,切换叶子会卸载表格,而
useState 会静默把排序清回窗口自身顺序),localStorage 覆盖刷新。
--no-webstorage 运行),此时内存层
仍然覆盖本次页面——那里"忘了排序"是少个便利,不是一张坏掉的表。NaN,行序乱成一片——看起来像排序 bug,其实不是。本视图声明宿主的 data-conversation-composer-overlay,也就是 trajectory 视图用的那套模式。
一个属性换来三件事:
--dsh-composer-height 留白——否则长表的
最后几行会压在输入框下面。这个模式是声明式的:属性随视图一起消失,宿主自己收尾。
插件自己的文案提供 zh 与 en 两种(宿主自带的两种),全部在 src/client/locales.ts。
两本字典都标注为 Record<MessagesKey, string>:往 MessagesKey 加一个键,两本补齐之前
编译不过,所以不会出现「某个键只是忘了翻译、于是静默退回英文」。这个保证比回退链本身更重要。
数字与时长同样走本插件的字典(12.2K / 1.2M、45.2秒 / 2分42秒),因为每一种语言有
自己的写法,它们不是可以照抄的常量。
session-usage-plugin/
package.json dsh.bundle + dsh.client 声明、exports 映射
cordis.patch.yml 层补丁:一行 insert,把自己登记为 Loader 条目(无 config 块)
build.mjs 构建脚本(tsdown 编程接口)
tsconfig.json 类型解析:给 IDE 与 npx tsc 用(noEmit,只读)
src/
index.ts Node 半:不提供服务、不提供配置,只让 Loader 行能解析
client/
index.ts 浏览器半:注册「用量」视图与模型名查找
UsageView.tsx 容器:会话总计、两个叶子、覆盖度行、偏好与形态
ModelUsageTable.tsx 按模型表;模型名回退到原始 id
MessageUsageTable.tsx 按轮表;排序规则与未知值归位
turn-facts.ts 事件窗 → 每轮事实(模型、用量、缓存、用时、提示词)
usage-by-model.ts 每轮事实 → 每个模型一行
format.ts 紧凑数字/时长 + 移植宿主的缓存命中格式
preferences.ts 五个读者偏好(排序 / 叶子 / 滚动 / 范围 / 自定义区间)
model-names.ts 模型显示名:宿主目录 → id 的查找表
Coverage.tsx 覆盖度行与「载入全部历史」
table-styles.ts 两张表共用的表格 chrome 与页面根样式
time-range.ts 固定范围、其本地日历边界,以及自定义区间的本地时间换算
locales.ts zh / en 两本字典
lib/ 构建产物(index.js / client.js)
从 npm 装(发布的包里已带 lib/):
pnpm dsh plugin --profile web add dsh-session-usage
或者从本仓库装——这时构建由你自己跑,且改了 src 就要重跑:
# ① 构建(改了 src 就要重跑)—— 全程在仓库根执行
cd session-usage-plugin && npm run build && cd ..
# ② 安装到 profile(此时已回到仓库根)
pnpm dsh plugin --profile web add ./session-usage-plugin
构建也可用 node_modules/.bin/tsx session-usage-plugin/build.mjs(在仓库根执行,不必进子目录)。
注意这条依赖 node_modules/.bin/ 下的 shell 桩,Windows 上没有它,请用上面的 npm run build。
然后重启 pnpm dsh web(新增 bundle 层、以及 bundle 内容变化,都需要重启)。
卸载:
pnpm dsh plugin --profile web remove dsh-session-usage
cd session-usage-plugin
npx tsc -p tsconfig.json # 类型检查(无输出 = 0 错误)
npm run build # 产出 lib/index.js 与 lib/client.js
typescript 是 devDependency,所以类型检查在插件目录内就能跑,不需要宿主仓库。
oxlint 是宿主仓库自己的 linter,本插件没有装它;把插件放进 dsh 仓库下时才可用:
cd .. && node_modules/.bin/oxlint session-usage-plugin
重启后即可在会话标签页里看到「用量」。会话视图的标签是标签页,不是弹窗——它和「对话」 「轨迹」并排,占据会话区整块。
已统计 N / M 轮),更早的部分
由「载入全部历史」翻入——载入不回缩,刷新前一直有效,且无法取消。正在进行的轮次要等它
结束后才会出现用量。remote 层。 组合里没有它时,表格退回打印原始 id(如 deepseek-flash),
其余行为不变。zh 与 en。 语言包让其他语言可选时,本插件的文案会走 en 回退。conversation.blocks 是外壳自己的单槽登记处,
视图无法隐藏它,这是契约提供的唯一杠杆)。dsh.bundle 层按包名插入一行 Loader 条目,那一行必须能解析,
而这个插件的全部工作都在另一半。