统计 SillyTavern(酒馆)的 Token 用量、请求次数与费用。
和常见的用量统计不同,它能抓到装在酒馆助手上的插件 / 前端界面发出的请求 —— 那些脚本跑在
iframe 里,用的是它们自己的 window.fetch / XMLHttpRequest,普通的 fetch 钩子看不见。
本项目是 Operit / TokenFlow 的修改版(fork),不是原创作品。
| 原项目 | ddsadasfdxc/TokenFlow |
| 原作者 | Operit |
| 原许可 | AGPL-3.0 |
| 本修改版许可 | AGPL-3.0(继承原许可,完整全文见 LICENSE) |
| 修改时间 | 2026-09 |
按 AGPL-3.0 第 5 条的要求,此处明确声明本作品经过修改,改动内容见下一节。
如需再分发,请一并保留本声明与 LICENSE 文件。
本修改版为个人自用而做,同时公开发布。原项目的版权归原作者所有。
原版只在主窗口上 patch 一次 window.fetch,且只认一个 URL。于是下面这些全部漏掉:
- 酒馆助手的脚本 / 前端界面跑在无沙盒 iframe 里,有自己的
fetch与XMLHttpRequest jQuery.ajax/ axios 走的是 XHR,根本不经过fetchTavernHelper.generateRaw()这类宿主函数,网络层可能不出现 fetch- TauriTavern 或其它扩展重写
window.fetch时,一次普通赋值就会把捕获层整个丢掉
现在改为每个同源窗口都装一遍,对每个窗口装上 fetch(属性访问器)/ XMLHttpRequest /
TavernHelper.generateRaw,并用 500ms 看门狗周期补装。跨层去重保证同一次生成只记一条。
原版 recordUsage() 里的 cost 变量声明在 for 循环体内、却在循环外使用,
每次记录都抛 ReferenceError,导致今日统计、历史归档、用量限额、悬浮球角标全部不生效,
saveSettingsDebounced() 也从未执行(数据不落盘)。同时修掉了「网络失败时会把同一个请求重发一遍」。
对齐 DeepSeek 开放平台 / New API 的视图:
- 时间范围 今天 / 近 7 天 / 近 30 天 / 全部
- 日柱图按连续日期铺满(缺日补 0,不会错位),点柱子看当天按模型明细(平板没有 hover)
- 概览:消费金额、总 Token、输入 / 输出 / 缓存命中、缓存命中率
- 明细:每个模型的费用 / 占比 / Token / 请求数,支持按模型或按厂商分组
原版在记录当时就把费用算死。如果一开始没填价格,之后补上也不会回填。
现在每个模型桶都保留 输入/输出/缓存/请求数,今日与历史归档也存按模型用量,
于是补上单价后 累计 / 会话 / 今日 / 历史 会全部重算。
- 归一化:去
vendor/前缀、_→-、去日期尾巴、去:free/-preview之类的装饰 - 自动推断:
deepseek-flash能匹配到价格表里的deepseek-v4-flash - 手动覆盖:价格表新增「别名」列,可以自己把外部模型名钉到某一档价格
- 只有请求名不带版本时才允许跨版本推断 ——
gemini-2.5-flash绝不会被并到gemini-3.7-flash - 界面上会标出每个模型的计价来源(精确 / 别名 / 模糊 / 未设单价)
检测到价格表里没有的模型时弹一次 Toast,面板顶部也有警告条与「一键补入价格表」。 未定价的模型费用记 0,但会被明确标注出来,不会静默消失。
借鉴了 worldbook-manager 的 cache-inspector 做法:定长环形缓冲(400 条)、写入前截断、 暴露全局诊断入口。面板底部有日志区,可复制日志 / 复制诊断 / 导出 / 清空。
没有 DevTools 的设备(比如平板)也能自查:面板上那一行
🛰 已捕获 N · 估算 M · 未捕获 K · 窗口 W 就是抓取健康度。
- 历史归档用
+=反复累加当天总额(n 次请求会膨胀成 n(n+1)/2 倍) - 月预算 / 周额度把今天算了两次;跨夜后把昨天的数当成今天
- 周重置时刻
12:19被setMinutes解析成00:12 - OpenAI 语义下
prompt_tokens已含cached_tokens,被重复计数 + 重复计费; Anthropic 同时有读写时cache_read被丢掉;DeepSeek 的prompt_cache_hit_tokens从未被识别 Ctrl+Shift+T等若干小问题;删掉了从未被调用的 280 行死代码
- 时间范围扩到 今天 / 近 7 天 / 本周 / 近 30 天 / 本月 / 全部,外加自定义起止日期
- 5 套配色主题(横竖屏、平板触屏都验过),文字对比度按 WCAG AA 逐项校验
- 「精确追踪」开关立刻生效:关掉马上停止记账,打开立刻补装捕获层 —— 不用再重启酒馆(旧写法只在启动时读一次,勾了没反应)
- 导入数据前二次确认,并把导入的 JSON 当不可信输入处理(挡掉
__proto__之类字段) - 修掉一个栈溢出:宿主把
fetch包成「转调我们这一层」时, 非生成端点的透传会绕回自己无限递归;透传现在也走深度保护 - 同一帧内的多次面板重绘合并成一次,历史记录多时点控件不再卡顿
- 位置从「像素」改成「占可用空间的比例」。旧写法把像素坐标存进设置,而这份设置
由酒馆同步到服务端、PC 和手机共用:在电脑上把球拖到屏幕右侧(x≈1850),换手机
打开时
innerWidth只有 390 —— 球被画到屏幕外,表现就是**「手机上看不见悬浮球」** - 恢复位置时按当前屏幕夹取;横竖屏切换、手机地址栏收放都会重新摆一次
- 拖动不再无条件吸附到左右边缘:只有松手时贴着边(≤24px)才吸附, 其余位置就停在你放的地方(旧版永远只能待在屏幕两条边上)
- 窄屏(手机)的默认位置抬到输入栏之上,并留出系统手势安全区
真机截图:概览那 6 张统计卡占着版面位置,却连同文字一起没画出来(下面的区块 没有被顶上去)。
- 这 6 张卡原是面板里唯一带
backdrop-filter的元素,而面板背景在 5 套主题里 全是不透明的 —— 卡片去模糊"背后"其实什么都模糊不到:零收益,却每张卡各多一个 合成层。移动 GPU 上这类层被整块丢掉时,表现正是"占位但不绘制"。已去掉, 背景改走扩展自己的主题变量(不再依赖酒馆的--SmartThemeBlurTintColor) - 遮罩层补上
-webkit-backdrop-filter前缀(WebKit 内核只认前缀版) - 新增渲染后自检:万一还有别的原因让概览渲染不全,会往运行日志写一条
ui.blank,带上容器 / 子节点数 / 视口 / 主题 —— 面板底部就能看到、能复制, 平板上没有 DevTools 也能留证 - 测试补强:L2 每次交互后都断言「6 张卡都在、都有数字、有实际尺寸、没有 backdrop-filter」,真机测试连点 15 次刷新复查(旧断言只看金额那一块,卡片全没了 照样全绿,所以这个 bug 一直没被测出来)
外观:旧版球体背景全靠酒馆的 --SmartThemeBlurTintColor 调,浅色宿主下就是一颗
"白底 + 浅色图标"的白圈,几乎看不见(真机截图实证);而且主题变量块只挂在
[data-tf-theme] 上,悬浮球没这个属性,永远拿不到扩展自己的主题色。
- 改成深色球体 + 主题强调色,任何宿主主题下都清楚;球本身也挂
data-tf-theme, 跟着面板那 5 套配色一起变 - 图标换成内联 SVG(上扬折线 + 箭头),不再依赖酒馆的 FontAwesome 是否加载
- 去掉球体的
backdrop-filter和无限 box-shadow 动画(每帧重绘,平板纯耗电) - 触屏上不再让
:hover状态粘住
价格角标:真机反馈"那个价格有时候会消失",两个原因:
updateOrbBadge只看今天(dailyStats.cost),等于 0 就display:none。 跨天、或今天还没产生消费时,角标直接没了。现在今天为 0 就退回显示累计费用, 真的没有任何数据时才隐藏- 只要进过一次预算预警态,
updateOrbBadge就return,价格再也回不来 (clearOrbAlert只把 display 设成 none)。现在角标渲染收拢到updateOrbBadge一个出口,预警解除后价格自动恢复 - 金额改成紧凑格式:
$0.97/$24.9/$1.2K(旧版是$0.974171, 六位小数把角标撑成横条盖住整颗球);不足一分钱显示<0.01而不是0.00 - 角标从"横着拖出球外的长条"改成右上角的小吊牌,并且球靠到屏幕边缘时不会被挤出去 (贴边留白从 8px 提到 16px 给角标让位)
- 预警角标的红底加深,白字
!才算过 AA(原先 3.1:1,10px 的字根本看不清)
问题一:失败的请求被当成消费记账了
原则是「没拿到 usage ≠ 请求成功」。原来只有「网络抛错 / 4xx / 5xx / 流读取失败」 被排除,中转商最常见的三种报错回法全部漏网:
| 回法 | 原来 | 现在 |
|---|---|---|
200 + 一页 HTML 错误页 |
被估算记账 | 记「未捕获」 |
200 + {"error":{...}}(New API / one-api 标准报错) |
被估算记账 | 记「未捕获」 |
200 + SSE 里装 error(假流式中转) |
被估算记账 | 记「未捕获」(有正文也不算) |
| 宿主函数把错误当正常返回值给回来 | 被估算记账 | 记「未捕获」 |
判定顺序固定为 usage > 错误 > 正文/完成形状 > 不估:既不是完成响应、又抠不出正文的 响应,一律只记「未捕获」(运行日志里能看到具体原因,不是静默丢弃)。
问题二:没有 usage 时输出恒按 0 算
非流式路径原来只传了个 reason,没把响应里的正文交下去,于是回复几千字也只算输入。
现在会把正文抠出来估输出,支持 OpenAI / Anthropic / Gemini 三种响应形状;
纯工具调用(正文为空、只有 tool_calls)的参数也算输出 token。
同时补了一个反向漏洞:有的中转声明了 stream: true 却直接回 JSON,那种会被误判成
"流中断"而漏记 —— 现在会回退按 JSON 解析。
怎么自查:面板底部运行日志里找 capture.estimated / capture.missed,
reason 字段直接告诉你原因(例如 响应里返回了错误: insufficient quota)。
新增 4 档(2026-09-16 查的官方价,USD / 1M tokens):
| 模型 | 输入 | 输出 | 缓存输入 | 来源 |
|---|---|---|---|---|
gemini-3-flash |
0.50 | 3.00 | 0.05 | Google 官方 |
gemini-2.5-pro |
1.25 | 10.00 | 0.125 | Google 官方(≤200K 上下文;>200K 为 2.50 / 15) |
gemma-4-31b-it |
0.15 | 0.60 | — | Google Vertex AI |
gemma-4-26b-a4b-it |
0.15 | 0.60 | — | Google Vertex AI |
gemini-3.1-pro(2.00 / 12.00 / 0.40)原表已有,这次核对过与官方一致。
模型名归一化现在能剥中转前缀:
假流式-gemini-2.5-pro-search→gemini-2.5-pro(中文前缀 +-search装饰)[NV]gemma-26b-a4b-it→gemma-4-26b-a4b-it(方括号标记)google/gemma-4-31b-it→gemma-4-31b-it(vendor/ 前缀,原本就支持)
顺带修了一个连带 bug:26b / 120b 这类参数量标签原来被当成"代际版本",
于是 gemma-26b-a4b-it 与价格表里的 gemma-4-26b-a4b-it 被判成"跨版本"而拒绝匹配
(表现为一直显示未设单价)。现在参数量标签不参与代际比较,跨版本红线照旧
(gemini-2.5-flash 仍然不会被算成 gemini-3.x-flash)。
没查到官方价的(开源权重,价格由托管方定,没有唯一"官方价"):
google/diffusiongemma-26b-a4b-it—— Artificial Analysis 上报价为 $0.00(未收录)nvidia/nemotron-3-super-120b-a12b—— NVIDIA 自家按 GPU 时长计费,第三方按 token 计费
这两个保持「未设单价」,面板会提示你一键补入;要按你中转的实际标价填。
最后一轮端到端验收抓出来的:改了币种 / 汇率 / 预算开关 / 限额之后,悬浮球上的 价格角标不会立刻更新,要等下一次请求才变。
- 勾上「启用预算预警」并填一个很小的限额 → 角标要等下一次请求才变成
!, 看着像开关没生效(旧版只有"取消勾选"才会动角标) - 改汇率或币种 → 角标还挂着旧币种的旧数字
现在这几个设置项改完都会立刻重画角标。这一类问题(改了设置但界面没跟着动) 前面已经修过一次(「精确追踪」开关、悬浮球开关),这次把剩下的补齐了。
真机反馈两条,都复现出来了:
① 打开统计面板后关不掉
窄屏下面板头部的 7 个按钮摆不下,把最右边的 ✕ 挤出了面板右边缘;而面板是
overflow: hidden,于是关闭按钮被整块裁掉 —— 看得到面板,点不到关闭。
实测(390px 视口):面板 right 375,关闭按钮 right 397(超出 22px)。
改法:
- 表头允许换行、允许收缩(宁可换到第二行,也不能把关闭按钮推出去)
- 窄屏(≤560px)面板改成从底部升起:
bottom:0 + top:auto + transform:none是结构上的保证 —— 高度被max-height限住,表头就永远在屏幕内 - 面板高度补三级兜底:
82vh→min(82vh,640px)→min(82dvh,640px)。 原来只有中间那条,万一引擎不支持min(),整条声明会被丢掉 → 面板比屏幕还高 → 居中之后表头(含关闭按钮)被顶到屏幕上方,就是截图里那种"卡在顶部" .tf-panel-body补min-height: 0(flex 子项默认auto,内容高会把父容器顶破)
② 手机上还是没有悬浮球
位置是按比例跨设备同步的,但比例映射原先按整个视口算。手机底部是酒馆的输入栏 加系统手势条 —— 在电脑上把球拖到底部(fy≈1),到手机上就正好贴着视口底边, 落在底栏后面 = 看不见。
改法:位置改在「安全区」内映射 —— 窄屏底部预留 96px(与 CSS 默认位置一致), 于是电脑上拖到底部的球,到手机上会落在输入栏之上。
另外补了两处:
- 兜底:摆完之后实测一次,球跑出视口就退回默认角落,并把现场写进运行日志
(
ui.orb),真机再出问题至少能留证 - 修掉一个同类漏洞:启动时开关如果是关的,悬浮球压根没被创建,之后把开关打开
只改
display是没用的(旧版要重启酒馆才有球)—— 现在会当场补建
这条是 v2.7.4 没修好的真正根因,而且我把它在本地复现出来了。
真机日志(荣耀手机 Edge):
ui.orb · 悬浮球位置超出视口 {"left":663,"top":-152,"right":719,"bottom":-96,"viewport":"732x1333"}
right:14px 解出来是对的(732−14≈718),但 bottom:96px 解出来是 −96 ——
只有当包含块的底边在 y=0 时才成立。也就是说:position: fixed 的包含块不是视口,
而是一个「满宽、高度 0、位于页面最顶部」的盒子。
成因:荣耀 Edge 给 body 加了 transform(GPU 合成),body 因此成为
position: fixed 的包含块;而酒馆移动端布局里元素几乎全是 fixed,body 的文档流高度是 0。
于是:
| 元素 | 正常 | 被劫持后 |
|---|---|---|
悬浮球(bottom:96px) |
视口底部往上 96px | 屏幕上方 96px → 看不见 |
面板(top:50%) |
垂直居中 | 中心跑到 y=0 → 上半截出屏、关不掉 |
遮罩(inset:0) |
铺满视口 | 缩成 732×0 → 点外面也关不掉 |
v2.7.4 把窄屏面板改成 bottom:0,在这个环境下正好整块被推到屏幕外 —— 所以你会觉得更严重。
修法(两道防线):
- 浮层改挂到
<html>而不是<body>—— 直接绕开 body 这个包含块 - 摆完实测一次,把「预期位置 − 实际位置」的差值用
margin补回来。 margin 只挪位置、不干扰transform,而且不管包含块是谁,这个差值都能测出来 —— 对其它没见过的浏览器怪癖同样有效
顺带把 ensureOrbVisible 的兜底也改了:以前是"清掉内联样式",那只会退回同一套算错的
CSS 默认位置(等于没救);现在直接强制摆到右上角。
复现与回归:测试页新增 ?hostilefixed=1(给 body 加 transform + 让文档流高度为 0),
在 732×1333 下复现出与真机完全一致的几何(球 top −152 / bottom −96),
并把这组断言固化进 L2 扫描和端到端验收 —— 以后这个坑不会再回来。
v2.7.5 只是把"body 劫持"这一种情况救回来了。真机继续反馈「面板超过某个缩放就不行、 一屏灰色」——查下去发现根子不在某一行代码,而在方向:
这套 UI 的可见性建立在 7 个隐式假设上,每个都能被某个浏览器单独打破。
| # | 假设 | 被谁打破 |
|---|---|---|
| ① | position:fixed 相对视口 |
祖先 transform / 根元素 zoom |
| ② | bottom/right 能正确贴边 |
包含块被劫持时方向就错了 |
| ③ | vh/dvh = 真实视口高 |
地址栏、缩放、字体放大 |
| ④ | transform 居中可用 |
与校准打架、缩放下有偏差 |
| ⑤ | margin 能移动元素 |
对 bottom 锚定的盒子完全无效(v2.7.4 的错) |
| ⑥ | 媒体查询切换定位策略 | 阈值两侧两套代码,只有一套被测到 |
| ⑦ | env(safe-area-*) 一定可用 |
不支持的引擎整条 calc 失效 |
荣耀 Edge 上 ①②③④⑥ 被同时打破(根元素带 transform / 缩放),于是球看不见、 面板跑到屏幕外、关不掉。逐条打补丁只会等来第 8 个假设。
现在改成三条不随环境变化的规矩:
- 只认实测值:先把浮层摆到
left/top = 0量一次,得到「包含块原点」在屏幕上的 真实位置,再把目标位置换算成left/top像素。不管包含块是视口还是被劫持的怪盒子, 这个换算都成立 - 先钳高度再算位置:
H = min(内容高, 视口高 − 2m),位置必然落在[m, 视口高 − m − H]—— 数学上保证表头与底边永远在屏内, 跟字体多大、内容多长、dvh取什么值都无关 - 摆完就验,验不过降级:正常 → 贴满全屏 → 放弃
fixed回到文档流。 三档里任何一档都保证「表头在屏内 + 底部关闭按钮可点」
配套改动:
- CSS 不再参与定位:
top/left/right/bottom/transform/width/max-height全由 JS 用像素写; 窄屏也不再由媒体查询改定位(那正是上一版踩的坑),媒体查询只留外观 - 底部加大号「关闭」按钮:表头万一被顶出屏幕,这里一定点得到(结构性保险)
px缩放系数:有些浏览器(Edge 的字体大小/缩放)在根元素上做zoom, 这时写进去的716px在屏幕上量回来是716 × zoom—— 用一根 1000px 探针实测出系数 再除掉;球的安全区也按"屏幕上的实际占位"(56 × zoom)算,不会把自己挤出屏幕- 新增诊断日志
ui.place:记录视口、缩放系数、包含块原点、面板最终矩形与降级档位。 真机再出问题,一行日志就能定位
回归矩阵:宿主怪癖(正常 / body 带 transform / html 带 transform / 根级 zoom 0.6 / 根级 zoom 1.5)× 视口(732×1333 / 408×746 / 360×640 / 1280×800)—— 共 24 组, 每组都验「球在屏内、面板在屏内、表头在屏内、两个关闭按钮都能点到、遮罩铺满」。
- 酒馆 → 扩展 → 安装扩展 → 粘贴本仓库地址
- 或者手动:把整个文件夹放到
public/scripts/extensions/third-party/SillyToken/ - 重启酒馆
⚠️ 如果你以前装过 TokenFlow,请先删掉它。 两份捕获层同时存在会让每次请求被记两次。
- 右下角悬浮球:点击展开统计面板
- 面板顶部切换时间范围与分组维度
- 点柱子看当天按模型明细,再点一次取消
- 设置面板里可以编辑模型价格、别名、汇率、保留天数(默认 365 天)
价格表是 USD 基准,面板里通过「显示币种 + 汇率」换算成你想看的货币。 DeepSeek 官方报价是 CNY,想让面板和官方平台对得上,把汇率设成当前汇率即可。
控制台里有全局入口:
__sillyTokenDiagnostics() // 每个窗口各层是否装在最外层、抓取计数、未定价模型
__sillyTokenLogs() // 最近的运行日志面板底部的「复制诊断」会把同样内容复制到剪贴板。