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

206
replay.html Normal file
View File

@@ -0,0 +1,206 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>rrweb Replay</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; font-family: sans-serif; background: #1a1a2e; color: #eee; }
#dropzone {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 999999; background: #1a1a2e;
}
#dropzone h2 { font-size: 22px; margin-bottom: 8px; }
#dropzone p { color: #aaa; margin-bottom: 20px; }
#dropzone .hint { font-size: 13px; color: #666; margin-top: 12px; }
#fileInput { display: none; }
.open-btn { padding: 12px 28px; border: 2px dashed #4CAF50; border-radius: 8px; background: transparent; color: #4CAF50; font-size: 16px; cursor: pointer; }
.open-btn:hover { background: #4CAF50; color: white; }
#controls {
position: fixed; top: 0; left: 0; right: 0; z-index: 99999;
background: #16213e; padding: 10px 16px; display: none; align-items: center; gap: 12px;
border-bottom: 1px solid #0f3460;
}
#controls button { padding: 6px 14px; border: none; border-radius: 4px; cursor: pointer; color: white; font-size: 13px; }
#controls button:hover { opacity: 0.85; }
.btn-play { background: #4CAF50; }
.btn-pause { background: #ff9800; }
.btn-speed { background: #2196F3; }
.btn-open { background: #9C27B0; }
#timeline { flex: 1; height: 6px; -webkit-appearance: none; appearance: none; background: #0f3460; border-radius: 3px; outline: none; }
#timeline::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #e94560; cursor: pointer; }
#info { font-size: 12px; color: #aaa; min-width: 120px; text-align: right; }
#replay-container { margin-top: 50px; }
</style>
<!-- Embed replay CSS inline so no external files needed -->
<script src="packages/rrweb/dist/rrweb.umd.min.cjs"></script>
<link rel="stylesheet" href="packages/rrweb/dist/style.min.css">
</head>
<body>
<!-- Drop zone: show when no events loaded -->
<div id="dropzone">
<h2>rrweb Replay</h2>
<p>Drag & drop a recording JSON file here</p>
<button class="open-btn" onclick="document.getElementById('fileInput').click()">Choose File</button>
<input type="file" id="fileInput" accept=".json">
<div class="hint">Or paste JSON via Ctrl+V</div>
</div>
<!-- Player controls -->
<div id="controls">
<button class="btn-open" id="openBtn">Open</button>
<button class="btn-play" id="playBtn">Play</button>
<button class="btn-pause" id="pauseBtn">Pause</button>
<input type="range" id="timeline" min="0" max="100" value="0">
<span id="info">0 / 0s</span>
<button class="btn-speed" id="speedBtn">1x</button>
</div>
<div id="replay-container"></div>
<script>
const dropzone = document.getElementById('dropzone');
const controls = document.getElementById('controls');
const container = document.getElementById('replay-container');
const playBtn = document.getElementById('playBtn');
const pauseBtn = document.getElementById('pauseBtn');
const timeline = document.getElementById('timeline');
const infoEl = document.getElementById('info');
const speedBtn = document.getElementById('speedBtn');
const openBtn = document.getElementById('openBtn');
const fileInput = document.getElementById('fileInput');
let replayer = null;
let playing = false;
let totalMs = 0;
let startMs = 0;
const speeds = [1, 2, 4, 8];
let speedIdx = 0;
function formatTime(ms) {
const s = Math.floor(ms / 1000);
const m = Math.floor(s / 60);
return m > 0 ? m + ':' + String(s % 60).padStart(2, '0') : s + 's';
}
function updateInfo(currentMs) {
infoEl.textContent = formatTime(currentMs) + ' / ' + formatTime(totalMs);
timeline.value = Math.round((currentMs / totalMs) * 100);
}
function startReplay(events) {
if (replayer) replayer.destroy();
container.innerHTML = '';
dropzone.style.display = 'none';
controls.style.display = 'flex';
playing = false;
playBtn.textContent = 'Play';
replayer = new rrweb.Replayer(events, {
root: container,
speed: speeds[speedIdx],
skipInactive: true,
showWarning: true,
});
const meta = replayer.getMetaData();
totalMs = meta.totalTime;
startMs = meta.startTime;
updateInfo(0);
replayer.on('finish', () => {
playing = false;
playBtn.textContent = 'Play';
updateInfo(totalMs);
});
}
function handleFile(file) {
const reader = new FileReader();
reader.onload = function(e) {
try {
const events = JSON.parse(e.target.result);
if (!Array.isArray(events) || events.length === 0) {
alert('Invalid or empty events file');
return;
}
startReplay(events);
} catch(err) {
alert('Failed to parse JSON: ' + err.message);
}
};
reader.readAsText(file);
}
// File input
fileInput.addEventListener('change', (e) => {
if (e.target.files[0]) handleFile(e.target.files[0]);
});
// Open button
openBtn.addEventListener('click', () => fileInput.click());
// Drag & drop
document.addEventListener('dragover', (e) => { e.preventDefault(); });
document.addEventListener('drop', (e) => {
e.preventDefault();
const file = e.dataTransfer.files[0];
if (file) handleFile(file);
});
// Paste JSON
document.addEventListener('paste', (e) => {
const text = e.clipboardData.getData('text');
try {
const events = JSON.parse(text);
if (Array.isArray(events) && events.length > 0) startReplay(events);
} catch {}
});
// Playback controls
playBtn.addEventListener('click', () => {
if (!playing && replayer) {
replayer.play();
playing = true;
playBtn.textContent = 'Playing...';
}
});
pauseBtn.addEventListener('click', () => {
if (replayer) {
replayer.pause();
playing = false;
playBtn.textContent = 'Play';
}
});
speedBtn.addEventListener('click', () => {
speedIdx = (speedIdx + 1) % speeds.length;
if (replayer) replayer.setConfig({ speed: speeds[speedIdx] });
speedBtn.textContent = speeds[speedIdx] + 'x';
});
timeline.addEventListener('input', () => {
if (!replayer) return;
const pct = parseInt(timeline.value) / 100;
const targetMs = totalMs * pct;
replayer.pause();
playing = false;
playBtn.textContent = 'Play';
replayer.play(targetMs);
setTimeout(() => { replayer.pause(); updateInfo(targetMs); }, 50);
});
// Timeline animation
function tick() {
if (playing && replayer) {
const ct = replayer.getCurrentTime();
updateInfo(ct);
}
requestAnimationFrame(tick);
}
tick();
</script>
</body>
</html>