1. 从「读到哪忘了」到「打开就续上」:这个插件到底解决什么问题
如果你经常啃 React、Playwright、Codex 这类官方文档,大概率遇到过同一个尴尬:三栏布局左边目录、中间正文、右边页内锚点,查资料时很爽,但想系统读完一章就很容易断片。浏览器书签只能记住 URL,记不住你滚到了哪一段;笔记软件又太重,每次手动抄「读到 Hooks 第二节」纯属折磨。我做这个 Chrome 插件的出发点特别小:只解决技术文档阅读进度记录这一件事,顺便用 TaoToken 统一 Key 给插件加一个「AI 摘要当前章节」的轻能力,让你隔几天回来时不仅知道读到哪,还能三秒回忆这节讲了啥。
它适合谁?适合需要分多次读完一份官方文档的前端/测试/全栈同学,适合同时开 React Learn、Playwright Docs、Codex Docs 好几个标签页的人,也适合不想把阅读上下文全交给 AI、但偶尔想要一句摘要提神的人。它不适合当稍后读工具,也不适合当通用网页阅读器——范围就锁在「开发者文档 + 阅读位置 + 轻摘要」。
技术实现上分两块:进度记录走chrome.storage.local,纯本地、不上传页面内容;AI 摘要走 TaoToken 的统一 API 通道,Key 只存在本地,插件通过 background service worker 发请求。下面我把可复制的manifest.json、background 骨架、Key 注入方式、以及加载后怎么验证进度同步和摘要生效,一步步拆开讲。你照着做,半小时内能跑起来一个能用的版本。
2. 前置准备:TaoToken 统一 Key 与通道配置
在写代码之前,先把「AI 能力从哪来」这件事定下来。我选择 TaoToken 的原因很实际:插件里如果直接写某一家模型的地址和 Key,后面换模型、加摘要长度、调温度都要改代码;用统一 Key 和统一 API 通道后,插件只认一个baseURL和一个apiKey,模型名当参数传,切换成本几乎为零。
你需要先拿到两样东西:
第一是 API Key。打开控制台里的 API Keys 页面创建一个,建议命名成chrome-docs-tracker这种能一眼看出用途的名字,方便以后按插件维度吊销。创建后立刻复制,页面刷新就看不到了。
第二是确认请求地址。TaoToken 的 API 入口是https://taotoken.net/api,注意这里不要加任何查询参数,插件里拼接路径时保持干净。模型对话相关的能力都走这个 base,具体模型名按你账号里可用的填。
注意:Key 不要硬编码进
content.js或提交到 Git。Chrome 插件的正确做法是放在 background service worker 里,通过chrome.storage.local读取,content script 只发消息、不碰 Key。这样即使页面脚本被注入,也拿不到你的凭证。
如果你只是想先验证模型通不通,可以先用模型对话页面手动发一条消息,确认账号和 Key 是活的,再进插件开发。这一步能省掉后面「到底是 Key 错还是代码错」的排查时间。
3. 可复制配置:manifest.json 与 background 骨架
先建目录结构,我习惯这样:
docs-progress-tracker/ ├── manifest.json ├── background.js ├── content.js ├── popup.html └── popup.js3.1 manifest.json(Manifest V3)
{ "manifest_version": 3, "name": "Developer Docs Progress Tracker", "version": "0.1.0", "description": "记录技术文档阅读进度,并用 TaoToken 统一 Key 生成章节摘要。", "permissions": ["storage", "activeTab", "scripting"], "host_permissions": [ "https://react.dev/*", "https://playwright.dev/*", "https://taotoken.net/*" ], "background": { "service_worker": "background.js" }, "action": { "default_popup": "popup.html", "default_title": "Docs Progress" }, "content_scripts": [ { "matches": [ "https://react.dev/*", "https://playwright.dev/*" ], "js": ["content.js"], "run_at": "document_idle" } ] }几个关键点:host_permissions里必须显式加上https://taotoken.net/*,否则 background 发请求会被拦;storage权限用于保存进度和 Key;content_scripts的matches先只放你确定支持的文档站,跑通后再加 Docusaurus、VitePress 这类通用匹配。
3.2 background.js:Key 读取 + 摘要请求
const API_BASE = "https://taotoken.net/api"; const MODEL = "gpt-4o-mini"; // 按你账号可用模型替换 async function getApiKey() { const { tt_api_key } = await chrome.storage.local.get("tt_api_key"); return tt_api_key || ""; } async function summarize(text) { const apiKey = await getApiKey(); if (!apiKey) throw new Error("NO_API_KEY"); const res = await fetch(`${API_BASE}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` }, body: JSON.stringify({ model: MODEL, messages: [ { role: "system", content: "你是技术文档助手,用不超过80字总结这段文档的核心内容。" }, { role: "user", content: text.slice(0, 4000) } ], temperature: 0.3 }) }); if (!res.ok) { const err = await res.text(); throw new Error(`API_${res.status}: ${err}`); } const data = await res.json(); return data.choices?.[0]?.message?.content?.trim() || ""; } chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.type === "SUMMARIZE") { summarize(msg.text) .then((summary) => sendResponse({ ok: true, summary })) .catch((e) => sendResponse({ ok: false, error: e.message })); return true; // 异步响应必须返回 true } });这里有两个我踩过的坑:一是onMessage里做异步必须return true,否则sendResponse会失效;二是text.slice(0, 4000)要留,文档正文动辄上万字,不截断既慢又费额度。
3.3 content.js:进度记录与恢复
const KEY = `progress:${location.origin}${location.pathname}`; function saveProgress() { const scrollTop = window.scrollY; const total = document.body.scrollHeight - window.innerHeight; const percent = total > 0 ? Math.min(100, Math.round((scrollTop / total) * 100)) : 0; chrome.storage.local.set({ [KEY]: { scrollTop, percent, ts: Date.now() } }); } function restoreProgress() { chrome.storage.local.get(KEY, (data) => { const saved = data[KEY]; if (saved && saved.scrollTop > 200) { window.scrollTo({ top: saved.scrollTop, behavior: "instant" }); } }); } let timer = null; window.addEventListener("scroll", () => { clearTimeout(timer); timer = setTimeout(saveProgress, 500); }); restoreProgress();节流 500ms 是必须的,不然滚动一次写几十遍 storage,Chrome 会警告。恢复时加scrollTop > 200判断,避免刚进页面就被拉到奇怪位置。
3.4 popup.js:注入 Key 与触发摘要
const input = document.getElementById("apiKey"); const saveBtn = document.getElementById("save"); const sumBtn = document.getElementById("summarize"); const out = document.getElementById("output"); chrome.storage.local.get("tt_api_key", ({ tt_api_key }) => { if (tt_api_key) input.value = tt_api_key; }); saveBtn.onclick = () => { chrome.storage.local.set({ tt_api_key: input.value.trim() }, () => { out.textContent = "Key 已保存到本地"; }); }; sumBtn.onclick = async () => { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); const [{ result }] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => document.querySelector("main")?.innerText || document.body.innerText }); out.textContent = "摘要生成中..."; chrome.runtime.sendMessage({ type: "SUMMARIZE", text: result }, (res) => { out.textContent = res.ok ? res.summary : `失败:${res.error}`; }); };popup.html里放一个input、两个button、一个div#output即可,结构简单不展开。
4. 验证请求:加载插件后确认进度同步与摘要生效
代码写完,进chrome://extensions,打开右上角「开发者模式」,点「加载已解压的扩展程序」,选中docs-progress-tracker目录。加载成功后你应该看到插件卡片,没有红色报错。
验证进度同步:打开https://react.dev/learn,往下滚到「Describing the UI」附近,停两秒,然后关掉标签页重新打开同一 URL。页面应该自动滚回你上次的位置。再打开 popup,如果之前存过 Key,输入框应该回显。想确认存储内容,可以在扩展的 service worker 控制台执行:
chrome.storage.local.get(null, console.log)你会看到形如progress:https://react.dev/learn的键,里面是scrollTop、percent、ts。
验证 AI 摘要:在 popup 里粘贴 TaoToken 的 Key,点保存,再点「生成摘要」。正常情况下output区域会先显示「摘要生成中...」,一两秒后出现一段不超过 80 字的中文总结。如果返回API_401,说明 Key 没存进去或复制时带了空格;返回API_404,检查API_BASE后面拼的路径是不是/v1/chat/completions;返回NO_API_KEY,说明 popup 保存和 background 读取用的键名不一致,两边都必须是tt_api_key。
实测下来,React Learn 这种正文在<main>里的站点摘要质量最稳;Playwright Docs 有些页面正文分散在多个article,可以在executeScript里把选择器改成document.querySelectorAll("article")再拼接。
5. 本篇常见错排查
报错Could not establish connection. Receiving end does not exist:通常是 content script 没注入到当前页面。检查manifest.json的matches是否包含你正在测的域名,改完 manifest 必须在扩展页点「重新加载」,光刷新网页没用。
滚动位置恢复了但摘要按钮没反应:popup 里chrome.scripting.executeScript需要scripting权限和activeTab,两个都要在 manifest 里声明。另外chrome://开头的页面不允许注入脚本,别在扩展管理页测试。
Key 保存后刷新 popup 又空了:chrome.storage.local.set是异步的,如果你在onclick里没等回调就关 popup,可能没写进去。加个回调里更新 UI 提示,确认写入完成再关。
摘要返回空字符串:模型可能把内容放进了reasoning_content或返回结构不同。先在模型对话页面用同样的 prompt 手动发一次,确认返回结构,再对照改data.choices[0].message.content的取值路径。
进度在不同文档站串了:KEY用的是origin + pathname,如果两个站点路径相同不会串;真正会串的是你把 KEY 写成了固定字符串。检查模板字符串有没有漏掉location.pathname。
service worker 休眠导致首次请求慢:MV3 的 background 会休眠,第一次发消息要唤醒,属正常现象。如果超过 5 秒没响应,在扩展页点 service worker 的「检查」看控制台有没有报错。
6. 把 Key 管好,把进度留住
这个插件的定位一直很克制:进度记录是主角,AI 摘要只是锦上添花。所以 Key 的管理要单独说一句——它只存在chrome.storage.local,不随插件代码分发,也不经过任何第三方页面。你可以在 popup 里加一个「清除 Key」按钮,对应chrome.storage.local.remove("tt_api_key"),换机器或怀疑泄露时一键处理。
如果你后面想把摘要能力扩展到更多文档站,或者想给插件加一个「按章节自动生成阅读清单」的 Agent 式流程,建议把模型调用统一收口到 background,content script 永远只发消息。这样无论你换哪个模型、调什么参数,插件前端一行都不用动。需要长期跑编码类任务或 Agent 工作流的话,可以看看 Coding Plan 的额度方案;只想先验证模型通不通,模型对话页面最快;Key 的创建和吊销都在 API Keys 页面完成,接入细节对照接入文档即可。