本教程教你用 Claude Code 从零开发一个 AI 驱动的 Chrome 浏览器扩展——PageMind 智能网页摘要助手,覆盖 Manifest V3 架构设计、Content Script 注入、Service Worker 后台逻辑、Popup 交互界面、消息通信机制、AI API 集成、自动化测试到 Chrome Web Store 上架审核全流程。适合有基础前端经验、想掌握浏览器扩展开发的开发者。
| 条件 | 说明 |
|---|---|
| Node.js >= 18 | 构建工具链基础 |
| Claude Code CLI | 已安装并配置 API Key |
| Chrome 浏览器 | 开发调试必需 |
| 基础前端知识 | HTML/CSS/JS/TypeScript |
| AI API Key(可选) | Anthropic/OpenAI 用于 AI 功能 |
在动手之前,必须理解 MV3 的核心架构变化。MV2 的 Background Page 已被 Service Worker 取代,远程代码加载被禁止,权限模型更加严格。
graph TB
subgraph "Chrome Extension MV3 架构"
A[manifest.json<br/>扩展配置中心] --> B[Popup<br/>弹出界面]
A --> C[Content Script<br/>页面注入脚本]
A --> D[Service Worker<br/>后台事件处理]
A --> E[Options Page<br/>设置页面]
B -- "chrome.runtime.sendMessage" --> D
D -- "chrome.tabs.sendMessage" --> C
C -- "chrome.runtime.sendMessage" --> D
D -- "fetch API" --> F[外部 API<br/>Claude/OpenAI]
G[Chrome Storage API] -.-> B
G -.-> D
H[Chrome Alarms API] -.-> D
end
style A fill:#4285f4,color:#fff
style D fill:#34a853,color:#fff
style C fill:#fbbc05,color:#333
style B fill:#ea4335,color:#fff
三大核心组件职责:
| 组件 | 运行环境 | 生命周期 | 核心能力 |
|---|---|---|---|
| Content Script | 网页上下文 | 页面打开时存活 | DOM 操作、页面数据提取 |
| Service Worker | 扩展独立线程 | 事件驱动,空闲休眠 | API 调用、状态管理、事件监听 |
| Popup | 独立窗口 | 点击图标时创建 | 用户交互、结果展示 |
关键限制: Service Worker 在 MV3 中没有持久化状态,30秒无活动会被 Chrome 终止。所有长期状态必须存入 chrome.storage。
用 Claude Code 初始化项目。在终端输入:
mkdir pagemind-extension && cd pagemind-extension
claude
在 Claude Code 交互界面中输入:
初始化一个 Chrome Extension MV3 项目,技术栈要求:
1. TypeScript + Vite 构建
2. React 19 做 Popup 和 Options 页面
3. @crxjs/vite-plugin 集成
4. TailwindCSS 样式
5. 项目结构如下:
- src/background/ — Service Worker
- src/content/ — Content Script
- src/popup/ — Popup 页面
- src/options/ — Options 页面
- src/lib/ — 共享工具库
- src/types/ — TypeScript 类型定义
请创建所有配置文件和基础入口文件
Claude Code 将生成的核心配置文件:
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import crx from '@crxjs/vite-plugin'
import { manifest } from './src/manifest'
export default defineConfig({
plugins: [
react(),
crx({ manifest }),
],
build: {
rollupOptions: {
input: {
content: 'src/content/index.ts',
background: 'src/background/index.ts',
},
},
},
})
// package.json 核心依赖
{
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@crxjs/vite-plugin": "^2.0.0-beta.28",
"@types/chrome": "^0.0.287",
"typescript": "^5.5.0",
"vite": "^6.0.0",
"tailwindcss": "^4.0.0",
"vitest": "^3.0.0",
"puppeteer": "^23.0.0"
}
}
预期结果: Claude Code 会在 30 秒内生成完整项目骨架,包含 12-15 个文件。
常见问题:
| 问题 | 解决方案 |
|---|---|
@crxjs/vite-plugin 与 Vite 6 不兼容 |
使用 beta.28 或更高版本,或降级到 Vite 5 |
| Chrome 类型报错 | 确保安装了 @types/chrome 并在 tsconfig 中配置 |
创建项目级 CLAUDE.md,让 Claude Code 深刻理解项目上下文:
# PageMind - AI 智能网页摘要 Chrome Extension
## 项目架构
- MV3 架构,不允许使用远程代码加载
- Service Worker 生命周期:事件驱动,不能依赖全局变量保持状态
- 所有持久化数据必须使用 chrome.storage API
## 开发规则
1. Content Script 中不允许直接调用外部 API(CORS 限制)
2. API 调用统一在 Service Worker 中处理
3. Content Script 与 Service Worker 通过 chrome.runtime.sendMessage 通信
4. 权限申请遵循最小权限原则
5. 用户 API Key 存储在 chrome.storage.local,禁止明文写入代码
## 关键文件
- src/manifest.ts — 扩展清单配置
- src/background/index.ts — Service Worker 入口
- src/content/index.ts — Content Script 入口
- src/popup/App.tsx — Popup 主界面
## 测试要求
- vitest 单元测试覆盖 lib/ 下的工具函数
- puppeteer e2e 测试覆盖核心用户流程
这条 CLAUDE.md 是项目开发效率的关键倍增器。Claude Code 每次对话都会读取它,自动遵循 MV3 的架构约束,避免生成违反规范的代码。
// src/manifest.ts
import { defineManifest } from '@crxjs/vite-plugin'
export default defineManifest({
manifest_version: 3,
name: 'PageMind - AI Page Summarizer',
version: '1.0.0',
description: 'AI-powered web page summarization and analysis',
// 权限声明 — 最小权限原则
permissions: [
'activeTab', // 访问当前标签页
'storage', // 本地存储
'alarms', // 定时任务(MV3 替代 setInterval)
],
host_permissions: [],
// Service Worker
background: {
service_worker: 'src/background/index.ts',
type: 'module',
},
// Content Script — 按需注入,不在 manifest 中静态声明
// 改用 chrome.scripting.executeScript 动态注入
// Popup 页面
action: {
default_popup: 'src/popup/index.html',
default_icon: {
'16': 'icons/icon-16.png',
'48': 'icons/icon-48.png',
'128': 'icons/icon-128.png',
},
},
// Options 页面
options_page: 'src/options/index.html',
// 图标
icons: {
'16': 'icons/icon-16.png',
'48': 'icons/icon-48.png',
'128': 'icons/icon-128.png',
},
})
MV2 → MV3 关键迁移点:
| MV2 写法 | MV3 写法 | 原因 |
|---|---|---|
background.scripts |
background.service_worker |
性能与安全 |
browser_action / page_action |
action |
统一API |
chrome.browserAction |
chrome.action |
API重命名 |
background.persist: true |
不支持 | 强制事件驱动 |
XMLHttpRequest |
fetch() |
移除同步请求 |
| 远程代码加载 | 禁止 | 安全隔离 |
Content Script 是扩展与网页交互的唯一桥梁。它运行在页面上下文中,可以访问 DOM,但不能直接调用 Chrome API。
// src/types/messages.ts
export interface ExtractRequest {
type: 'EXTRACT_CONTENT'
tabId?: number
}
export interface ExtractResponse {
title: string
url: string
content: string
wordCount: number
lang: string
}
export interface SummarizeRequest {
type: 'SUMMARIZE'
content: string
style: 'brief' | 'detailed' | 'bullet-points'
}
export interface SummarizeResponse {
summary: string
keyPoints: string[]
readingTime: string
}
// src/content/index.ts
// Content Script — 注入到网页中执行
// 监听来自 Service Worker 的消息
chrome.runtime.onMessage.addListener(
(request, sender, sendResponse) => {
if (request.type === 'EXTRACT_CONTENT') {
const result = extractPageContent()
sendResponse(result)
return true // 保持消息通道开放
}
}
)
interface ExtractionResult {
title: string
url: string
content: string
wordCount: number
lang: string
}
function extractPageContent(): ExtractionResult {
// 智能内容提取:优先提取 article/main 区域
const selectors = [
'article',
'main',
'[role="main"]',
'.post-content',
'.article-body',
'#content',
]
let contentEl: Element | null = null
for (const sel of selectors) {
contentEl = document.querySelector(sel)
if (contentEl) break
}
// 降级到 body
const source = contentEl || document.body
// 清理无关元素
const cloned = source.cloneNode(true) as Element
cloned.querySelectorAll(
'script, style, nav, footer, header, .ad, .advertisement, .sidebar'
).forEach(el => el.remove())
const content = cloned.textContent
?.replace(/\s+/g, ' ')
.trim() || ''
return {
title: document.title,
url: window.location.href,
content,
wordCount: content.split(/\s+/).filter(Boolean).length,
lang: document.documentElement.lang || 'en',
}
}
// 注入浮动操作按钮(可选功能)
function injectFloatingButton(): void {
if (document.getElementById('pagemind-float-btn')) return
const btn = document.createElement('div')
btn.id = 'pagemind-float-btn'
btn.innerHTML = '🧠'
btn.style.cssText = `
position: fixed; bottom: 24px; right: 24px;
width: 48px; height: 48px; border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex; align-items: center; justify-content: center;
cursor: pointer; z-index: 999999; font-size: 20px;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
transition: transform 0.2s;
`
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'scale(1.1)'
})
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'scale(1)'
})
btn.addEventListener('click', () => {
// 通知 Service Worker 开始摘要
chrome.runtime.sendMessage({
type: 'QUICK_SUMMARIZE',
content: extractPageContent().content,
})
})
document.body.appendChild(btn)
}
// 仅在文章类页面注入浮动按钮
if (document.querySelector('article, main, [role="main"]')) {
injectFloatingButton()
}
Claude Code Prompt 优化技巧:
当需要 Claude Code 优化 Content Script 时,使用精确的上下文描述:
优化 src/content/index.ts 的 extractPageContent 函数:
1. 增加对论坛帖子(.post-body)和文档站(.markdown-body)的选择器支持
2. 处理 paywall 保护区的内容跳过
3. 中文内容用字符数而非单词数统计
4. 提取 <meta name="description"> 作为摘要补充
不要改变现有的消息协议接口
Service Worker 是 MV3 的后台核心,负责 API 调用、状态管理和事件响应。
// src/background/index.ts
// Service Worker — 扩展的后台大脑
// ============ 类型定义 ============
interface AIConfig {
provider: 'anthropic' | 'openai'
apiKey: string
model: string
}
interface SummarizeStyle {
brief: string
detailed: string
'bullet-points': string
}
// ============ 系统提示词 ============
const SYSTEM_PROMPTS: Record<string, string> = {
brief: `你是一个精准的网页内容摘要助手。请用2-3句话概括网页核心内容。
要求:直接输出摘要,不要加前缀。使用与原文相同的语言。`,
detailed: `你是一个详细的网页内容分析助手。请提供:
1. 内容摘要(3-5句话)
2. 核心要点(列出所有关键信息)
3. 目标读者分析
直接输出,使用与原文相同的语言。`,
'bullet-points': `你是一个结构化信息提取助手。请用要点列表形式呈现:
1. 核心主题(一句话)
2. 关键要点(5-8个要点)
3. 重要数据/引用
直接输出,使用与原文相同的语言。`,
}
// ============ AI API 调用 ============
async function callAI(
config: AIConfig,
prompt: string,
systemPrompt: string
): Promise<string> {
if (config.provider === 'anthropic') {
return callAnthropic(config, prompt, systemPrompt)
}
return callOpenAI(config, prompt, systemPrompt)
}
async function callAnthropic(
config: AIConfig,
prompt: string,
systemPrompt: string
): Promise<string> {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': config.apiKey,
'anthropic-version': '2023-06-01',
'anthropic-dangerous-direct-browser-access': 'true',
},
body: JSON.stringify({
model: config.model || 'claude-sonnet-4-20250514',
max_tokens: 2048,
system: systemPrompt,
messages: [{ role: 'user', content: prompt }],
}),
})
if (!response.ok) {
const err = await response.json()
throw new Error(`Anthropic API error: ${err.error?.message}`)
}
const data = await response.json()
return data.content[0].text
}
async function callOpenAI(
config: AIConfig,
prompt: string,
systemPrompt: string
): Promise<string> {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`,
},
body: JSON.stringify({
model: config.model || 'gpt-4o',
max_tokens: 2048,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt },
],
}),
})
if (!response.ok) {
const err = await response.json()
throw new Error(`OpenAI API error: ${err.error?.message}`)
}
const data = await response.json()
return data.choices[0].message.content
}
// ============ 消息处理 ============
chrome.runtime.onMessage.addListener(
(request, sender, sendResponse) => {
handleMessage(request, sender)
.then(sendResponse)
.catch(error => sendResponse({ error: error.message }))
return true // 异步响应必须返回 true
}
)
async function handleMessage(
request: any,
_sender: chrome.runtime.MessageSender
): Promise<any> {
switch (request.type) {
case 'SUMMARIZE':
return handleSummarize(request)
case 'QUICK_SUMMARIZE':
return handleQuickSummarize(request)
case 'GET_SETTINGS':
return handleGetSettings()
case 'SAVE_SETTINGS':
return handleSaveSettings(request)
default:
throw new Error(`Unknown message type: ${request.type}`)
}
}
async function handleSummarize(request: any): Promise<any> {
const config = await getAIConfig()
if (!config.apiKey) {
throw new Error('请先在设置中配置 API Key')
}
const style = request.style || 'brief'
const systemPrompt = SYSTEM_PROMPTS[style]
// 截断过长内容(API Token 限制)
const content = request.content.slice(0, 50000)
const prompt = `请分析以下网页内容:\n\n${content}`
const summary = await callAI(config, prompt, systemPrompt)
// 保存到历史记录
await saveToHistory({
summary,
style,
timestamp: Date.now(),
wordCount: content.length,
})
return { summary }
}
async function handleQuickSummarize(request: any): Promise<any> {
const result = await handleSummarize({
...request,
style: 'brief',
})
// 使用 chrome.notifications 显示快捷摘要
if (result.summary) {
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon-128.png',
title: 'PageMind 摘要',
message: result.summary.slice(0, 300),
})
}
return result
}
// ============ 存储工具 ============
async function getAIConfig(): Promise<AIConfig> {
const result = await chrome.storage.local.get([
'aiProvider', 'apiKey', 'aiModel',
])
return {
provider: result.aiProvider || 'anthropic',
apiKey: result.apiKey || '',
model: result.aiModel || 'claude-sonnet-4-20250514',
}
}
async function saveToHistory(entry: any): Promise<void> {
const result = await chrome.storage.local.get('history')
const history = result.history || []
history.unshift(entry)
// 保留最近 100 条
await chrome.storage.local.set({
history: history.slice(0, 100),
})
}
async function handleGetSettings(): Promise<any> {
return await chrome.storage.local.get([
'aiProvider', 'apiKey', 'aiModel', 'summaryStyle',
])
}
async function handleSaveSettings(request: any): Promise<any> {
await chrome.storage.local.set({
aiProvider: request.provider,
apiKey: request.apiKey,
aiModel: request.model,
summaryStyle: request.style,
})
return { success: true }
}
// ============ 扩展安装事件 ============
chrome.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
// 首次安装,打开设置页
chrome.runtime.openOptionsPage()
}
})
// ============ Alarms(替代 setInterval)============
chrome.alarms.create('cleanup', { periodInMinutes: 60 })
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'cleanup') {
// 定期清理过期历史记录
cleanupOldHistory()
}
})
async function cleanupOldHistory(): Promise<void> {
const result = await chrome.storage.local.get('history')
const history = result.history || []
const thirtyDaysAgo = Date.now() - 30 * 24 * 60 * 60 * 1000
const filtered = history.filter(
(h: any) => h.timestamp > thirtyDaysAgo
)
await chrome.storage.local.set({ history: filtered })
}
Service Worker 开发要点:
chrome.storage 代替内存存储 — 任何需要持久化的数据chrome.alarms 代替 setInterval — 定时器在 SW 休眠后失效fetch() 替代 XMLHttpRequest — MV3 中 XHR 在 SW 中不可用true — 告诉 Chrome 保持消息通道用于异步响应// src/popup/App.tsx
import { useState, useEffect } from 'react'
type SummaryStyle = 'brief' | 'detailed' | 'bullet-points'
interface SummaryResult {
summary: string
loading: boolean
error: string | null
wordCount: number
}
export default function App() {
const [result, setResult] = useState<SummaryResult>({
summary: '', loading: false, error: null, wordCount: 0,
})
const [style, setStyle] = useState<SummaryStyle>('brief')
const [configured, setConfigured] = useState(true)
useEffect(() => {
// 检查是否已配置 API Key
chrome.runtime.sendMessage(
{ type: 'GET_SETTINGS' },
(settings) => {
setConfigured(!!settings?.apiKey)
}
)
}, [])
const handleSummarize = async () => {
setResult(prev => ({ ...prev, loading: true, error: null }))
try {
// Step 1: 从当前标签页提取内容
const [tab] = await chrome.tabs.query({
active: true, currentWindow: true,
})
if (!tab?.id) throw new Error('无法获取当前标签页')
const pageContent: any = await new Promise((resolve, reject) => {
chrome.tabs.sendMessage(
tab.id!,
{ type: 'EXTRACT_CONTENT' },
(response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
} else {
resolve(response)
}
}
)
})
if (!pageContent?.content) {
throw new Error('无法提取页面内容')
}
// Step 2: 发送到 Service Worker 进行 AI 摘要
chrome.runtime.sendMessage(
{
type: 'SUMMARIZE',
content: pageContent.content,
style,
},
(response) => {
if (response?.error) {
setResult({
summary: '', loading: false,
error: response.error, wordCount: 0,
})
} else {
setResult({
summary: response.summary,
loading: false,
error: null,
wordCount: pageContent.wordCount,
})
}
}
)
} catch (err: any) {
setResult({
summary: '', loading: false,
error: err.message, wordCount: 0,
})
}
}
return (
<div className="w-[400px] min-h-[300px] p-4 bg-gradient-to-br from-slate-50 to-blue-50">
{/* 头部 */}
<header className="flex items-center justify-between mb-4">
<h1 className="text-lg font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
PageMind
</h1>
<button
onClick={() => chrome.runtime.openOptionsPage()}
className="text-xs text-gray-400 hover:text-gray-600"
>
⚙️ 设置
</button>
</header>
{/* 未配置提示 */}
{!configured && (
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-700">
请先在设置中配置 AI API Key
</div>
)}
{/* 摘要风格选择 */}
<div className="flex gap-2 mb-4">
{(['brief', 'detailed', 'bullet-points'] as const).map((s) => (
<button
key={s}
onClick={() => setStyle(s)}
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-all
${style === s
? 'bg-blue-600 text-white shadow-md'
: 'bg-white text-gray-600 hover:bg-gray-100'
}`}
>
{s === 'brief' ? '简洁' : s === 'detailed' ? '详细' : '要点'}
</button>
))}
</div>
{/* 摘要按钮 */}
<button
onClick={handleSummarize}
disabled={result.loading || !configured}
className="w-full py-2.5 rounded-lg font-medium text-white
bg-gradient-to-r from-blue-600 to-purple-600
hover:from-blue-700 hover:to-purple-700
disabled:opacity-50 disabled:cursor-not-allowed
transition-all shadow-md hover:shadow-lg"
>
{result.loading ? '正在分析...' : '🧠 智能摘要'}
</button>
{/* 结果展示 */}
{result.error && (
<div className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{result.error}
</div>
)}
{result.summary && (
<div className="mt-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs text-gray-400">
原文 {result.wordCount} 词
</span>
<button
onClick={() => navigator.clipboard.writeText(result.summary)}
className="text-xs text-blue-500 hover:text-blue-700"
>
📋 复制
</button>
</div>
<div className="p-3 bg-white rounded-lg border border-gray-100
text-sm leading-relaxed text-gray-700 whitespace-pre-wrap">
{result.summary}
</div>
</div>
)}
</div>
)
}
让 Claude Code 为你生成 Popup 的 HTML 入口和 Tailwind 配置:
为 Popup 页面创建入口 HTML 文件 src/popup/index.html,
引入 React 应用。同时配置 TailwindCSS v4,
确保构建产物中不包含未使用的 CSS。
MV3 中各组件的通信是最容易出错的环节。以下是完整的通信流程图:
sequenceDiagram
participant U as 用户
participant P as Popup
participant CS as Content Script
participant SW as Service Worker
participant AI as AI API
U->>P: 点击"智能摘要"
P->>CS: chrome.tabs.sendMessage<br/>{type: EXTRACT_CONTENT}
CS->>CS: 提取页面 DOM 内容
CS-->>P: 返回 {title, content, wordCount}
P->>SW: chrome.runtime.sendMessage<br/>{type: SUMMARIZE, content, style}
SW->>SW: 读取 API 配置
SW->>AI: fetch() 调用 AI API
AI-->>SW: 返回摘要结果
SW->>SW: 保存到 chrome.storage
SW-->>P: 返回 {summary}
P->>P: 渲染摘要结果
Note over U,P: 快捷摘要流程
U->>CS: 点击浮动按钮 🧠
CS->>SW: chrome.runtime.sendMessage<br/>{type: QUICK_SUMMARIZE}
SW->>AI: fetch() 调用
AI-->>SW: 返回摘要
SW->>SW: chrome.notifications 显示
通信模式对照表:
| 场景 | 发送方 | API | 注意事项 |
|---|---|---|---|
| Popup → Content Script | Popup | chrome.tabs.sendMessage(tabId, msg) |
需要知道 tabId |
| Popup → Service Worker | Popup | chrome.runtime.sendMessage(msg) |
最常用 |
| Content → Service Worker | Content | chrome.runtime.sendMessage(msg) |
无需 tabId |
| Service Worker → Content | SW | chrome.tabs.sendMessage(tabId, msg) |
需要主动注入时先获取 tabId |
| Service Worker → Popup | SW | 无直接API | Popup 监听 chrome.runtime.onMessage |
常见通信陷阱:
// ❌ 错误:忘记返回 true,异步响应丢失
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
fetch(apiUrl).then(data => sendResponse(data))
// 缺少 return true!
})
// ✅ 正确:异步处理必须返回 true
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
fetch(apiUrl).then(data => sendResponse(data))
return true // 保持消息通道开放
})
// ❌ 错误:在 Content Script 中直接调用 fetch 外部 API
// 受 CORS 限制,会被浏览器拦截
// ✅ 正确:通过消息让 Service Worker 代为请求
chrome.runtime.sendMessage(
{ type: 'SUMMARIZE', content },
(response) => { /* 使用结果 */ }
)
让 Claude Code 生成设置页面,这是管理 AI 配置和历史记录的地方:
创建 src/options/App.tsx 设置页面,包含:
1. AI 服务商选择(Anthropic / OpenAI)
2. API Key 输入框(密码类型,带显示/隐藏切换)
3. 模型选择下拉框
4. 默认摘要风格选择
5. 历史记录列表(可删除、可搜索)
6. 导出历史为 JSON 功能
使用 TailwindCSS 设计,与 Popup 风格统一
// src/lib/__tests__/extract.test.ts
import { describe, it, expect, vi } from 'vitest'
describe('extractPageContent', () => {
it('should extract article content', () => {
document.body.innerHTML = `
<article>
<p>This is a test article with enough content.</p>
<p>Second paragraph with more details.</p>
</article>
`
// 模拟 extractPageContent 的核心逻辑
const article = document.querySelector('article')
const content = article?.textContent?.replace(/\s+/g, ' ').trim()
expect(content).toContain('test article')
expect(content).toContain('Second paragraph')
})
it('should fall back to body when no article element', () => {
document.body.innerHTML = `
<div>No article element here</div>
`
const content = document.body.textContent?.trim()
expect(content).toBe('No article element here')
})
})
// e2e/extension.test.ts
import puppeteer, { Browser } from 'puppeteer'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
describe('PageMind Extension E2E', () => {
let browser: Browser
const extensionPath = `${process.cwd()}/dist`
beforeAll(async () => {
browser = await puppeteer.launch({
headless: false, // 扩展测试需要 headed 模式
args: [
`--disable-extensions-except=${extensionPath}`,
`--load-extension=${extensionPath}`,
],
})
})
afterAll(async () => {
await browser.close()
})
it('should load extension and show popup', async () => {
const page = await browser.newPage()
await page.goto('https://example.com')
await page.waitForSelector('article, main, body')
// 验证 Content Script 注入了浮动按钮
const floatBtn = await page.$('#pagemind-float-btn')
expect(floatBtn).toBeTruthy()
})
it('should extract content from page', async () => {
const page = await browser.newPage()
await page.goto('https://example.com')
// 模拟消息发送到 Content Script
const result = await page.evaluate(() => {
return new Promise((resolve) => {
chrome.tabs.query(
{ active: true, currentWindow: true },
(tabs) => {
chrome.tabs.sendMessage(
tabs[0].id!,
{ type: 'EXTRACT_CONTENT' },
resolve
)
}
)
})
})
expect(result).toHaveProperty('content')
expect(result).toHaveProperty('title')
})
})
测试策略配置:
// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
include: ['src/lib/**/*.ts'],
},
},
})
运行测试:
# 单元测试
npx vitest run
# E2E 测试(需要先构建)
npx vite build && npx vitest run --config e2e/vitest.config.ts
用 Claude Code 从零到完成一个功能版本的实际操作流程:
# 1. 创建项目
mkdir pagemind-extension && cd pagemind-extension
git init
# 2. 启动 Claude Code
claude
# 3. 输入项目初始化 Prompt
> 初始化 Chrome Extension MV3 项目,技术栈:TypeScript + Vite + React 19 + CRXJS + TailwindCSS v4
> 项目结构:src/background | src/content | src/popup | src/options | src/lib | src/types
> 创建 manifest.ts、所有入口文件、CLAUDE.md
# 4. 安装依赖
npm install
# 5. 首次构建验证
npm run build
# 在 Claude Code 中分步输入:
> 实现 src/content/index.ts:页面内容提取功能
> 要求支持 article/main/markdown-body 等多种选择器
> 添加浮动快捷按钮,注入条件:页面包含 article 或 main 元素
> 实现 src/background/index.ts:Service Worker
> 支持 Anthropic 和 OpenAI 双 API
> 处理 SUMMARIZE/QUICK_SUMMARIZE/GET_SETTINGS/SAVE_SETTINGS 消息
> 使用 chrome.alarms 定期清理历史记录
> 实现 src/popup/App.tsx:React Popup 界面
> 三种摘要风格切换:简洁/详细/要点
> 加载状态、错误提示、一键复制
> 渐变紫蓝配色,圆角卡片设计
# 1. 构建
npm run build
# 2. 加载扩展
# Chrome → 扩展程序 → 开发者模式 → 加载已解压的扩展程序 → 选择 dist/ 目录
# 3. 使用 Claude Code Chrome 连接调试
claude --chrome
# Claude Code 可以直接查看 Console 日志、操作 DOM、截图验证
> 为 src/lib/ 下的工具函数编写 vitest 单元测试
> 编写 puppeteer E2E 测试:验证扩展加载、内容提取、消息通信
> 运行测试并修复所有失败用例
> 审查代码安全性:
> 1. API Key 是否有泄露风险
> 2. Content Script 是否有 XSS 隐患
> 3. 消息通信是否验证了发送方
# 1. 生产构建
npm run build
# 2. 打包 zip
cd dist && zip -r ../pagemind-v1.0.0.zip . && cd ..
# 3. 提交到 Chrome Web Store
# https://chrome.google.com/webstore/devconsole
graph LR
subgraph "开发阶段"
A[Claude Code<br/>项目初始化] --> B[CLAUDE.md<br/>上下文配置]
B --> C[Manifest V3<br/>清单配置]
C --> D[Content Script<br/>内容提取]
D --> E[Service Worker<br/>API 调用]
E --> F[Popup/Options<br/>交互界面]
end
subgraph "测试阶段"
F --> G[Vitest<br/>单元测试]
G --> H[Puppeteer<br/>E2E 测试]
H --> I[Chrome 连接<br/>实时调试]
end
subgraph "发布阶段"
I --> J[生产构建<br/>npm run build]
J --> K[打包 ZIP]
K --> L[Chrome Web Store<br/>提交审核]
L --> M[审核通过<br/>上线发布]
end
M -.-> |"收集反馈"| N[迭代更新]
N -.-> A
style A fill:#4285f4,color:#fff
style E fill:#34a853,color:#fff
style L fill:#ea4335,color:#fff
style M fill:#fbbc05,color:#333
2026 年 Chrome Web Store 审核拒绝率高达 62%,主要拒绝原因:
| 拒绝原因 | 占比 | 我们的应对 |
|---|---|---|
| 权限声明不充分 | 28% | activeTab + 按需请求,不做过度权限申请 |
| 隐私政策缺失 | 22% | 提前准备隐私政策页面 |
| 远程代码加载 | 18% | MV3 架构天然禁止,不存在此问题 |
| 误导性描述 | 15% | 准确描述功能,提供真实截图 |
| 内容安全违规 | 10% | CSP 配置严格,无 inline script |
| 其他 | 7% | 遵循 Chrome Web Store 开发者计划政策 |
让 Claude Code 帮你做最后的全面检查:
执行发布前检查:
1. manifest.json 中所有字段是否正确(version、permissions、icons)
2. 是否存在 console.log 调试代码
3. API Key 硬编码检查(应该全部从 chrome.storage 读取)
4. 图标文件是否齐全(16/48/128 三种尺寸)
5. 构建产物体积是否合理(建议 < 2MB)
6. 隐私政策是否就绪
7. 截图和宣传图片是否准备(1280x800 和 640x400)
manifest.json 中添加 homepage_url 指向项目主页--chrome 标志实时调试claude --chrome
Claude Code 直接连接 Chrome 浏览器,可以: - 查看扩展的 Console 日志 - 检查 Popup 的 DOM 渲染 - 验证 Content Script 注入效果 - 截图对比 UI 效果
在 Claude Code 中同时启动多个开发任务:
使用 Task 工具并行处理以下任务:
1. Agent 1: 完成 src/content/index.ts 的内容提取逻辑
2. Agent 2: 完成 src/background/index.ts 的 API 调用逻辑
3. Agent 3: 完成 src/popup/App.tsx 的 UI 组件
三个任务相互独立,最后合并
当前代码在 [文件路径] 中存在 [问题描述]。
修复时注意:
1. 不要改变消息协议接口
2. 保持 MV3 架构约束
3. 添加对应的单元测试
对整个扩展项目做安全审计,检查:
1. CSP 配置是否严格
2. Content Script 是否存在 XSS 风险
3. 消息通信是否验证了 sender
4. API Key 存储是否安全
5. 是否存在权限滥用
| 优化项 | 方法 | 影响 |
|---|---|---|
| Service Worker 冷启动 | 核心逻辑保持在 50ms 内 | 用户等待时间 |
| Content Script 注入 | 使用 chrome.scripting.executeScript 按需注入 |
页面加载性能 |
| 打包体积 | Tree-shaking + 代码分割 | 安装速度 |
| AI API 调用 | 流式响应 + 请求去重 | 响应速度 |
| 存储读写 | 批量操作 + debounce | 数据一致性 |
// 流式 AI 响应示例(Service Worker 中)
async function callAnthropicStream(
config: AIConfig, prompt: string, systemPrompt: string
): Promise<ReadableStream> {
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': config.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: config.model,
max_tokens: 2048,
stream: true, // 开启流式
system: systemPrompt,
messages: [{ role: 'user', content: prompt }],
}),
})
return response.body!
}
| 维度 | 评估 |
|---|---|
| 适合训练营类型 | 前端进阶营、全栈工程师营、AI 应用开发营 |
| 预计教学时长 | 4-6 小时(含实操) |
| 难度等级 | ⭐⭐⭐ 中等(需前端基础) |
| 学员产出 | 每人完成一个可上架的 Chrome Extension |
| 扩展方向 | 添加多语言支持、集成更多 AI 模型、支持 PDF 摘要 |
| 硬件要求 | 普通笔记本 + Chrome 浏览器即可 |
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
