Skip to content

架构设计

本文档介绍 AI-SideChat 的整体架构设计和技术实现。

技术栈

核心技术

  • 原生 JavaScript:无框架依赖,轻量高效
  • Manifest V3:最新的浏览器扩展标准
  • Vite 5.0:现代化构建工具
  • @crxjs/vite-plugin:Chrome 扩展开发插件

浏览器 API

API用途
chrome.storage.local本地数据存储
chrome.runtime消息传递、资源访问
chrome.tabs标签页管理和跳转
chrome.sidePanel侧边栏面板(可选)

Web API

API用途
MutationObserverDOM 变化监听
IntersectionObserver元素可见性检测
sessionStorage跨页状态保持
Shadow DOM样式隔离

架构概览

┌─────────────────────────────────────────┐
│           Content Scripts               │
│  ┌────────────┐      ┌──────────────┐  │
│  │ content.js │─────▶│  drawer.js   │  │
│  └────────────┘      └──────────────┘  │
│         │                    │          │
│         ▼                    ▼          │
│  ┌────────────────────────────────┐    │
│  │     chrome.storage.local       │    │
│  └────────────────────────────────┘    │
│                  ▲                      │
└──────────────────┼──────────────────────┘

         ┌─────────┴──────────┐
         │                    │
┌────────▼────────┐  ┌───────▼────────┐
│  background.js  │  │  sidepanel.js  │
│  (Service       │  │  (Optional)    │
│   Worker)       │  │                │
└─────────────────┘  └────────────────┘

模块设计

content.js - 内容脚本

职责

  • 监听页面 DOM 变化
  • 注入收藏按钮
  • 处理收藏/取消收藏操作
  • 执行跳转定位逻辑
  • 高亮显示目标元素

核心函数

javascript
// 初始化
function init() {
  detectPlatform()
  observeDOM()
  syncFavorites()
  checkPendingJump()
}

// DOM 监听
function observeDOM() {
  const observer = new MutationObserver(() => {
    addCollectButtons()
  })
  observer.observe(document.body, {
    childList: true,
    subtree: true
  })
}

// 收藏操作
function collectItem(bubble) {
  const data = extractData(bubble)
  saveFavorite(data)
  updateButton(bubble, true)
}

// 跳转定位
function performJump(questionHash, bubbleIndex) {
  const strategies = [
    strategyA_IndexWithHash,
    strategyB_HashSearch,
    strategyC_IndexFallback
  ]

  for (const strategy of strategies) {
    if (strategy(questionHash, bubbleIndex)) {
      return true
    }
  }
}

drawer.js - 悬浮坞

职责

  • 渲染悬浮坞入口按钮
  • 显示/隐藏抽屉面板
  • 渲染收藏列表
  • 处理搜索和筛选
  • 显示预览窗口

核心函数

javascript
// 创建悬浮坞
function createDrawer() {
  const container = document.createElement('div')
  container.attachShadow({ mode: 'open' })
  injectStyles()
  renderDrawerButton()
  renderDrawerPanel()
}

// 渲染列表
function renderList(items) {
  const list = items.map(item => {
    return createItemElement(item)
  })
  listContainer.innerHTML = ''
  list.forEach(el => listContainer.appendChild(el))
}

// 搜索筛选
function filterItems(keyword, tags, site) {
  return allItems.filter(item => {
    const matchKeyword = item.question.includes(keyword) ||
                         item.answer.includes(keyword)
    const matchTags = tags.every(tag => item.tags.includes(tag))
    const matchSite = !site || item.site === site
    return matchKeyword && matchTags && matchSite
  })
}

// 显示预览
function showPreview(item, position) {
  const preview = createPreviewElement(item)
  positionPreview(preview, position)
  document.body.appendChild(preview)
}

background.js - 后台脚本

职责

  • 处理跨标签页消息
  • 管理侧边栏状态
  • 执行标签页操作

核心函数

javascript
// 消息监听
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.type) {
    case 'OPEN_TAB':
      openTab(message.url)
      break
    case 'JUMP_TO_CONVERSATION':
      jumpToConversation(message.data)
      break
  }
})

// 标签页跳转
async function jumpToConversation(data) {
  const tab = await findOrCreateTab(data.url)
  chrome.tabs.update(tab.id, { active: true })

  // 通知 content script 执行定位
  chrome.tabs.sendMessage(tab.id, {
    type: 'PERFORM_JUMP',
    data: data
  })
}

sidepanel.js - 侧边栏(可选)

职责

  • 提供独立的侧边栏界面
  • 与抽屉面板功能相同
  • 更好的多任务体验

数据流

收藏流程

用户点击收藏按钮

content.js 提取数据

chrome.storage.local 保存

触发 onChanged 事件

所有 content.js 实例更新 UI

drawer.js 刷新列表

跳转流程

用户点击收藏条目

drawer.js 发送消息

background.js 接收

打开/切换到目标标签页

content.js 接收跳转指令

执行定位策略

高亮显示目标

存储设计

数据结构

javascript
{
  "aiClipData": [
    {
      id: number,              // 唯一 ID
      questionHash: string,     // 问题哈希
      bubbleIndex: number,      // 索引位置
      contentHash: string,      // 回答哈希
      site: string,            // 平台名称
      siteColor: string,       // 主题色
      convTitle: string,       // 对话标题
      question: string,        // 问题文本
      answer: string,          // 回答文本
      answerHtml: string,      // 回答 HTML
      url: string,             // 对话 URL
      tags: string[],          // 标签数组
      timestamp: number        // 时间戳
    }
  ]
}

存储操作

javascript
// 读取
async function getFavorites() {
  const result = await chrome.storage.local.get('aiClipData')
  return result.aiClipData || []
}

// 保存
async function saveFavorite(data) {
  const favorites = await getFavorites()
  favorites.push(data)
  await chrome.storage.local.set({ aiClipData: favorites })
}

// 删除
async function removeFavorite(id) {
  const favorites = await getFavorites()
  const filtered = favorites.filter(item => item.id !== id)
  await chrome.storage.local.set({ aiClipData: filtered })
}

// 更新
async function updateFavorite(id, updates) {
  const favorites = await getFavorites()
  const index = favorites.findIndex(item => item.id === id)
  if (index !== -1) {
    favorites[index] = { ...favorites[index], ...updates }
    await chrome.storage.local.set({ aiClipData: favorites })
  }
}

性能优化

1. 防抖和节流

javascript
// 搜索防抖
function debounce(func, wait) {
  let timeout
  return function(...args) {
    clearTimeout(timeout)
    timeout = setTimeout(() => func.apply(this, args), wait)
  }
}

// DOM 监听节流
function throttle(func, limit) {
  let inThrottle
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

2. 事件委托

javascript
// 不要为每个按钮单独绑定事件
// ❌ 不好的做法
buttons.forEach(btn => {
  btn.addEventListener('click', handler)
})

// ✅ 使用事件委托
document.addEventListener('click', (e) => {
  if (e.target.matches('.collect-btn')) {
    handleCollect(e.target)
  }
})

3. 虚拟滚动

javascript
// 长列表优化(规划中)
class VirtualList {
  constructor(items, itemHeight) {
    this.items = items
    this.itemHeight = itemHeight
    this.visibleCount = Math.ceil(window.innerHeight / itemHeight)
  }

  getVisibleItems(scrollTop) {
    const startIndex = Math.floor(scrollTop / this.itemHeight)
    return this.items.slice(startIndex, startIndex + this.visibleCount)
  }
}

安全性

XSS 防护

javascript
// 清理用户输入
function sanitizeHTML(html) {
  const temp = document.createElement('div')
  temp.textContent = html
  return temp.innerHTML
}

// 使用 textContent 而非 innerHTML
element.textContent = userInput

CSP 兼容

manifest.json 中的 CSP 配置:

json
{
  "content_security_policy": {
    "extension_pages": "script-src 'self'; object-src 'self'"
  }
}

权限最小化

只请求必需的权限:

json
{
  "permissions": ["storage", "tabs"],
  "host_permissions": [
    "https://gemini.google.com/*",
    "https://chatgpt.com/*"
  ]
}

错误处理

全局错误捕获

javascript
window.addEventListener('error', (e) => {
  console.error('[AI-SideChat Error]', e.error)
  // 可选:上报错误
})

window.addEventListener('unhandledrejection', (e) => {
  console.error('[AI-SideChat Promise Rejection]', e.reason)
})

优雅降级

javascript
async function safeOperation() {
  try {
    await riskyOperation()
  } catch (error) {
    console.error('Operation failed:', error)
    // 降级方案
    fallbackOperation()
  }
}

测试策略

单元测试

javascript
// 使用 Jest 测试工具函数
describe('simpleHash', () => {
  test('should return same hash for same input', () => {
    const input = 'test string'
    expect(simpleHash(input)).toBe(simpleHash(input))
  })

  test('should return different hash for different input', () => {
    expect(simpleHash('a')).not.toBe(simpleHash('b'))
  })
})

集成测试

手动测试清单:

  • [ ] 收藏按钮正确显示
  • [ ] 收藏/取消收藏功能正常
  • [ ] 跳转定位准确
  • [ ] 预览窗口显示正确
  • [ ] 标签编辑保存成功
  • [ ] 搜索筛选结果正确

构建配置

vite.config.js

javascript
import { defineConfig } from 'vite'
import { crx } from '@crxjs/vite-plugin'
import manifest from './manifest.json'

export default defineConfig({
  plugins: [crx({ manifest })],
  build: {
    rollupOptions: {
      input: {
        popup: 'popup.html',
        sidepanel: 'sidepanel.html'
      }
    }
  }
})

下一步

Released under the MIT License.