如何优雅拦截 fetch 与 SSE 流?Claude Counter 注入层 bridge.js 源码实战
【免费下载链接】claude-counterA minimal browser extension that shows token count, cache timer, and usage bars on claude.ai.项目地址: https://gitcode.com/gh_mirrors/cl/claude-counter
Claude Counter 是一款轻量级浏览器扩展,在 claude.ai 页面上实时显示 token 数、缓存倒计时和用量进度条。它实现这些功能的秘诀,正是优雅地拦截 fetch 请求与 SSE(Server-Sent Events)流式响应——整个注入层由一个仅约 180 行的 bridge.js 完成。本文将带你读懂这套"注入层 + 消息桥"的经典架构,新手也能轻松理解浏览器扩展拦截网络请求的完整套路。
🌉 为什么需要注入层:Content Script 的"权限盲区"
很多新手会踩第一个坑:直接在 content script 里写window.fetch = ...,发现页面自己的请求根本拦不到。
原因很简单——content script 运行在隔离世界(isolated world),它有独立的window对象。页面应用调用的是它自己世界里的fetch,两者互不可见。
Claude Counter 的解法非常干净:
- 通过
<script>标签把 bridge.js 注入到页面主世界(main world); - 注入动作由 bridge-client.js 中的
injectBridgeOnce()完成,用runtime.getURL('src/injected/bridge.js')取出脚本地址,且靠getElementById检查保证只注入一次(SPA 不刷新页面,脚本不能重复跑); - manifest.json 中把该文件声明为
web_accessible_resources,这是脚本能被页面加载的前提。
一句话总结:隔离世界负责"搭桥",主世界负责"偷听"网络。
⏱️ 抢在框架之前:先存原件,再包一层
拦截网络请求最忌讳"包了又包、丢了原件"。bridge.js 在文件最开头就做了两件关键动作:
const originalFetch = window.fetch; // 抢在任何人之前保存原件 const originalPushState = history.pushState.bind(history);随后再给window.fetch包一层 async 代理。这里有个容易忽略的细节:包fetch的同时还包了history.pushState / replaceState——因为 claude.ai 是单页应用,切换会话时 URL 会变但页面不刷新,而前端框架可能会提前缓存这些方法。bridge.js 抢在框架初始化前包裹它们,每次导航就派发一个cc:urlchange自定义事件,main.js 侧监听该事件即可及时刷新 token 统计。
💡 核心原则:注入脚本一定要尽早执行,保存原始引用,再用"调用原件 + 旁路处理"的方式包装。
🔍 优雅拦截 fetch:只挑自己关心的请求
包裹后的fetch代理并不是无脑处理所有请求,而是精准识别三类流量:
| 请求特征 | 识别条件 | 用途 |
|---|---|---|
| 生成开始信号 | POST且 URL 含/completion或/retry_completion | 派发cc:generation_start,UI 开始显示缓存倒计时 |
| SSE 流式响应 | content-type含event-stream | 进入流解析,提取实时用量 |
| 会话树数据 | URL 含/chat_conversations/且带tree=参数 | 用正则提取orgId与conversationId,供 token 统计 |
注意 bridge.js 第 70-78 行 的toAbsoluteUrl():fetch的入参可能是字符串、URL或Request对象,三种形态都做了归一化——这种防御性处理是拦截层代码的标配。
📡 解析 SSE 流:克隆响应 + 逐行缓冲
这是整篇文章最值得学的一段。页面正在消费一个流式响应,你不能直接读它的 body(会掐断页面的流)。Claude Counter 的做法是:
response.clone()克隆一份响应,让页面和插件各读各的;- 在克隆体上调用
body.getReader()拿流读取器; - 用
TextDecoder带{ stream: true }增量解码,解决分片恰好把一个data:行切成两半的问题; - 按
\r\n|\r|\n切行,最后一行不完整就留在 buffer 等下一片; - 只挑
data:开头的行解析 JSON,命中type === 'message_limit'时通过消息桥转发出去。
对应源码在 handleEventStream。这段流解析拿到的message_limit是未四舍五入的精确用量,比 Claude 官方/usage页面的取整百分比更准——这正是插件进度条"更精确"的来源。
📮 消息通道:两个世界如何对话
注入脚本(主世界)与 content script(隔离世界)之间没有共享变量,只能靠window.postMessage。Claude Counter 定义了一套极简协议:
- 统一标记:所有消息都带
cc: 'ClaudeCounter'字段,双方收到消息先校验标记,避免误吞页面其他脚本的 postMessage; - 事件单向推送:如
cc:generation_start、cc:conversation、cc:message_limit,content script 侧用bridge.on(type, fn)订阅; - 请求-响应配对:BridgeClient.request() 生成
requestId,把 Promise 的 resolve/reject 存进_pendingMap,等cc:response回来时按 id 取走结算,并附带超时自动清理。
bridge.js 侧收到cc:request后按kind分发,目前支持三种任务(见 消息监听器):
hash:用页面世界的crypto.subtle计算 SHA-256 摘要(content script 拿不到该 API);usage:以credentials: 'include'调用/usage接口,借用页面 Cookie 身份;conversation:拉取会话树 JSON 并转发。
🛡️ 容错哲学:插件绝不能拖垮宿主页面
翻遍 bridge.js 源码会发现大量try/catch,且捕获后一律静默降级(ignore parse failures、best-effort; don't break claude.ai)。再对比 main.js 中刷新失败也只是 return 不报错——这体现了一个成熟扩展的底线思维:
- 拦截层旁路失败不影响主流程(clone 解析失败?跳过,页面请求照常返回);
- SSE 读取全程"尽力而为",任何异常都吞掉;
- 所有数据本地处理,不上传任何外部服务器(见 README 的 Privacy 一节)。
📚 动手看源码:推荐阅读路径
想完整走一遍数据流,按这个顺序读:
- src/injected/bridge.js —— 注入层:fetch 拦截 + SSE 解析 + 请求分发
- src/content/bridge-client.js —— 客户端:注入脚本 + 消息桥
- src/content/main.js —— 调度层:URL 变化、用量刷新、SSE 事件消费
- manifest.json —— 权限与资源声明
另外,不想装扩展的同学还可以直接看油猴版实现 userscript/claude-counter.user.js,它把注入逻辑直接内嵌进脚本,适合对比两种分发形态的差异。
✅ 总结:拦截 fetch 与 SSE 的四个黄金动作
🧭一句话记住全文:
- 尽早保存原始引用(
originalFetch),再包一层异步代理; - 精准识别目标流量,用 URL/方法/
content-type过滤,别处理无关请求; response.clone()+getReader()无损旁读 SSE 流,注意分片缓冲;- postMessage + requestId 协议打通隔离世界,并用标记字段防串扰。
Claude Counter 用不到 200 行代码展示了浏览器扩展网络拦截的完整范式,无论是做 AI 用量监控、接口调试工具,还是页面增强插件,这套"注入层 + 消息桥"的架构都值得直接借鉴。
【免费下载链接】claude-counterA minimal browser extension that shows token count, cache timer, and usage bars on claude.ai.项目地址: https://gitcode.com/gh_mirrors/cl/claude-counter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考