Install
Inside DeepSeek Harness, with dsh-market
dsh plugin --profile web add dshmarket
Or from the command line
dsh plugin --profile web add github:Yinxe/deepseek-harness-plugins#path:/plugins/token-meter
Installing runs third-party code with your own permissions — it can read your files, use your credentials and reach the network. Review the source first, and pin a commit (github:owner/repo#sha) when you can.
Screenshots
README
This plugin publishes its README in Chinese only.
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 ./plugins/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的 taggit 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,子目录 plugins/token-meter,包名 @dshp/token-meter):
1. git clone monorepo 并 pnpm install
2. dsh plugin --profile web add ./plugins/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 web |
URL 直装对 update 免疫;两个 pnpm 坑见根 README |
| 方式三 git 依赖 | dsh plugin --profile web update @dshp/\<pkg\> + dsh web |
一条命令;git 解析约 35s/插件(实测) |
界面预览
截图在
images/目录(4 张,screenshots.json同步收录):分享面板(封面)/ 用量统计 / 在线统计,外加一张重构前的旧版额度查询界面(quota.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 合法,且解码后与原文逐字节相同(只「能加载」不够 ——<若不解码,(width<=300px)变成(width<=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%,与官方组件同一套),也不声明 4px、圆环 50%)属于图形几何,不受主题半径影响。无额外依赖。 |
| 同源路由 | corner-shape —— 主题包已用 *,:before,:after{corner-shape:var(--dsw-corner-shape)} 全局接管,插件再写一遍(哪怕写 round)就会把调主题插件的选择顶掉。图表内部的微圆角(点/条/热力格 1.5GET /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) |
老行为:任意非交互区域按住可拖,原位让位、回归 收回 |
两条硬约束(也是这套设计的由来):
- 可选依赖不能写进顶层
inject:export const inject = ['slots'],宿主用ctx.inject(['widgets'], …)挂载。否则用户没装框架时,本插件的额度按钮与设置页会一起消失 —— 小组件只是增强,不是本插件的存在前提。 - 卡片不占活动栏图标(
trayIcon: false),所以注册是常驻的:启动时与供应商增删时 由syncFloatWidgets()把注册表对齐到当前该有的卡片,否则刷新后「还开着」的卡片长不回来。
2026-09 UI 重构后只剩统计族(stats:<kind>),宿主 id 的映射因此变得很直白
(额度已不是可弹出的卡片,峰谷显示器随额度显示面一起删除,供应商 id 的 slug + 哈希编码一并下线):
| 插件内部 id | 宿主 id(@dshp/widget-kit) |
|---|---|
stats:cards |
token-meter:stats-cards |
stats:trend |
token-meter:stats-trend |
stats:heat |
token-meter:stats-heat |
stats:donut |
token-meter:stats-donut |
stats:today |
token-meter:stats-today |
每张卡的初始尺寸各自适配(不是一套尺寸打天下):热力图是宽而扁的周 × 日网格,给它高度纯属浪费;
模型分布是圆环 + 多列列表,矮了会压成滚动条;今日消耗只有一张卡,天生就小。数值只有一处
(widget-bridge.ts 的 FLOAT_SIZES),改尺寸只改那里:
| 小组件 | 初始 | 可缩到最小 | 依据 |
|---|---|---|---|
指标卡(stats:cards) |
460 × 360 | 300 × 220 | 3 列 minmax(148px) 网格 + 三段(全量 / 可视化 / 构成) |
趋势(stats:trend) |
520 × 320 | 340 × 200 | 折线图:横向空间直接决定可读性 |
热力(stats:heat) |
560 × 230 | 360 × 170 | 周 × 日热力网格宽而扁 |
模型分布(stats:donut) |
460 × 420 | 320 × 320 | 圆环 + 多列模型列表(窄于 480 会塌成一列,底部要留高) |
今日消耗(stats:today) |
320 × 200 | 260 × 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跳过(父会话已计),所以历史对话不会重复计时; - 「对话进行中」另算一路:
turn/start → turn/end的并集(不含轮次之间的空档); - 日志只在有事件时打点 —— 窗口开着但没事件的时间在数据里不存在,所以任何阈值下都是下界,不是「坐在电脑前」的时长;
- 阈值的三条规则(面板顶部常驻说明 +
online.stats/online.ts文件头同步维护):① 间隔 ≤ 阈值 → 整段算在线(中间空档一并计入,例如 10:00 与 10:50 两个事件、阈值 60 分钟,中间 50 分钟计入);② 间隔 > 阈值 → 断开,中间那段一秒不计,新段从下一个事件重新起算;③ 每段只算到最后一个事件,之后的时间(含关窗口前那几分钟、或之后 3 小时没产生事件)都不计 —— 所以任何档位都是下界,不是「坐在电脑前」的时长。 - 阈值是口径不是精度:同一份日志 1 分钟档与 60 分钟档能差近一倍,所以面板把五档(1/5/15/30/60 分钟)连同各自实测的累计值与边际增量一起摆出来(数据来自快照里已有的
totalMs[gap],点卡片即可切换)——只给当前档一个数字,用户没法判断该选哪个。(旧文案里写死过一句「60 分钟档比 1 分钟档多约 90h」,那是某个人的数据,对别人就是错的,已删除。) - 推荐默认 15 分钟(
DEFAULT_GAP_MIN,也是 Host 侧onlineGapMin的默认值):DSH 真正在干活时日志里持续有事件(模型 step、工具 call/result、子代理),不依赖大阈值;需要兜的是「读长回答、想下一个需求」这类几分钟量级的静默期。5 分钟以下会把这类静默期切断(偏低),60 分钟会把开会/吃饭整段算成在线(只适合回答「今天开着 DSH 多久」)。用户手动选过的值优先。
实现上分两级合并:fold 阶段把每会话的事件时刻按 1 分钟压成区间并随缓存落盘(on/tn 字段),
快照阶段再按所选阈值合并这些区间 —— 单链聚类可结合,因此换阈值不需要重扫日志。
(已用 238 个真实会话、50 万条事件校验:两级合并结果与「直接对全部事件按同一阈值合并」逐档一致。)
引擎时长(模型 / 工具)
与在线时长同一条扫描路径(同一个 readSession、同一次 foldSession、同一行缓存),但算术不同:
llmMs:Σ(step/start → assistant/message),只统计真正组装出消息的步骤(被取消的步骤不计);toolMs:Σ(tool/call → tool/result),按callId配对;turn/end时丢弃未落地的调用。
这两项是精确的:已用本机 126 个会话与 DSH 自带 sessionStats 投影缓存逐条对账,126/126 完全一致(差值 < 1ms)。
它们是跨会话相加的(同时开多个会话/子代理会重复计),所以「引擎合计」可能大于墙钟的「对话进行中」——这正是并行度的体现。
读取策略与「最小化重算」
日志会一直长(本机最大会话已 9.5 万条事件 / 压缩 28MB / 解压 71MB,按当前用量推 10 个月后可能 95 万条), 所以读取按代价从低到高三级,尽量避免整份重算:
- 会话级增量流(
stats/stream.ts):日志是 append-only 的多帧 zstd,会话已有流且只是变长时, 只consumeLogTail(file, offset)读新增的那几帧并推进折叠器; - 整份直读(
stats/logread.ts):首读或文件被重写时,逐帧解压 + 切掉 fork 继承前缀 (继承段是连续前缀,跳过time < createdAt的事件),事件解析出来就喂折叠器、 不驻留事件数组; - 官方 reader:只在没有日志文件 / 直读失败 / 需要 live 内存态时使用。
实测(本机 238 会话 / 250MB 日志):
| 场景 | 结果 |
|---|---|
| 流式首读 vs 整份直读 | 246/246 逐会话聚合完全一致 |
| 追加 867 字节后续读 vs 整份重读 | 结果完全一致;耗时 1ms vs 1190ms(≈1190×) |
| 流式首读内存峰值(最大日志) | 0MB(整份直读构造事件数组是 133MB) |
| 冷启动全量扫描(238 会话) | 5.7s,RSS 峰值 426MB |
| 热快照(全部命中缓存) | 19ms |
| 删掉缓存文件后重算 | 与首次完全一致(缓存是纯派生数据) |
逐帧解压与 zstdcat 在 246 个日志上逐字节一致;直读 238/238 成功;
fork 语义抽查与官方口径一致(整份继承的会话记 0、种子会话只记自己那 1.64 亿)。
zlib.zstd* 需要 Node ≥ 22.15/23.8,老版本自动退化为官方 reader。
哪些数据是精确的、哪些是估算的
| 指标 | 来源 | 聚合方式 | 性质 |
|---|---|---|---|
| Token(i/o/cr/cw) | 供应商上报的 usage |
按天/小时/模型求和 | 精确(按上报值);未上报 usage 的步骤算不到 → 略低 |
| 模型 / 工具时长 | 事件时间戳(step / tool 边界) | 逐会话求和(并行相加) | 精确(已与 sessionStats 逐会话对账) |
| 对话进行中 | 事件时间戳(turn 边界) | 区间并集(墙钟去重) | 估算;含少量等你操作的时间(审批等待实测约 0.7h) |
| 在线时长 | 全部事件时间戳 | 并集 + 空闲阈值合并 | 估算且是下界;阈值(1/5/15/30/60 分钟)直接决定结果 |
| 会话数 / 活跃天数 | sessionQuery 会话列表 |
计数 | 精确(扫描失败的会话会单独报数) |
面板的「口径与准确性」卡片把这张表直接渲染出来(精确/估算/下界 三色徽标),无需查文档;
同一张卡还会给出会话口径:有用量 / 总数 以及其余会话的去向
(请求失败/中断、fork 继承空壳、从没发起请求、供应商未上报 usage、读不出来)。
面板默认值
| 位置 | 默认 | 说明 |
|---|---|---|
| token 用量统计 | 全部 | 设置页可改 defaultRange(7/30/90/全部) |
| Token 活动热力图 | 按宽度自动 | 只保留 6 / 12 个月两档;卡片宽 ≥900px 默认 12 个月,否则 6 个月;手动切换后以手动为准并持久化(localStorage: tm-heat-span);格子横竖同一间隙(2/3/4px 随宽度) |
| 在线时长 · 空闲阈值 | 5 分钟 | onlineGapMin,面板上可切 1/5/15/30/60 |
| 在线时长 · 每日在线图 | 全部 | 面板上可切 近14天/近30天/近90天/全部 |
配置项
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
version |
number |
1 |
配置版本号(保留字段)。 |
activeVendor |
string |
'' |
当前供应商 ID;'' = 极简模式(不显示任何额度)。 |
refreshSec |
number |
60 |
自动刷新秒数;Host 按此间隔拉取已启用的供应商,0=关闭,其余钳制 10–3600。 |
enabled |
boolean |
true |
遗留字段,已无任何消费方(旧侧栏额度卡开关;Host 侧只原样回显,界面不读它)。自动刷新只看 refreshSec —— 写 enabled: false 不会关掉自动刷新。 |
vendors |
Vendor[] |
[] |
供应商列表({ id, name, type, params, enabled? },id 主键 2–31 位小写/数字/横线)。 |
showToday |
boolean |
false |
遗留字段(旧左栏开关,已无界面消费,仅保留做存储兼容)。 |
defaultRange |
'7' | '30' | '90' | 'all' |
'all' |
token 用量统计默认时间范围(缺省「全部」)。 |
onlineGapMin |
number |
5 |
在线时长空闲阈值(分钟),吸附到 1/5/15/30/60;只影响快照合并,不需要重扫日志。 |
供应商 type(providerTypes 下发,设置页下拉据此渲染):
| type | 说明 | 凭据 |
|---|---|---|
opencode |
Opencode Go+Zen 合并(/go 三窗口+订阅、/billing 余额,同 workspace 一次拉双页) |
workspaceId + cookie |
deepseek |
DeepSeek 单入口自动选路:sk- 开头走官方余额接口,会话票据走网页账单(余额+累计消费+趋势),一路 401/403 自动换路 |
apiKey 和/或 token(+可选 cookie) |
commandcode |
Command Code 额度:5 小时/每周滚动窗口 + 剩余额度 + 本计费周期用量/请求数/成功率/tokens | apiKey(user_ 开头,缺省回退 $COMMAND_CODE_API_KEY) |
manual |
本地账本,billing: rolling 填 total/used,payg 填 balance/granted/lowWarn |
无 |
旧 opencode-go / opencode-zen / deepseek-api / deepseek-web 均为别名:读路径自动归一,新写入只落主 type,不会改写你已有的配置文件。
供应商级 enabled(余额查询开关):缺省 = 启用,只有显式 enabled: false 才是禁用。禁用后该供应商不参与 Host 的主动定时拉取(含启动后的首拉),但手动「拉取」、已有快照展示、切为当前供应商都不受影响;开关在 设置 → Token 总览 的「额度配置」供应商列表里(写 POST /set-vendor-enabled)。启用时字段会被删掉而不是写成 true,因此旧配置零迁移、YAML 也不会冒出冗余字段。
显示偏好(不是配置项):它是浏览器本地偏好袋,不在持久化配置里(见「供应商自己的偏好袋」)。Host 没有这个字段、没有校验函数、也没有写它的路由;控件由该供应商的详情模板画在自己卡片底部(设置页没有这类控件),点一下写 localStorage 的 tm-quota-prefs 里它那一袋键值,侧边栏按钮立刻重画。因此它不随配置跨机器同步,也不会在编辑供应商后被覆盖(压根不在配置里)。
配置(profile 条目 config:)
profile 条目 dshp-token-meter 的 config:(标准 settings 存储,手工编辑条目重启后生效,设置页写入即热更):
dshp-token-meter:
activeVendor: oc-main
refreshSec: 60
enabled: true
showToday: false
defaultRange: '30'
vendors:
- id: oc-main
name: Opencode 官方
type: opencode
params: { workspaceId: wrk_xxx, cookie: $OPENCODE_COOKIE }
- id: ds-main
name: DeepSeek 官方
type: deepseek
params: { token: $DS_TOKEN, apiKey: $DEEPSEEK_API_KEY, lowWarn: 5 }
- id: m1
name: 手动账本
type: manual
params: { billing: payg, balance: 50, granted: 100 }
- id: cc-main
name: Command Code
type: commandcode
params: { apiKey: $COMMAND_CODE_API_KEY, lowWarn: 10 }
# apiKey 留空即自动回退读 $COMMAND_CODE_API_KEY(与 CLI 同一约定)
- id: old-main
name: 不常看的旧账号
type: opencode
enabled: false # 禁用余额查询:退出定时拉取,需要时仍可在卡片上手动「拉取」
params: { workspaceId: wrk_old, cookie: $OPENCODE_COOKIE_OLD }
activeVendor: "" = 极简模式(不显示任何额度)。
cookie即浏览器登录态:开发者工具 → Application → Cookies → 复制auth的值, 整段 Cookie(含oc_locale等)原样粘贴亦可;各参数的含义与获取方式见设置页表单 每个字段下方的说明(由 provider 的fields[].hint下发)。 旧写法兼容:opencode-go/opencode-zen、deepseek-api/deepseek-web均为别名, 读时自动归一到主 type(不落库、不改写你的文件);params.auth(opencode 系)→cookie值不变;{cred:}/{env:}→$NAME。
provider 两套模板(按钮级 / 详情级)
Host 侧:每个适配器 return 自己的数据(ProviderResult:windows/billing/view/extra)
外加可选的 icon(图标名)。
Client 侧:每个 provider 在 src/client/providers/ui/<type>.tsx 里导出一个 ProviderTemplates
对象,两套独立模板各管一段 —— 这就是「UI 也基于 provider 灵活分层」:
- 按钮级
button(ctx, K) => ReactNode:侧边栏那枚按钮里画什么 —— 完整节点、自由渲染 (环 / 余额大数字 / 多条窗口迷你条 / 供应商图标 / 任意组合),不限定「一枚环」; - 详情级
detail(ctx, K) => ReactNode:浮层里该供应商的主体内容(完整节点,自由排版)。
外加一个按钮专属声明:buttonHint(悬停 / 无障碍说明 —— 窄栏没有可见文字,全靠它;缺省回落
通用环语义)。「按钮上显示什么」没有统一声明:由该供应商在自己的 detail 里画控件、写 ctx.prefs
(见下文「供应商自己的偏好袋」)。
Host src/host/providers/<type>.ts → return 自己的数据 + icon(图标名)
Client src/client/providers/templates.ts → 契约(ProviderTemplates / ButtonRenderCtx / QuotaRingSpec + 工具函数)
src/client/providers/ui/<type>.tsx → 该供应商的 button + buttonHint + detail(控件也画在 detail 里)
src/client/providers/ui/index.ts → 注册表(一个 type 一行 registerProviderTemplates)
src/client/providers/registry.tsx → 调度:renderButton / buttonTitle / renderBody(各自兜底)
src/client/providers/kit.tsx → 通用可复用零件(ProviderUIKit,含 PrefSwitch / PrefChoice)
src/client/providers/sections.tsx → 声明式区块渲染(没有专属 detail 时用)
src/client/quota-prefs.ts → 供应商偏好袋(浏览器本地,见下)
src/client/provider-icons.tsx → 供应商图标(官方品牌路径 + 自绘线稿;iconOf 名字解析)
src/client/QuotaRing.tsx → 通用环:把 QuotaRingSpec 画成 SVG
src/client/QuotaTrayPanel.tsx → 浮层:纯展示 + 交互回调,不认识任何供应商
按钮级契约(自由节点)
interface ProviderTemplates {
button?: (ctx: ButtonRenderCtx, K: ProviderUIKit) => ReactNode; // 按钮内容(自由)
buttonHint?: (ctx: ButtonRenderCtx, K: ProviderUIKit) => string; // 悬停 / 无障碍说明
detail?: (ctx: RenderCtx, K: ProviderUIKit) => ReactNode; // 详情主体(显示控件也画在这里)
}
ButtonRenderCtx 在详情级 RenderCtx(snap / now / type / vendorName / prefs)之上补两样按钮独有的信息:
| 字段 | 类型 | 说明 |
|---|---|---|
variant |
'wide' | 'rail' | 'row' |
三档位置(见下表) |
icon |
string |
供应商图标名(Host 元数据 → 客户端兜底表 → 通用) |
(prefs 是按供应商分作用域的键值袋,按钮级与详情级都能拿到 —— 见下文「供应商自己的偏好袋」。)
variant |
位置 | 常规做法 |
|---|---|---|
wide |
展开的侧边栏(42px、拉满宽) | K.ButtonLayout:图标 + 主图形 + 名字 + 右对齐数值 |
rail |
收起的 56px 轨道 | 只放主图形(缺省「图标 + 主图形」并排),文字收起 |
row |
浮层里的供应商切换行 | 只放主图形(与那一行 16px 的图标槽对齐) |
按钮级零件(ProviderUIKit)
| 零件 | 说明 |
|---|---|
ButtonLayout |
骨架:宽栏 = 图标 + 主图形 + 名字 + 数值;窄栏 = rail 覆盖(缺省「图标 + 主图形」);切换行 = 只有主图形(glyph 优先) |
Ring |
QuotaRingSpec → SVG 环(几何 / 描边 / 动画 / 配色统一在这里) |
WindowBars |
多条滚动窗口的迷你条(5 小时 / 每周 / 每月并排;宽栏推荐画法,比三个环省地方) |
ProviderIcon |
供应商图标(16×16 网格 / 描边 1.4 的自绘线稿 + 官方品牌路径;未知名字给通用那枚;尺寸宽栏 16 · 窄栏 15 · 切换行 16) |
shortWindowLabel |
窗口短标签(5 小时 → 5时、每周 → 周) |
tinyMoney |
极短金额:$46.6 → $47、¥12345 → ¥1.2万(金额要塞进窄槽位时用;窄栏是表盘,没有数值位) |
PrefSwitch |
偏好开关:标签 + 原生 checkbox(键盘可达、可读屏),勾选即 onChange —— 供应商在自己的 detail 里用它画「数值位显示余额」 |
PrefChoice |
偏好单选组:role="radiogroup" + 每项 role="radio",当前项标 data-on —— 如 Goat 的 5h / 1w / 1m(默认 1m) |
环仍然是「一种常见画法」,不是契约:想画环的模板用 QuotaRingSpec 描述它 —— pct(0–100 或 null)/
tone(ok/warn/bad/idle,阈值 70 / 90 → warn / bad)/ text(环内极短文案)/ title(悬停说明,必填)/
custom(完全自绘覆盖)。两个特殊态:pct: null + tone: 'idle' 只画轨道(不编假比例);
pct: null + tone: 'bad' 画整圈警告色。失败快照走 failureRingSpec():空环 + 满圈警告色
(红 = 要人管的、黄 = 临时性的,与切换列表的状态点同源)。推荐尺寸由 ringSizeOf(variant) 给
(宽栏 / 切换行 16px,窄栏 28px = 表盘外圈)。契约里的工具函数(templates.ts):toneOfPct / clampPct / windowPct /
worstWindowPct / windowOfMetric(按供应商自己定的窗口键取一条窗口)/ balanceText /
defaultRingSpec / failureRingSpec。
供应商图标
图标名由 Host 的 provider 元数据声明(ProviderAdapter.icon → describeProviders() 下发
ProviderMeta.icon),客户端 iconOf(type, metaIcon) 解析:元数据名字优先 → 该 type 的兜底表
(TYPE_ICON_FALLBACK)→ 通用那枚。所以新增供应商不写客户端代码也有图标(通用那枚),
写了名字就换成自己的。五枚记号都是自绘线稿(currentColor,深/浅主题都成立,不贴位图、不带底色方块):
deepseek 官方鲸鱼路径(填充)、opencode 竖向圆角方框、commandcode 命令键 ⌘(中间一方块 + 四角四个回路)、manual 账本、
generic 通用兜底。
按 16×16 网格画、描边 1.4(PROVIDER_ICON_GRID),实际渲染 14–16px:接近 1:1,没有缩放模糊 ——
早先按 24 网格画再缩到 10–12px,0.42 倍缩放让 1.6 的描边只剩 0.67px,屏幕上就是「对不上、太小、发虚」。
每枚标记占满 12–13 个单位(四周留 1.5 左右),并排时视觉重量一致;外壳加 display:block 去掉 inline-SVG
的基线间隙。图标出现在宽栏(主图形前,16px)与窄栏(14px + 主图形 + 3px 间距,塞在 36×36 里)。
供应商自己的偏好袋
没有统一的「按钮显示」面板(曾经做过一排单选胶囊,被否掉了:那是把各家的口味硬塞进同一套控件)。 谁有偏好谁负责画 —— 控件由该供应商的详情模板摆在自己的卡片里,偏好存进一个按供应商分作用域的键值袋:
const window = ctx.prefs.get('window', 'monthly'); // 读(带兜底)
ctx.prefs.set('balance', '1'); // 写 → 侧边栏按钮立刻重画
- 键名与取值完全由供应商模板自己定(Goat 用
window=5h/weekly/monthly与balance=0/1; deepseek / manual 只用一个balance)。插件不定义任何语义,也没有auto这类保留值;换供应商想加 「显示的币种」之类的键,直接加即可。契约只保证三件事:读得到、写下去、写完全局重画。 - 存储是浏览器本地(
src/client/quota-prefs.ts的providerPrefs):localStorage键tm-quota-prefs, 值是{ [vendorId]: { [key]: value } }。读取时消毒:坏 JSON / 非法 id / 非法键值一律忽略;键名限[a-z0-9-]{1,24}、值限[a-z0-9._-]{0,32}(短字符串),每家 ≤16 键、≤64 家。写空串 = 删掉这个键。 - 它是组件级通信:控件
set()→ 写 localStorage 并通知订阅者(subscribe)→ 侧边栏按钮与浮层立刻重画。 零往返、不会失败,也没有「路由没热更新就弹一行『连接 Host 失败』」这种事。 - 为什么不做成 Host 配置:这是纯展示口味(换一台机器、换一个人,本就该不一样),写进持久化配置
会跟着配置漂到别的机器;而且为点一下按钮走一趟
/ext/...路由毫无必要。所以 Host 侧没有这个字段、 没有校验函数、没有写它的路由。 prune()在配置就绪后清掉已被删除供应商的偏好(等就绪再跑,避免启动瞬间误删);scope(vendorId)对 同一供应商永远返回同一个引用,可安全放进依赖数组。- 上一版的单键格式
tm-button-metric({vendorId: metric})已废弃,加载时顺手removeItem删掉。
四家的控件与画法
| provider | 详情卡片底部的控件(它自己画) | 按钮怎么画(button) |
详情(detail)的专有表达 |
|---|---|---|---|
deepseek |
「数值位显示余额」开关(PrefSwitch) |
环一直画总额度消耗(已用 /(赠送 + 充值);官方网页账单路线拿不到总额度 → 空环);数值位默认是百分比,勾了开关换成余额(余额不足转红) | 版式对齐官方客户端的「账户余额」卡:余额 / 赠送 / 充值 三行右对齐(多币种追加 USD 总额 这类行),往下是总额度消耗进度、消费统计、使用趋势(网页账单路线才有趋势,官方余额接口明确写「不提供历史趋势」的说明)、数据来源徽标(官方余额接口 / 网页账单接口 / 票据会过期;不再挂「凭据」徽标) |
commandcode(Goat) |
「数值位显示余额」开关 + 5h / 1w / 1m 单选组(PrefChoice,默认 1m) |
环一直画所选窗口的占用比例(默认月额度;没有窗口时回落「空环 + 余额」,低于预警线整环转红);数值位默认是该窗口的百分比,勾了开关换成余额(窄栏没有数值位) | 计划徽标(只标「计划 · 状态」,窗口百分比在窗口行上)+ 余额大数字(币种只出现一次)+ 三条窗口 + 额度构成 + 「账户与用量」两列网格(只取前 6 项、一格一项,不再成对合并 —— 合并会把两对指标挤成 $23.83 · 1.1 万 这种乱码流) |
opencode |
与 Goat 同构(开关 + 5h / 1w / 1m,默认 1m) |
同上;环的配额来自订阅三窗口(纯按量 Zen 用户没有窗口 → 空环 + 余额) | 顶部只留一枚徽标(「订阅 + 计划」或「按量计费」,窗口百分比不重复挂)+ 三窗口 + 充值余额 |
manual(本地账本) |
只有「数值位显示余额」开关(本地账本没有多条滚动窗口) | 环一直画账本「已用 / 总额」;数值位默认百分比,勾了开关换成手填余额 | 标注「本地手填,非实时」(原有版式不变) |
| 没写专属按钮模板的 type | 没有控件(详情也走声明式层) | 通用兜底 defaultRingSpec():最紧窗口 → 空环 + 余额,配上 ButtonLayout 骨架 |
声明式区块 → legacy(零客户端代码可用) |
窄栏(36px 圆)始终是一枚表盘:环当外圈、供应商图标居中在环心。数字放不下,说明走 tooltip(buttonHint)—— 勾没勾「数值位显示余额」都一样。
调度与兜底
registry.tsx 暴露三个函数,各条路径各自 try/catch,任一抛错不影响其它:
renderButton(input):专属button(自由节点)→ 通用兜底(defaultRingSpec()+ButtonLayout+Ring) —— 模板抛错、或返回null/undefined时照样有得画,侧边栏按钮不会凭空消失;buttonTitle(input):专属buttonHint→defaultRingSpec().title兜底;renderBody(input):专属detail→ 声明式区块 → legacy 快照 —— 详情永远有内容。
三条路径都会把 prefs: providerPrefs.scope(input.vendorId) 放进上下文(按钮级与详情级拿到的是同一个供应商的那一份),
所以模板里读写的偏好天然按供应商隔离。
失败快照不进专属模板(模板只承诺「成功快照的渲染」):拉取失败由弹层统一走结构化 ErrorCard
(结论 + 排查步骤 + 重试),任何 provider 一致;按钮侧则由各模板自己调 failureRingSpec() 画警示。
适配器没给 view 也不会退化:Host 用 defaultView() 从 windows/billing/extra 兜底推导;
只给 view 不给 extra 也不会让老客户端空白:viewToExtra() 反向补齐。
适配器还可声明 defaultParams(新增该类型时的表单初始值),如 commandcode 默认填 $COMMAND_CODE_API_KEY。
详情级通用零件(kit.tsx / ProviderUIKit)
(按钮级专用零件见上文「按钮级零件」;下面这些两处都能用。)
| 零件 | 说明 |
|---|---|
RollingBar |
滚动刷新进度条:label + 进度 + 占比 + 剩余时长(最常用,一行一个窗口) |
WindowGroup |
多条窗口成组,自动附「最高占用 / 最早重置」汇总行 |
BalanceBlock |
余额主数字块(币种/可用状态/赠送充值拆分/多币种/低余额告警) |
MetricRows |
键值指标行集合 |
MetricGrid |
两列指标网格(一格一项;可选 mergePairs 把相邻两项合成一格 —— 挤成 $23.83 · 1.1 万 会像乱码流,token-meter 的「账户与用量」就不用它) |
SplitBar |
分段占比条(含图例与百分比);每段声明语义色调 tone(brand/ok/warn/bad/info/muted),颜色由主题 token 决定 |
NoteLine |
提示条(info/warn/bad 三色) |
MiniChart |
趋势折线(纯 SVG,零依赖) |
Chips |
徽标组(计划/状态/来源等小标签) |
SectionTitle |
区块标题 |
ErrorCard |
失败卡(结构化结论 + 排查步骤 + 原始信息,见下节) |
| 工具 | num / fmt / fmtLeft / curSymbol / remainOf / timeAgo / levelOf / tinyMoney(极短金额) |
供应商排序
浮层里的「切换供应商」列表按「当前供应商 → 已启用 → 已禁用」(同档保持条目 config: 顺序)排列;
禁用项沉底是刻意设计(多为留档备查,不该占视线),行上直接标「已禁用」。设置页的供应商列表用同一套排序,
两处观感一致。
失败提示(apiKey 失效 / Cookie 过期 / 订阅结束…)
拉取失败一律转成结构化结论再渲染,不再只丢一句原始报错。分类与文案由 Host 产出
(src/host/errors.ts),客户端只按 kind 上色渲染 —— 与「provider 两套模板」同一思路,客户端不认识任何供应商。
| kind | 触发场景 | 配色 | 卡片给什么 |
|---|---|---|---|
auth |
API Key 无效/被撤销/复制不全 | 🔴 | 「密钥无效或已失效」+ 去平台重新生成;明文与 $NAME 引用分别给处置 |
session |
Cookie / Bearer 会话票据过期(含 HTTP 200 信封 40003 invalid token) |
🔴 | 「登录状态已过期」+ 重新登录抓取步骤 |
plan |
订阅到期/被取消/扣款失败(past_due/unpaid)/计划不支持该接口 |
🔴 | 续订升级指引;周期已过但状态未翻转时提前预警 |
balance |
余额不足(402) | 🔴 | 充值指引 + 调低预警线建议 |
rate |
限流(429) | 🟡 | 等待重试 + 调大刷新间隔 / 禁用定时拉取 |
network |
DNS / 代理 / 超时(fetch failed) | 🟡 | 网络与代理排查 |
parse |
接口改版(结构不符、字段缺失) | 🟡 | 提示需插件适配 + 复制详情反馈 |
config |
凭据或参数没配全(含 $NAME 解析为空) |
🟡 | 回设置页补齐(标 * 字段提示) |
server |
上游 5xx | 🟡 | 稍后重试 |
卡片交互:
- 结论行 —— 图标 + 标题 + 一句话解释,点击展开/收起;
- 处置行 ——
→ 具体去哪改,配「设置页」快捷入口; - 重试策略 —— 可直接重试的(限流/网络/5xx)显示「↻ 重试」;需先改配置的(密钥/会话/订阅/余额) 改显示「✓ 已处理,验证」,避免诱导用户盲目重试;
- 展开详情 —— 分类专属的有序排查步骤 + 原始信息(等宽、可「复制详情」整段贴给维护者)+ 官方文档链接;
- 状态点同步着色 —— 要人管的失败(密钥/会话/订阅/余额)红点,临时性失败(限流/网络)黄点。
扩展新供应商(4 步,前 3 步即可用)
- 复制模板:
cp src/host/providers/manual.ts src/host/providers/<新type>.ts(抓包型复制opencode.ts,API 型复制commandcode.ts更接近); - 改适配器:填
type/label/title/secretField/fields/defaultParams/icon/fetch,用view.ts的构造器产出view(契约见src/host/providers/base.ts的ProviderAdapter接口); - 注册一行:在
src/host/providers/index.ts底部registerProvider()加一行, 跑pnpm --filter @dshp/token-meter build,重启dsh web—— 设置页下拉与表单、 侧边栏按钮、浮层详情自动出现新类型,quota.ts与 client 零改动(按钮走通用环 + 骨架、 详情走声明式层,图标走通用兜底); - (可选)想要专属按钮内容、指标与详情版式:
cp src/client/providers/ui/manual.tsx src/client/providers/ui/<新type>.tsx, 写button(自由节点,常用K.ButtonLayout/K.Ring/K.WindowBars)、buttonHint
…
Comments
Comments live in GitHub Discussions. Sign in with GitHub to post or react.