Skip to content

数据结构

本文档详细说明 AI-SideChat 使用的所有数据结构。

Favorite - 收藏条目

单条收藏的完整数据结构。

typescript
interface Favorite {
  // 基本信息
  id: number                // 唯一标识符(时间戳)
  timestamp: number         // 收藏时间(时间戳)

  // 平台信息
  site: 'Gemini' | 'ChatGPT'  // 平台名称
  siteColor: string            // 平台主题色 (#8850e5 或 #10a37f)

  // 对话信息
  convTitle: string         // 对话标题
  url: string              // 对话完整 URL

  // 问题信息
  question: string         // 用户提问的纯文本
  questionHash: string     // 问题内容的哈希值
  bubbleIndex: number      // 问题在对话中的索引位置(从 0 开始)

  // 回答信息
  answer: string          // AI 回答的纯文本
  answerHtml: string      // AI 回答的 HTML
  contentHash: string     // 回答内容的哈希值

  // 标签信息
  tags: string[]          // 标签数组
}

示例数据

json
{
  "id": 1706014823456,
  "timestamp": 1706014823456,
  "site": "Gemini",
  "siteColor": "#8850e5",
  "convTitle": "Python 文件操作教程",
  "url": "https://gemini.google.com/app/abc123",
  "question": "如何使用 Python 读取 CSV 文件?",
  "questionHash": "123456789",
  "bubbleIndex": 2,
  "answer": "在 Python 中读取 CSV 文件有多种方法...",
  "answerHtml": "<p>在 Python 中读取 CSV 文件有多种方法...</p>",
  "contentHash": "987654321",
  "tags": ["Python", "CSV", "文件操作"]
}

Platform - 平台配置

AI 平台的配置信息。

typescript
interface Platform {
  name: 'Gemini' | 'ChatGPT'    // 平台名称
  color: string                  // 主题色
  selectors: {
    userBubble: string          // 用户气泡选择器
    answer: string              // AI 回答选择器
    convTitle: string           // 对话标题选择器
  }
}

Gemini 配置

javascript
{
  name: 'Gemini',
  color: '#8850e5',
  selectors: {
    userBubble: 'user-query',
    answer: '.model-response-text, message-content, model-response',
    convTitle: '.conversation-title, .conversation'
  }
}

ChatGPT 配置

javascript
{
  name: 'ChatGPT',
  color: '#10a37f',
  selectors: {
    userBubble: '[data-message-author-role="user"]',
    answer: '.markdown',
    convTitle: 'nav a'
  }
}

Storage - 存储结构

Chrome 本地存储的数据结构。

typescript
interface StorageData {
  aiClipData: Favorite[]  // 所有收藏的数组
}

存储示例

json
{
  "aiClipData": [
    {
      "id": 1706014823456,
      "question": "...",
      // ... 其他字段
    },
    {
      "id": 1706014823457,
      "question": "...",
      // ... 其他字段
    }
  ]
}

Message - 消息结构

扩展内部通信的消息格式。

PerformJumpMessage

执行跳转定位的消息。

typescript
interface PerformJumpMessage {
  type: 'PERFORM_JUMP'
  data: {
    questionHash: string
    bubbleIndex: number
  }
}

UpdateFavoritesMessage

更新收藏列表的消息。

typescript
interface UpdateFavoritesMessage {
  type: 'UPDATE_FAVORITES'
}

OpenTabMessage

打开新标签页的消息。

typescript
interface OpenTabMessage {
  type: 'OPEN_TAB'
  url: string
}

SessionStorage - 会话存储

用于跨页状态保持的数据。

PendingJump

待执行的跳转信息。

typescript
interface PendingJump {
  questionHash: string
  bubbleIndex: number
  timestamp: number
}

存储键名:aiClip_pendingJump

javascript
sessionStorage.setItem('aiClip_pendingJump', JSON.stringify({
  questionHash: '123456',
  bubbleIndex: 2,
  timestamp: Date.now()
}))

FilterState - 筛选状态

悬浮坞的筛选状态。

typescript
interface FilterState {
  keyword: string           // 搜索关键词
  selectedTags: string[]    // 选中的标签
  selectedSite: 'Gemini' | 'ChatGPT' | 'all'  // 选中的站点
}

示例

javascript
{
  keyword: 'Python',
  selectedTags: ['教程', 'API'],
  selectedSite: 'Gemini'
}

PreviewState - 预览状态

预览窗口的状态。

typescript
interface PreviewState {
  visible: boolean          // 是否可见
  pinned: boolean          // 是否钉住
  position: {
    x: number              // X 坐标
    y: number              // Y 坐标
  }
  item: Favorite | null    // 预览的收藏条目
}

TagStats - 标签统计

标签的使用统计。

typescript
interface TagStats {
  [tag: string]: number    // 标签名 -> 使用次数
}

示例

javascript
{
  "Python": 15,
  "JavaScript": 10,
  "教程": 8,
  "API": 6
}

计算方法

javascript
function calculateTagStats(favorites) {
  const stats = {}
  favorites.forEach(fav => {
    fav.tags.forEach(tag => {
      stats[tag] = (stats[tag] || 0) + 1
    })
  })
  return stats
}

数据验证

验证收藏数据

javascript
function validateFavorite(data) {
  const required = [
    'id', 'timestamp', 'site', 'siteColor',
    'convTitle', 'url', 'question', 'questionHash',
    'bubbleIndex', 'answer', 'answerHtml', 'contentHash'
  ]

  for (const field of required) {
    if (!(field in data)) {
      throw new Error(`Missing required field: ${field}`)
    }
  }

  if (!Array.isArray(data.tags)) {
    throw new Error('tags must be an array')
  }

  if (!['Gemini', 'ChatGPT'].includes(data.site)) {
    throw new Error('Invalid site value')
  }

  return true
}

数据迁移

版本兼容

当数据结构升级时,需要迁移旧数据:

javascript
async function migrateData() {
  const data = await chrome.storage.local.get('aiClipData')
  const favorites = data.aiClipData || []

  // 检查版本
  const version = await getDataVersion()

  if (version < 2) {
    // 迁移到 v2
    favorites.forEach(fav => {
      if (!fav.tags) {
        fav.tags = []
      }
    })
  }

  if (version < 3) {
    // 迁移到 v3
    favorites.forEach(fav => {
      if (!fav.contentHash) {
        fav.contentHash = simpleHash(fav.answer)
      }
    })
  }

  await chrome.storage.local.set({ aiClipData: favorites })
  await setDataVersion(3)
}

数据备份

导出格式

typescript
interface BackupData {
  version: string          // 备份格式版本
  timestamp: number        // 备份时间
  favorites: Favorite[]    // 收藏数据
  metadata: {
    totalCount: number     // 总数
    tags: string[]        // 所有标签
  }
}

导出示例

javascript
async function exportData() {
  const favorites = await getFavorites()
  const allTags = [...new Set(favorites.flatMap(f => f.tags))]

  const backup = {
    version: '1.0',
    timestamp: Date.now(),
    favorites: favorites,
    metadata: {
      totalCount: favorites.length,
      tags: allTags
    }
  }

  return JSON.stringify(backup, null, 2)
}

下一步

Released under the MIT License.