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,25 @@
// background.js - service worker
const events = [];
let recording = false;
let activeTabId = null;
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.action === 'startRecording') {
events.length = 0;
recording = true;
activeTabId = msg.tabId;
chrome.tabs.sendMessage(msg.tabId, { action: 'startRecord' });
sendResponse({ ok: true });
} else if (msg.action === 'stopRecording') {
recording = false;
chrome.tabs.sendMessage(activeTabId, { action: 'stopRecord' });
sendResponse({ ok: true, eventCount: events.length });
} else if (msg.action === 'getEvents') {
sendResponse({ events: events });
} else if (msg.action === 'event') {
events.push(msg.data);
} else if (msg.action === 'getStatus') {
sendResponse({ recording, eventCount: events.length });
}
return true;
});

View File

@@ -0,0 +1,46 @@
// content.js - content script (runs in content script world)
(function() {
// Inject the record library + inject script into page
function loadScripts(callback) {
// Load record.umd.cjs first
const script1 = document.createElement('script');
script1.src = chrome.runtime.getURL('record.umd.cjs');
script1.onload = function() {
script1.remove();
// Then load inject.js
const script2 = document.createElement('script');
script2.src = chrome.runtime.getURL('inject.js');
script2.onload = function() {
script2.remove();
};
(document.head || document.documentElement).appendChild(script2);
};
(document.head || document.documentElement).appendChild(script1);
}
// Listen for messages from inject.js (page context)
window.addEventListener('message', function(event) {
if (event.source !== window) return;
const data = event.data;
if (!data || !data.__rrweb_action) return;
if (data.__rrweb_action === 'event') {
// Forward event to background
chrome.runtime.sendMessage({ action: 'event', data: data.__rrweb_data });
}
});
// Listen for messages from background
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if (msg.action === 'startRecord') {
loadScripts();
sendResponse({ ok: true });
} else if (msg.action === 'stopRecord') {
window.postMessage({ __rrweb_action: 'stop' }, '*');
sendResponse({ ok: true });
}
// Don't return true for unrelated messages
});
console.log('[rrweb] Content script loaded');
})();

View File

@@ -0,0 +1,30 @@
// inject.js - runs in page context, auto-starts recording
(function() {
'use strict';
let stopFn = null;
// Auto-start recording
stopFn = rrwebRecord.record({
emit: function(event) {
window.postMessage({
__rrweb_action: 'event',
__rrweb_data: event
}, '*');
},
recordCrossOriginIframes: true
});
// Listen for stop command
window.addEventListener('message', function(event) {
if (event.source !== window) return;
if (event.data && event.data.__rrweb_action === 'stop') {
if (stopFn) {
stopFn();
stopFn = null;
}
}
});
console.log('[rrweb] Recording started');
})();

View File

@@ -0,0 +1,20 @@
{
"manifest_version": 3,
"name": "rrweb Simple Recorder",
"version": "1.0.0",
"description": "Simple rrweb page recorder",
"permissions": ["activeTab", "storage", "scripting"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_title": "rrweb Recorder",
"default_popup": "popup.html"
},
"web_accessible_resources": [
{
"resources": ["record.umd.cjs", "inject.js"],
"matches": ["<all_urls>"]
}
]
}

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { width: 300px; padding: 16px; font-family: sans-serif; }
button { padding: 8px 16px; margin: 4px; cursor: pointer; border: none; border-radius: 4px; color: white; }
.start { background: #4CAF50; }
.start:hover { background: #45a049; }
.stop { background: #f44336; }
.stop:hover { background: #da190b; }
.view { background: #2196F3; }
.view:hover { background: #0b7dda; }
#status { margin-top: 12px; padding: 8px; background: #f5f5f5; border-radius: 4px; font-size: 13px; }
#events { margin-top: 8px; max-height: 200px; overflow-y: auto; font-size: 11px; white-space: pre-wrap; word-break: break-all; }
.disabled { opacity: 0.5; cursor: not-allowed; }
</style>
</head>
<body>
<h3>rrweb Simple Recorder</h3>
<div>
<button id="startBtn" class="start">Start Recording</button>
<button id="stopBtn" class="stop disabled" disabled>Stop Recording</button>
</div>
<div id="status">Ready</div>
<div>
<button id="viewBtn" class="view disabled" disabled>View Events</button>
<button id="downloadBtn" class="view disabled" disabled>Download JSON</button>
</div>
<div id="events"></div>
<script src="popup.js"></script>
</body>
</html>

93
rrweb-simple-ext/popup.js Normal file
View File

@@ -0,0 +1,93 @@
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const viewBtn = document.getElementById('viewBtn');
const downloadBtn = document.getElementById('downloadBtn');
const statusDiv = document.getElementById('status');
const eventsDiv = document.getElementById('events');
function updateStatus(text) {
statusDiv.textContent = text;
}
function setRecording(isRecording) {
startBtn.disabled = isRecording;
startBtn.classList.toggle('disabled', isRecording);
stopBtn.disabled = !isRecording;
stopBtn.classList.toggle('disabled', !isRecording);
}
startBtn.addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
if (!tab) return;
// First inject content script programmatically
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
files: ['content.js']
});
} catch(e) {
// Might already be injected, that's ok
}
chrome.runtime.sendMessage({ action: 'startRecording', tabId: tab.id }, (resp) => {
if (resp && resp.ok) {
setRecording(true);
updateStatus('Recording...');
}
});
});
stopBtn.addEventListener('click', () => {
chrome.runtime.sendMessage({ action: 'stopRecording' }, (resp) => {
if (resp && resp.ok) {
setRecording(false);
updateStatus('Stopped. Captured ' + resp.eventCount + ' events.');
viewBtn.disabled = false;
viewBtn.classList.remove('disabled');
downloadBtn.disabled = false;
downloadBtn.classList.remove('disabled');
}
});
});
viewBtn.addEventListener('click', () => {
chrome.runtime.sendMessage({ action: 'getEvents' }, (resp) => {
if (resp && resp.events) {
const summary = resp.events.map((e, i) =>
`${i}: type=${e.type} ts=${e.timestamp}`
).join('\n');
eventsDiv.textContent = `Total: ${resp.events.length} events\n\n${summary}`;
}
});
});
downloadBtn.addEventListener('click', () => {
chrome.runtime.sendMessage({ action: 'getEvents' }, (resp) => {
if (resp && resp.events) {
const blob = new Blob([JSON.stringify(resp.events, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'rrweb-events.json';
a.click();
URL.revokeObjectURL(url);
}
});
});
// Check current status on open
chrome.runtime.sendMessage({ action: 'getStatus' }, (resp) => {
if (resp) {
setRecording(resp.recording);
updateStatus(resp.recording
? 'Recording... (' + resp.eventCount + ' events)'
: 'Ready');
if (resp.eventCount > 0) {
viewBtn.disabled = false;
viewBtn.classList.remove('disabled');
downloadBtn.disabled = false;
downloadBtn.classList.remove('disabled');
}
}
});

File diff suppressed because it is too large Load Diff