主题
Offscreen Document 剪贴板兼容层
本文引用的源码文件
本文引用的文件
目录
引言
本文件聚焦 MV3 扩展中的 Offscreen Document 剪贴板兼容层。该兼容层解决的是:在 Manifest V3 下,Background Service Worker 和 Content Script 对系统剪贴板的访问受到严格限制;而扩展需要为 AI 工具提供稳定的“复制”“粘贴”“获取选中内容”能力。为此,项目通过一个独立的 Offscreen Document 作为剪贴板操作的安全边界,由 Background Service Worker 按需创建、复用并通信。
关键设计动机包括:
- MV3 的 Background Service Worker 是短暂进程,不适合长期持有敏感状态或 UI 交互上下文。
- Content Script 运行在网页上下文中,直接读写剪贴板会暴露给页面脚本,存在权限泄露风险。
- Offscreen Document 拥有独立文档上下文,可声明
clipboardRead/clipboardWrite权限,并通过chrome.offscreen.createDocument以CLIPBOARD理由申请权限,从而把剪贴板访问收敛到最小可信边界。
项目结构定位
Offscreen Document 剪贴板兼容层涉及以下代码位置:
- 扩展清单中声明
offscreen、clipboardRead、clipboardWrite权限。 - Background 侧负责检查、创建和复用 Offscreen Document,并封装剪贴板工具调用。
- Offscreen HTML 页面承载最小 DOM,用于兼容旧式
document.execCommand回退路径。 - Offscreen JS 监听来自 Background 的消息,执行实际的剪贴板读写。
- 构建脚本将 offscreen 目录复制到产物目录,并对 ES Module 形式的 offscreen.js 做 IIFE 打包处理。
graph TB A["扩展清单<br/>manifest.json"] --> B["Background Service Worker<br/>tool-executor.js"] B --> C["Offscreen Document<br/>offscreen.html + offscreen.js"] B --> D["媒体工具定义<br/>media-tools.js"] E["构建脚本<br/>fix-build.js"] --> C
图表来源
- manifest.json#L7-L27
- src/background/tool-executor.js#L4881-L5067
- src/offscreen/offscreen.html#L1-L12
- src/offscreen/offscreen.js#L1-L68
- src/background/tools/media-tools.js#L29-L49
- scripts/fix-build.js#L355-L394
章节来源
- manifest.json#L7-L27
- src/background/tool-executor.js#L4881-L5067
- src/offscreen/offscreen.html#L1-L12
- src/offscreen/offscreen.js#L1-L68
- src/background/tools/media-tools.js#L29-L49
- scripts/fix-build.js#L355-L394
核心组件
本节从职责角度拆解兼容层的核心部分。
Background 剪贴板协调器
Background 侧承担以下职责:
- 维护 Offscreen Document 生命周期:检测是否存在、避免重复创建、并发等待创建完成。
- 将 AI 工具调用转换为 Offscreen Document 消息。
- 统一返回结构化结果,包含成功标志、用户可读消息或错误信息。
- 对
get_selected走 Content Script 路径,因为选中文本属于页面上下文,不属于系统剪贴板。
关键实现要点:
- 使用
chrome.runtime.getContexts查询是否已有目标 Offscreen Document。 - 使用
creatingOffscreenDocument变量防止并发重复创建。 - 使用
chrome.offscreen.createDocument并以reasons: ['CLIPBOARD']声明用途。 - 通过
chrome.runtime.sendMessage向 Offscreen Document 发送COPY_TO_CLIPBOARD或PASTE_FROM_CLIPBOARD消息。
章节来源
Offscreen Document 剪贴板处理器
Offscreen Document 承担实际剪贴板操作:
- 注册国际化文案,使失败提示具备多语言支持。
- 监听
chrome.runtime.onMessage,根据消息类型分发到复制或粘贴逻辑。 - 优先使用现代
navigator.clipboardAPI。 - 当现代 API 失败时,回退到隐藏
<textarea>配合document.execCommand('copy' | 'paste')的兼容路径。 - 始终返回
{ success, message? | text? | error? }结构,便于 Background 统一处理。
章节来源
媒体工具定义
media-tools.js 对外暴露 clipboard 工具定义,供 Agent 工具调度系统识别和执行。其参数契约包括:
action:copy、paste、get_selected。text:复制时需要。format:获取选中内容时可选,支持text或html。
章节来源
架构总览
下图展示 MV3 扩展中剪贴板能力的完整调用链:AI 工具 → Background 工具执行器 → Offscreen Document → 系统剪贴板。
sequenceDiagram
participant User as "用户"
participant Agent as "Agent 工具调度"
participant BG as "Background 工具执行器"
participant OS as "Offscreen Document"
participant Clipboard as "系统剪贴板"
User->>Agent : 触发复制/粘贴工具
Agent->>BG : 调用 clipboard 工具
BG->>BG : ensureOffscreenDocument()
BG->>OS : sendMessage(COPY_TO_CLIPBOARD 或 PASTE_FROM_CLIPBOARD)
OS->>Clipboard : navigator.clipboard.writeText/readText
alt 现代 API 成功
Clipboard-->>OS : 成功
OS-->>BG : { success, message|text }
BG-->>Agent : 工具结果
else 现代 API 失败
OS->>OS : 回退 textarea + execCommand
OS-->>BG : { success|error }
BG-->>Agent : 工具结果
end
图表来源
详细组件分析
Offscreen Document 生命周期管理
Background 侧的 ensureOffscreenDocument 是生命周期控制入口。其行为可概括为:
- 计算 Offscreen Document URL。
- 查询当前已存在的 OFFSCREEN_DOCUMENT 上下文。
- 若已存在,直接返回。
- 若正在创建,则等待已有 Promise。
- 否则发起
chrome.offscreen.createDocument,并使用 finally 清理创建锁。
flowchart TD
Start(["进入 ensureOffscreenDocument"]) --> CheckExisting["查询现有 Offscreen Document"]
CheckExisting --> Exists{"已存在?"}
Exists --> |是| ReturnOK["直接返回"]
Exists --> |否| HasCreating{"正在创建?"}
HasCreating --> |是| WaitPromise["等待已有创建 Promise"]
HasCreating --> |否| CreateDoc["调用 chrome.offscreen.createDocument"]
CreateDoc --> AwaitCreate["await 创建完成"]
AwaitCreate --> Cleanup["清理 creatingOffscreenDocument"]
Cleanup --> ReturnOK
WaitPromise --> ReturnOK
图表来源
章节来源
剪贴板读写权限模型
权限模型分为三层:
- 扩展清单层:声明
offscreen、clipboardRead、clipboardWrite。 - Chrome 权限层:通过
createDocument的reasons: ['CLIPBOARD']说明用途。 - 运行时层:Offscreen Document 内使用
navigator.clipboard进行实际读写。
flowchart TD Manifest["manifest.json<br/>声明 offscreen / clipboardRead / clipboardWrite"] --> BrowserPolicy["Chrome 权限策略"] BrowserPolicy --> CreateDoc["Background 调用 createDocument<br/>reasons: CLIPBOARD"] CreateDoc --> OffscreenContext["Offscreen Document 上下文"] OffscreenContext --> NavigatorClipboard["navigator.clipboard API"] NavigatorClipboard --> SystemClipboard["系统剪贴板"]
图表来源
章节来源
剪贴板操作封装接口
Background 侧暴露两个主要导出函数:
executeCopyToClipboard(args, toolCallId)executePasteFromClipboard(args, toolCallId)
它们共同遵循以下流程:
- 校验必要参数(例如复制时必须提供文本)。
- 确保 Offscreen Document 可用。
- 发送消息到 Offscreen Document。
- 解析响应并包装为标准工具结果。
flowchart TD Entry(["executeCopyToClipboard / executePasteFromClipboard"]) --> Validate["参数校验"] Validate --> EnsureOS["ensureOffscreenDocument()"] EnsureOS --> SendMsg["chrome.runtime.sendMessage"] SendMsg --> ParseResp["解析 response.success / error / text"] ParseResp --> WrapResult["包装为工具结果"] WrapResult --> Exit(["返回给上层工具调度"])
图表来源
章节来源
Offscreen Document 内部处理流程
Offscreen Document 内部对复制和粘贴分别实现主路径与回退路径。
复制流程
flowchart TD
CopyEntry(["收到 COPY_TO_CLIPBOARD"]) --> TryModern["尝试 navigator.clipboard.writeText"]
TryModern --> ModernOk{"成功?"}
ModernOk --> |是| RespondSuccess["返回 success + 成功消息"]
ModernOk --> |否| Fallback["隐藏 textarea 设置值并 select"]
Fallback --> ExecCopy["document.execCommand('copy')"]
ExecCopy --> CopyOk{"execCommand 成功?"}
CopyOk --> |是| RespondFallbackSuccess["返回 success + 成功消息"]
CopyOk --> |否| RespondError["返回 success=false + 错误消息"]
图表来源
粘贴流程
flowchart TD
PasteEntry(["收到 PASTE_FROM_CLIPBOARD"]) --> TryRead["尝试 navigator.clipboard.readText"]
TryRead --> ReadOk{"成功?"}
ReadOk --> |是| RespondText["返回 success + text"]
ReadOk --> |否| FocusTextarea["清空并聚焦 textarea"]
FocusTextarea --> ExecPaste["document.execCommand('paste')"]
ExecPaste --> ReadValue["读取 textarea.value"]
ReadValue --> PasteOk{"成功且有文本?"}
PasteOk --> |是| RespondFallbackText["返回 success + text"]
PasteOk --> |否| RespondPasteError["返回 success=false + 错误消息"]
图表来源
章节来源
为什么不在 Content Script 中直接访问剪贴板
虽然 Content Script 在某些场景下可以访问 navigator.clipboard,但本项目选择 Offscreen Document 的原因包括:
- 权限隔离:Content Script 与网页共享 DOM 环境,若允许网页脚本间接读取剪贴板,会造成隐私泄露。
- Background 进程约束:MV3 Background Service Worker 可能被终止,不适合长期持有剪贴板相关状态。
- 最小可信边界:Offscreen Document 是扩展自有页面,可通过清单权限精确控制剪贴板访问范围。
- 兼容性兜底:Offscreen Document 可安全使用
document.execCommand等旧 API 作为回退,而不污染网页上下文。
因此,get_selected 仍走 Content Script,因为选中内容是页面级状态;而 copy 和 paste 走 Offscreen Document,因为它们是系统级剪贴板操作。
章节来源
依赖关系分析
Offscreen Document 剪贴板兼容层的依赖关系如下:
graph LR Manifest["manifest.json"] --> ToolExecutor["tool-executor.js"] MediaTools["media-tools.js"] --> ToolExecutor ToolExecutor --> OffscreenHTML["offscreen.html"] ToolExecutor --> OffscreenJS["offscreen.js"] FixBuild["fix-build.js"] --> OffscreenDir["offscreen 目录产物"]
图表来源
- manifest.json#L7-L27
- src/background/tools/media-tools.js#L29-L49
- src/background/tool-executor.js#L4881-L5067
- src/offscreen/offscreen.html#L1-L12
- src/offscreen/offscreen.js#L1-L68
- scripts/fix-build.js#L355-L394
章节来源
- manifest.json#L7-L27
- src/background/tools/media-tools.js#L29-L49
- src/background/tool-executor.js#L4881-L5067
- src/offscreen/offscreen.html#L1-L12
- src/offscreen/offscreen.js#L1-L68
- scripts/fix-build.js#L355-L394
性能与资源优化
当前实现已经做了若干基础优化:
- 按需创建:仅在首次需要剪贴板功能时创建 Offscreen Document。
- 去重查询:通过
chrome.runtime.getContexts判断是否已存在,避免重复创建。 - 并发保护:使用
creatingOffscreenDocument变量防止多个调用同时创建。 - 轻量 DOM:Offscreen HTML 仅包含一个隐藏 textarea,不加载重型 UI。
- IIFE 打包:构建脚本将 offscreen.js 打包为 IIFE,减少模块加载开销。
可进一步优化的方向:
- 显式销毁:当前未看到主动销毁 Offscreen Document 的逻辑;可在长时间无剪贴板操作后考虑关闭空闲上下文。
- 消息队列:若未来出现高频剪贴板请求,可增加队列合并与节流。
- 错误重试上限:对反复失败的剪贴板操作增加降级策略,避免无限重试。
- 日志采样:频繁复制粘贴场景下建议降低调试日志频率。
章节来源
- src/background/tool-executor.js#L4881-L4912
- src/offscreen/offscreen.html#L7-L9
- scripts/fix-build.js#L355-L394
故障恢复与排错指南
常见问题与定位思路
| 现象 | 可能原因 | 排查建议 |
|---|---|---|
| 复制失败 | Offscreen Document 未创建或权限不足 | 检查 manifest 是否声明 offscreen、clipboardWrite;确认 createDocument 是否成功 |
| 粘贴失败 | 浏览器不允许读取剪贴板或用户未授权 | 检查 manifest 是否声明 clipboardRead;查看 Offscreen 回退路径是否被触发 |
| 工具调用无响应 | Background 与 Offscreen 消息通道异常 | 检查 chrome.runtime.sendMessage 是否返回;确认 Offscreen 监听器是否注册 |
| 构建后 Offscreen 不可用 | offscreen 目录未被正确复制或打包 | 检查 fix-build.js 是否复制 offscreen 目录并打包 offscreen.js |
错误处理机制
- Background 层捕获异常并返回结构化错误,避免上层工具调度崩溃。
- Offscreen 层先尝试现代 API,失败后进入回退路径,再失败则返回明确错误消息。
- 所有错误消息均经过 i18n 处理,便于用户理解。
flowchart TD Call["工具调用"] --> BGTry["Background 调用 try/catch"] BGTry --> OSTry["Offscreen 调用 try/catch"] OSTry --> ModernAPI["navigator.clipboard"] ModernAPI --> Fallback["textarea + execCommand"] Fallback --> ErrorReturn["返回 success=false + error"] ModernAPI --> SuccessReturn["返回 success=true + data"] ErrorReturn --> BGWrap["Background 包装错误"] SuccessReturn --> BGWrap BGWrap --> Result["返回工具结果"]
图表来源
章节来源
结论
Offscreen Document 剪贴板兼容层在本项目中承担了 MV3 环境下剪贴板访问的关键桥接职责。它通过 Background 侧的生命周期管理和 Offscreen 侧的最小化剪贴板处理器,实现了:
- 安全的权限边界:剪贴板访问集中在扩展自有页面。
- 可靠的兼容性:现代 API 失败时可回退到旧式命令。
- 清晰的职责分离:Background 负责协调,Offscreen 负责执行。
- 可扩展的工具契约:通过
media-tools.js暴露统一的 clipboard 工具定义。
未来若扩展更多敏感系统能力,可沿用该模式:将高风险操作收敛到 Offscreen Document,并由 Background 统一编排、鉴权和监控。