一、浏览器扩展基础(Manifest、content script、background、sidepanel、options、popup、DevTools)
- 浏览器插件核心概念与角色
- Manifest: 扩展的“配置与权限清单”。在本项目由 WXT 自动产出,入口在 wxt.config.ts 的 manifest 字段配置。常见字段:permissions、action、side_panel。
- Background: 扩展的“常驻后台服务”,负责事件监听、跨页面/跨域操作、集中调度。对应 entrypoints/background.ts。
- Content Script: 注入到网页的脚本,能访问页面 DOM,负责嵌入 UI、采集上下文。对应 entrypoints/content/index.ts 与 entrypoints/content/App.vue。
- Sidepanel: 浏览器侧边栏中的独立页面,适合承载复杂交互和长会话 UI。对应 entrypoints/sidepanel/main.ts + entrypoints/sidepanel/App.vue。
- Options: 扩展设置页,用于管理 API Key、参数、偏好。对应 entrypoints/options/main.ts 与 entrypoints/options/index.html。
- Popup: 扩展图标点击后弹出的轻量级操作窗口,适合快速操作。对应 entrypoints/popup/main.ts 与 entrypoints/popup/App.vue
- DevTools: 用于扩展浏览器自带的 DevTools(开发者工具)。对应 entrypoints/devtools/main.ts 与 entrypoints/devtools/App.vue
为什么选择 WXT 做构建工具?
- 支持多种前端框架,如:Vanilla、Vue 、React 、Svelte 、Solid 都支持。
- 生态丰富,开源的例子比较多。
- 另一个 Plasmo 构建工具也不错,支持 Vue 、React 、Svelte
二、通信
项目中用 @webext-core/messaging 作为各模块通信基础,因为其支持 TS(参考 messags/messaging.ts)。 @webext-core/messaging 基础实现是依赖 tabs.sendMessage(其他组件向 content script 通信)和 runtime.sendMessage(除了 content script 的其他组件之间通信)。
通信规则:
- content script ↔ background 相互直接通信
- content script 无法直接与 sidepanel 通信,必须通过 background 服务工作器作为中介
- content script → background → sidepanel
- content script 发送配置更新消息到 background
- background 接收消息后,将其转发给当前打开的 sidepanel
- sidepanel 监听消息并更新界面
- sidepanel ↔ background 相互直接通信
- sidepanel → content script 单向通信
- sidepanel ↔ options 单向通信
总结:只要是 content script 与其他组件通信,都需要通过 background 转发,其他组件之间都可以相互通信,只有与 content script 是单向通信。
// 通信例子
import { defineExtensionMessaging } from "@webext-core/messaging";
export const { sendMessage, onMessage } = defineExtensionMessaging();
export const sendAIMessage = (data) => {
sendMessage("seting", data);
};
export const onAIMessage = (callback) => {
onMessage("aiMessage", (message) => {
callback(message.data);
});
};
// 发送数据到content script模块
export const sendSeekToTime = async (data: number) => {
const activeTabId = await getActiveTabId(); // 获取激活的 tab ID
sendMessage("seekToTime", data, activeTabId);
};
三、B 站视频页字幕抓取
抓取视频字幕三步实现
// 获取当前视频页信息
const view = await fetch.get(
"https://api.bilibili.com/x/web-interface/wbi/view/detail",
{ bvid } // bvid视频页唯一标识
);
// 获取当前视频字幕链接
const subtitleUrl = await fetch.get(
"https://api.bilibili.com/x/player/wbi/v2",
{ aid: view.aid, cid: view.cid }
);
// 根据视频字幕链接获取字幕
await fetch.get(subtitleUrl);
四、封装 AI 请求与错误治理
// 智谱文档: https://docs.bigmodel.cn/api-reference/%E6%A8%A1%E5%9E%8B-api/%E5%AF%B9%E8%AF%9D%E8%A1%A5%E5%85%A8%E5%BC%82%E6%AD%A5
async function createChatCompletion(messages) {
const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
const body = {
model: "glm-4.6",
messages: [
{
role: "system",
content: `你是一名思维导图设计专家,分析内容。根据内容输出思维导图要求符合快速了解学习当前内容信息。请以markdown格式输出`,
},
...messages,
// {
// role: "user",
// content: `视频字幕xxxxxxxx`,
// },
],
/**
* 采样温度,控制输出的随机性和创造性,取值范围为 [0.0, 1.0],限两位小数。值越低,输出越确定、越保守;值越高,输出越随机、越有创造性
* 较高的值(如0.8)会使输出更随机、更具创造性,适合创意写作和头脑风暴;较低的值(如0.2)会使输出更稳定、更确定,适合事实性问答和代码生成
*/
temperature: 1,
// 模型输出的最大令牌token数量限制。GLM-4.6最大支持128K输出长度,GLM-4.5最大支持96K输出长度,建议设置不小于1024。
max_tokens: 65536,
stream: true, // 流式输出
};
const response = await fetch(url, options);
return streamToJSON(response.data);
}
// 处理流式数据
async function* streamToJSON(stream, doneMarked) {
const reader = stream.getReader();
const decoder = new TextDecoder();
let buffer = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
for (let i = 0; i < lines.length - 1; i++) {
const line = lines[i].trim();
if (line.startsWith("data: ") && line !== doneMarked) {
yield JSON.parse(line.slice(6));
}
}
buffer = lines[lines.length - 1];
}
if (buffer.trim() && buffer.trim() !== doneMarked) {
yield JSON.parse(buffer.trim().slice(6));
}
} finally {
reader.releaseLock();
}
}
const process = async (messages, callback) => {
const completion = await createChatCompletion(messages);
let fullResponse = "";
for await (const chunk of completion) {
const { reasoning_content = "", content = "" } =
chunk?.choices?.[0]?.delta || {};
if (reasoning_content || content) {
fullResponse += reasoning_content || content;
callback({
id: chunk.id,
messages: fullResponse,
isEnd: false,
});
}
}
callback({ messages: fullResponse, isEnd: true });
};
export default process;
五、可视化与知识导图
<svg fill="white" ref="svgRef" class="markmap-svg"></svg>
import { Transformer } from "markmap-lib";
import { Markmap } from "markmap-view";
import * as htmlToImage from "html-to-image";
import { saveAs } from "file-saver";
const svgRef = ref();
const transformer = new Transformer();
// 初始化markmap思维导图
const markmap = Markmap.create(svgRef.value);
const update = (newestMessages) => {
// 更新思维导图渲染
const values = newestMessages.match(/\`\`\`markdown([\s\S]*?)\`\`\`/);
const text = values && values[1] ? values[1] : newestMessages;
const { root } = transformer.transform(text);
markmap.setData(root);
};
// 保持图片
const onSave = async () => {
const canvas = await htmlToImage.toCanvas(svgRef.value, {
pixelRatio: window.devicePixelRatio * 4,
backgroundColor: "#fff",
});
const dataUrl = await canvas.toDataURL();
if (dataUrl) {
saveAs(dataUrl, "pastking.png");
}
};