chrome-extensions
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseChrome 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 ) or leave them out. Never reference non-existent files.
references/extensions/icons.md❌ 错误示例 — 引用不存在的文件或用单个文件适配所有尺寸:
"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.md2. Side panel: you MUST provide a way to open it
2. 侧边栏:必须提供打开方式
Defining does NOT make it openable. Add a trigger:
"side_panel": {"default_path": "..."}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 . Alternatively, use — but the property is , NOT ; the "Icon" variant causes a synchronous TypeError that silently aborts the service worker. Do NOT also define when using . See .
chrome.sidePanel.open()chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })openPanelOnActionClickopenPanelOnActionIconClickdefault_popupsetPanelBehaviorreferences/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中添加调用的按钮。或者使用 — 注意属性是,而非;带"Icon"的变体会导致同步TypeError,使service worker静默终止。使用时不要同时定义。详见。
chrome.sidePanel.open()chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })openPanelOnActionClickopenPanelOnActionIconClicksetPanelBehaviordefault_popupreferences/extensions/side-panel.md3. Code execution: sandboxed iframes ONLY
3. 代码执行:仅允许沙箱iframe
Extension CSP blocks , , inline in all extension pages.
eval()new Function()<script>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 for full details.
references/extensions/csp-sandbox.md扩展的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.md4. tab.url
requires the tabs
permission
tab.urltabs4. tab.url
需要tabs
权限
tab.urltabsWithout it, silently returns — no error thrown.
tab.urlundefinedjs
// manifest.json — REQUIRED if you read tab.url or tab.title anywhere:
{ "permissions": ["tabs"] }See .
references/extensions/tab-management.md没有该权限时,会静默返回 — 不会抛出错误。
tab.urlundefinedjs
// manifest.json — 若在任何地方读取tab.url或tab.title,必须添加:
{ "permissions": ["tabs"] }详见。
references/extensions/tab-management.md5. Always use async/await — never .then()
chains
.then()5. 始终使用async/await — 绝不使用.then()
链式调用
.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 listeners that do async work:
runtime.onMessagejs
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.onMessagejs
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 and yield between batches:
requestAnimationFramejs
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元素时,使用分批处理,并在批次间让出主线程:
requestAnimationFramejs
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.md7. 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 instead of /. See .
chrome.alarmssetTimeoutsetIntervalreferences/extensions/service-worker.mdjs
// ❌ 错误示例 — 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.alarmssetTimeoutsetIntervalreferences/extensions/service-worker.md8. 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 field to manifest.json:
"key"- Pack the extension once (chrome://extensions → Pack)
- Extract the public key from the .crx
- Add to manifest.json
"key": "MIIBIjANBgkqh..."
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"- 打包一次扩展(chrome://extensions → 打包扩展)
- 从.crx文件中提取公钥
- 将添加到manifest.json
"key": "MIIBIjANBgkqh..."
务必记录:"发布到Chrome Web Store后,使用商店分配的扩展ID更新OAuth客户端。" 详见。
references/extensions/auth-identity.md9. 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 for a complete toast implementation.
references/extensions/context-menus.md当上下文菜单项执行操作(保存、复制等)时,需向用户确认。可使用通知、徽章闪烁或注入的提示框 — 不要让操作静默执行。详见获取完整的提示框实现。
references/extensions/context-menus.md10. Prompt API: available in service workers, popup, and side panel
10. Prompt API:可在service worker、popup和侧边栏中使用
The API works in all extension contexts — service worker, popup, and side panel — with no additional manifest permissions required. Extensions also get , which is unavailable on the web:
LanguageModelLanguageModel.params()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 skill. See for the extension-specific wiring example.
modern-web-guidancereferences/extensions/prompt-api.mdLanguageModelLanguageModel.params()js
const params = await LanguageModel.params();
// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 }关于通用Prompt API模式(可用性检查、会话创建、流式传输),使用技能。详见获取扩展特定的接线示例。
modern-web-guidancereferences/extensions/prompt-api.md11. chrome.action
API requires action
in manifest
chrome.actionaction11. chrome.action
API需要在manifest中声明action
chrome.actionactionUsing , , or requires
an key in manifest.json — even if it's empty. Without it, is .
chrome.action.setBadgeTextchrome.action.setIconchrome.action.onClicked"action"chrome.actionundefinedjs
// ❌ 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" } }使用、或时,manifest.json中必须包含键 — 即使是空对象。否则会是。
chrome.action.setBadgeTextchrome.action.setIconchrome.action.onClicked"action"chrome.actionundefinedjs
// ❌ 错误示例 — 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
activeTab12. activeTab
仅在直接用户交互时生效 — 不适用于侧边栏
activeTabactiveTab- Clicking the extension action icon
- A context menu item
- A keyboard shortcut from the API
commands - 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.mdactiveTab- 点击扩展操作图标
- 上下文菜单项
- API定义的键盘快捷键
commands - 接受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.md13. 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路径是相对于扩展根目录,而非调用的devtools页面。
chrome.devtools.panels.create()js
// ❌ 错误示例 — 路径相对于devtools/目录
chrome.devtools.panels.create("My Panel", "", "panel/panel.html");
// ✅ 正确示例 — 扩展根目录下的完整路径
chrome.devtools.panels.create("My Panel", "", "devtools/panel/panel.html");详见。
references/extensions/devtools.md14. Offscreen documents have NO access to most chrome.* APIs
14. 离屏文档无法访问大多数chrome.* API
Offscreen documents () are severely restricted. Most APIs
are unavailable, including , , , and others.
chrome.offscreenchrome.*chrome.downloadschrome.tabschrome.actionjs
// ❌ 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' }); // TypeErrorThe only APIs available in offscreen documents are:
- /
chrome.runtime.sendMessagechrome.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 to bridge between them. See .
chrome.runtime.sendMessagereferences/extensions/message-passing.md离屏文档()限制严格。大多数 API不可用,包括、、等。
chrome.offscreenchrome.*chrome.downloadschrome.tabschrome.actionjs
// ❌ 错误示例 — 离屏文档中chrome.downloads为undefined
chrome.downloads.download({ url, filename: 'recording.webm' }); // TypeError
// ❌ 错误示例 — 离屏文档中chrome.action为undefined
chrome.action.setBadgeText({ text: 'REC' }); // TypeError离屏文档仅可使用以下API:
- /
chrome.runtime.sendMessagechrome.runtime.onMessage chrome.runtime.getURL- 标准Web API(DOM、fetch、MediaRecorder、Canvas、Web Audio等)
经验法则: 离屏文档处理Web API相关工作(录制、解析、音频)。service worker处理所有chrome.* API工作(下载、徽章更新、通知)。使用在两者间通信。详见。
chrome.runtime.sendMessagereferences/extensions/message-passing.md15. Notifications and badge icons must reference real image files
15. 通知和徽章图标必须引用真实图像文件
chrome.notifications.create()iconUrl"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, ,
context menu icons, etc. If you reference a file, it must exist.
chrome.action.setIconchrome.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.setIcon16. Tab capture: guard against double-start with state locking
16. 标签页捕获:通过状态锁防止重复启动
chrome.tabCapture.getMediaStreamId()"Cannot capture a tab with an active stream"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:
, , (only one
offscreen document allowed at a time). See .
chrome.tabCapturechrome.desktopCapturechrome.offscreen.createDocumentreferences/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.tabCapturechrome.desktopCapturechrome.offscreen.createDocumentreferences/extensions/media-capture.md17. chrome.desktopCapture
requires a target tab with URL access
chrome.desktopCapture17. chrome.desktopCapture
需要有权限访问URL的目标标签页
chrome.desktopCaptureWhen calling from a service worker, you must pass
the active tab as the parameter. The tab object must have its field populated,
which requires the permission.
chrome.desktopCapture.chooseDesktopMedia()targetTaburl"tabs"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 for tab-only recording. Use only when the user should choose which screen/window to capture. See .
chrome.tabCapture.getMediaStreamId()chrome.desktopCapturereferences/extensions/media-capture.md从service worker调用时,必须将活动标签页作为参数传递。标签对象必须包含字段,这需要权限。
chrome.desktopCapture.chooseDesktopMedia()targetTaburl"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.desktopCapturereferences/extensions/media-capture.md18. chrome.windows
has NO .query()
method — use getAll
, getLastFocused
, or getCurrent
chrome.windows.query()getAllgetLastFocusedgetCurrent18. chrome.windows
没有.query()
方法 — 使用getAll
、getLastFocused
或getCurrent
chrome.windows.query()getAllgetLastFocusedgetCurrentUnlike , the API does NOT have a method.
chrome.tabs.query()chrome.windows.query()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.windowsgetAllgetLastFocusedgetCurrentget(windowId)createupdateremovereferences/extensions/tab-management.md与不同, API没有方法。
chrome.tabs.query()chrome.windows.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.windowsgetAllgetLastFocusedgetCurrentget(windowId)createupdateremovereferences/extensions/tab-management.mdAlways Manifest V3
始终使用Manifest V3
Never generate Manifest V2 code.
- not
background.service_workerbackground.scripts - not
chrome.actionchrome.browserAction - not
chrome.scripting.executeScriptchrome.tabs.executeScript - is separate from
host_permissionspermissions - No inline scripts in HTML — use
<script src="file.js"> - No inline event handlers — use
addEventListener
绝不生成Manifest V2代码。
- 使用而非
background.service_workerbackground.scripts - 使用而非
chrome.actionchrome.browserAction - 使用而非
chrome.scripting.executeScriptchrome.tabs.executeScript - 与
host_permissions分开声明permissions - HTML中禁止内联脚本 — 使用
<script src="file.js"> - 禁止内联事件处理器 — 使用
addEventListener
Part 2 — Publishing to the Chrome Web Store
第二部分 — 发布到Chrome Web Store
Manage — 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列表元数据、权限说明、隐私披露、版本历史和发布准备情况的唯一可信来源。
CHROMEWEBSTORE.mdCore Workflow
核心工作流
Every time you touch a Chrome extension project in a way that affects its store presence,
update (or create) 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.
CHROMEWEBSTORE.md每当你对Chrome扩展项目进行会影响其商店展示的修改时,更新(或创建)项目根目录下的。该文件记录了开发者在Chrome开发者控制台中需要填写的所有内容,这样他们在发布时只需从单个文档中复制粘贴,无需临时拼凑信息。
CHROMEWEBSTORE.mdWhen 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 as your starting point. Read it
before generating the file.
references/webstore/chromewebstore-template.md当以下任一情况发生时立即创建:
- 用户表示想要发布扩展
- 用户询问"为商店做准备"或"准备发布"
- 你正在构建一个显然会发布到商店的新扩展
- 用户询问商店列表要求
以中的模板为起点。生成文件前请先阅读该模板。
references/webstore/chromewebstore-template.mdWhen 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:
- Read to extract name, version, description, permissions, host_permissions
manifest.json - Scan the codebase for data collection (storage, fetch calls, analytics)
- Check for icon files and their dimensions
- 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.
对于每个部分,从实际项目文件中提取信息:
- 读取提取名称、版本、描述、permissions、host_permissions
manifest.json - 扫描代码库查找数据收集相关逻辑(存储、fetch调用、分析)
- 检查图标文件及其尺寸
- 查看扩展UI以理解功能,用于撰写描述
撰写面向商店的文案时,要具体、诚实且突出价值。Chrome Web Store审核团队会拒绝模糊的描述。"让你的生活更轻松"会被拒绝。"在任何网页上高亮搜索结果,并允许你将高亮内容保存到本地列表"会通过审核。
CHROMEWEBSTORE.md Sections
CHROMEWEBSTORE.md章节
Read 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
for guidance on generating a privacy policy.
references/webstore/chromewebstore-template.mdreferences/webstore/privacy-policy.md生成文件前请阅读 — 该文件定义了每个章节的内容和填写方式。风险最高的章节是权限说明:为每个permission和host_permission撰写具体的直白理由。"需要让扩展正常工作"会被拒绝。阅读获取生成隐私政策的指导。
references/webstore/chromewebstore-template.mdreferences/webstore/privacy-policy.mdPre-Publish Checklist
发布前检查清单
Before submission, run through . The most common
first-submission failures:
references/webstore/review-checklist.md- 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/,.envCHROMEWEBSTORE.md
提交前,请对照检查。首次提交最常见的失败原因:
references/webstore/review-checklist.md- 每个permission和host_permission都必须有具体理由(不能是"需要正常工作")
- 隐私政策URL必须可用,且与数据使用披露表单一致
- 至少有1张1280×800或640×400的截图
- ZIP包必须排除、
.git/、node_modules/、.envCHROMEWEBSTORE.md
Store Listing Copy Guidelines
商店列表文案指南
For copy guidelines and common rejection reasons, see .
Key rule: lead with function ("Highlights search terms on any webpage"), not feeling ("Enjoy
searching again").
references/webstore/store-listing.md关于文案指南和常见拒绝原因,详见。核心规则:先讲功能("在任何网页上高亮搜索关键词"),再讲感受("重新享受搜索体验")。
references/webstore/store-listing.mdReference Files
参考文件
For detailed API patterns and publishing guidance, read the relevant file BEFORE writing code or content:
| Topic | Reference |
|---|---|
| Side panels | |
| Content scripts & DOM | |
| Popups | |
| Service worker lifetime | |
| Code execution & CSP | |
| API calls | |
| Declarative Net Request | |
| Chrome Prompt API | |
| DevTools panels | |
| Authentication | |
| Context menus | |
| Omnibox | |
| Storage | |
| Tab & window management | |
| Tab/desktop capture | |
| Message passing | |
| Icons | |
| CHROMEWEBSTORE.md template | |
| Privacy policy guidance | |
| Pre-publish review checklist | |
| Store listing tips & rejections | |
编写代码或内容前,请阅读相关参考文件获取详细的API模式和发布指导:
| 主题 | 参考文件 |
|---|---|
| 侧边栏 | |
| 内容脚本与DOM | |
| Popup | |
| Service worker生命周期 | |
| 代码执行与CSP | |
| API调用 | |
| Declarative Net Request | |
| Chrome Prompt API | |
| DevTools面板 | |
| 身份验证 | |
| 上下文菜单 | |
| Omnibox | |
| 存储 | |
| 标签页与窗口管理 | |
| 标签页/桌面捕获 | |
| 消息传递 | |
| 图标 | |
| CHROMEWEBSTORE.md模板 | |
| 隐私政策指导 | |
| 发布前审核检查清单 | |
| 商店列表技巧与拒绝原因 | |
Output Checklist
输出检查清单
Verify EVERY item before delivering:
- — no V2 APIs anywhere
manifest_version: 3 - 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 in extension pages
eval() - permission declared if
tabsortab.urlis accessedtab.title - All code uses /
async— noawaitchains.then() - 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
- (or more) present in manifest if using
"action": {}APIschrome.action.* - If reading/scripting tabs from a side panel: use +
tabs(NOThost_permissions)activeTab - DevTools panel paths in are relative to extension root
chrome.devtools.panels.create() - Offscreen documents use ONLY messaging — no
chrome.runtime,chrome.downloads, etc.chrome.action - All image refs in ,
chrome.notifications, etc. point to real files (or use data URLs)chrome.action.setIcon - Tab/desktop capture uses state locking to prevent double-start errors
- passes
chrome.desktopCapture.chooseDesktopMediawithtargetTabpermissiontabs - calls use
chrome.windows/getAll/getLastFocused— NOTgetCurrent(it doesn't exist).query() - uses
sidePanel.setPanelBehavior— NOTopenPanelOnActionClickopenPanelOnActionIconClick - Error handling on all async operations
- scoped to specific domains (not
host_permissionsunless needed)<all_urls> - in
return truelisteners with async responsesonMessage
交付前请验证以下每一项:
- — 绝对不能出现V2 API
manifest_version: 3 - manifest中引用的所有图标文件都真实存在且尺寸正确 — 或已省略图标
- 侧边栏有明确的打开触发逻辑(不只是manifest声明)
- 代码执行使用沙箱/blob/srcdoc — 扩展页面中无
eval() - 若访问或
tab.url,已声明tab.title权限tabs - 所有代码使用/
async— 无await链式调用.then() - 内容脚本使用分批更新DOM
requestAnimationFrame - Service worker未在全局变量中存储状态 — 使用
chrome.storage - HTML中无内联脚本或事件处理器
- 上下文菜单操作会向用户显示确认信息
- 若使用API,manifest中存在
chrome.action.*(或更详细配置)"action": {} - 若从侧边栏读取/脚本化标签页:使用+
tabs(而非host_permissions)activeTab - 中的DevTools面板路径相对于扩展根目录
chrome.devtools.panels.create() - 离屏文档仅使用消息传递 — 无
chrome.runtime、chrome.downloads等chrome.action - 、
chrome.notifications等中的所有图像引用都指向真实文件(或使用data URL)chrome.action.setIcon - 标签页/桌面捕获使用状态锁防止重复启动错误
- 传递了
chrome.desktopCapture.chooseDesktopMedia且已声明targetTab权限tabs - 调用使用
chrome.windows/getAll/getLastFocused— 不使用getCurrent(该方法不存在).query() - 使用
sidePanel.setPanelBehavior— 不使用openPanelOnActionClickopenPanelOnActionIconClick - 所有异步操作都有错误处理
- 限定为特定域名(除非必要,不使用
host_permissions)<all_urls> - 异步响应的监听器中返回
onMessagetrue