chrome-extensions

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Chrome Extensions

Chrome扩展

Build production-quality Chrome extensions using Manifest V3 and publish them to the Chrome Web Store.
使用Manifest V3构建生产级Chrome扩展并发布到Chrome Web Store。

Part 1 — Building Extensions

第一部分 — 构建扩展

Mandatory Rules

强制规则

These address the most common causes of broken extensions. Violating any produces a non-functional build.
这些规则针对扩展失效的最常见原因。违反任何一条都会导致构建出无法正常运行的扩展。

1. Icons: only reference files you create — or omit icons entirely

1. 图标:仅引用你创建的文件 — 或完全省略图标

❌ BROKEN — referencing files that don't exist or reusing one file for all sizes:
   "icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }

✅ CORRECT — each size is a separate file at the correct pixel dimensions:
   "icons": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" }
   (where icon-16.png is 16×16px, icon-48.png is 48×48px, icon-128.png is 128×128px)

✅ ALSO CORRECT — omit icons from manifest if you cannot generate real PNG files:
   (just remove the "icons" and "default_icon" fields — Chrome uses a default icon)
If you include icon references, you MUST create the actual image files. Generate them with a script (see
references/extensions/icons.md
) or leave them out. Never reference non-existent files.
❌ 错误示例 — 引用不存在的文件或用单个文件适配所有尺寸:
   "icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" }

✅ 正确示例 — 每个尺寸对应单独的、像素规格正确的文件:
   "icons": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" }
   (其中icon-16.png为16×16px,icon-48.png为48×48px,icon-128.png为128×128px)

✅ 另一种正确示例 — 若无法生成真实PNG文件,可在manifest中省略图标:
   (只需移除"icons"和"default_icon"字段 — Chrome会使用默认图标)
如果包含图标引用,必须创建实际的图像文件。 可通过脚本生成(详见
references/extensions/icons.md
)或直接省略。绝对不要引用不存在的文件。

2. Side panel: you MUST provide a way to open it

2. 侧边栏:必须提供打开方式

Defining
"side_panel": {"default_path": "..."}
does NOT make it openable. Add a trigger:
js
// In service-worker.js — open side panel on extension icon click
// IMPORTANT: chrome.action.onClicked ONLY fires when there is NO default_popup
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.sidePanel.open({ windowId: tab.windowId });
});
If the extension has both a popup AND side panel, add a button in the popup that calls
chrome.sidePanel.open()
. Alternatively, use
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
— but the property is
openPanelOnActionClick
, NOT
openPanelOnActionIconClick
; the "Icon" variant causes a synchronous TypeError that silently aborts the service worker. Do NOT also define
default_popup
when using
setPanelBehavior
. See
references/extensions/side-panel.md
.
仅定义
"side_panel": {"default_path": "..."}
无法让侧边栏被打开。需添加触发逻辑:
js
// 在service-worker.js中 — 点击扩展图标时打开侧边栏
// 重要提示:chrome.action.onClicked仅在没有default_popup时才会触发
chrome.action.onClicked.addListener(async (tab) => {
  await chrome.sidePanel.open({ windowId: tab.windowId });
});
如果扩展同时拥有popup和侧边栏,需在popup中添加调用
chrome.sidePanel.open()
的按钮。或者使用
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
— 注意属性是
openPanelOnActionClick
,而非
openPanelOnActionIconClick
;带"Icon"的变体会导致同步TypeError,使service worker静默终止。使用
setPanelBehavior
时不要同时定义
default_popup
。详见
references/extensions/side-panel.md

3. Code execution: sandboxed iframes ONLY

3. 代码执行:仅允许沙箱iframe

Extension CSP blocks
eval()
,
new Function()
, inline
<script>
in all extension pages.
js
// ❌ BROKEN — direct iframe DOM access throws SecurityError
iframe.contentDocument.write(html);

// ❌ BROKEN — eval in extension page
eval(userCode); // CSP blocks this

// ✅ OPTION A: Sandbox in manifest + postMessage
// manifest.json: { "sandbox": { "pages": ["sandbox.html"] } }
iframe.contentWindow.postMessage({ html, css, js }, '*');
// sandbox.html receives and runs:
window.addEventListener('message', (e) => { eval(e.data.js); /* allowed in sandbox */ });

// ✅ OPTION B: Blob URL (creates separate origin, bypasses extension CSP)
iframe.src = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));

// ✅ OPTION C: srcdoc
iframe.srcdoc = `<style>${css}</style>${html}<script>${js}<\/script>`;
See
references/extensions/csp-sandbox.md
for full details.
扩展的CSP会阻止所有扩展页面中的
eval()
new Function()
和内联
<script>
js
// ❌ 错误示例 — 直接访问iframe DOM会抛出SecurityError
iframe.contentDocument.write(html);

// ❌ 错误示例 — 在扩展页面中使用eval
eval(userCode); // CSP会阻止此操作

// ✅ 选项A:Manifest中配置沙箱 + postMessage
// manifest.json: { "sandbox": { "pages": ["sandbox.html"] } }
iframe.contentWindow.postMessage({ html, css, js }, '*');
// sandbox.html接收并执行:
window.addEventListener('message', (e) => { eval(e.data.js); /* 沙箱中允许此操作 */ });

// ✅ 选项B:Blob URL(创建独立源,绕过扩展CSP)
iframe.src = URL.createObjectURL(new Blob([doc], { type: 'text/html' }));

// ✅ 选项C:srcdoc
iframe.srcdoc = `<style>${css}</style>${html}<script>${js}<\/script>`;
详见
references/extensions/csp-sandbox.md
获取完整说明。

4.
tab.url
requires the
tabs
permission

4.
tab.url
需要
tabs
权限

Without it,
tab.url
silently returns
undefined
— no error thrown.
js
// manifest.json — REQUIRED if you read tab.url or tab.title anywhere:
{ "permissions": ["tabs"] }
See
references/extensions/tab-management.md
.
没有该权限时,
tab.url
会静默返回
undefined
— 不会抛出错误。
js
// manifest.json — 若在任何地方读取tab.url或tab.title,必须添加:
{ "permissions": ["tabs"] }
详见
references/extensions/tab-management.md

5. Always use async/await — never
.then()
chains

5. 始终使用async/await — 绝不使用
.then()
链式调用

js
// ❌ BAD
chrome.tabs.query({active: true, currentWindow: true}).then(tabs => {
  chrome.scripting.executeScript({target: {tabId: tabs[0].id}, files: ['content.js']}).then(() => {});
});

// ✅ GOOD
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
For
runtime.onMessage
listeners that do async work:
js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  (async () => {
    const data = await chrome.storage.local.get('key');
    sendResponse({ data });
  })();
  return true; // keeps channel open
});
js
// ❌ 不推荐写法
chrome.tabs.query({active: true, currentWindow: true}).then(tabs => {
  chrome.scripting.executeScript({target: {tabId: tabs[0].id}, files: ['content.js']}).then(() => {});
});

// ✅ 推荐写法
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content.js'] });
对于处理异步任务的
runtime.onMessage
监听器:
js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  (async () => {
    const data = await chrome.storage.local.get('key');
    sendResponse({ data });
  })();
  return true; // 保持通信通道开放
});

6. Content scripts: don't block the main thread

6. 内容脚本:不要阻塞主线程

When modifying many DOM elements, batch with
requestAnimationFrame
and yield between batches:
js
async function highlightAll(elements) {
  const BATCH = 20;
  for (let i = 0; i < elements.length; i += BATCH) {
    await new Promise(r => requestAnimationFrame(() => {
      elements.slice(i, i + BATCH).forEach(el => el.style.backgroundColor = 'yellow');
      r();
    }));
    if (globalThis.scheduler?.yield) await scheduler.yield();
  }
}
See
references/extensions/content-scripts.md
.
修改大量DOM元素时,使用
requestAnimationFrame
分批处理,并在批次间让出主线程:
js
async function highlightAll(elements) {
  const BATCH = 20;
  for (let i = 0; i < elements.length; i += BATCH) {
    await new Promise(r => requestAnimationFrame(() => {
      elements.slice(i, i + BATCH).forEach(el => el.style.backgroundColor = 'yellow');
      r();
    }));
    if (globalThis.scheduler?.yield) await scheduler.yield();
  }
}
详见
references/extensions/content-scripts.md

7. Service workers are ephemeral — never store state in variables

7. Service worker是临时的 — 绝不在变量中存储状态

js
// ❌ BROKEN — state lost when SW terminates (~30s of inactivity)
let count = 0;
chrome.tabs.onUpdated.addListener(() => { count++; });

// ✅ CORRECT — persist in chrome.storage, read on every event
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
  if (changeInfo.status !== 'complete') return;
  const { count = 0 } = await chrome.storage.local.get('count');
  await chrome.storage.local.set({ count: count + 1 });
  await chrome.action.setBadgeText({ text: String(count + 1) });
});
Use
chrome.alarms
instead of
setTimeout
/
setInterval
. See
references/extensions/service-worker.md
.
js
// ❌ 错误示例 — SW终止(闲置约30秒)后状态丢失
let count = 0;
chrome.tabs.onUpdated.addListener(() => { count++; });

// ✅ 正确示例 — 持久化到chrome.storage,每次事件触发时读取
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo) => {
  if (changeInfo.status !== 'complete') return;
  const { count = 0 } = await chrome.storage.local.get('count');
  await chrome.storage.local.set({ count: count + 1 });
  await chrome.action.setBadgeText({ text: String(count + 1) });
});
使用
chrome.alarms
替代
setTimeout
/
setInterval
。详见
references/extensions/service-worker.md

8. chrome.identity: extension ID differs between dev and production

8. chrome.identity:开发环境与生产环境的扩展ID不同

When using Google sign-in, the OAuth client_id is tied to a specific extension ID. The ID changes between unpacked development and the Chrome Web Store.
To stabilize the ID during development, add a
"key"
field to manifest.json:
  1. Pack the extension once (chrome://extensions → Pack)
  2. Extract the public key from the .crx
  3. Add
    "key": "MIIBIjANBgkqh..."
    to manifest.json
Always document: "After publishing to the Chrome Web Store, update the OAuth client with the store-assigned extension ID." See
references/extensions/auth-identity.md
.
使用Google登录时,OAuth client_id与特定扩展ID绑定。未打包的开发环境和Chrome Web Store中的扩展ID不同。
为了在开发环境中稳定ID,需在manifest.json中添加
"key"
字段:
  1. 打包一次扩展(chrome://extensions → 打包扩展)
  2. 从.crx文件中提取公钥
  3. "key": "MIIBIjANBgkqh..."
    添加到manifest.json
务必记录:"发布到Chrome Web Store后,使用商店分配的扩展ID更新OAuth客户端。" 详见
references/extensions/auth-identity.md

9. Context menus: show user feedback after action

9. 上下文菜单:操作后向用户提供反馈

When a context menu item performs an action (save, copy, etc.), confirm it to the user. Use a notification, badge flash, or injected toast — don't let actions happen silently. See
references/extensions/context-menus.md
for a complete toast implementation.
当上下文菜单项执行操作(保存、复制等)时,需向用户确认。可使用通知、徽章闪烁或注入的提示框 — 不要让操作静默执行。详见
references/extensions/context-menus.md
获取完整的提示框实现。

10. Prompt API: available in service workers, popup, and side panel

10. Prompt API:可在service worker、popup和侧边栏中使用

The
LanguageModel
API works in all extension contexts — service worker, popup, and side panel — with no additional manifest permissions required. Extensions also get
LanguageModel.params()
, which is unavailable on the web:
js
const params = await LanguageModel.params();
// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 }
For general Prompt API patterns (availability checks, session creation, streaming), use the
modern-web-guidance
skill. See
references/extensions/prompt-api.md
for the extension-specific wiring example.
LanguageModel
API可在所有扩展上下文(service worker、popup、侧边栏)中使用,无需额外的manifest权限。扩展还能使用网页环境中不可用的
LanguageModel.params()
js
const params = await LanguageModel.params();
// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 }
关于通用Prompt API模式(可用性检查、会话创建、流式传输),使用
modern-web-guidance
技能。详见
references/extensions/prompt-api.md
获取扩展特定的接线示例。

11.
chrome.action
API requires
action
in manifest

11.
chrome.action
API需要在manifest中声明
action

Using
chrome.action.setBadgeText
,
chrome.action.setIcon
, or
chrome.action.onClicked
requires an
"action"
key in manifest.json — even if it's empty. Without it,
chrome.action
is
undefined
.
js
// ❌ BROKEN — manifest has no "action" key
await chrome.action.setBadgeText({ text: '5' });
// TypeError: Cannot read properties of undefined (reading 'setBadgeText')

// ✅ FIX — add "action" to manifest.json (at minimum an empty object)
{ "action": {} }
// or with a popup:
{ "action": { "default_popup": "popup/popup.html" } }
使用
chrome.action.setBadgeText
chrome.action.setIcon
chrome.action.onClicked
时,manifest.json中必须包含
"action"
键 — 即使是空对象。否则
chrome.action
会是
undefined
js
// ❌ 错误示例 — manifest中没有"action"键
await chrome.action.setBadgeText({ text: '5' });
// TypeError: Cannot read properties of undefined (reading 'setBadgeText')

// ✅ 修复方案 — 在manifest.json中添加"action"(至少是空对象)
{ "action": {} }
// 或带popup的写法:
{ "action": { "default_popup": "popup/popup.html" } }

12.
activeTab
only works on direct user gestures — not from side panels

12.
activeTab
仅在直接用户交互时生效 — 不适用于侧边栏

activeTab
grants temporary access to the current tab ONLY when triggered by:
  • Clicking the extension action icon
  • A context menu item
  • A keyboard shortcut from the
    commands
    API
  • Accepting an omnibox suggestion
It does NOT grant access when clicking a button in a side panel, popup button that opens later, or any programmatic trigger.
js
// ❌ BROKEN — activeTab does NOT work from a side panel button click
document.getElementById('summarize').addEventListener('click', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => document.body.innerText });
});

// ✅ FIX — use "tabs" permission + specific host_permissions instead
// manifest.json: { "permissions": ["tabs", "scripting"], "host_permissions": ["<all_urls>"] }
See
references/extensions/side-panel.md
.
activeTab
仅在以下触发场景下临时授予当前标签页访问权限:
  • 点击扩展操作图标
  • 上下文菜单项
  • commands
    API定义的键盘快捷键
  • 接受omnibox建议
当点击侧边栏中的按钮、后续打开的popup按钮或任何程序化触发时,不会授予访问权限。
js
// ❌ 错误示例 — activeTab不适用于侧边栏按钮点击
document.getElementById('summarize').addEventListener('click', async () => {
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => document.body.innerText });
});

// ✅ 修复方案 — 使用"tabs"权限 + 特定host_permissions替代
// manifest.json: { "permissions": ["tabs", "scripting"], "host_permissions": ["<all_urls>"] }
详见
references/extensions/side-panel.md

13. DevTools panel URLs are relative to the extension root

13. DevTools面板URL相对于扩展根目录

When creating a DevTools panel, the panel HTML path is relative to the extension root, NOT relative to the devtools page that calls
chrome.devtools.panels.create()
.
js
// ❌ BROKEN — path relative to devtools/ directory
chrome.devtools.panels.create("My Panel", "", "panel/panel.html");

// ✅ CORRECT — full path from extension root
chrome.devtools.panels.create("My Panel", "", "devtools/panel/panel.html");
See
references/extensions/devtools.md
.
创建DevTools面板时,面板HTML路径是相对于扩展根目录,而非调用
chrome.devtools.panels.create()
的devtools页面。
js
// ❌ 错误示例 — 路径相对于devtools/目录
chrome.devtools.panels.create("My Panel", "", "panel/panel.html");

// ✅ 正确示例 — 扩展根目录下的完整路径
chrome.devtools.panels.create("My Panel", "", "devtools/panel/panel.html");
详见
references/extensions/devtools.md

14. Offscreen documents have NO access to most chrome.* APIs

14. 离屏文档无法访问大多数chrome.* API

Offscreen documents (
chrome.offscreen
) are severely restricted. Most
chrome.*
APIs are unavailable, including
chrome.downloads
,
chrome.tabs
,
chrome.action
, and others.
js
// ❌ BROKEN — chrome.downloads is undefined in offscreen documents
chrome.downloads.download({ url, filename: 'recording.webm' }); // TypeError

// ❌ BROKEN — chrome.action is undefined in offscreen documents
chrome.action.setBadgeText({ text: 'REC' }); // TypeError
The only APIs available in offscreen documents are:
  • chrome.runtime.sendMessage
    /
    chrome.runtime.onMessage
  • chrome.runtime.getURL
  • Standard Web APIs (DOM, fetch, MediaRecorder, Canvas, Web Audio, etc.)
Rule of thumb: Offscreen documents do the Web API work (recording, parsing, audio). The service worker does all chrome.* API work (downloads, badge updates, notifications). Use
chrome.runtime.sendMessage
to bridge between them. See
references/extensions/message-passing.md
.
离屏文档(
chrome.offscreen
限制严格。大多数
chrome.*
API不可用,包括
chrome.downloads
chrome.tabs
chrome.action
等。
js
// ❌ 错误示例 — 离屏文档中chrome.downloads为undefined
chrome.downloads.download({ url, filename: 'recording.webm' }); // TypeError

// ❌ 错误示例 — 离屏文档中chrome.action为undefined
chrome.action.setBadgeText({ text: 'REC' }); // TypeError
离屏文档仅可使用以下API:
  • chrome.runtime.sendMessage
    /
    chrome.runtime.onMessage
  • chrome.runtime.getURL
  • 标准Web API(DOM、fetch、MediaRecorder、Canvas、Web Audio等)
经验法则: 离屏文档处理Web API相关工作(录制、解析、音频)。service worker处理所有chrome.* API工作(下载、徽章更新、通知)。使用
chrome.runtime.sendMessage
在两者间通信。详见
references/extensions/message-passing.md

15. Notifications and badge icons must reference real image files

15. 通知和徽章图标必须引用真实图像文件

chrome.notifications.create()
requires a valid
iconUrl
pointing to an actual image file. If the file doesn't exist or the path is wrong, the call fails with
"Unable to download all specified images."
js
// ❌ BROKEN — icon file doesn't exist
chrome.notifications.create('reminder', {
  type: 'basic',
  iconUrl: 'icons/icon-128.png', // File not in extension!
  title: 'Reminder',
  message: 'Time is up!'
});

// ✅ Generate a data URL at runtime via OffscreenCanvas — no file needed.
// See `references/extensions/icons.md` for a reusable implementation.
const iconUrl = await getIconDataUrl();
chrome.notifications.create('reminder', { type: 'basic', iconUrl, title: 'Reminder', message: 'Time is up!' });
This applies to ALL image references in chrome.* APIs — notifications,
chrome.action.setIcon
, context menu icons, etc. If you reference a file, it must exist.
chrome.notifications.create()
需要指向实际图像文件的有效
iconUrl
。如果文件不存在或路径错误,调用会失败并返回
"Unable to download all specified images."
js
// ❌ 错误示例 — 图标文件不存在
chrome.notifications.create('reminder', {
  type: 'basic',
  iconUrl: 'icons/icon-128.png', // 扩展中无此文件!
  title: 'Reminder',
  message: 'Time is up!'
});

// ✅ 运行时通过OffscreenCanvas生成data URL — 无需文件。
// 详见`references/extensions/icons.md`获取可复用实现。
const iconUrl = await getIconDataUrl();
chrome.notifications.create('reminder', { type: 'basic', iconUrl, title: 'Reminder', message: 'Time is up!' });
此规则适用于所有chrome.* API中的图像引用 — 通知、
chrome.action.setIcon
、上下文菜单图标等。如果引用文件,该文件必须存在。

16. Tab capture: guard against double-start with state locking

16. 标签页捕获:通过状态锁防止重复启动

chrome.tabCapture.getMediaStreamId()
fails with
"Cannot capture a tab with an active stream"
if called while a previous capture is still active. Fast double-clicks on the extension icon easily trigger this. Use explicit state locking:
js
// ❌ BROKEN — no guard against rapid clicks
let isRecording = false;
chrome.action.onClicked.addListener(async (tab) => {
  if (isRecording) { stopRecording(); isRecording = false; }
  else { isRecording = true; startRecording(tab); } // Second click = "active stream" error
});

// ✅ CORRECT — use transitional states to lock out concurrent operations
// State machine: 'idle' → 'starting' → 'recording' → 'stopping' → 'idle'
// Store state in chrome.storage.session (survives SW restart, cleared on browser close)
chrome.action.onClicked.addListener(async (tab) => {
  const { recordingState = 'idle' } = await chrome.storage.session.get('recordingState');

  if (recordingState === 'starting' || recordingState === 'stopping') return;

  if (recordingState === 'idle') {
    await chrome.storage.session.set({ recordingState: 'starting' });
    try {
      await startRecording(tab);
      await chrome.storage.session.set({ recordingState: 'recording' });
      await chrome.action.setBadgeText({ text: 'REC' });
      await chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });
    } catch (err) {
      console.error('Failed to start recording:', err);
      await chrome.storage.session.set({ recordingState: 'idle' });
    }
  } else if (recordingState === 'recording') {
    await chrome.storage.session.set({ recordingState: 'stopping' });
    try { await stopRecording(); }
    finally {
      await chrome.storage.session.set({ recordingState: 'idle' });
      await chrome.action.setBadgeText({ text: '' });
    }
  }
});
This pattern applies to any chrome API that manages exclusive resources:
chrome.tabCapture
,
chrome.desktopCapture
,
chrome.offscreen.createDocument
(only one offscreen document allowed at a time). See
references/extensions/media-capture.md
.
如果在已有捕获流活跃时调用
chrome.tabCapture.getMediaStreamId()
,会失败并返回
"Cannot capture a tab with an active stream"
。快速双击扩展图标很容易触发此问题。需使用显式状态锁:
js
// ❌ 错误示例 — 无防重复点击保护
let isRecording = false;
chrome.action.onClicked.addListener(async (tab) => {
  if (isRecording) { stopRecording(); isRecording = false; }
  else { isRecording = true; startRecording(tab); } // 第二次点击会触发"active stream"错误
});

// ✅ 正确示例 — 使用过渡状态锁定并发操作
// 状态机:'idle' → 'starting' → 'recording' → 'stopping' → 'idle'
// 将状态存储在chrome.storage.session(SW重启后仍保留,浏览器关闭时清除)
chrome.action.onClicked.addListener(async (tab) => {
  const { recordingState = 'idle' } = await chrome.storage.session.get('recordingState');

  if (recordingState === 'starting' || recordingState === 'stopping') return;

  if (recordingState === 'idle') {
    await chrome.storage.session.set({ recordingState: 'starting' });
    try {
      await startRecording(tab);
      await chrome.storage.session.set({ recordingState: 'recording' });
      await chrome.action.setBadgeText({ text: 'REC' });
      await chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });
    } catch (err) {
      console.error('Failed to start recording:', err);
      await chrome.storage.session.set({ recordingState: 'idle' });
    }
  } else if (recordingState === 'recording') {
    await chrome.storage.session.set({ recordingState: 'stopping' });
    try { await stopRecording(); }
    finally {
      await chrome.storage.session.set({ recordingState: 'idle' });
      await chrome.action.setBadgeText({ text: '' });
    }
  }
});
此模式适用于任何管理独占资源的Chrome API:
chrome.tabCapture
chrome.desktopCapture
chrome.offscreen.createDocument
(同一时间仅允许一个离屏文档)。详见
references/extensions/media-capture.md

17.
chrome.desktopCapture
requires a target tab with URL access

17.
chrome.desktopCapture
需要有权限访问URL的目标标签页

When calling
chrome.desktopCapture.chooseDesktopMedia()
from a service worker, you must pass the active tab as the
targetTab
parameter. The tab object must have its
url
field populated, which requires the
"tabs"
permission.
js
// ❌ BROKEN — called without targetTab from service worker
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], (streamId) => { ... });
// Error: "A target tab is required when called from a service worker context."

// ❌ BROKEN — tab doesn't have url field (missing "tabs" permission)
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => { ... });
// Error: "targetTab doesn't have URL field set."

// ✅ CORRECT — "tabs" permission in manifest + pass tab object
// manifest.json: { "permissions": ["tabs", "desktopCapture"] }
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => {
  if (!streamId) return; // User cancelled
});
Note: Prefer
chrome.tabCapture.getMediaStreamId()
for tab-only recording. Use
chrome.desktopCapture
only when the user should choose which screen/window to capture. See
references/extensions/media-capture.md
.
从service worker调用
chrome.desktopCapture.chooseDesktopMedia()
时,必须将活动标签页作为
targetTab
参数传递。标签对象必须包含
url
字段,这需要
"tabs"
权限。
js
// ❌ 错误示例 — 从service worker调用时未传递targetTab
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], (streamId) => { ... });
// 错误:"A target tab is required when called from a service worker context."

// ❌ 错误示例 — 标签无url字段(缺少"tabs"权限)
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => { ... });
// 错误:"targetTab doesn't have URL field set."

// ✅ 正确示例 — manifest中添加"tabs"权限 + 传递标签对象
// manifest.json: { "permissions": ["tabs", "desktopCapture"] }
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], tab, (streamId) => {
  if (!streamId) return; // 用户取消操作
});
注意: 仅录制标签页时优先使用
chrome.tabCapture.getMediaStreamId()
。仅当需要让用户选择录制哪个屏幕/窗口时,才使用
chrome.desktopCapture
。详见
references/extensions/media-capture.md

18.
chrome.windows
has NO
.query()
method — use
getAll
,
getLastFocused
, or
getCurrent

18.
chrome.windows
没有
.query()
方法 — 使用
getAll
getLastFocused
getCurrent

Unlike
chrome.tabs.query()
, the
chrome.windows
API does NOT have a
.query()
method.
js
// ❌ BROKEN — chrome.windows.query does not exist
const windows = await chrome.windows.query({ focused: true });
// TypeError: chrome.windows.query is not a function

// ✅ CORRECT — use the right method for your need
const focused = await chrome.windows.getLastFocused({ populate: true });
const current = await chrome.windows.getCurrent({ populate: true });
const all     = await chrome.windows.getAll({ populate: true });
chrome.windows
methods:
getAll
,
getLastFocused
,
getCurrent
,
get(windowId)
,
create
,
update
,
remove
. See
references/extensions/tab-management.md
.
chrome.tabs.query()
不同,
chrome.windows
API没有
.query()
方法。
js
// ❌ 错误示例 — chrome.windows.query不存在
const windows = await chrome.windows.query({ focused: true });
// TypeError: chrome.windows.query is not a function

// ✅ 正确示例 — 根据需求使用正确的方法
const focused = await chrome.windows.getLastFocused({ populate: true });
const current = await chrome.windows.getCurrent({ populate: true });
const all     = await chrome.windows.getAll({ populate: true });
chrome.windows
方法:
getAll
getLastFocused
getCurrent
get(windowId)
create
update
remove
。详见
references/extensions/tab-management.md

Always Manifest V3

始终使用Manifest V3

Never generate Manifest V2 code.
  • background.service_worker
    not
    background.scripts
  • chrome.action
    not
    chrome.browserAction
  • chrome.scripting.executeScript
    not
    chrome.tabs.executeScript
  • host_permissions
    is separate from
    permissions
  • No inline scripts in HTML — use
    <script src="file.js">
  • No inline event handlers — use
    addEventListener

绝不生成Manifest V2代码。
  • 使用
    background.service_worker
    而非
    background.scripts
  • 使用
    chrome.action
    而非
    chrome.browserAction
  • 使用
    chrome.scripting.executeScript
    而非
    chrome.tabs.executeScript
  • host_permissions
    permissions
    分开声明
  • HTML中禁止内联脚本 — 使用
    <script src="file.js">
  • 禁止内联事件处理器 — 使用
    addEventListener

Part 2 — Publishing to the Chrome Web Store

第二部分 — 发布到Chrome Web Store

Manage
CHROMEWEBSTORE.md
— the single source of truth for all Chrome Web Store listing metadata, permissions justifications, privacy disclosures, version history, and publishing readiness for a Chrome extension project.
管理
CHROMEWEBSTORE.md
— 这是Chrome扩展项目所有Chrome Web Store列表元数据、权限说明、隐私披露、版本历史和发布准备情况的唯一可信来源。

Core Workflow

核心工作流

Every time you touch a Chrome extension project in a way that affects its store presence, update (or create)
CHROMEWEBSTORE.md
in the project root. The file tracks everything the developer needs to fill out in the Chrome Developer Dashboard, so they can copy-paste from a single doc instead of scrambling at publish time.
每当你对Chrome扩展项目进行会影响其商店展示的修改时,更新(或创建)项目根目录下的
CHROMEWEBSTORE.md
。该文件记录了开发者在Chrome开发者控制台中需要填写的所有内容,这样他们在发布时只需从单个文档中复制粘贴,无需临时拼凑信息。

When to create CHROMEWEBSTORE.md

何时创建CHROMEWEBSTORE.md

Create it the moment any of these happen:
  • The user says they want to publish an extension
  • The user asks to "prepare for the store" or "get ready to publish"
  • You're building a new extension that will clearly end up on the store
  • The user asks about store listing requirements
Use the template in
references/webstore/chromewebstore-template.md
as your starting point. Read it before generating the file.
当以下任一情况发生时立即创建:
  • 用户表示想要发布扩展
  • 用户询问"为商店做准备"或"准备发布"
  • 你正在构建一个显然会发布到商店的新扩展
  • 用户询问商店列表要求
references/webstore/chromewebstore-template.md
中的模板为起点。生成文件前请先阅读该模板。

When to update CHROMEWEBSTORE.md

何时更新CHROMEWEBSTORE.md

Update it whenever:
  • User-facing changes: Bump the "Last Updated" date, update the feature list in descriptions, and add an entry to Version History
  • manifest.json changes: If permissions, host_permissions, or content_scripts changed, update the Permissions Justification section — every permission needs a plain-English reason the review team can understand
  • New release: Add a Version History entry with version number, date, and summary
  • Privacy-relevant changes: If data collection, storage, or transmission changed, update the Privacy & Data Use section and the privacy policy
  • Asset changes: If icons or UI changed, note which screenshots need refreshing
  • Rejection response: If the user reports a CWS rejection, update the file with the fix and add a note to Version History
在以下任一情况发生时更新:
  • 用户可见的变更:更新"最后更新"日期,修改描述中的功能列表,并在版本历史中添加条目
  • manifest.json变更:如果permissions、host_permissions或content_scripts发生变化,更新权限说明部分 — 每个权限都需要审核团队能理解的直白理由
  • 新版本发布:在版本历史中添加包含版本号、日期和摘要的条目
  • 隐私相关变更:如果数据收集、存储或传输方式发生变化,更新隐私与数据使用部分及隐私政策
  • 资源变更:如果图标或UI发生变化,记录需要更新哪些截图
  • 审核拒绝回应:如果用户反馈CWS审核拒绝,在文件中记录修复方案并在版本历史中添加说明

How to fill it out

如何填写

For each section, pull information from the actual project files:
  1. Read
    manifest.json
    to extract name, version, description, permissions, host_permissions
  2. Scan the codebase for data collection (storage, fetch calls, analytics)
  3. Check for icon files and their dimensions
  4. Look at the extension's UI to understand features for the description
Write store-facing copy in a tone that is specific, honest, and benefit-oriented. The Chrome Web Store review team rejects vague descriptions. "Makes your life easier" will be rejected. "Highlights search results on any webpage and lets you save highlights to a local list" will pass.
对于每个部分,从实际项目文件中提取信息:
  1. 读取
    manifest.json
    提取名称、版本、描述、permissions、host_permissions
  2. 扫描代码库查找数据收集相关逻辑(存储、fetch调用、分析)
  3. 检查图标文件及其尺寸
  4. 查看扩展UI以理解功能,用于撰写描述
撰写面向商店的文案时,要具体、诚实且突出价值。Chrome Web Store审核团队会拒绝模糊的描述。"让你的生活更轻松"会被拒绝。"在任何网页上高亮搜索结果,并允许你将高亮内容保存到本地列表"会通过审核。

CHROMEWEBSTORE.md Sections

CHROMEWEBSTORE.md章节

Read
references/webstore/chromewebstore-template.md
before generating the file — it defines what each section covers and how to fill it out. The highest-risk section is Permissions Justification: write a specific plain-English reason per permission and per host_permission. "Needed for the extension to work" will be rejected. Read
references/webstore/privacy-policy.md
for guidance on generating a privacy policy.
生成文件前请阅读
references/webstore/chromewebstore-template.md
— 该文件定义了每个章节的内容和填写方式。风险最高的章节是权限说明:为每个permission和host_permission撰写具体的直白理由。"需要让扩展正常工作"会被拒绝。阅读
references/webstore/privacy-policy.md
获取生成隐私政策的指导。

Pre-Publish Checklist

发布前检查清单

Before submission, run through
references/webstore/review-checklist.md
. The most common first-submission failures:
  • Every permission and host_permission must have a specific justification (not "needed to work")
  • Privacy policy URL must be live and match the data use disclosure form
  • At least 1 screenshot at 1280×800 or 640×400
  • ZIP must exclude
    .git/
    ,
    node_modules/
    ,
    .env
    ,
    CHROMEWEBSTORE.md
提交前,请对照
references/webstore/review-checklist.md
检查。首次提交最常见的失败原因:
  • 每个permission和host_permission都必须有具体理由(不能是"需要正常工作")
  • 隐私政策URL必须可用,且与数据使用披露表单一致
  • 至少有1张1280×800或640×400的截图
  • ZIP包必须排除
    .git/
    node_modules/
    .env
    CHROMEWEBSTORE.md

Store Listing Copy Guidelines

商店列表文案指南

For copy guidelines and common rejection reasons, see
references/webstore/store-listing.md
. Key rule: lead with function ("Highlights search terms on any webpage"), not feeling ("Enjoy searching again").

关于文案指南和常见拒绝原因,详见
references/webstore/store-listing.md
。核心规则:先讲功能("在任何网页上高亮搜索关键词"),再讲感受("重新享受搜索体验")。

Reference Files

参考文件

For detailed API patterns and publishing guidance, read the relevant file BEFORE writing code or content:
TopicReference
Side panels
references/extensions/side-panel.md
Content scripts & DOM
references/extensions/content-scripts.md
Popups
references/extensions/popup-ui.md
Service worker lifetime
references/extensions/service-worker.md
Code execution & CSP
references/extensions/csp-sandbox.md
API calls
references/extensions/api-calling.md
Declarative Net Request
references/extensions/declarative-net-request.md
Chrome Prompt API
references/extensions/prompt-api.md
DevTools panels
references/extensions/devtools.md
Authentication
references/extensions/auth-identity.md
Context menus
references/extensions/context-menus.md
Omnibox
references/extensions/omnibox.md
Storage
references/extensions/storage.md
Tab & window management
references/extensions/tab-management.md
Tab/desktop capture
references/extensions/media-capture.md
Message passing
references/extensions/message-passing.md
Icons
references/extensions/icons.md
CHROMEWEBSTORE.md template
references/webstore/chromewebstore-template.md
Privacy policy guidance
references/webstore/privacy-policy.md
Pre-publish review checklist
references/webstore/review-checklist.md
Store listing tips & rejections
references/webstore/store-listing.md
编写代码或内容前,请阅读相关参考文件获取详细的API模式和发布指导:
主题参考文件
侧边栏
references/extensions/side-panel.md
内容脚本与DOM
references/extensions/content-scripts.md
Popup
references/extensions/popup-ui.md
Service worker生命周期
references/extensions/service-worker.md
代码执行与CSP
references/extensions/csp-sandbox.md
API调用
references/extensions/api-calling.md
Declarative Net Request
references/extensions/declarative-net-request.md
Chrome Prompt API
references/extensions/prompt-api.md
DevTools面板
references/extensions/devtools.md
身份验证
references/extensions/auth-identity.md
上下文菜单
references/extensions/context-menus.md
Omnibox
references/extensions/omnibox.md
存储
references/extensions/storage.md
标签页与窗口管理
references/extensions/tab-management.md
标签页/桌面捕获
references/extensions/media-capture.md
消息传递
references/extensions/message-passing.md
图标
references/extensions/icons.md
CHROMEWEBSTORE.md模板
references/webstore/chromewebstore-template.md
隐私政策指导
references/webstore/privacy-policy.md
发布前审核检查清单
references/webstore/review-checklist.md
商店列表技巧与拒绝原因
references/webstore/store-listing.md

Output Checklist

输出检查清单

Verify EVERY item before delivering:
  • manifest_version: 3
    — no V2 APIs anywhere
  • All icon files referenced in manifest exist as real files with correct dimensions — or icons are omitted
  • Side panel has an explicit open trigger (not just a manifest declaration)
  • Code execution uses sandbox/blob/srcdoc — no
    eval()
    in extension pages
  • tabs
    permission declared if
    tab.url
    or
    tab.title
    is accessed
  • All code uses
    async
    /
    await
    — no
    .then()
    chains
  • Content scripts batch DOM updates with
    requestAnimationFrame
  • Service worker stores NO state in global variables — uses
    chrome.storage
  • No inline scripts or event handlers in HTML
  • Context menu actions show user confirmation
  • "action": {}
    (or more) present in manifest if using
    chrome.action.*
    APIs
  • If reading/scripting tabs from a side panel: use
    tabs
    +
    host_permissions
    (NOT
    activeTab
    )
  • DevTools panel paths in
    chrome.devtools.panels.create()
    are relative to extension root
  • Offscreen documents use ONLY
    chrome.runtime
    messaging — no
    chrome.downloads
    ,
    chrome.action
    , etc.
  • All image refs in
    chrome.notifications
    ,
    chrome.action.setIcon
    , etc. point to real files (or use data URLs)
  • Tab/desktop capture uses state locking to prevent double-start errors
  • chrome.desktopCapture.chooseDesktopMedia
    passes
    targetTab
    with
    tabs
    permission
  • chrome.windows
    calls use
    getAll
    /
    getLastFocused
    /
    getCurrent
    — NOT
    .query()
    (it doesn't exist)
  • sidePanel.setPanelBehavior
    uses
    openPanelOnActionClick
    — NOT
    openPanelOnActionIconClick
  • Error handling on all async operations
  • host_permissions
    scoped to specific domains (not
    <all_urls>
    unless needed)
  • return true
    in
    onMessage
    listeners with async responses
交付前请验证以下每一项:
  • manifest_version: 3
    — 绝对不能出现V2 API
  • manifest中引用的所有图标文件都真实存在且尺寸正确 — 或已省略图标
  • 侧边栏有明确的打开触发逻辑(不只是manifest声明)
  • 代码执行使用沙箱/blob/srcdoc — 扩展页面中无
    eval()
  • 若访问
    tab.url
    tab.title
    ,已声明
    tabs
    权限
  • 所有代码使用
    async
    /
    await
    — 无
    .then()
    链式调用
  • 内容脚本使用
    requestAnimationFrame
    分批更新DOM
  • Service worker未在全局变量中存储状态 — 使用
    chrome.storage
  • HTML中无内联脚本或事件处理器
  • 上下文菜单操作会向用户显示确认信息
  • 若使用
    chrome.action.*
    API,manifest中存在
    "action": {}
    (或更详细配置)
  • 若从侧边栏读取/脚本化标签页:使用
    tabs
    +
    host_permissions
    (而非
    activeTab
  • chrome.devtools.panels.create()
    中的DevTools面板路径相对于扩展根目录
  • 离屏文档仅使用
    chrome.runtime
    消息传递 — 无
    chrome.downloads
    chrome.action
  • chrome.notifications
    chrome.action.setIcon
    等中的所有图像引用都指向真实文件(或使用data URL)
  • 标签页/桌面捕获使用状态锁防止重复启动错误
  • chrome.desktopCapture.chooseDesktopMedia
    传递了
    targetTab
    且已声明
    tabs
    权限
  • chrome.windows
    调用使用
    getAll
    /
    getLastFocused
    /
    getCurrent
    — 不使用
    .query()
    (该方法不存在)
  • sidePanel.setPanelBehavior
    使用
    openPanelOnActionClick
    — 不使用
    openPanelOnActionIconClick
  • 所有异步操作都有错误处理
  • host_permissions
    限定为特定域名(除非必要,不使用
    <all_urls>
  • 异步响应的
    onMessage
    监听器中返回
    true