☰
Claude Counter 架构拆解:Manifest V3 下 Content Script 与 MAIN World 的 postMessage 桥接通信设计
2026/10/4 2:00:38 网站建设 项目流程

Claude Counter 架构拆解:Manifest V3 下 Content Script 与 MAIN World 的 postMessage 桥接通信设计

【免费下载链接】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是一款轻量级 Chrome 浏览器扩展,能在 claude.ai 页面顶部实时显示当前对话的 Token 计数、5 分钟缓存倒计时,以及 5 小时/每周的使用量进度条。它的核心架构难题在于:Manifest V3 扩展的 Content Script 运行在**隔离世界(isolated world)**中,无法直接读取页面 JS 发出的 fetch 请求,也无法拿到会话 Cookie。本文拆解 Claude Counter 如何用postMessage在 Content Script 与MAIN World之间搭建一座通信桥,让扩展"偷听"到 API 响应并精准注入 UI——这是所有想拦截页面网络数据的 MV3 扩展都会遇到的经典问题。

🧩 先看懂问题:MV3 扩展的"双世界"隔离

在 Manifest V3 中,浏览器把每个页面分成了两个互不相通的 JS 世界:

世界能做什么不能做什么
isolated world(Content Script 默认所在)安全访问 DOM、调用chrome.runtimeAPI看不到页面 JS 对象,无法拦截window.fetch,读不到会话 Cookie
MAIN world(页面本身)拥有完整页面权限:fetch、Cookie、SSE 流不能直接调用扩展 API

Claude Counter 需要三样东西,全在 MAIN world 里:

  1. 拦截window.fetch—— 捕获 Claude 前端的 completion 请求、对话树请求和 SSE 流;
  2. 带 Cookie 的请求—— 以用户身份调用/api/organizations/{orgId}/usage;
  3. crypto.subtle哈希—— 隔离世界里的 Content Script 没有 SubtleCrypto,没法对消息做 SHA-256 指纹。

解决方案就是本文的主角:桥接(bridge)模式—— 向 MAIN world 注入一个脚本当"内应",两边用window.postMessage对话。

🏗️ 架构总览:一份 manifest 两个角色

打开 manifest.json,整个通信骨架就藏在两段配置里:

"content_scripts": [ { "matches": ["https://claude.ai/*"], "js": ["src/content/constants.js", "src/content/bridge-client.js", "src/vendor/o200k_base.js", "src/content/tokens.js", "src/content/ui.js", "src/content/main.js"] } ], "web_accessible_resources": [ { "resources": ["src/injected/bridge.js"], "matches": ["https://claude.ai/*"] } ]
  • content_scripts(manifest.json#L31-L44):按依赖顺序加载 6 个脚本到隔离世界,它们共享一个全局命名空间globalThis.ClaudeCounter(下称CC);
  • web_accessible_resources(manifest.json#L45-L50):把 src/injected/bridge.js 声明为可被页面访问的资源——这是 MAIN world 侧"内应"的门票,没有它页面无法加载该脚本。
┌─────────────────── claude.ai 页面 ───────────────────┐ │ │ │ MAIN world postMessage(窗口级广播) │ │ ┌─────────────┐ ◄────────────────────────────────► │ │ │ bridge.js │ { cc:'ClaudeCounter', type, … } │ │ │ 包装 fetch/ │ │ │ │ history/SSE │ isolated world │ │ └──────▲──────┘ ┌──────────────────────────────┐ │ │ │<script> │ bridge-client.js (桥客户端) │ │ │ 注入 <script> │ main.js / tokens.js / ui.js │ │ └─────────┴─────────┴──────────────────────────────┘ │

📡 第一步:Content Script 把"内应"注入页面

注入动作由 src/content/bridge-client.js 中的injectBridgeOnce()完成,核心逻辑在 bridge-client.js#L91-L111:

const script = document.createElement('script'); script.src = runtime.getURL('src/injected/bridge.js'); (document.head || document.documentElement).appendChild(script);

三个值得注意的工程细节:

  1. runtime.getURL()把扩展内部路径解析成chrome-extension://绝对地址,配合web_accessible_resources才能被页面加载;
  2. 单例保证:bridgeReadyPromise缓存 Promise,重复调用不会二次注入;
  3. 就绪门控:src/content/main.js#L139 中所有依赖桥的请求都会先await bridgeReady,确保"内应"上线后才发令。

🤝 postMessage 协议设计:请求-响应 + 事件两种模式

这是整个架构的灵魂。bridge.js 和 bridge-client.js 之间约定了一个极简协议,所有消息都带cc: 'ClaudeCounter'标记做身份过滤:

① 事件推送(MAIN → Content,单向)

// bridge.js 中只有两行(bridge.js#L52-L54) window.postMessage({ cc: 'ClaudeCounter', type, payload }, '*');

事件名都以cc:前缀命名,避免和页面自身消息混淆:

事件触发时机用途
cc:generation_start拦截到/completionPOST 请求UI 立刻把缓存条置为"等待中"
cc:conversation抓到对话树响应重新计算 Token 数
cc:message_limit从 SSE 流里解析出用量数据更新 5h/7d 进度条
cc:urlchangehistory.pushState被调用感知 SPA 路由切换

② 请求-响应(Content → MAIN → Content)

客户端发起请求时生成requestId,把 Promise 存进_pendingMap,超时默认 10 秒(bridge-client.js#L54-L74);服务端完成后用同一requestId回cc:response,客户端据此 resolve/reject。目前支持三种kind:

  • hash:借用 MAIN world 的crypto.subtle.digest('SHA-256')计算消息指纹,供 src/content/tokens.js 做 Token 缓存(见后文);
  • usage:以用户身份(credentials: 'include')请求/api/organizations/{orgId}/usage;
  • conversation:主动拉取对话树,作为被动拦截之外的兜底。

安全细节值得抄作业:两侧监听器都先校验event.source !== window直接丢弃,再检查data.cc !== 'ClaudeCounter',防止页面里其他脚本伪造消息、或跨 iframe 消息误伤(bridge.js#L130-L133、bridge-client.js#L19-L22)。

🔍 MAIN World 侧的"内应"都在拦什么

bridge.js 全文只有 183 行,但每处都卡得极准:

  1. 抢在框架前包装window.fetch(bridge.js#L7)。注释写得很直白——"Capture original fetch before anyone else can wrap it"。React 等框架可能在之后缓存 fetch 的引用,包装太晚就漏单了;
  2. 同样抢先包装history.pushState / replaceState(bridge.js#L10-L23),每次调用后广播cc:urlchange自定义事件。Content Script 侧再叠加popstate监听,前后覆盖 SPA 前进/后退/编程式跳转三种导航路径(main.js#L61-L81);
  3. SSE 流式解析:对content-type: text/event-stream的响应执行response.clone(),用getReader()逐行读data:事件,只挑出message_limit转发(bridge.js#L96-L128)。注意它克隆的是 Response 本体、原样返回给页面,且整体包在 try/catch 里——注释写着 "best-effort; don't break claude.ai",桥挂了也不能影响用户正常聊天;
  4. 对话树嗅探:匹配/chat_conversations/…?tree=的 URL,解析出orgId和conversationId后把 JSON 转发出去(bridge.js#L86-L94)。

🎯 Content Script 侧:事件驱动的 UI 更新

main.js 把桥事件接成三条业务线(main.js#L217-L219):

  • cc:generation_start→ UI 缓存条进入"pending"态,提示"新一轮生成中";
  • cc:conversation→ 调用CC.tokens.computeConversationMetrics()重算 Token;
  • cc:message_limit→ SSE 带来的是精确未取整的用量小数,比 Claude 原生/usage页更准,直接刷新进度条。

DOM 注入则靠MutationObserver轮询代替定时轮询:waitForElement()(main.js#L30-L57)会在锚点元素出现的第一帧把 UI 挂上去。此外还有一个秒级tick()(main.js#L291-L316),负责倒计时走表,并在 5h/7d 窗口到期时主动刷新——因为 SSE 只在用户发消息时才推数据,窗口翻转那一刻必须自己伸手去拉。

⚡ 一个隐藏亮点:把 SHA-256 指纹也走桥

Token 计算是最贵的操作,tokens.js 的TokenCache用"消息 ID + 内容指纹"避免重复计数。但隔离世界没有crypto.subtle,于是它把待哈希文本通过桥发给 MAIN world 算完再拿回 8 字节前缀(tokens.js#L135-L151)——又一次印证桥的价值:它不只是数据通道,还是能力代理。

📁 文件地图与阅读路线

想亲手验证上述设计,按这个顺序读代码效率最高:

文件角色
manifest.json注入配置总入口,先看两个 script 列表
src/content/bridge-client.js桥客户端:注入 + 请求-响应 + 事件订阅
src/injected/bridge.jsMAIN world 内应:fetch/SSE/history 拦截
src/content/main.js业务编排:导航感知、用量刷新、秒级 tick
src/content/tokens.js对话树解析 + 缓存 Token 计数
src/content/ui.js进度条、倒计时、长按提示等纯 UI
src/content/constants.jsDOM 选择器、5 分钟缓存窗口、200k 上限

不想装扩展的用户也可以看 userscript/claude-counter.user.js,它是同一套逻辑的用户脚本版本。

✅ 总结:三条可复用的 MV3 桥接经验

  1. 能"看"不到页面 JS,就用web_accessible_resources+ 动态<script>把逻辑送进 MAIN world,隔离世界只留"指挥官";
  2. 协议要双向完备:用requestId把 Promise 和消息对上(请求-响应),同时保留单向事件通道(推送),再给每个request配超时,桥死了也不会卡死页面逻辑;
  3. 防御式拦截:克隆 Response、全程 try/catch、优先缓存原始 fetch——扩展是寄生者,永远不能影响宿主页面的正常行为。

看懂了 Claude Counter 这座 183 行的桥,你也就掌握了 Manifest V3 下"Content Script ↔ MAIN World 通信"的标准答案。

【免费下载链接】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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询