Yinxe/dshp

archive/token-meter

DSH 额度与用量统计统一视图:多供应商滚动/按量额度(侧边栏卡片 + 供应商运维),外加会话日志 token 聚合(趋势、热力图、按模型拆分)。已归档并冻结在固定版本:作者不再为它适配新版 DSH。

用量与计费deepseek-harnessdshmcwiki

@dshp/token-meter

已归档(2026-10-20),不再维护。 本插件已从 plugins/ 移入 archive/,源码与 lib/ 构建产物仅作历史参考;安装路径为 ./archive/token-meter,后续不再适配新版 DSH;文中「更新」节仅作历史保留。

DeepSeek Harness(DSH)Token 总览插件:额度 + 用量二合一——一边看“外面还剩多少”(多供应商滚动额度/按量余额:opencode、DeepSeek、Command Code、手动账本),一边看“里面用了多少”(本机会话日志聚合:累计/峰值/连续天数、日/周趋势、24 小时分布、GitHub 风格热力图、模型分布、今日消耗,以及在线时长——在线 / 对话进行中 / 模型 + 工具三口径 + 每日排行)。额度只有一个入口:左侧边栏底部一枚按钮,上面画什么由该供应商的按钮级模板自由渲染(环 / 余额大数字 / 5 小时·每周·每月三条迷你条 / 供应商图标…,不限于环)——宽栏是「图标 + 主图形 + 名字 + 数值」,56px 窄栏只剩主图形;点开在按钮上方弹出浮层——当前供应商的详情(详情模板产出 + 数据新鲜度)+ 该供应商自己的显示开关(画在它的卡片底部,如 Goat 的「数值位显示余额」+ 5h / 1w / 1m)+ 「切换供应商」列表(每行左侧的图形同样来自该 provider 的按钮模板,点击即切换,一次只激活一个)。其余展示都在会话中心区注册的一个「Token 总览」tab 里(与原生「轨迹」同机制),tab 内用左侧菜单切换 用量统计 / 在线统计 / 设置 三块(设置分区与侧边栏 设置 → Token 总览 是同一个组件,两处改一处同步)。配置按 0.1.7 契约通过官方 settings API 持久化到 profile 条目 dshp-token-meter 的 config:,设置页改动即时热更新。配置只认 dshp-token-meter 一个键:旧双插件的 storages/token-quota.json 与历史 key(dshp-inx-token-quota / dshp-inx-token-stats)不再读取、不再迁移(手工迁移步骤见“常见问题”)。

设计原则:零依赖、零残留、可逆副作用。密钥只存引用($NAME)或脱敏展示;统计无网络上报、不额外落盘真相数据(会话日志即持久层;storages/token_stats.json 只是可丢的派生缓存,删了会自动凭日志重算);卸载即干净。

安装

方式一(推荐):克隆 monorepo 安装 —— 更新只需 git pull,旧版 DSH 可 checkout tag

# 1. 克隆 monorepo(lib/ 构建产物已提交,clone 下来就能用,无需 build)
git clone git@github.com:Yinxe/deepseek-harness-plugins.git
cd deepseek-harness-plugins
pnpm install

# 2. 本地安装到 profile(路径按你执行命令时的 cwd 解析)
dsh plugin --profile web add ./archive/token-meter

# 3. 卸载旧双插件(避免双份侧边栏/设置节重复;旧 settings 段需手工并到 dshp-token-meter,
#    本插件不做任何自动迁移)
dsh plugin --profile web remove "@dshp-inx/token-quota"
dsh plugin --profile web remove "@dshp-inx/token-stats"

# 4. 重启生效
dsh web
  • 更新:仓库内 git pull + dsh web(最快——lib 已提交,未改 src 无需 build);兼容旧版 DSH:按 compat.json 的 tag git checkout <tag> 后重跑上面的 add。

DSH 是最新版就到此为止。只有 DSH 停在老版本才需要先 git checkout <历史tag>(对照根目录 compat.json,现在是空的,不用管)。

dsh plugin 会把包写进 profile 的 dsh.profile.bundles —— 无需手动改配置文件。

改源码后:src/host 或 src/client 改完跑 pnpm --filter @dshp/token-meter build 重新打出 lib/host.js + lib/client.js,然后 dsh web 重启(client 半强刷页面即可)。

验证:打开 web → 设置 → Token 总览(统计设置 / 额度配置双 Tab);左侧边栏底部出现一枚额度按钮(内容由该供应商的按钮模板渲染:宽栏是「图标 + 主图形 + 名字 + 数值」,56px 窄栏只剩主图形),点开在按钮上方弹出额度浮层(当前供应商详情 + 它卡片底部的显示开关 + 供应商切换);打开任一会话,中心区标题栏出现「Token 总览」Tab(与「对话 / 轨迹」并列),点进去用左侧菜单切换 用量统计 / 在线统计 / 设置(配置项不必再绕去侧边栏,界面效果见下文「界面预览」);或 curl http://127.0.0.1:3080/ext/dshp-token-meter/state 返回 {ok:true, ...}。

一键 AI 安装:把下面这段发给你的 DSH AI 即可:

帮我安装 TokenMeter 插件(monorepo Yinxe/deepseek-harness-plugins,子目录 archive/token-meter,包名 @dshp/token-meter):
1. git clone monorepo 并 pnpm install
2. dsh plugin --profile web add ./archive/token-meter,并卸载旧 @dshp-inx/token-quota 与 @dshp-inx/token-stats
3. dsh web 重启,确认无报错,设置页出现「Token 总览」即成功
4. 打开设置页的 Token 总览,帮我添加一个供应商并刷新一次额度,再告诉我累计 Token 总量和今日用量

方式二:从 Release 安装(无需 clone;更新需手动重跑命令)

latest 滚动版(跟随 main 最新构建;滚动更新 = 重跑同一条 add 命令):

dsh plugin --profile web add \
  https://github.com/Yinxe/deepseek-harness-plugins/releases/download/latest/dshp-token-meter-latest.tgz

dsh web   # 重启生效

历史版本(兼容旧版 DSH)

main 永远跟随最新 DSH。老版本 DSH 用户装静态历史版本:按 compat.json 记录的兼容 tag,到 Releases 找对应版本 Release(v* tag 触发,静态存档、永不滚动),资产名 = dshp-token-meter-<tag>.tgz:

# 以 v0.1.5-rc.1 为例
dsh plugin --profile web add \
  https://github.com/Yinxe/deepseek-harness-plugins/releases/download/v0.1.5-rc.1/dshp-token-meter-v0.1.5-rc.1.tgz

dsh web   # 重启生效

更新

三种安装方式对应的更新方式

安装方式更新命令说明
方式一 clone(推荐)仓库内 git pull + dsh web最快;未改 src 免 build;旧版 DSH 用 git checkout <tag>
方式二 Release重跑同一条 add 命令 + dsh webURL 直装对 update 免疫;两个 pnpm 坑见根 README
方式三 git 依赖dsh plugin --profile web update @dshp/\<pkg\> + dsh web一条命令;git 解析约 35s/插件(实测)

界面预览

截图在 images/ 目录(4 张,screenshots.json 同步收录):分享面板(封面)/ 用量统计 / 在线统计,外加一张重构前的旧版额度查询界面(quota.png,只作历史对照,不再是现有功能)。

分享面板:用量统计与在线统计内聚成一块,可下载 PNG / 复制图片

中心区 Tab「Token 总览」—— 一个 tab + 左侧菜单

面板不占右侧栏,而是像原生「轨迹」一样在会话中心区注册一个 tab(conversation.view,order 40,与「对话 / 轨迹」并列);tab 内用左侧菜单切换三块内容:用量统计 / 在线统计,外加一条分隔线之下的 设置(偏好、供应商增删改、数据来源与统计缓存;与侧边栏 设置 → Token 总览 共用同一个组件,改一处两边同步)。额度不在这里——它只有左侧边栏按钮一个入口(见下)。选中项记在 localStorage(tm-center-section),下次打开回到上次看的那块;设置分区沿用设置页的行式表单布局,宽屏下收窄到 920px 居中(数据分区才吃满宽度)。

布局按「宽画布仪表板」设计(不是把右侧栏的窄单列搬过来):

  • 滚动:tab 页自己是滚动的那个——左菜单与分区标题栏在滚动容器之外,滚内容时它们一动不动。会话外壳里负责高度的那层(hashed 类 .wSkVaW_viewArea,激活态 flex:1 0 auto; min-height:auto)会让 height:100% 退化成 auto,所以插件用官方稳定钩子把它改成确定高度项:[data-conversation-scroll]:has([data-slot="conversation.view"] .tm-cview) > [data-slot="conversation.session"] > *。:has() 的参数刻意写全以把特异性抬到 (0,4,0),压过外壳那条 (0,3,0) 的激活态规则 —— 不依赖「插件样式晚于外壳注入」这种时序假设。随后 .tm-cmain 内的 .tm-cbody 成为唯一滚动容器(overflow:clip 而非 hidden,滚轮不会被截住)。两个坑:① 左菜单容器必须保持 overflow:visible,一旦 overflow:auto/hidden 它自己就成了滚动容器,里面的 position:sticky 会改为相对它定位、而它永远不滚,于是外层一滚菜单就跟着滚;② 万一钩子失效,会退化成「外层滚动 + 菜单 position:sticky 钉住」的旧模型,仍可用(两条路径都已验证)。
  • 不显示会话列宽拖拽条(原生轨迹视图同样不显示,仪表板也用不到窄列)。实现是对「有 .tm-cview 后代且直接挂着 [data-width-handle]」的那层隐藏。
  • 卡片区走 12 栏栅格,排布按「谁吃横向空间」定:用量统计是 基础数据 → 趋势(通栏)→ 热力图(通栏)→ 模型分布(通栏,内部纵向分栏兜底),趋势与热力图都是越宽越好读、并排只会各自被压窄;在线统计是 基础数据(在线时长)→ 每日在线图(通栏)→ 每日在线排行(7 栏)与口径表(5 栏)并排。容器窄于 1180px 时并排项自动逐级落回单列。
  • 两个分区的「基础数据」是同一套指标卡阵:grid-auto-rows:1fr 让同一行等高,卡片末行 margin-top:auto 让数值落在同一条网格线上,说明允许两行(不再被省略号切掉)。在线时长固定列数(≥1320 容器 4 列 / 820~1320 两列 / 更窄一列),7 张卡里让「引擎合计」(= 模型生成 + 工具执行)跨两列,最后一行永远是满的;标题行右侧带 准确性徽标图例(精确 = 时间戳直接算出 / 估算 = 由区间推断 / 下界 = 实际只会更多),不用猜颜色含义。
  • 图表统一「按容器实测宽度设 viewBox + 固定高度」,不用「固定 viewBox + width:100%/height:auto」——后者会按 viewBox 比例等比放大:趋势图原来固定 780×250,铺满中心区(约 1166px)时整幅被放到 374px 高,连 10.5px 的轴标签、1px 网格线一起被放大 1.49×,表现就是「图太大、太高、字太大」。改成 1:1 后:高度不再随宽度增长(趋势图 clamp(150, W*0.24, 230),中心区 230px、卡片总高 463 → 319px,字号回到 10.8px)、同样的 30 天拿到更多横向像素。每日在线图同理(原固定 320 宽会在宽屏下等比缩到 320px 居中留白,现在真正铺满并自动加高)。
  • 中心区拖窄到 620px 以下时左菜单收成图标轨道。
  • 分享面板:tab 头部「分享」按钮把 用量统计 + 在线统计里已经在用的那些组件原样内聚到一块面板上 —— 基础数据(13 张指标卡,通栏)/ 趋势 | 热力图 / 模型用量分布 / 在线时长(三口径)/ 每日在线 / 每日在线排行(后四块各占一整行)。页头分三层:① 标题区(左)+ 作者区(右,git 著者与邮箱);② 细线;③ 来源信息条 —— 插件名 / 版本 / 仓库左对齐独占一行,会话数·记录天数·生成时间靠右。插件名、版本、仓库地址全部由 tsup 的 define 在构建期从 package.json 注入(PKG,仓库取 homepage 去掉协议与 #readme),不写死字面量 —— 只注入版本而把包名/仓库写死的话,改包名时就会对不上。不自绘、不改配色:颜色、字号、图形、分档全部由组件自己走 --dsw-alias-* 主题 token 决定。尺寸模型:板宽跟随容器(width:100%)→ 组件按真实像素宽度排版、字号 100%;高度由内容决定,装不下就在舞台内滚动(与视图 .tm-body 的滚动模型一致),长图无所谓,不再为了塞进 16:9 而整体缩放。导出走 DOM → SVG <foreignObject> → Image → canvas → toBlob(零依赖)。这里有三个必须记住的坑:① 必须禁掉动画/过渡(animation:none!important)—— 卡片带入场动画 .tm-card{animation:tm-fadeup .42s backwards},而 @keyframes tm-fadeup{from{opacity:0}},<img> 里的 SVG 是静态图像上下文不会推进 CSS 动画,于是每张卡片都以 opacity:0 被光栅化,导出的图只剩没有动画的页头页脚(「下载/复制出来什么都没有」的真因);② 导出前要等两帧再量尺寸,否则首次导出会拿到还没排定的高度、切掉最后一行;③ 内联 CSS 必须做 XML 转义 —— SVG 是 XML 文档,而压缩后的样式表里带着容器查询的范围语法(@container i1tZMq_tmc (width<=1320px),当前产物里 16 处),文本里一个裸 < 就让整份文档解析失败:下载与复制同时报「SVG 渲染失败」,一张图都拿不到(CSS Module 迁移前内联的是未压缩的 styles.ts 文本,所以那时踩不到)。组装统一走 share-svg.ts 的 shareSvgDocument(),check-share-svg.mjs 拿构建产物里那份真实 CSS 做往返断言:XML 合法,且解码后与原文逐字节相同(只「能加载」不够 —— &lt; 若不解码,(width<=300px) 变成 (width&lt;=300px),容器查询整条被丢弃,图能出来但样式是塌的)。另外导出时要把插件 CSS 与用到的 28 个主题 token 计算值内联进 SVG(SHARE_TOKENS,pnpm test 里有覆盖自检),因为 foreignObject 读不到 DSH 壳层的样式表;输出为板子尺寸的 2×。板子上会隐藏档位/小组件开关(纯 CSS,组件代码一行没改)—— 静态图上的可点控件没有意义。

「额度」—— 侧边栏按钮 + 上方浮层

额度不再是中心区的一块面板,而是左侧边栏底部的一枚按钮:注册进官方 sidebar.footer.action(list / root 作用域,owner props 只有 { wide };本插件 id dshp-token-meter,order 10),与 sidebar 自带的两条 footer action 同尺寸(高 42px / 圆角 12px)。footer actions 在侧边栏里是垂直堆叠的(既有规则 div:has(> div[data-slot='sidebar.footer.action']) 把容器改成 flex-direction: column,否则与 Cordis / 手机端徽章并排会横向溢出被裁),所以每个入口独占一行、拉满宽度。

按钮里画什么是该供应商自己的事(ProviderTemplates.button → ReactNode,见下文「provider 两套模板」):可以是一枚环、余额大数字、5 小时 / 每周 / 每月三条迷你条、供应商图标,或任意组合,只要装得进一枚按钮。三档位置由 ctx.variant 告诉模板:

  • 宽栏(wide,42px 高、拉满宽)= 供应商图标 + 主图形 + 名字 + 右对齐数值(如 ⌘ ◍ Goat …… 92%),名字长了走省略号;
  • 窄栏(rail,56px 轨道)只剩 36×36 圆里那枚表盘(缺省 = 环当外圈 + 供应商图标 15px 居中在环心,见 ButtonLayout),没有可见文字也没有数值位,说明走 tooltip(side: top);放不下全精度金额的 balance 画法统一用 K.tinyMoney() 换成极短金额($46.6 → $47、¥12345 → ¥1.2万),不再各自写一份 compactMoney;
  • 浮层里的供应商切换行是第三档 row,只留主图形(16px,与那一行的图标槽对齐)。

环的推荐尺寸统一由 ringSizeOf(variant) 给(宽栏 / 切换行 16px:与 42px 行高、14px 字号相称,且要与浮层里 16px 的图标槽对齐;窄栏 28px:36px 的圆里「图标 + 小环并排」(早先是 14 + 16 + 3px = 33)会读成两个互不相干的小记号,16px 的弧长也看不出比例,所以让环放大成表盘外圈,28 + 2.2 描边还剩 4px 呼吸)。供应商图标两种画法并存:官方品牌路径直接按官方 viewBox 填充 currentColor(deepseek = 官方鲸鱼 0 0 23.16 17.04,按原宽高比渲染、不硬塞方框),自绘线稿按 16×16 网格 / 描边 1.4 画(commandcode 命令键 ⌘ / opencode 竖向圆角方框 / manual 账本 / 通用兜底),尺寸定为宽栏 16 / 窄栏 15(RAIL_ICON)/ 切换行 16;名字由 Host 元数据 icon 下发,未知名字与未知 type 都有兜底。

「按钮上显示什么」的控件由该供应商自己画(不提供统一的偏好面板 / 单选按钮组):供应商在自己的详情模板里用 ProviderUIKit.PrefSwitch(原生 checkbox)与 PrefChoice(分段单选,role="radiogroup")摆控件,写在 ctx.prefs 这个按供应商分作用域的键值袋里(浏览器本地)。Goat 的卡片底部就是「数值位显示余额」开关 + 5h / 1w / 1m 单选。

点开在按钮上方弹出浮层(useAnchoredPosition side top,portal 到 document.body,宽 344px,max-height: min(70vh, 560px),Esc / 点外部关闭):

┌──────────────────────────────────┐
│ 额度                           ⟳ │  ← 面板头:标题 + 刷新(拉取中显示转圈)
├──────────────────────────────────┤
│ ◍ Goat                           │  ← 卡片头只有 16px 主图形 + 供应商名(不重复类型标签、也不重复按钮上的名字/比例)
│ $12.34                           │  ← 当前供应商的**详情模板**产出(余额币种只出现一次)
│ 5 小时                       20% │  ← 窗口行右缘对齐:百分比定宽 34px、重置倒计时定宽 62px
│ 每周                         55% │
│ 每月                         92% │
│ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈ │
│ 数值位显示余额  ☐                │  ← Goat 自己的控件(画在它的详情卡片底部):勾上 → 数值位换成余额
│ 按钮画哪个窗口  [5h][1w][1m]     │  ← 图表(环)**一直画**,默认 1m(月额度);这三项写浏览器本地偏好
│ 10 秒钟前                      ⟳ │  ← 数据新鲜度 + 刷新
├──────────────────────────────────┤
│ 切换供应商                       │
│ ◍ DeepSeek 官方           当前 ✓ │  ← 每行左侧的图形来自该 provider 的按钮模板(`row` 档,16px)
│ ◍ Opencode 官方         拉取正常 │
│ ◍ 不常看的旧账号          已禁用 │
├──────────────────────────────────┤
│ 连接 Host 失败                   │  ← 错误提示挪到**页脚**,不再占第一屏第一行
│ 增删改供应商:设置 → TokenMeter  │
└──────────────────────────────────┘
  • 顶部只放必要信息:卡片头就是「16px 主图形 + 供应商名」(类型标签在下面的切换列表里已经有了,不再重复;卡片头也不复用宽栏按钮内容,所以不会再出现「Goat 68%」和「Goat CC」同时挂在顶部)。详情模板顶部同样不再重复窗口百分比徽标 —— commandcode / opencode 只留「计划 · 状态」或「按量计费」一枚徽标(百分比在下面的窗口行上),deepseek 去掉了「凭据」徽标,只留「官方余额接口 / 网页账单接口 / 票据会过期」。浮层里没有第二层卡片(详情区不画背景 / 边框,浮层本身即卡片)。
  • 控件画在供应商自己的详情卡片底部(详情主体与它之间一条细分隔线,类名 .prefBlock / .prefRow):谁有偏好谁负责画,插件不提供统一的「显示设置」面板。
  • 错误提示在页脚(与「增删改供应商:设置 → TokenMeter → 额度配置」同一块),不再占第一屏第一行。
  • 一次只激活一个:点某一行即写 activeVendor(Host 只主动拉取「已启用」的供应商);行右侧状态分 拉取正常 / 拉取失败 / 未拉取 / 已禁用,状态点与失败分类同源着色。
  • 偏好是浏览器本地偏好:控件写的是 localStorage 键 tm-quota-prefs 里该供应商那一小袋键值(键名与取值由供应商模板自己定),按钮立刻重画 —— 组件级通信,零往返、不会失败;不进持久化配置、没有 Host 路由(换机器 / 换浏览器就是另一份口味,详见下文「供应商自己的偏好袋」)。上一版的单键 tm-button-metric 已废弃,加载时顺手删掉。
  • 拉取失败时详情位显示结构化错误卡(ErrorCard:结论 + 处置 + 有序排查步骤 + 原始信息 + 重试),不是一句原始报错;同时按钮上的模板若走失败分支,会画空环 + 满圈警告色(红 = 要人管的、黄 = 临时性的)。分类学见下文「失败提示」。
  • 列表顺序 = 当前供应商 → 已启用 → 已禁用(同档保持条目 config: 顺序),见下文「供应商排序」。

旧版额度查询界面(重构前)

上图是重构前的旧版额度查询界面(中心区面板 + 供应商卡片网格 + 峰谷横幅),相关显示面已全部删除;现在额度只在侧边栏按钮的上方浮层里。

「用量统计」—— 统计仪表盘

顶部指标卡(模型调用次数 / 首次使用 / 最近使用)+ Token 使用趋势折线(近 24 小时/7 天/30 天,按天聚合,悬浮查看明细)+ Token 活力热力图(6 / 12 个月两档 —— 卡片够宽自动铺 12 个月、窄了自动退回 6 个月,也可手动切换并记住;悬浮查看当日明细:总消耗、会话数、输入/输出构成、缓存读/写与占比)+ 模型用量分布环形图(中心累计 Token,悬浮查看各供应商×模型构成)。

用量统计:指标卡 + 趋势折线 + 活力热力图 + 模型分布

「在线统计」—— 三口径 + 每日在线 + 排行

顶部常驻空闲阈值说明:三条规则(相邻事件间隔 ≤ 阈值则整段计入含中间空档、超过则断开、每段只算到最后一个事件所以任何档位都是下界)+ 五档实测对比(1/5/15/30/60 分钟各自的累计在线与边际增量,直接取自快照,点击即切换,推荐 15 分钟并标注)。下方是 7 张三口径指标卡(今日在线 / 累计在线 / 活跃日均 / 对话进行中 / 模型生成 / 工具执行 / 引擎合计,各带 精确 / 估算 / 下界 徽标)、每日在线柱线图(柱 = 对话中 + 空档,虚线 = 引擎合计,峰值标注)、每日在线排行(柱状图与列表合体,悬浮看当日明细)与「口径与准确性」表。

在线统计:空闲阈值说明与五档对比 + 三口径 + 每日在线 + 排行

功能

部分内容
Host(src/host/ → lib/host.js)额度:provider 注册表委托拉取(opencode / deepseek 自动选路 / commandcode / manual,别名 opencode-go/opencode-zen/deepseek-api/deepseek-web 读路径自动归一),每个 provider 同时产出自己的数据与 UI 视图(ProviderView 声明式区块 + icon 图标名,见「provider 两套模板」),密钥 $NAME/{env:}/{cred:} 解析(凭据服务优先、环境变量兜底),明文脱敏(只写不读、留空保留)、secret-to-cred 一键转存、内存快照(st.snaps,失败保旧)。统计:sessionQuery 全量扫描 + 文件指纹缓存(storageDomain 域 token_stats,域名保持不变;记录版本 v=4)+ 在线时长估算(stats/online.ts)+ 后台分批泵(每批 3 会话——批内并发受内存约束、setImmediate 让出、单会话超时 20s 起按次退避至 90s、失败 3 次跳过)+ session/event 失效。通过官方 ctx.settings + schemastery 持久化到 profile 条目 dshp-token-meter 的 config:;配置只认该 NS,不读旧文件/旧 key。
Client(src/client/ → lib/client.js)「设置 → Token 总览」分成两个 Tab:
  • 统计设置:数据来源提示(所有统计都由 $DSH_HOME/sessions/ 的会话日志重算,迁移/备份必须保留该目录;storages/token_stats.json 可丢)、缓存与扫描状态(复用 / 缓存命中 / 扫描失败 / 直读 / 存储)、清除统计缓存(确认后清空派生缓存,下一次快照全量重算)、默认范围、在线时长空闲阈值;
  • 额度配置:自动刷新间隔 + 供应商添加与管理(当前选择/拉取/启用-禁用/编辑/存凭据/删除)。这里没有「按钮显示」控件 —— 那项偏好放在额度浮层里(你看到按钮的地方),而且它存在浏览器本地,不写持久化配置。

图表全部搬到中心区。失效的侧边栏显示开关已移除。中心区注册一个 tab(官方 conversation.view,order 40,与原生「对话 / 轨迹」并列),tab 内用左侧菜单切换三块:用量统计为完整统计(指标卡、趋势线、热力图、模型环形图 + 顶部的「小组件」工具条);在线统计为每日/累计时长统计:在线(你 + DSH)/ 对话进行中(DSH 的钟)/ 模型 + 工具(DSH 的活,精确)三口径并列,柱线混合图(柱 = 对话中 + 空档,虚线 = 引擎合计)带悬浮数据提示;每日在线排行把柱状图与列表合体(每行既是名次也是一根横向堆叠条,悬浮出当日明细,可展开全部活跃日);另有口径与准确性表(精确/估算/下界徽标,含会话口径分解)。(在线统计面板不支持弹出为浮窗,顶栏抓手已移除。)额度入口是左侧边栏底部的一枚按钮(官方 sidebar.footer.action,不占活动栏图标、也不占右侧栏原生 tab):按钮内容、浮层详情、以及卡片底部那些显示控件都来自该 provider 的两套模板;点开在按钮上方弹出浮层。供应商级余额查询开关:每个供应商可单独「禁用」——禁用后它退出 Host 主动定时拉取,浮层里那一行标注「已禁用」,但手动「拉取」、已有快照展示与切换为当前均不受影响(开关在 设置 → Token 总览 的「额度配置」里)。小组件化:统计图表(指标卡 / 趋势 / 热力 / 模型分布 / 今日消耗)各是一个独立小组件,入口是中心区「用量统计」顶部的「小组件」工具条(逐张开合)。装了 @dshp/widget-kit 时窗口由框架画(含吸附预览、主题色玻璃、动效、刷新后原地恢复),没装时自动退回插件自带浮层——两种承载下原位都会消失,回归 按钮收回原位。详见「小组件」。UI 全部使用 DSH 官方设计 token(dsw-alias-*),与官方设置页风格一致。图标全部自绘、零依赖:导航图标在 src/client/icons.ts,供应商图标(官方鲸鱼路径 / 命令键 ⌘ / 圆角方框 / 账本 / 通用兜底)在 src/client/provider-icons.tsx,数据语义图标在 src/client/glyphs.ts(23 枚:累计=堆叠、缓存=圆柱、峰值=闪电/山、日均=基准线+起伏、中位=中位线、连续=火苗/奖杯、活跃=日历、调用=芯片、首次=旗、最近/在线=时钟、今日=太阳、累计在线=沙漏、对话=气泡、工具=终端、引擎=齿轮、热力=九宫格、模型分布=圆环、每日在线=柱状、排行=榜单、口径=同心靶…)。同一枚图标用两处:1314px 贴在指标标签前 / 卡片标题前,以及放大到 58px、约 8% 透明度摆在各指标卡右下角当水印底纹(被卡片圆角裁切),让每张基础数据卡带一点与数据意境相符的图形而不是纯色块;色调态卡略强(12%)。未知图标名回退成圆点,不会崩也不会空白。圆角只用产品默认值,且不覆盖全局角形:不设私有半径变量层(卡片/浮窗 12px · 按钮/输入 8px · chip 6px · 胶囊 999px · 圆形 50%,与官方组件同一套),也不声明 corner-shape —— 主题包已用 *,:before,:after{corner-shape:var(--dsw-corner-shape)} 全局接管,插件再写一遍(哪怕写 round)就会把调主题插件的选择顶掉。图表内部的微圆角(点/条/热力格 1.54px、圆环 50%)属于图形几何,不受主题半径影响。无额外依赖。 | | 同源路由 | GET /ext/dshp-token-meter/state(配置脱敏 + 快照 + provider 元数据)、POST /ext/dshp-token-meter/config(通用偏好补丁)、POST /ext/dshp-token-meter/refresh、POST /ext/dshp-token-meter/set-active、POST /ext/dshp-token-meter/set-refresh、POST /ext/dshp-token-meter/set-enabled(旧客户端兼容,当前界面不再调用)、POST /ext/dshp-token-meter/set-vendor-enabled(单供应商余额查询开关,{ id, enabled })、POST /ext/dshp-token-meter/add-vendor、POST /ext/dshp-token-meter/update-vendor、POST /ext/dshp-token-meter/delete-vendor、POST /ext/dshp-token-meter/secret-to-cred、GET /ext/dshp-token-meter/stats(聚合快照,别名 /data)、POST /ext/dshp-token-meter/clear-cache(清空派生统计缓存 → 下次快照全量重算),均带同源校验(Origin 与 Host 一致或缺失才放行)+ no-store。 || 同源路由 | GET /ext/dshp-token-meter/state(配置脱敏 + 快照 + provider 元数据)、POST /ext/dshp-token-meter/config(通用偏好补丁)、POST /ext/dshp-token-meter/refresh、POST /ext/dshp-token-meter/set-active、POST /ext/dshp-token-meter/set-refresh、POST /ext/dshp-token-meter/set-enabled(旧客户端兼容,当前界面不再调用)、POST /ext/dshp-token-meter/set-vendor-enabled(单供应商余额查询开关,{ id, enabled })、POST /ext/dshp-token-meter/add-vendor、POST /ext/dshp-token-meter/update-vendor、POST /ext/dshp-token-meter/delete-vendor、POST /ext/dshp-token-meter/secret-to-cred、GET /ext/dshp-token-meter/identity(分享卡用的 git 身份:先 ctx.get('shell') 跑 git config --get user.name|user.email,拿不到 shell 服务或命令失败就退化为直接解析 $XDG_CONFIG_HOME/git/config / ~/.gitconfig;可用 DSHP_TOKEN_METER_GIT_NAME / _GIT_EMAIL 覆盖;结果含失败都会缓存)、GET /ext/dshp-token-meter/stats(聚合快照,别名 /data)、POST /ext/dshp-token-meter/clear-cache(清空派生统计缓存 → 下次快照全量重算),均带同源校验(Origin 与 Host 一致或缺失才放行)+ no-store。 | | 工具 | 无模型工具(本插件为运维/展示型,不注册 tools)。 |

小组件(框架卡片 / 自带浮窗)

入口:中心区「用量统计」顶部的**「小组件」工具条** —— 五张统计卡(指标卡 / 用量趋势 / 用量热力 / 模型分布 / 今日消耗)各有一个开关按钮,就地开合。本插件不占活动栏图标(一个都不占): 额度入口是侧边栏按钮,统计卡的开关是这块工具条,所以没有「一个图标点开小组件菜单」这回事。

每张统计图表都可以从工具条(或卡片自己的开关)弹出为独立小组件,原位随之隐藏(回归 收回)。 同一份组件、两种承载:

条件谁画窗口你得到什么
装了 @dshp/widget-kit(推荐)框架(ctx.widgets.register,trayIcon: false 的自由卡片)拖拽 / 八向缩放 / 最小化成胶囊 / 锁定位置 / 八向吸附与主题色预览 / 外观与动效跟随全局设置 / 刷新后位置与开关原地恢复
没装(或服务形状不符 / 被停用)插件自带浮层(widgets.tsx 的 WidgetFloat + portal,坐标存 localStorage: tm-widgets)老行为:任意非交互区域按住可拖,原位让位、回归 收回

两条硬约束(也是这套设计的由来):

  1. 可选依赖不能写进顶层 inject:export const inject = ['slots'],宿主用 ctx.inject(['widgets'], …) 挂载。否则用户没装框架时,本插件的额度按钮与设置页会一起消失 —— 小组件只是增强,不是本插件的存在前提。
  2. 卡片不占活动栏图标(trayIcon: false),所以注册是常驻的:启动时与供应商增删时 由 syncFloatWidgets() 把注册表对齐到当前该有的卡片,否则刷新后「还开着」的卡片长不回来。

2026-09 UI 重构后只剩统计族(stats:<kind>),宿主 id 的映射因此变得很直白 (额度已不是可弹出的卡片,峰谷显示器随额度显示面一起删除,供应商 id 的 slug + 哈希编码一并下线):

插件内部 id宿主 id(@dshp/widget-kit)
stats:cardstoken-meter:stats-cards
stats:trendtoken-meter:stats-trend
stats:heattoken-meter:stats-heat
stats:donuttoken-meter:stats-donut
stats:todaytoken-meter:stats-today

每张卡的初始尺寸各自适配(不是一套尺寸打天下):热力图是宽而扁的周 × 日网格,给它高度纯属浪费; 模型分布是圆环 + 多列列表,矮了会压成滚动条;今日消耗只有一张卡,天生就小。数值只有一处 (widget-bridge.ts 的 FLOAT_SIZES),改尺寸只改那里:

小组件初始可缩到最小依据
指标卡(stats:cards)460 × 360300 × 2203 列 minmax(148px) 网格 + 三段(全量 / 可视化 / 构成)
趋势(stats:trend)520 × 320340 × 200折线图:横向空间直接决定可读性
热力(stats:heat)560 × 230360 × 170周 × 日热力网格宽而扁
模型分布(stats:donut)460 × 420320 × 320圆环 + 多列模型列表(窄于 480 会塌成一列,底部要留高)
今日消耗(stats:today)320 × 200260 × 150单张卡,天生小

宽度是按 styles.module.css 里的容器查询断点(300 / 340 / 420 / 480)挑的:取在断点之上, 内容才是「宽屏那套排版」,而不是把窄屏排版塞进一个宽窗里。

编码/标题/尺寸是纯函数(src/client/widget-bridge.ts),由 scripts/check-widget-bridge.mjs 看守 (7 组:id 合法 / 稳定 / 不撞车、统计族可反解、isFloatId 只认统计族、标题表、尺寸表 5 族); 另有 scripts/check-widget-kit-bridge.mjs 把两个插件的真实产物对接起来跑一遍(无浏览器), 确认「5 张统计卡真的进了框架注册表、没有框架时也照常退化」、0 个带活动栏图标的描述符、 侧边栏按钮真的注册进了 sidebar.footer.action(宽栏 = 图标 + 名字 + 数值,窄栏 = 一枚表盘:环当外圈、供应商图标居中在环心), 以及在供应商自己的卡片里操作控件:断言详情里恰好 1 个 role="radiogroup"(含 5h/1w/1m 三项)与 1 个 input[type=checkbox];prefs.window=5h|weekly|monthly → 按钮分别显示 20%|55%|92%;勾上开关 → 数值位变 $12.34 且环仍在(百分比消失);点选项 / 勾开关都写进 localStorage 的 tm-quota-prefs,重开一份 bundle(≈刷新页面)仍读得到。另有 scripts/check-quota-templates.mjs(9 组)看守按钮模板的通用语义、 偏好袋与配色护栏(占用档位、多窗口取最高、失败环、没有写死色值、偏好袋作用域隔离 / 坏数据不崩 / 可订阅 / 能清孤儿、色调类不许有裸规则)。

统计口径

  • 总 Token = inputTokens + cacheReadTokens + cacheWriteTokens + outputTokens(reasoningTokens 已含在 outputTokens,不重复计)
  • 同一 (turn, step) 的 usage chunk 为早期采样、assistant/message usage 为终值,覆盖不重复累计
  • fork 会话跳过其继承前缀(readSession.inheritedEventCount,即落盘 seedLength;父会话已计),不重复计数
  • 含子代理会话(它们是真实用量)

数据源是 sessionQuery 服务实时扫描会话日志:会话日志是唯一真相源,storages/token_stats.json 只是可丢的派生缓存(每会话一行,含 token 桶与时长区间),删了会自动重算;因此备份/迁移请保留 $DSH_HOME/sessions/。设置页提供「清除统计缓存并重算」作为手动兜底。

在线时长口径(stats/online.ts)

「在线」= 会话日志里有事件、且相邻事件间隔不超过阈值的墙钟时间:

  • 跨会话按区间并集合并:同一时刻开着多个会话/子代理不重复计时(各会话时长直接相加会虚高约 30%);
  • fork/续接会话的继承前缀按 inheritedEventCount 跳过(父会话已计),所以历史对话不会重复计时;
  • 「对