Add SuperRPA integration guide, simple extension and standalone replay page
Some checks failed
Tests / Tests (push) Has been cancelled
ESLint Check / ESLint Check and Report Upload (push) Has been cancelled
Prettier Check / Format Check (push) Has been cancelled
Prettier Check / Format Code (push) Has been cancelled
ESLint Check / Build Base for Bundle Size Comparison (push) Has been cancelled

- docs/integration/superrpa-integration.zh_CN.md: complete integration guide
- rrweb-simple-ext/: minimal Chrome extension for page recording
- replay.html: standalone drag-and-drop replay viewer
- CLAUDE.md: project instructions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
zhaoyilun
2026-04-10 17:08:24 +08:00
parent 87c94ae3a9
commit 27a17d7068
10 changed files with 14102 additions and 0 deletions

View File

@@ -0,0 +1,819 @@
# rrweb 与 SuperRPA 集成适配指南
本文档面向 SuperRPA魔改版 Chromium项目开发团队说明如何将 rrweb 录制/回放能力集成到浏览器中。
---
## 1. rrweb 概述
rrweb 是一个 Web 页面操作录制与回放库。它通过以下机制工作:
1. **全量快照 (Full Snapshot)**:将当前页面 DOM 树序列化为一个带唯一 ID 的 JSON 数据结构
2. **增量快照 (Incremental Snapshot)**:通过 MutationObserver 和各种事件监听器,持续捕获 DOM 变更和用户交互
3. **回放 (Replay)**:从全量快照重建 DOM然后按时间戳顺序重放增量事件
所有录制产物都是纯 JSON 数据(`eventWithTime[]`),可序列化传输和持久存储。
### 录制覆盖的用户操作
| 类别 | 具体内容 |
|------|----------|
| DOM 变更 | 节点增删、属性变化、文本内容变化 |
| 鼠标操作 | 移动、点击、双击、右键、悬停 |
| 键盘输入 | 输入框文本变化、复选框/单选框状态 |
| 滚动 | 页面滚动、元素内滚动 |
| 触摸 | TouchStart、TouchMove、TouchEnd |
| Canvas | 2D 绑定调用记录、WebGL 调用记录 |
| 媒体 | 音视频播放、暂停、跳转、音量变化 |
| 样式 | CSS 规则增删改、行内样式变化 |
| 字体 | 动态加载的字体 |
| 文本选择 | 选区变化 |
| Shadow DOM | Shadow DOM 内的变更 |
| 自定义元素 | Custom Element 注册 |
| 跨域 iframe | 通过 postMessage 桥接录制 |
---
## 2. 构建产物
### 2.1 构建
```bash
# 在 rrweb 项目根目录
yarn install
yarn build:all
```
### 2.2 关键产物文件
| 文件 | 用途 | 大小参考 |
|------|------|----------|
| `packages/rrweb/dist/rrweb.umd.cjs` | 全量包(录制+回放UMD 格式 | 较大 |
| `packages/rrweb/dist/rrweb.js` | 全量包ESM 格式 | 较大 |
| `packages/record/dist/record.umd.cjs` | **仅录制**UMD 格式(推荐用于注入) | ~40KB gzip |
| `packages/record/dist/record.js` | 仅录制ESM 格式 | ~40KB gzip |
| `packages/replay/dist/replay.js` | 仅回放ESM 格式 | ~50KB gzip |
| `packages/replay/dist/style.css` | 回放必需的 CSS 样式 | — |
| `packages/rrweb/dist/style.css` | 全量包回放样式 | — |
**建议**SuperRPA 注入录制脚本时,使用 `@rrweb/record`(仅录制)以减小注入体积。回放端使用 `@rrweb/replay``rrweb-player`
### 2.3 UMD 全局变量名
注入后rrweb 的 API 会挂载到全局变量上:
- `rrweb.umd.cjs``window.rrweb`(包含 `record``Replayer`
- `record.umd.cjs``window.rrwebRecord`(仅包含 `record`
---
## 3. 集成方案
SuperRPA 作为魔改版 Chromium有以下几种集成方式
### 方案 A通过 CDP 注入(推荐,改动最小)
利用 Chrome DevTools Protocol 的 `Page.addScriptToEvaluateOnNewDocument` 在每个页面加载前注入录制脚本。
```
┌─────────────────────────────────────────────┐
│ SuperRPA (Chromium) │
│ │
│ ┌──────────┐ CDP ┌───────────────┐ │
│ │ 控制层 │ ────────→ │ 渲染进程 │ │
│ │ (Browser │ │ │ │
│ │ Process)│ │ Web Page │ │
│ │ │ │ ┌──────────┐ │ │
│ │ 收集事件 │ ←──────── │ │ rrweb │ │ │
│ │ 存储/转发 │ postMsg │ │ record() │ │ │
│ └──────────┘ │ └──────────┘ │ │
│ └───────────────┘ │
└─────────────────────────────────────────────┘
```
**步骤:**
1. 读取录制脚本文件内容
2. 在页面导航前通过 CDP 注入
```cpp
// C++ 侧伪代码SuperRPA Browser Process
std::string record_script = ReadFile("record.umd.cjs");
std::string injection_code = record_script + R"(
(function() {
window.__rrweb_events = [];
window.__rrweb_stop = rrwebRecord.record({
emit: function(event) {
// 方式1存到全局变量由 CDP 定时读取
window.__rrweb_events.push(event);
// 方式2通过 postMessage 发给 content script
window.postMessage({
type: '__rrweb_event',
event: event
}, '*');
}
});
// 通知控制层录制已就绪
window.postMessage({ type: '__rrweb_ready' }, '*');
})();
)";
// 通过 CDP 注入
cdp_session->Send("Page.addScriptToEvaluateOnNewDocument", {
{"source", injection_code}
});
```
```python
# Python 侧伪代码(如果用 Puppeteer/Playwright 驱动)
import json
cdp = await page.context().new_cdp_session(page)
await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
"source": record_script + """
window.__rrweb_events = [];
window.__rrweb_stop = rrwebRecord.record({
emit(event) {
window.__rrweb_events.push(event);
}
});
"""
})
# 导航到目标页面
await page.goto("https://example.com")
# 定时采集事件
events = await page.evaluate("window.__rrweb_events.splice(0)")
# 停止录制
await page.evaluate("window.__rrweb_stop()")
```
### 方案 BChromium 源码层注入(改动较大,性能最优)
在 Chromium 的 `ContentRendererClient::RenderFrameCreated``RenderFrameImpl::DidCreateDocumentElement` 中直接注入。
```cpp
// content/renderer/render_frame_impl.cc 或对应的 SuperRPA 扩展点
void SuperRPARenderFrameObserver::DidCreateDocumentElement() {
auto& web_frame = render_frame_->GetWebFrame();
std::string script = Load rrweb_record_script + R"(
window.__rrweb_events = [];
window.__rrweb_stop = rrwebRecord.record({
emit(event) {
window.__rrweb_events.push(event);
}
});
)";
web_frame.ExecuteScript(
WebScriptSource(WebString::FromUTF8(script)));
}
```
### 方案 CChrome Extension 方式
rrweb 项目已包含一个浏览器扩展示例(`packages/web-extension/`),可直接参考或改造。
---
## 4. 核心 API
### 4.1 录制
```js
// 开始录制
const stopFn = rrwebRecord.record({
emit(event) {
// event: eventWithTime 类型,每次用户操作或 DOM 变更触发
// 必须保证所有 event 按 timestamp 顺序存储
sendToBackend(event);
},
});
// 停止录制
stopFn();
```
### 4.2 回放
```js
// 创建回放器
const replayer = new rrweb.Replayer(events, {
root: document.getElementById('container'), // 回放容器 DOM
speed: 1,
});
// 获取元数据
const meta = replayer.getMetaData();
// meta = { startTime: number, endTime: number, totalTime: number }
// 播放控制
replayer.play(); // 从头播放
replayer.play(5000); // 从 5000ms 处播放
replayer.pause(); // 暂停在当前位置
replayer.pause(3000); // 暂停在 3000ms 处
// 配置变更
replayer.setConfig({ speed: 2 }); // 修改播放速度
// 销毁
replayer.destroy();
```
### 4.3 事件监听
```js
replayer.on('finish', () => { /* 回放结束 */ });
replayer.on('resize', (dimension) => { /* 视口大小变化 */ });
```
---
## 5. 事件数据结构
### 5.1 顶层结构
每个事件的顶层结构为 `eventWithTime`
```ts
{
timestamp: number; // Unix 时间戳(毫秒),相对于页面加载的偏移量
type: EventType; // 事件类型(数值枚举)
data: object; // 事件数据,结构因 type 不同而异
}
```
### 5.2 EventType 枚举
```ts
enum EventType {
DomContentLoaded = 0, // DOM 加载完成
Load = 1, // 页面完全加载
FullSnapshot = 2, // 全量 DOM 快照
IncrementalSnapshot = 3, // 增量快照(最重要的类型)
Meta = 4, // 页面元信息URL、视口尺寸
Custom = 5, // 自定义事件
Plugin = 6, // 插件事件
}
```
### 5.3 一个完整的录制会话包含的事件序列
```
1. Meta → { href: "页面URL", width: 1920, height: 1080 }
2. FullSnapshot → 完整序列化的 DOM 树
3. IncrementalSnapshot × N → 各种增量变更事件
...可选DomContentLoaded / Load
```
**关键点**:每个会话必须以一个 `Meta` 事件 + 一个 `FullSnapshot` 事件开头,后续才有意义。
### 5.4 IncrementalSnapshot 的 data.source 枚举
```ts
enum IncrementalSource {
Mutation = 0, // DOM 变更(增删改节点/属性/文本)
MouseMove = 1, // 鼠标移动
MouseInteraction = 2, // 鼠标交互(点击、双击、右键、聚焦等)
Scroll = 3, // 滚动
ViewportResize = 4, // 视口尺寸变化
Input = 5, // 输入框值变化
TouchMove = 6, // 触摸移动
MediaInteraction = 7, // 媒体播放/暂停/跳转
StyleSheetRule = 8, // CSS 规则变更
CanvasMutation = 9, // Canvas 绑定调用
Font = 10, // 字体加载
Log = 11, // 控制台日志(需插件)
Drag = 12, // 拖拽
StyleDeclaration = 13, // 行内样式声明变更
Selection = 14, // 文本选区变化
AdoptedStyleSheet = 15, // Adopted Stylesheet
CustomElement = 16, // 自定义元素注册
}
```
### 5.5 常见事件数据示例
**Meta 事件**
```json
{
"type": 4,
"data": { "href": "https://example.com/page", "width": 1920, "height": 1080 },
"timestamp": 1712600000000
}
```
**全量快照**
```json
{
"type": 2,
"data": {
"node": {
"type": 0,
"id": 1,
"childNodes": [
{ "type": 2, "tagName": "html", "id": 2, "attributes": { "lang": "zh" }, "childNodes": [...] }
]
},
"initialOffset": { "top": 0, "left": 0 }
},
"timestamp": 1712600000001
}
```
**鼠标点击**
```json
{
"type": 3,
"data": {
"source": 2,
"type": 2,
"id": 42,
"x": 320,
"y": 150
},
"timestamp": 1712600002500
}
```
**输入框变化**
```json
{
"type": 3,
"data": {
"source": 5,
"id": 15,
"text": "用户输入的内容",
"isChecked": false
},
"timestamp": 1712600003000
}
```
**DOM 变更**
```json
{
"type": 3,
"data": {
"source": 0,
"texts": [],
"attributes": [{ "id": 7, "attributes": { "class": "active" } }],
"removes": [],
"adds": [
{
"parentId": 5,
"nextId": null,
"node": { "type": 2, "tagName": "div", "id": 99, "attributes": {}, "childNodes": [] }
}
]
},
"timestamp": 1712600004000
}
```
**滚动**
```json
{
"type": 3,
"data": { "source": 3, "id": 1, "x": 0, "y": 500 },
"timestamp": 1712600005000
}
```
### 5.6 序列化节点类型
全量快照和增量快照中的新增节点使用以下类型:
```ts
enum NodeType {
Document = 0, // 文档节点
DocumentType = 1, // <!DOCTYPE ...>
Element = 2, // HTML 元素
Text = 3, // 文本节点
CDATA = 4, // CDATA 节点
Comment = 5, // 注释节点
}
```
每个序列化节点都有唯一的 `id`(自增数字),用于增量快照中引用和定位。
---
## 6. 录制配置详解
```js
rrwebRecord.record({
// 必填:事件回调
emit(event) {
// 每个 event 都必须保存,不可丢弃
// events 必须按 timestamp 有序
},
// ---- 隐私保护 ----
// 遮挡所有输入框内容(记录为 *
maskAllInputs: false,
// 细粒度控制哪些输入类型被遮挡
maskInputOptions: {
password: true,
text: false,
email: false,
// 支持所有 input type
},
// 自定义遮挡函数
maskInputFn: (text) => text.replace(/./g, '*'),
// 通过 CSS 选择器遮挡元素内的文本
maskTextSelector: '.sensitive-data',
// 自定义文本遮挡函数
maskTextFn: (text) => '***',
// CSS 类名方式遮挡文本(默认 'rr-mask'
maskTextClass: 'rr-mask',
// ---- 屏蔽/忽略 ----
// 不录制该 CSS 类名的元素及其子树(显示为灰色色块)
blockClass: 'rr-block',
blockSelector: '.no-record-area',
// 完全忽略该元素(不录制,回放时也不出现)
ignoreClass: 'rr-ignore',
ignoreSelector: '.ignore-me',
// ---- 采样策略(影响性能和存储体积的关键配置)----
sampling: {
// 鼠标移动false=不录制,数字=节流间隔(ms)
mousemove: 50,
// 鼠标交互false=不录制true=全部录制,或细粒度控制
mouseInteraction: {
MouseUp: true,
MouseDown: true,
Click: true,
ContextMenu: false,
DblClick: true,
Focus: true,
Blur: true,
TouchStart: true,
TouchEnd: true,
},
// 滚动节流间隔(ms)
scroll: 150,
// 媒体交互节流间隔(ms)
media: 800,
// 输入事件:'all'=记录每次按键,'last'=只记录最终值
input: 'last',
// Canvas'all'=记录每次API调用数字=每秒截图次数(1-60)
canvas: 'all',
},
// ---- 高级功能 ----
recordCanvas: false, // 是否录制 Canvas
recordCrossOriginIframes: false, // 是否录制跨域 iframe
recordAfter: 'DOMContentLoaded', // 何时开始录制: 'DOMContentLoaded' | 'load'
inlineStylesheet: true, // 是否内联外部样式表
inlineImages: false, // 是否内联图片为 base64
collectFonts: false, // 是否收集动态加载的字体
// 定期生成全量快照(防止增量快照链过长导致回放失败)
checkoutEveryNth: 100, // 每 N 个增量事件生成一次
checkoutEveryNms: 60000, // 每 N 毫秒生成一次
// 数据压缩(需配合 @rrweb/packer
// packFn: pack,
// 错误处理
errorHandler: (error) => {
console.error('rrweb error:', error);
// return true 可跳过该错误继续录制
},
// 插件
plugins: [
// console 录制插件
// getRecordConsolePlugin(),
],
});
```
---
## 7. SuperRPA 集成注意事项
### 7.1 注入时机
录制脚本必须在页面的 DOM 构建之前或 `DOMContentLoaded` 之前注入。使用 `Page.addScriptToEvaluateOnNewDocument` 可以保证在页面任何脚本执行前注入。
如果注入太晚,会丢失页面初始状态的快照。
### 7.2 事件采集
在 SuperRPA 中,需要将录制事件从渲染进程传递到控制层。推荐方式:
| 方式 | 适用场景 | 注意事项 |
|------|----------|----------|
| `Runtime.evaluate` 定时轮询 `window.__rrweb_events` | 简单场景 | 高频轮询有性能开销 |
| CDP `Runtime.binding` | 高性能 | 需要在 CDP session 上创建 binding |
| `window.postMessage` + Content Script | 扩展模式 | 需要 content script 中转 |
| V8 Inspector 自定义消息 | 深度集成 | 改动较大 |
推荐 **CDP `Runtime.binding`** 方案:
```js
// 注入脚本
window.__rrweb_events = [];
rrwebRecord.record({
emit(event) {
// 通过 CDP binding 发送
if (typeof window.__rrwebSend === 'function') {
window.__rrwebSend(JSON.stringify(event));
}
// 同时存入数组作为备份
window.__rrweb_events.push(event);
}
});
```
```python
# 控制端
await cdp.send("Runtime.addBinding", {"name": "__rrwebSend"})
cdp.on("Runtime.bindingCalled", lambda params: handle_event(
json.loads(params["payload"])
))
```
### 7.3 会话管理
一个「录制会话」对应一次页面导航navigation。需要注意
1. **单页应用 (SPA)**页面内路由切换不会触发新的导航rrweb 会持续录制整个 SPA 会话
2. **多页导航**:每次导航需要重新注入录制脚本并开始新会话
3. **跨域 iframe**:每个跨域 iframe 也需要单独注入录制脚本rrweb 会通过 `postMessage` 桥接
4. **事件合并**:一个完整会话的所有事件必须按 `timestamp` 有序存储
### 7.4 性能影响
| 配置 | 性能影响 | 存储体积 |
|------|----------|----------|
| 默认配置 | 中等 | 中等 |
| `sampling.mousemove = false` | 低 | 小 |
| `sampling.input = 'last'` | 低 | 小 |
| `recordCanvas = true` | 高 | 大 |
| `recordCanvas = true` + `sampling.canvas = 10` | 中 | 中 |
| `inlineStylesheet = true` | 中 | 大(取决于页面样式复杂度) |
| `checkoutEveryNms = 30000` | 略高 | 略大(但提高回放鲁棒性) |
**建议基线配置**
```js
{
sampling: {
mousemove: 50,
scroll: 150,
input: 'last',
media: 800,
},
recordCanvas: false,
checkoutEveryNms: 300000, // 每 5 分钟一次全量快照
}
```
### 7.5 存储优化
1. **使用 packFn 压缩**
```js
import { pack } from '@rrweb/packer';
record({ emit(event) {}, packFn: pack });
```
2. **服务端批量压缩**:采集原始事件,服务端用 gzip/zstd 压缩效果更好
3. **定期全量快照**:通过 `checkoutEveryNms` 避免增量链过长
4. **跨会话去重**:相同样式表、相同页面的全量快照可以做去重
### 7.6 回放端集成
回放可以在 SuperRPA 内的专用标签页中展示,或在外部系统中:
```js
// 在空白页面中创建回放器
const replayer = new rrweb.Replayer(events, {
root: document.getElementById('replay-container'),
speed: 1,
skipInactive: true, // 跳过无操作的静默期
inactivePeriodThreshold: 5000, // 超过 5 秒无操作则跳过
showWarning: true,
insertStyleRules: [ // 注入自定义样式(如鼠标光标样式)
'div.replayer-mouse { cursor: pointer; }'
],
});
// 监听事件
replayer.on('finish', () => console.log('回放结束'));
replayer.on('resize', (d) => console.log('视口变化', d));
// 播放控制
replayer.play();
replayer.pause();
replayer.setConfig({ speed: 4 });
```
回放需要加载 CSS 样式:
```html
<link rel="stylesheet" href="replay/dist/style.css">
```
---
## 8. 录制事件中提取 RPA 可用信息
如果 SuperRPA 不仅需要视觉回放,还需要从事件中提取 **可复用的操作信息**(如元素定位、操作类型),可以从以下字段获取:
### 8.1 鼠标点击事件 → 点击坐标 + 目标元素
```json
{
"type": 3,
"data": {
"source": 2,
"type": 2, // MouseInteractions.Click = 2
"id": 42, // 序列化节点 ID
"x": 320, "y": 150 // 相对于视口的坐标
}
}
```
- `id` 对应全量快照中的节点 ID可反查到 `tagName``attributes`(含 `class``id` 等)
- `x``y` 为点击坐标
### 8.2 输入事件 → 输入值 + 目标元素
```json
{
"type": 3,
"data": {
"source": 5,
"id": 15,
"text": "用户输入",
"isChecked": false
}
}
```
### 8.3 滚动事件 → 滚动位置
```json
{
"type": 3,
"data": { "source": 3, "id": 1, "x": 0, "y": 500 }
}
```
### 8.4 从全量快照反查元素定位信息
全量快照中包含完整的 DOM 树,每个节点有 `id``tagName``attributes`。可以通过 ID 反查来构造 CSS 选择器或 XPath。
```js
// 从全量快照中构建 ID → 节点映射
function buildNodeMap(fullSnapshot) {
const map = {};
function walk(node) {
map[node.id] = node;
if (node.childNodes) node.childNodes.forEach(walk);
}
walk(fullSnapshot.data.node);
return map;
}
```
### 8.5 MouseInteractions 枚举
```ts
enum MouseInteractions {
MouseUp = 0,
MouseDown = 1,
Click = 2,
ContextMenu = 3,
DblClick = 4,
Focus = 5,
Blur = 6,
TouchStart = 7,
TouchMove_Departed = 8,
TouchEnd = 9,
TouchCancel = 10,
}
```
---
## 9. 完整注入示例
以下是一个可直接使用的注入脚本模板:
```js
// === rrweb-recorder-inject.js ===
// 此文件应包含 record.umd.cjs 的完整内容
// 然后附加以下启动代码
(function() {
'use strict';
var SESSION_ID = '__SESSION_ID_PLACEHOLDER__'; // 由控制层替换
var events = [];
function sendEvent(event) {
// 使用 CDP binding如果可用
if (typeof window.__rrwebSendEvent === 'function') {
try {
window.__rrwebSendEvent(JSON.stringify({
sessionId: SESSION_ID,
event: event
}));
} catch (e) {
// fallback
events.push(event);
}
} else {
events.push(event);
}
}
// 开始录制
var stopFn = rrwebRecord.record({
emit: sendEvent,
maskAllInputs: false,
recordCanvas: false,
recordCrossOriginIframes: true,
recordAfter: 'DOMContentLoaded',
sampling: {
mousemove: 50,
scroll: 150,
input: 'last',
media: 800,
},
checkoutEveryNms: 300000,
});
// 暴露控制接口
window.__rrweb = {
stop: function() {
if (stopFn) stopFn();
},
getEvents: function() {
return events;
},
drainEvents: function() {
var result = events.slice();
events.length = 0;
return result;
},
getSessionId: function() {
return SESSION_ID;
}
};
// 通知就绪
if (typeof window.__rrwebReady === 'function') {
window.__rrwebReady(SESSION_ID);
}
})();
```
---
## 10. 故障排查
| 问题 | 可能原因 | 解决方案 |
|------|----------|----------|
| 回放白屏 | 缺少 Meta + FullSnapshot 事件 | 确保从页面加载开始就注入了录制脚本 |
| 回放缺少样式 | `inlineStylesheet` 未开启或外部样式表加载失败 | 设置 `inlineStylesheet: true` |
| 回放中元素错位 | 视口尺寸与录制时不一致 | 使用 Meta 事件中的 width/height 创建同尺寸回放容器 |
| 录制数据过大 | Canvas 动画或频繁 DOM 更新 | 配置 `sampling``blockClass`,或关闭 `recordCanvas` |
| 跨域 iframe 无录制 | 未对 iframe 注入脚本 | 开启 `recordCrossOriginIframes: true` 并确保 iframe 中也注入了脚本 |
| 回放报错 "node not found" | 增量快照链中丢失了事件 | 确保所有事件都被保存,使用 `checkoutEveryNms` 定期全量快照 |
| 注入后页面行为异常 | rrweb 脚本与页面脚本冲突 | 使用 `ignoreSelector` 排除 rrweb 自身的 UI 元素 |
---
## 11. 参考链接
- rrweb 官方文档:`docs/` 目录下
- 事件数据结构定义:`packages/types/src/index.ts`
- 录制逻辑:`packages/rrweb/src/record/``packages/record/src/`
- 回放逻辑:`packages/rrweb/src/replay/``packages/replay/src/`
- 浏览器扩展示例:`packages/web-extension/`
- DOM 序列化设计:`docs/serialization.md`
- 增量快照设计:`docs/observer.md`
- 回放沙箱设计:`docs/sandbox.md`