System Design

Chrome Extension Runtime Environments

环境对比

环境 上下文 生命周期 能访问页面 DOM
Content Script 注入到网页,和页面共享 JS 上下文 随页面存在
Side Panel 独立面板上下文,类似 iframe 随用户打开存在
Background (Service Worker) 插件后台,独立浏览器实例 插件运行期间(可休眠)
Popup 点击图标弹出的临时 UI 点击期间存在,关闭即销毁

通信全图

┌─────────────────────────────────────────────────────────┐
│ 插件扩展上下文                                           │
│                                                          │
│  ┌──────────┐  ┌────────────────┐  │                    │
│  │  Popup   │◄──────►│   Background    │◄─── chrome.runtime.sendMessage
│  │          │ ① │ (Service Worker)│  │                    │
│  └──────────┘  └───────┬────────┘ ② │                    │
│  ┌────────────┐         │          │                    │
│  │ Side Panel │◄───────────────┤          │                    │
│  └────────────┘         │          │                    │
│                         │          │                    │
│  ┌──────────────────────┴───────┐ │                    │
│  │ chrome.runtime.sendMessage ③   │ │                    │
│  │ chrome.runtime.onMessage        │ │                    │
│  └──────────────────────────────────────┘ │                    │
└─────────────────────────────────────────────────────────┘
                          │
                          │ chrome.tabs.sendMessage ④
                          ▼
         ┌───────────────────────┐
         │   Content Script      │◄── inject 到网页
         │   (页面上下文 / DOM)  │
         └───────────────────────┘

API 选择指南

发消息:sendMessage vs tabs.sendMessage

场景 用哪个 说明
Background → Content Script chrome.tabs.sendMessage(tabId, msg) 发送到指定标签页的 content script
Popup → Content Script chrome.tabs.sendMessage(tabId, msg) 经由 background 中转,或直接 tabs
Side Panel → Content Script chrome.tabs.sendMessage(tabId, msg) 同上
Content Script → Background chrome.runtime.sendMessage(msg) content script 用 runtime
Popup → Background chrome.runtime.sendMessage(msg) popup 和 background 直连
Side Panel → Background chrome.runtime.sendMessage(msg) side panel 和 background 直连

规律:只要目标是 Content Script,就用 tabs.sendMessage(tabId, ...)

收消息:onMessage vs tabs.onMessage

监听位置 用哪个 说明
Background chrome.runtime.onMessage.addListener 接收 popup、side panel、content script 的消息
Background chrome.tabs.onMessage.addListener 专门接收来自 content script 的消息
Content Script chrome.runtime.onMessage.addListener 接收来自 background 的响应
Popup chrome.runtime.onMessage.addListener 接收 background 的响应
Side Panel chrome.runtime.onMessage.addListener 接收 background 的响应

规律:在 background 里,两个都能用,通常用 runtime.onMessage 统一处理所有扩展内部消息;如果需要区分消息来源 tab(区分不同标签页的 content script),用 tabs.onMessage

实际例子(ClawSide 场景)

Content Script 发现不兼容,上报给 Background

// content script → background(用 runtime)
chrome.runtime.sendMessage({
  type: 'INCOMPATIBILITY_REPORT',
  url: window.location.href,
  detail: 'Ollama v0.12.4 不支持 macOS 12'
});

// background 接收(统一用 runtime)
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'INCOMPATIBILITY_REPORT') {
    // 处理并存储
    chrome.storage.local.set({ lastReport: msg });
    sendResponse({ ok: true });
  }
});

Background 通知 Side Panel 显示内容

// background → side panel(用 runtime)
chrome.runtime.sendMessage({
  type: 'SHOW_TRANSLATION',
  content: translatedText
}, (response) => {
  // side panel 已接收后的回调
});

// side panel 接收
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'SHOW_TRANSLATION') {
    renderTranslation(msg.content);
  }
});

Background 向 Content Script 发指令(比如清空页面翻译)

// background → content script(必须用 tabs + tabId)
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  chrome.tabs.sendMessage(tabs[0].id, {
    type: 'CLEAR_TRANSLATIONS'
  });
});

// content script 接收
chrome.runtime.onMessage.addListener((msg, sender) => {
  if (msg.type === 'CLEAR_TRANSLATIONS') {
    // 清除页面上的翻译占位符
  }
});

一句话总结

接收端:chrome.runtime.onMessage 通用,chrome.tabs.onMessage 专用于 content script 来消息的场景(background 里用它可额外拿到 sender.tab 信息)


Architecture v2.0.0

@startuml
@enduml

Architecture v1.0.0

@startuml
!theme plain

skinparam component {
  BackgroundColor white
  ArrowColor #333
  BorderColor #333
}

node "Chrome Browser" {
  [Popup.js\nFloating Bubble]
  [Dock.js\nRadial Menu]
  [Page.js\nTranslator]
  [Settings.js]
  [ChatSession.js]
  [TabContext.js]
  [Background Service Worker]
  database "chrome.storage.local" as storage
}

node "OpenClaw Gateway" {
  [HTTP Server]
  [Chat Completions API]
  [Models API]
}

database "LLM Provider" as llm

actor "User" as user
node "Web Page" as webpage

user -> webpage : browse
webpage -.-> [Popup.js] : select text
webpage -.-> [Dock.js] : long-press

[Popup.js] --> [Background Service Worker] : sendMessage
[Dock.js] --> [Background Service Worker] : sendMessage

[Background Service Worker] --> [HTTP Server] : HTTP POST /v1/chat

[HTTP Server] --> [Chat Completions API] : route
[Chat Completions API] --> [Models API] : route
[Models API] --> llm : API call

[Settings.js] --> storage : read/write
[ChatSession.js] --> storage : read/write
[TabContext.js] --> storage : read/write

note right of [Popup.js]
  1. User selects text
  2. Bubble shows actions
  3. User clicks action
  4. Background calls gateway
  5. Gateway calls LLM
  6. Response streamed back
end note

@enduml