<!-- title: 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 取代，远程代码加载被禁止，权限模型更加严格。

```mermaid
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 初始化项目。在终端输入：

```bash
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 将生成的核心配置文件：**

```jsonc
// 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',
      },
    },
  },
})
```

```json
// 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 深刻理解项目上下文：

```markdown
# 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 清单配置

```typescript
// 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。

```typescript
// 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
}
```

```typescript
// 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 调用、状态管理和事件响应。

```typescript
// 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 保持消息通道用于异步响应

## 第六步：Popup 交互界面

```tsx
// 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 中各组件的通信是最容易出错的环节。以下是完整的通信流程图：

```mermaid
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` |

**常见通信陷阱：**

```typescript
// ❌ 错误：忘记返回 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）

```typescript
// 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）

```typescript
// 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')
  })
})
```

**测试策略配置：**

```typescript
// 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'],
    },
  },
})
```

运行测试：

```bash
# 单元测试
npx vitest run

# E2E 测试（需要先构建）
npx vite build && npx vitest run --config e2e/vitest.config.ts
```

## 实战案例：完整开发流程演示

用 Claude Code 从零到完成一个功能版本的实际操作流程：

### 阶段一：初始化与基础架构（15分钟）

```bash
# 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分钟）

```bash
# 在 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分钟）

```bash
# 1. 构建
npm run build

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

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

### 阶段四：测试与优化（15分钟）

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

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

### 阶段五：打包发布（10分钟）

```bash
# 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
```

## 完整工作流程图

```mermaid
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` 标志实时调试

```bash
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 | 数据一致性 |

```typescript
// 流式 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 浏览器即可 |

## 延伸阅读

- [Chrome Extensions 官方文档](https://developer.chrome.com/docs/extensions/) — MV3 权威参考
- [Claude Code Chrome 集成文档](https://code.claude.com/docs/en/chrome) — 浏览器调试指南
- [CRXJS Vite Plugin](https://crxjs.dev/vite-plugin) — 现代扩展构建工具
- [Chrome Web Store 发布指南](https://developer.chrome.com/docs/extensions/develop/migrate/publish-mv3) — 上架流程

## 参考来源

1. [How to Develop Browser Extensions with Claude Code](https://claudecode-lab.com/en/blog/claude-code-browser-extension-development/) — Claude Code 扩展开发实战指南
2. [Building Chrome Extensions in 2026: A Practical Guide with Manifest V3](https://dev.to/ryu0705/building-chrome-extensions-in-2026-a-practical-guide-with-manifest-v3-12h2) — MV3 开发全景教程
3. [Vibe Coding Chrome Extensions With Claude Code](https://blog.greenflux.us/vibe-coding-chrome-extensions-with-claude-code/) — 实际项目经验分享
4. [Integrating AI APIs in Chrome Extensions](https://bestchromeextensions.com/2025/03/21/chrome-extension-ai-api-integration/) — AI API 集成方案对比
5. [Testing Chrome Extensions with Puppeteer](https://developer.chrome.com/docs/extensions/how-to/test/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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
