首页 › tutorials › Claude Code浏览器扩展开发:MV3到上架全实战

概述

本教程教你用 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 功能

Manifest V3 架构速览

在动手之前,必须理解 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.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 的架构约束,避免生成违反规范的代码。

第三步:Manifest V3 清单配置

// 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 — 页面内容提取

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 — 后台大脑

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 开发要点:

  1. 永远不要依赖全局变量保持状态 — Service Worker 随时可能被终止
  2. 使用 chrome.storage 代替内存存储 — 任何需要持久化的数据
  3. 使用 chrome.alarms 代替 setInterval — 定时器在 SW 休眠后失效
  4. fetch() 替代 XMLHttpRequest — MV3 中 XHR 在 SW 中不可用
  5. 消息处理返回 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) => { /* 使用结果 */ }
)

第八步:Options 设置页面

让 Claude Code 生成设置页面,这是管理 AI 配置和历史记录的地方:

创建 src/options/App.tsx 设置页面,包含:
1. AI 服务商选择(Anthropic / OpenAI)
2. API Key 输入框(密码类型,带显示/隐藏切换)
3. 模型选择下拉框
4. 默认摘要风格选择
5. 历史记录列表(可删除、可搜索)
6. 导出历史为 JSON 功能
使用 TailwindCSS 设计,与 Popup 风格统一

第九步:自动化测试

单元测试(Vitest)

// 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 测试(Puppeteer)

// 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 从零到完成一个功能版本的实际操作流程:

阶段一:初始化与基础架构(15分钟)

# 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

阶段二:核心功能开发(30分钟)

# 在 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 界面
> 三种摘要风格切换:简洁/详细/要点
> 加载状态、错误提示、一键复制
> 渐变紫蓝配色,圆角卡片设计

阶段三:本地调试(10分钟)

# 1. 构建
npm run build

# 2. 加载扩展
# Chrome → 扩展程序 → 开发者模式 → 加载已解压的扩展程序 → 选择 dist/ 目录

# 3. 使用 Claude Code Chrome 连接调试
claude --chrome
# Claude Code 可以直接查看 Console 日志、操作 DOM、截图验证

阶段四:测试与优化(15分钟)

> 为 src/lib/ 下的工具函数编写 vitest 单元测试
> 编写 puppeteer E2E 测试:验证扩展加载、内容提取、消息通信
> 运行测试并修复所有失败用例

> 审查代码安全性:
> 1. API Key 是否有泄露风险
> 2. Content Script 是否有 XSS 隐患
> 3. 消息通信是否验证了发送方

阶段五:打包发布(10分钟)

# 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

Chrome Web Store 上架指南

审核要点清单

2026 年 Chrome Web Store 审核拒绝率高达 62%,主要拒绝原因:

拒绝原因 占比 我们的应对
权限声明不充分 28% activeTab + 按需请求,不做过度权限申请
隐私政策缺失 22% 提前准备隐私政策页面
远程代码加载 18% MV3 架构天然禁止,不存在此问题
误导性描述 15% 准确描述功能,提供真实截图
内容安全违规 10% CSP 配置严格,无 inline script
其他 7% 遵循 Chrome Web Store 开发者计划政策

发布前 Checklist

让 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)

审核提速技巧

  1. 在 manifest.json 中添加 homepage_url 指向项目主页
  2. 提供详细的功能描述,包含使用场景说明
  3. 上传演示视频,展示扩展的实际使用流程
  4. 确保隐私政策覆盖所有数据收集行为
  5. 首次提交选择"未列出"发布,审核更快(约 1-3 个工作日)

进阶:Claude Code 加速扩展开发的高级用法

1. 利用 --chrome 标志实时调试

claude --chrome

Claude Code 直接连接 Chrome 浏览器,可以: - 查看扩展的 Console 日志 - 检查 Popup 的 DOM 渲染 - 验证 Content Script 注入效果 - 截图对比 UI 效果

2. 用 Subagent 并行开发

在 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 组件
三个任务相互独立,最后合并

3. 增量迭代 Prompt 模板

当前代码在 [文件路径] 中存在 [问题描述]。
修复时注意:
1. 不要改变消息协议接口
2. 保持 MV3 架构约束
3. 添加对应的单元测试

4. 安全审计 Prompt

对整个扩展项目做安全审计,检查:
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 浏览器即可

延伸阅读

参考来源

  1. How to Develop Browser Extensions with Claude Code — Claude Code 扩展开发实战指南
  2. Building Chrome Extensions in 2026: A Practical Guide with Manifest V3 — MV3 开发全景教程
  3. Vibe Coding Chrome Extensions With Claude Code — 实际项目经验分享
  4. Integrating AI APIs in Chrome Extensions — AI API 集成方案对比
  5. Testing Chrome Extensions with Puppeteer — 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 等场景全覆盖。

51学通信知识星球海报-扫码加入