Chrome Extension debugger 与 CDP 的能力边界
Chrome Extension 里有一个看起来很少用、但能力很强的权限:debugger。
它的作用可以理解为:让扩展通过 Chrome DevTools Protocol(CDP)连接到浏览器调试后端。扩展一旦拿到这个权限,就能对目标标签页发送一部分 DevTools 协议命令。
这类能力平时不应该随手用。它会触发明显的权限警告,调用时浏览器还会出现「某某扩展 started debugging this browser」一类提示。但在一些普通内容脚本做不到的场景里,它反而是更接近问题本质的工具。
这篇文章想讲清楚四件事:
chrome.debugger到底是什么。- 它能做哪些普通脚本做不到的事。
- 为什么飞书 canvas 表格复制这种场景,最后会落到 CDP。
- 真要使用时,应该怎么把风险压到最低。
chrome.debugger 是 CDP 的扩展入口
Chrome 官方文档 对 chrome.debugger 的定位很直接:它是 Chrome 远程调试协议的另一种传输方式。扩展可以 attach 到一个或多个标签页,用它观察网络交互、调试 JavaScript、修改 DOM 和 CSS,并通过 sendCommand 向目标发送 CDP 命令。
CDP 本身是 Chrome DevTools Protocol,DevTools、Puppeteer、Playwright 这类工具背后也会用到类似的浏览器调试协议。它把浏览器能力拆成一个个 domain,例如 Input、Runtime、DOM、CSS、Network、Page、Emulation。这些 domain 下文会单独说明,这里先把它们理解成「按浏览器能力分组的协议命名空间」即可。
CDP domain 可以理解成 DevTools 协议里的能力分组。比如
Input处理输入事件,Network处理网络观测,Runtime处理页面 JavaScript 运行时。调用时通常写成Domain.method,例如Input.dispatchKeyEvent。
普通扩展代码通常站在 Web 页面或扩展 API 这一层工作:
1 | |
debugger 则多接近一层浏览器调试后端:
1 | |
这个差异决定了它既有用,也危险。
- Chrome
debuggerAPI 说明了chrome.debugger的定位、权限声明、target、attach / detach / sendCommand 这些基础 API。- Chrome DevTools Protocol 是 CDP 的官方协议入口,能看到每个 domain 的方法和参数。
它开放的是受限 DevTools 能力
为了安全,chrome.debugger 并不开放全部 CDP domain。Chrome 官方文档 单独列了扩展可以访问的 domain,包括 Accessibility、Audits、CacheStorage、Console、CSS、Database、Debugger、DOM、DOMDebugger、DOMSnapshot、Emulation、Fetch、IO、Input、Inspector、Log、Network、Overlay、Page、Performance、Profiler、Runtime、Storage、Target、Tracing、WebAudio、WebAuthn 等。
只看名字就能感受到它的覆盖面:输入、运行时、DOM、CSS、网络、页面、环境模拟、性能追踪、存储缓存都在里面。
实际评估时,可以按几类能力理解。本节里的 CDP 代码都只保留关键调用形状,省略了 attach、detach、callback 返回值读取和错误处理;完整调用方式放在后面的「调用方式」一节。
输入和用户动作
Input.* 负责模拟键盘、鼠标、触摸等输入。典型关键词是:真实快捷键、复制、粘贴、鼠标、拖拽、触摸、canvas、复杂编辑器、虚拟表格、非标准选区。
这类能力和普通脚本派发事件的差别很大。普通脚本通常会写:
1 | |
或者:
1 | |
问题在于,这些操作仍然发生在页面脚本层。根据 MDN 对 Event.isTrusted 的说明,通过 dispatchEvent() 派发的事件会被标记为非可信事件。复杂 Web 应用完全可以不把这类合成事件当成真实用户输入。
CDP 的 Input.dispatchKeyEvent 走的是浏览器调试层的输入命令。它支持 modifiers,其中 Meta/Command 是 4,Ctrl 是 2;也支持随键盘事件发送编辑命令,例如 copy、selectAll 这一类和编辑器相关的命令。
迁移到 debugger 后,写法变成向浏览器输入层发送命令:
1 | |
页面运行时
Runtime.*、Console.*、Debugger.* 负责页面 JavaScript 运行时、Console、断点和执行上下文。
这类能力适合处理:
- 要在页面上下文里执行一段表达式。
- 要读取复杂框架运行态。
- 要监听 Console 输出或异常。
- 要判断一个跨 iframe / worker 的执行上下文在哪里。
普通 chrome.scripting.executeScript 也能往页面注入脚本,但 CDP 的运行时能力更接近 DevTools Console。能力越接近 DevTools,越要注意边界:不能拿它去读取和任务无关的敏感数据,也不要把它当成绕过页面正常权限和产品边界的工具。
普通扩展 API 更像「注入一段函数到页面」:
1 | |
CDP 的 Runtime.evaluate 更像 DevTools Console 里的表达式执行:
1 | |
DOM、CSS 和布局
DOM.*、CSS.*、DOMSnapshot.*、Overlay.* 对应 DevTools Elements 面板附近的能力。它们适合用来检查 DOM 树、样式规则、节点快照、布局盒模型、元素高亮和渲染定位。
如果只是 document.querySelector() 能解决的问题,没有必要升级到 debugger。但如果问题发生在 Shadow DOM、复杂 iframe、虚拟渲染、样式来源难以定位,或者要复用 DevTools 的节点高亮和快照能力,CDP 可以作为候选方案。
普通脚本通常直接读 DOM:
1 | |
CDP 可以先拿 DOM 树,再用 node id 继续查询样式、盒模型或高亮节点:
1 | |
网络和接口
Network.*、Fetch.*、IO.* 对应网络观察、请求拦截、响应读取和流式内容处理。
它们适合处理:
- 观察页面实际发出的请求。
- 查看 headers、status、response、preflight、缓存状态。
- 在调试工具里对接口做 mock 或拦截。
- 下载或读取 DevTools 网络层看到的内容。
Chrome Extension 本身也有 declarativeNetRequest、webRequest 等网络相关 API。选择时要看需求目标:如果是长期、稳定、面向发布的网络规则,优先考虑标准扩展 API;如果是内部调试、临时观测、需要贴近 DevTools 的 Network 视角,CDP 更直接。
这部分能力很强,使用边界也要更严。不要长期监听无关站点,不要采集用户敏感流量,不要把内部调试工具做成默认后台监控。
普通扩展能力更适合声明式规则或稳定拦截:
1 | |
CDP 更接近 DevTools Network。它可以打开网络观测,再从 chrome.debugger.onEvent 里接收网络事件:
1 | |
页面控制、target 和 frame
Page.*、Target.* 负责页面导航、reload、生命周期、frame tree、截图、PDF、target 管理等能力。
Chrome debugger 文档 提到,一个 tab 里可能有多个 execution context,跨进程 iframe 还可能成为新的 target。Chrome 125 以后,chrome.debugger 支持 flat sessions,可以在同一个调试会话里定位子 target。
这意味着:如果问题发生在 iframe、worker、跨域嵌套页面里,不能只问「我 attach 了 tab 吗」,还要问「命令发给了哪个 target / session」。
普通标签页控制一般用 chrome.tabs:
1 | |
CDP 的 Page.* 适合和页面生命周期、截图或 target 状态放在一起处理:
1 | |
环境模拟
Emulation.* 和部分 Network.* 能模拟 viewport、设备特征、触摸能力、UA、地理位置、时区、语言、网络条件等。
这类能力适合排查「只有某个 WebView / 移动设备 / 特定语言 / 特定网络环境才复现」的问题。它和普通响应式调试不同,因为它能更接近 DevTools 设备模拟层,影响范围也不止 CSS 宽度。
普通页面调试经常只改视口宽度:
1 | |
CDP 可以把设备指标、触摸能力、时区和网络条件放在同一套自动化里:
1 | |
存储、缓存和性能
Storage.*、CacheStorage.*、Performance.*、Profiler.*、Tracing.* 可以用于缓存、存储、性能指标、CPU profile、trace 等调试任务。
这里要区分两种需求:
- 如果只是给本地开发页写一段明确的
localStorage,普通chrome.scripting.executeScript更合适。 - 如果要理解页面缓存、Service Worker、CacheStorage、运行性能和 trace,CDP 才更接近问题发生的位置。
普通 localStorage 写入不需要上 CDP:
1 | |
CDP 更适合处理缓存和性能视角的问题:
1 | |
它也有明确做不到的事
debugger 很强,但它不是一个「浏览器万能后门」。从设计上看,它仍然是扩展连接某个调试目标(target)后,再向这个目标发送 CDP 命令。这个边界在写内部工具时很容易被忽略,尤其是目标页、localStorage 和跨浏览器这几类需求。
不能脱离 target 操作一个不存在的页面
Chrome debugger 文档 里对 target 的定义是:正在被调试的对象,可以是 tab、iframe 或 worker。扩展调用 sendCommand 时,也需要通过 tabId、targetId 或 sessionId 指向某个调试会话。
这意味着,debugger 不是一个可以凭 URL 直接操作任意网页状态的 API。它需要一个实际存在、可以 attach 的浏览器目标。比如要操作 http://localhost:8012,至少要有一个属于这个 origin 的页面或相关 target 存在;否则没有页面上下文,也没有可以执行 JavaScript 或接收 CDP 命令的地方。
1 | |
如果产品目标是「用户不需要手动打开页面」,更现实的做法不是上 debugger,而是让扩展自己创建一个目标页,例如用 chrome.tabs.create({ url: 'http://localhost:8012', active: false }) 打开一个非激活标签,等页面加载后再用 chrome.scripting.executeScript 或 CDP 写入。这个方案本质上仍然创建了页面,只是把「用户手动打开」变成「工具自动打开」。
不能直接写到另一个浏览器或手机 WebView
Chrome Extension 运行在哪个浏览器配置里,它能访问的 tab 和调试目标就属于哪个浏览器配置。用户如果在另一个 Chrome Profile、Safari、手机浏览器、App WebView 或 vConsole 里打开了目标页,当前桌面 Chrome 扩展不能跨过去 attach,也不能直接写入那边的 localStorage。
这种场景的推荐处理方式,是把「写入计划」生成成一段自包含控制台脚本,让用户复制到目标环境执行。脚本运行在哪个页面,就写入哪个页面自己的 localStorage。
1 | |
这条旁路听起来没有自动化那么漂亮,但边界非常清楚:扩展负责从飞书表格生成正确数据,目标浏览器负责在自己的页面上下文里执行写入。对手机 vConsole、App WebView、非当前浏览器这类环境来说,这比伪装成「扩展已经写过去了」更可靠。
Storage.* 不等于随便 set localStorage
CDP 里确实有一个 DOMStorage domain,它提供 DOMStorage.setDOMStorageItem 这类方法,用于查询和修改 DOM storage。不过,Chrome Extension 的 chrome.debugger 不是完整 CDP 通道。Chrome 官方列出的可用 domain 里有 Storage,但没有列出 DOMStorage。
这两个 domain 也不是一回事。Storage.* 更偏向站点存储管理、配额、CacheStorage / IndexedDB 追踪和清理,例如 Storage.clearDataForOrigin 可以按 origin 清理某些类型的存储;DOMStorage.* 才是直接面向 localStorage / sessionStorage key-value 的协议。
所以,在 Chrome Extension 里遇到「给当前打开的本地页写一段 localStorage」这种需求,最简单、最稳的方案仍然是注入页面上下文执行普通 Web API:
1 | |
只有当需求已经落到浏览器调试层,例如要观察缓存、清理多类站点存储、追踪 CacheStorage / IndexedDB 变化,或者和 Network / Runtime / Page 调试一起联动时,Storage.* 才更有价值。
localStorage 的边界仍然是 origin
localStorage 本身也不是全浏览器共享空间。根据 MDN 对 localStorage 的说明,它的数据和 document 的协议相关;http://example.com 和 https://example.com 会拿到不同的存储对象。更完整地说,origin 通常由 scheme、host 和 port 共同决定,所以 http://localhost:8012、http://127.0.0.1:8012、https://localhost:8012 是不同的存储边界。
这件事和 debugger 无关。即使用 CDP 或 Runtime.evaluate 执行 localStorage.setItem(),写入的也只是当前执行上下文所属 origin 的 localStorage。如果工具 UI 上写着「写入 8012」,就应该提前告诉用户具体会写入哪个 8012 页面;如果打开了多个候选页,也应该把候选页列出来,避免用户以为写到了手机或另一个浏览器。
用户可以中断调试会话
debugger 还不是一个可以稳定长期占用的连接。官方类型里,onDetach 的 reason 至少包括 target_closed 和 canceled_by_user。用户关闭目标 tab、打开 DevTools、关闭浏览器提示条,或者浏览器因为其他原因结束调试会话,扩展都要能恢复状态。
1 | |
写工具时要把它当成一次短暂动作,而不是长期连接。attach 后立刻完成必要命令,随后 detach;UI 上也不要假设调试会话永远还在。
一个真实案例:为什么飞书 canvas 表格复制要用 CDP
这次遇到的问题是:希望 Chrome Extension 自动读取飞书翻译表格,把表格里的多语言 key、tag 和文案写入本地开发页的 localStorage。
开始的直觉是走普通脚本:
- 找到飞书页面里的 canvas。
- 模拟点击左上角交叉格,让表格进入全选状态。
- 调用
document.execCommand('copy')或派发copy事件。 - 从剪贴板读取 TSV。
问题出在第 3 步。页面看起来已经选中了表格,但剪贴板没有变。调试日志能看到 execCommand('copy') 返回成功,也能看到点击目标是 canvas,但最终剪贴板仍然是旧内容。
这说明「命令被浏览器接受」和「飞书表格真的把选区写进剪贴板」是两件事。
飞书表格主体是 canvas,表格单元格、选区、复制内容都不一定是标准 DOM Selection。它很可能有自己的一套内部状态和复制管线。脚本合成的 click、keydown、copy 事件能让 UI 看起来动了,但不一定能触发内部复制逻辑。
用 CDP 后,流程变成:
- 扩展 attach 到当前飞书 tab。
- 先用普通脚本点击 canvas 左上角,让表格进入全选状态。
- 通过
Input.dispatchKeyEvent发送Command + C和copy编辑命令。 - 立刻 detach。
- 再读取剪贴板。
这次能成功,关键在于 CDP 触发了更接近真实用户快捷键的浏览器输入链路,让飞书自己的复制管线把 TSV 写进剪贴板。扩展本身没有直接读取飞书表格内部数据。
这个区别很重要。合理使用 debugger 的方向,通常是让页面按真实交互路径自己完成动作;直接绕开业务逻辑读取内部数据,会让工具边界变得危险。
关键代码可以拆成两段。第一段仍然用普通脚本完成页面内定位和点击,因为「选中哪个表格」属于页面内动作:
1 | |
第二段用 CDP 发复制快捷键,因为「让飞书把当前表格选区写进剪贴板」依赖浏览器输入层:
1 | |
这段代码只保留了关键动作,真实实现还需要补 chrome.runtime.lastError 检查、keyUp、剪贴板前后对比和 finally 式 detach。它的重点是说明两层分工:页面内能做的仍用普通脚本,普通脚本触发不了的浏览器输入链路再交给 CDP。
调用方式
扩展要使用 chrome.debugger,首先要在 manifest 里声明权限:
1 | |
最原始的调用方式是三步:attach 目标、sendCommand、detach。下面用原生 callback 写法展示完整结构,读者可以直接对照 Chrome API 文档。
1 | |
发送复制快捷键时,macOS 下 Meta/Command 对应 modifiers: 4;Windows / Linux 下通常是 Ctrl,对应 modifiers: 2。
Chrome 的扩展 API 很多仍然是 callback 风格。正式业务里把
attach、sendCommand、detach包成 Promise 会更好,因为可以用try/finally保证 detach 一定执行。封装是工程质量问题,不是理解debugger的前置条件。
1 | |
如果目标 tab 已经被 DevTools 或另一个调试会话占用,attach 可能失败。chrome.debugger.onDetach 也需要关注:官方文档说明,当 tab 关闭或 DevTools 被打开时,调试会话会被浏览器结束。
1 | |
无论使用 callback 还是 Promise,detach 都很关键。复制成功、复制失败、中途抛错,最后都要尽快结束调试会话。
浏览器会出现「正在调试」提示
调用 chrome.debugger.attach 后,Chrome / Chromium 会出现一条浏览器级提示,英文环境里常见文案是:
1 | |
这个提示来自浏览器本身,用来提醒用户发生过调试会话。Chromium 源码里的 IDS_DEV_TOOLS_INFOBAR_LABEL 对应这条文案,并且注释写明:即使 debugger 已经 detach,提示也不会自动消失,直到用户手动关闭;文案不应该暗示调试一定仍在进行,只表达它曾经发生过、也可能仍然存在。
源码里核心形状大概是这样:
1 | |
- Chromium 源码位置:chrome/app/generated_resources.grd,可搜索
IDS_DEV_TOOLS_INFOBAR_LABEL。
这条提示会影响用户感知。一个面向普通用户的公开扩展,如果频繁出现「正在调试此浏览器」,会很难解释清楚。内部开发工具、测试工具、调试工具更适合接受这类提示;如果要给普通用户使用,就必须在产品设计里提前解释为什么需要它。
权限警告也很重
Chrome 权限列表 里,debugger 对应的警告包括访问页面 debugger backend,以及读取和更改所有网站上的所有数据。它属于重权限。
Chrome 权限警告指南 也给出通用建议:权限应该和扩展的单一用途相关;能用可选权限就尽量在运行时申请;能用 activeTab 这类更轻权限就不要申请更重权限。
放到 debugger 上,可以得到几个判断:
- 如果普通 DOM、
chrome.scripting、tabs、storage就能解决,不要用debugger。 - 如果只是给本地开发页写
localStorage,不要用debugger。 - 如果是 canvas 表格、复杂编辑器、虚拟列表、非标准选区、复制/输入强依赖原生交互,应该尽早把 CDP 列为候选方案。
- 如果已经出现「UI 看起来选中,但剪贴板没有变化」「合成事件派发了,但页面内部逻辑没有触发」这类信号,可以先做最小 CDP 验证,不必在
execCommand、DOM Selection 和合成事件里反复试错。
最佳实践
这部分可以参考三类官方资料一起看:权限警告指南 关注用户安装和运行时看到什么,activeTab 文档 关注「用户触发后临时访问当前 tab」这类权限模型,Chrome Web Store Program Policies 则要求权限和用户数据使用与扩展功能匹配,并尽量请求实现功能所需的最窄权限。
只在用户明确触发时 attach
不要在后台静默 attach,也不要打开浏览器就 attach。最好是用户点了某个按钮,扩展明确知道这一次要处理哪个 tab,再短时间 attach。
这次飞书表格读取就是一个比较好的形态:用户打开飞书翻译表,点击 Popup 里的「读取当前表格」,扩展只处理当前飞书 tab。
如果功能可以设计成点击按钮后才生效,就不要把它做成后台自动监听。activeTab 文档 里强调,临时访问当前 tab 需要来自用户调用扩展的动作;这个思路同样适合 debugger 这种高权限能力:让用户动作成为清晰边界。
只 attach 目标 tab
业务代码应该先确认目标 tab 的 URL、host 和任务类型。比如只允许 feishu.cn / larksuite.com 的文档页面触发读取,不要对当前任意页面发送调试命令。
如果涉及 iframe、worker 或跨进程 frame,再根据 CDP target 机制扩展;不要一开始就把逻辑写成宽泛的全页面控制器。
实现上可以先做 allowlist:
1 | |
这类检查不只是安全防线,也能减少误操作。用户如果在本地开发页、邮箱页或其他无关页面打开 Popup,工具应该提示「请先切到飞书文档标签页」,而不是继续 attach。
只发送必要命令
为了复制表格,只需要 Input.dispatchKeyEvent。不要顺手打开 Network、Runtime、Storage,更不要为了日志把页面状态、请求响应、存储内容全读出来。
CDP 的能力边界应该按任务收窄,不能按它能做什么无限展开。
一个比较好的实现原则是:每个封装函数只对应一类 CDP 能力。例如 dispatchCopyByDebugger 只发送 Input.dispatchKeyEvent;如果另一个需求要读 Network,就单独写 observeNetworkResponse,不要把两类能力揉进一个万能 debugger helper。
立刻 detach
attach 后一定用 try / finally 包住,并在 finally 里 detach。
1 | |
这样即使中间报错,也不会让调试会话继续挂在浏览器上。
同时监听 chrome.debugger.onDetach。如果用户打开 DevTools、关闭 tab,或者浏览器主动结束调试会话,工具需要把内部状态切回未调试状态,不能让 UI 继续显示「处理中」。
1 | |
把调试协议封装在独立模块
业务代码不应该到处直接写 chrome.debugger.sendCommand。更好的方式是封装成很窄的函数,例如:
dispatchCopyByDebugger(tabId)captureCurrentPageByDebugger(tabId)readNetworkResponseByDebugger(tabId, matcher)
每个函数都应该说明它 attach 哪个 target、发送哪些 command、读取哪些数据、什么时候 detach。调用方只表达业务意图,不直接接触大权限 API。
封装层还应该统一处理 chrome.runtime.lastError。debugger 调用失败时,错误原因通常很关键:可能是权限没授予、target 不存在、tab 已经被其他 DevTools 会话占用,也可能是目标页面不允许当前操作。不要让这些错误静默失败。
权限声明要能解释给用户听
debugger 会带来明显权限警告。公开扩展如果需要它,商店介绍、隐私说明和产品 UI 里都应该解释清楚它用来完成什么用户可见功能。内部工具也应该在 README 或使用说明里写清楚:它会触发浏览器「正在调试」提示,处理完成后会立即 detach。
Chrome 权限警告指南 建议在功能允许时使用 optional permissions,让用户在运行时理解为什么需要额外权限。debugger 是否适合做 optional permission 取决于扩展结构,但「不要在安装时一次性申请所有可能用到的重权限」这个原则仍然成立。
Chrome Web Store Program Policies 也要求请求实现功能所需的最窄权限。debugger 这种权限最好能对应一个非常明确的主功能,而不是作为内部实现便利被顺手加进去。
日志要解释读了什么、写了什么、触发了什么
这类工具出了问题时,最常见的困惑是「看起来成功了,但结果没变」。日志需要回答几个问题:
- 当前 attach 的 tab 是哪个。
- 普通脚本复制是否执行过。
- CDP 输入是否执行过。
- 剪贴板前后是否变化。
- 解析出了哪些 key、tag 和语言文案。
- 最终写入了哪个页面、哪个
localStoragekey。
日志优先打在 Popup 自己的 Console 里。目标业务页只返回结构化结果,不要把调试过程塞进业务页面 Console。
日志里也要避免记录过多敏感信息。比如复制表格工具可以记录 key、tag、语言和写入目标,但不应该默认把整张表、所有接口响应、cookie、token 或用户输入内容长期保存下来。
UI 要提前解释提示条
浏览器的「started debugging this browser」提示是安全提示,不是错误。工具界面或使用说明里最好提前写一句:执行读取时 Chrome 可能显示正在调试提示,这是因为扩展需要通过浏览器调试协议触发真实复制;工具会在操作结束后立即释放调试会话。
这样做能减少用户误解。否则用户第一次看到提示条,很容易以为扩展一直在监控浏览器。
把 debugger 当成调试层能力,不当成默认扩展能力
debugger 的价值在于它接近浏览器调试层,适合处理普通脚本无法触达的输入、网络、运行时、页面和环境模拟问题。它不应该替代所有普通扩展 API。
一个简单判断是:如果需求关键词里没有真实输入、canvas、复杂编辑器、Network、Runtime、DOM Snapshot、Page lifecycle、Emulation、Tracing 这些调试层信号,大概率先不用 debugger。
在做方案评估时,可以先问两个问题:
- 普通扩展 API 是否已经能稳定完成这个动作。
- 如果普通 API 失败,失败点是否真的落在浏览器调试层。
只有第二个答案明确时,debugger 才值得进入正式方案。
不要把它当成跨环境写入能力
如果需求是写当前 Chrome 已打开页面的 localStorage,优先用 chrome.tabs.query 找目标 tab,再用 chrome.scripting.executeScript 注入页面上下文。按钮文案要直接提示会写到哪个页面;没有目标页时禁用写入动作,提示用户先打开目标页。
如果需求是写到另一个浏览器、手机 vConsole 或 App WebView,应该生成控制台脚本,让目标环境自己执行。debugger 不能跨浏览器、跨设备、跨 WebView attach,也不能凭 URL 直接写入一个尚未打开的页面。
怎么判断该不该用
可以用这张表做初步判断。
| 需求信号 | 优先方案 | 什么时候考虑 debugger / CDP |
|---|---|---|
| 读取普通 DOM 文本 | chrome.scripting.executeScript |
DOM 在 Shadow DOM、iframe 或虚拟渲染里,普通选择器不稳定 |
| 点击普通按钮、填写表单 | 内容脚本或页面 API | 页面只响应真实键鼠输入,合成事件不触发业务逻辑 |
| 复制普通选中文本 | Clipboard API / Selection API | canvas 表格、复杂编辑器、非标准选区,剪贴板不随合成事件变化 |
写本地开发页 localStorage |
chrome.scripting.executeScript,或生成控制台脚本让目标环境自己执行 |
一般不需要 CDP;如果目标页没打开、在另一个浏览器或手机 WebView,debugger 也不能直接跨过去写 |
| 观察或 mock 请求 | declarativeNetRequest、webRequest、代理 |
内部调试需要贴近 DevTools Network、读取特定响应或临时拦截 |
| 复现移动端、语言、时区、弱网问题 | DevTools / 浏览器设置 / 测试环境 | 需要在扩展工具里自动化设置 Emulation.* 或网络条件 |
| 截图、PDF、页面生命周期 | 普通截图 API 或浏览器手动操作 | 需要 CDP Page.* 能力,或要和 target / frame 状态结合 |
| 性能 trace、CPU profile | DevTools 手动分析 | 需要工具化采集并复用 Performance / Profiler / Tracing |
这张表的作用是提醒判断边界:当问题已经明显落在浏览器调试层时,早一点验证 CDP,可能比在页面脚本层绕很久更省时间。
结论
chrome.debugger 是 Chrome Extension 通向 CDP 的入口。它能做普通内容脚本做不到的一些事,尤其是输入事件、运行时调试、网络观察、页面控制和环境模拟。
这次飞书 canvas 表格复制的问题,关键是触发表格自己的复制管线。普通脚本能让 UI 看起来选中,却不能保证飞书把内容写进剪贴板;CDP 的 Input.dispatchKeyEvent 更接近真实用户快捷键,所以能触发页面内部复制逻辑。
这个经验可以迁移到其他场景:如果需求和 canvas、复杂编辑器、虚拟表格、非标准选区、真实快捷键、Network、Runtime、Emulation 这些关键词有关,应该尽早评估 debugger / CDP;如果只是普通 DOM 操作、标签页管理或本地缓存写入,就继续用更轻的扩展 API。
反过来看,debugger 也有很清楚的边界:它不能脱离 target 操作一个不存在的页面,不能直接写到另一个浏览器或手机 WebView,不能把 Storage.* 当成任意 localStorage.setItem(),也不能绕开 origin 边界。遇到本地多语言缓存写入这类需求,最稳的判断是:当前 Chrome 已打开目标页就注入写入;目标在别的环境里,就生成控制台脚本让目标环境自己执行。
它是一个高权限工具。使用时要让用户明确触发、只处理目标页面、只发送必要命令、立刻 detach,并且把权限和提示条带来的用户感知提前讲清楚。