我在45分钟内为Claude构建了一个Chrome扩展(以下是我的收获)
抱歉,我需要您提供要翻译的完整文本(即文章的正文内容)。目前只收到来源链接,若您把文章的文字粘贴在这里,我就可以按照要求把它翻译成简体中文并保留原有的格式、代码块和 URL。谢谢!
我构建的内容
一个在 claude.ai 上运行的 Chrome 扩展,能够在你输入时实时显示令牌估算。它会展示:
- 令牌计数
- 已使用上下文的百分比
- 大致费用
徽章会在每一次按键时更新,并且还能追踪整个会话中你发送的令牌数量。
TL;DR – 从空文件夹到可工作的未打包扩展,约 45 分钟完成。
Manifest V3 不再是可选的
Chrome 正在淘汰 Manifest V2 扩展。迁移已经“即将到来”三年,但实际上现在已经开始。Manifest V3 是你必须使用的。
让人卡住的主要点是:后台脚本现在是 service workers。它们没有 DOM 访问权限,不能使用 setInterval 来进行长时间任务(worker 会被挂起),也不能在内存中保持持久状态。
我的后台脚本最终只有 15 行,因为它基本上什么也做不了:
// background.js – service worker
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type === 'MODEL_CHANGED') {
chrome.tabs.query({ url: 'https://claude.ai/*' }, (tabs) => {
tabs.forEach((tab) => {
chrome.tabs.sendMessage(tab.id, msg);
});
});
sendResponse({ ok: true });
}
});
它只是把弹出窗口的模型更改消息转发给内容脚本。任何有状态的内容都存放在 chrome.storage.local 中。
MutationObserver,而非轮询
第一反应是使用 setInterval 每 200 毫秒轮询检查输入框。这在 MV2 中还能工作,但在 MV3 中是个坏主意——服务工作线程随时可能被挂起,而在内容脚本中使用 setInterval 感觉不对,因为还有更好的办法。
claude.ai 是一个 React 应用。消息输入框会在你在对话之间切换时挂载和卸载。如果在页面加载时获取元素,可能还不存在,而保存的引用在重新渲染后会变得陈旧。
MutationObserver 能干净利落地处理这个问题:
const observer = new MutationObserver(() => {
const el = findInputElement();
if (el) attachToInput(el);
});
observer.observe(document.body, { childList: true, subtree: true });
每当 DOM 变化时,我们检查编辑器是否出现,如果出现就附加监听器。attachToInput 会防止重复附加:
let inputEl = null;
function attachToInput(el) {
if (inputEl === el) return; // already watching this element
inputEl = el;
// attach input handlers…
}
这种模式适用于任何面向现代单页应用(SPA)的扩展。
定位编辑器
Claude 的消息输入是一个 contenteditable div,而不是 textarea。你应通过 el.innerText 读取其内容,而不是 el.value。
可靠地找到它是关键难点。React 应用中的类名经常变化——通常是哈希或自动生成的——因此不要依赖它们。相反,使用基于属性的选择器。Claude 为编辑器添加了 data-testid 属性,该属性用于测试,通常比较稳定:
function findInputElement() {
// Primary: contenteditable div used by Claude
const ce = document.querySelector('div[contenteditable="true"][data-testid]');
if (ce) return ce;
// Fallbacks: common patterns if the primary selector breaks
const fallback = document.querySelector(
'div[contenteditable="true"].ProseMirror,' +
'div[contenteditable="true"][class*="composer"],' +
'div[contenteditable="true"][placeholder]'
);
return fallback || document.querySelector('div[contenteditable="true"]');
}
使用多个回退方案,最不具体的放在最后,可让扩展在 UI 变化时有更大的存活机会。
在没有 API 的情况下进行令牌计数
你无法直接在 Chrome 扩展中调用 Anthropic 分词器。进行 API 调用既繁琐又会导致实时计数过慢。
对英文文本来说,一个简单的近似方法效果很好:words × 1.3。它并不完全精确——代码片段和特殊字符会使计数偏差——但足以区分“≈ 800 tokens”和“≈ 8 000 tokens”,这正是我们的目标所在。
function countTokens(text) {
if (!text || !text.trim()) return 0;
const words = text.trim().split(/\s+/).length;
return Math.round(words * 1.3);
}
在弹出窗口中,我还会显示一个粗略的费用估算。Claude 3.5 Sonnet 的费用为 $3 / 百万输入令牌,因此一条 1 000‑token 的消息大约花费 $0.003。这对关注 API 开支的用户非常实用。
徽章
在别人的页面中注入 UI 总是有点乱。我使用固定定位、高 z-index,并希望它不会与站点的 CSS 冲突。
当输入框为空时徽章会隐藏,一旦开始输入就会显示。它会在 input 和 keyup 事件上都进行更新,因为 input 并不总是在程序化更改时触发:
el.addEventListener('input', handler, { passive: true });
el.addEventListener('keyup', handler, { passive: true });
passive: true 告诉浏览器这些处理函数不会调用 preventDefault(),因此不会阻塞滚动。这是一个小的性能提升,但值得养成这个习惯。
清单
整个扩展的设置大约有 25 行:
{
"manifest_version": 3,
"name": "Claude Token Counter",
"version": "1.0.0",
"description": "Live token counter for claude.ai",
"permissions": ["storage", "tabs"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"content_scripts": [
{
"matches": ["https://claude.ai/*"],
"js": ["content.js"],
"css": ["styles.css"],
"run_at": "document_idle"
}
],
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
就这样!扩展可以正常工作,遵守 MV3 限制,并实时显示您在 Claude 上消耗的 token 数量。祝使用愉快!
Chrome 扩展清单 (MV3) – 替代示例
{
"manifest_version": 3,
"name": "Claude Token Counter",
"description": "Shows token usage for Claude AI chats.",
"version": "0.1",
"icons": {
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
},
"content_scripts": [
{
"matches": ["https://claude.ai/*"],
"js": ["content.js"],
"run_at": "document_idle"
}
],
"background": {
"service_worker": "background.js"
},
"permissions": ["storage"],
"host_permissions": ["https://claude.ai/*"]
}
注意:
host_permissions是 Manifest V3 中独立于permissions的设置。我花了一分钟才弄清楚为什么我的内容脚本会悄悄失效——两者都是必需的。
全屏控制(演示)
Enter fullscreen mode
Exit fullscreen mode
让我惊讶的事
会话跟踪
我想累计整个对话中发送的 token 总数。难点在于确定 何时 实际发送了一条消息。
没有干净的 “用户提交消息” 事件。变通办法是监听 Enter 键(不带 Shift,因为 Shift + Enter 会换行)以及发送按钮的点击:
document.addEventListener(
'keydown',
(e) => {
if (
e.key === 'Enter' &&
!e.shiftKey &&
inputEl &&
document.activeElement === inputEl
) {
// Slight delay, let Claude's UI process it first
setTimeout(onMessageSent, 100);
}
},
true
);
为什么要延迟 100 ms?
这个延迟是一个技巧。如果不加延迟,onMessageSent 会在输入框清空 之前 运行,导致计数器在错误的时刻被重置。加上延迟后,时机就能正确对齐。虽然脆弱,但能工作。
状态
- 扩展: 已提交至 Chrome 网上应用店;目前正在审查中(审查过程可能需要几天到几周不等)。
- 源代码: 您也可以在启用开发者模式后,通过
chrome://extensions/加载未打包的版本。 - 网页版: 使用相同的令牌计数逻辑(词数 × 1.3),无需任何安装。