feat: 添加录制功能,优化消息处理,修复拼写错误,更新下载逻辑

This commit is contained in:
雨霖铃
2026-02-04 23:29:42 +08:00
parent e6f41032c8
commit 2c0df95104
7 changed files with 117 additions and 54 deletions
+9 -4
View File
@@ -1,5 +1,8 @@
import '../.wxt/types/imports.d.ts'; import '../.wxt/types/imports.d.ts';
import { browser } from 'wxt/browser'; import { browser } from 'wxt/browser';
import { downloadHtmlInBackground } from '@/utils/recordUtils.tsx';
const events: unknown[] = [];
export default defineBackground(() => { export default defineBackground(() => {
// 监听扩展安装或更新事件 // 监听扩展安装或更新事件
@@ -36,8 +39,8 @@ export default defineBackground(() => {
if (msg.type === messages.popup.checkStatus) { if (msg.type === messages.popup.checkStatus) {
console.log('[bg] checkStatus received'); console.log('[bg] checkStatus received');
sendToActiveTab({ type: messages.content.checkStatus }) sendToActiveTab({ type: messages.content.checkStatus })
.then(() => { .then((res) => {
sendResponse({ ok: true }); sendResponse(res);
}) })
.catch(() => { .catch(() => {
console.error('Failed to check status in content script'); console.error('Failed to check status in content script');
@@ -63,7 +66,8 @@ export default defineBackground(() => {
console.log('[bg] stopRecording received'); console.log('[bg] stopRecording received');
sendToActiveTab({ type: messages.content.to.stopRecording }) sendToActiveTab({ type: messages.content.to.stopRecording })
.then(async () => { .then(async () => {
await chrome.runtime.sendMessage({ type: messages.popup.to.stoped }); await chrome.runtime.sendMessage({ type: messages.popup.to.stopped });
downloadHtmlInBackground(events);
sendResponse({ ok: true }); sendResponse({ ok: true });
}) })
.catch(() => { .catch(() => {
@@ -78,6 +82,7 @@ export default defineBackground(() => {
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg.type === messages.content.from.saveTrackeEvents) { if (msg.type === messages.content.from.saveTrackeEvents) {
console.log('[bg] saveTrackeEvents received:', msg.payload); console.log('[bg] saveTrackeEvents received:', msg.payload);
events.push(msg.payload);
sendResponse({ ok: true }); sendResponse({ ok: true });
} }
}); });
@@ -95,7 +100,7 @@ export default defineBackground(() => {
const activeTab = activeTabs[0]; const activeTab = activeTabs[0];
const sendTo = message.activeTabId || activeTab.id; const sendTo = message.activeTabId || activeTab.id;
try { try {
await chrome.tabs.sendMessage(sendTo!, message); return await chrome.tabs.sendMessage(sendTo!, message);
} catch (error) { } catch (error) {
console.error('Error sending message to active tab:', error); console.error('Error sending message to active tab:', error);
throw error; throw error;
+45 -15
View File
@@ -1,36 +1,66 @@
import '../.wxt/types/imports.d.ts'; import '../.wxt/types/imports.d.ts';
import { createRecorder } from '@/utils/useRecorder'; import { createRecorder } from '@/utils/useRecorder';
interface IResponse {
ok: boolean;
error?: string;
data?: unknown;
}
export default defineContentScript({ export default defineContentScript({
// matches: ['*://*.google.com/*'], // matches: ['*://*.google.com/*'],
matches: ['<all_urls>'], matches: ['<all_urls>'],
runAt: 'document_start', runAt: 'document_start',
main() { main() {
const recorder = createRecorder(); const recorder = createRecorder();
let isRecording = false;
// 监听来自 background script 的消息 // 监听来自 background script 的消息
chrome.runtime.onMessage.addListener((msg: { type: string }, _sender, sendResponse) => { chrome.runtime.onMessage.addListener(
if (msg.type === messages.content.checkStatus) { (msg: { type: string }, _sender, sendResponse: (response: IResponse) => void) => {
// 检查状态的处理逻辑 const handleAsyncMessage = async () => {
console.log('[content] checkStatus received'); try {
sendResponse({ ok: true }); switch (msg.type) {
// 在这里处理异步消息类型
case messages.content.checkStatus:
return { ok: true, data: { isRecording } };
case messages.content.to.startRecording: {
const started = await recorder.startRecord(messages.content.from.saveTrackeEvents);
if (started) {
isRecording = true;
return { ok: true };
} else {
// 如果启动失败,返回错误信息
return { ok: false, error: 'Failed to start recorder' };
}
} }
if (msg.type === messages.content.to.startRecording) { case messages.content.to.stopRecording:
// 开始录制的处理逻辑
console.log('[content] startRecording received');
recorder.startRecord(messages.content.from.saveTrackeEvents);
sendResponse({ ok: true });
}
if (msg.type === messages.content.to.stopRecording) {
// 停止录制的处理逻辑 // 停止录制的处理逻辑
console.log('[content] stopRecording received'); console.log('[content] stopRecording received');
recorder.stopRecord(); recorder.stopRecord();
sendResponse({ ok: true }); isRecording = false;
return { ok: true };
default:
return { ok: false, error: 'Unknown message type' };
} }
} catch (error) {
console.error('Error handling message:', error);
return { ok: false };
}
};
handleAsyncMessage().then((response) => {
if (sendResponse) {
sendResponse(response);
}
});
return true; return true;
}); },
);
}, },
}); });
+13 -2
View File
@@ -17,14 +17,25 @@ const RecordeReplayPage = () => {
setStatus(AppState.RECORDING); setStatus(AppState.RECORDING);
} }
if (msg.type === messages.popup.to.stoped) { if (msg.type === messages.popup.to.stopped) {
setStatus(AppState.READ); setStatus(AppState.READ);
} }
}; };
browser.runtime.onMessage.addListener(handleMessage); browser.runtime.onMessage.addListener(handleMessage);
browser.runtime.sendMessage({ type: messages.popup.checkStatus }); browser.runtime
.sendMessage({ type: messages.popup.checkStatus })
.then((res) => {
if (res?.data?.isRecording) {
setStatus(AppState.RECORDING);
} else {
setStatus(AppState.READ);
}
})
.catch((err) => {
console.error(err);
});
return () => browser.runtime.onMessage.removeListener(handleMessage); return () => browser.runtime.onMessage.removeListener(handleMessage);
}, []); }, []);
+1 -1
View File
@@ -5,7 +5,7 @@ export const messages = {
stop: 'popup:stop', stop: 'popup:stop',
}, },
to: { to: {
stoped: 'popup:stoped', stopped: 'popup:stopped',
started: 'popup:started', started: 'popup:started',
}, },
checkStatus: 'popup:check-status', checkStatus: 'popup:check-status',
+36 -24
View File
@@ -1,53 +1,65 @@
import { eventWithTime } from 'rrweb'; export const downloadHtmlInBackground = (events: unknown[]) => {
if (!events || events.length === 0) return;
export const downloadHtml = (events: eventWithTime[]) => { // 安全转义 (保持之前的修复)
if (events.length === 0) return; const safeEventsString = JSON.stringify(events)
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/<\/script>/g, '<\\/script>');
// 1. 构建 HTML 模板字符串
// 我们将 events 数据直接注入到 <script> 标签中
const htmlContent = ` const htmlContent = `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>RRWeb 回放</title>
<title>RRWeb 录像回放</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/style.css" />
</head> </head>
<body style="margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5;"> <body style="margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #f0f2f5;">
<div id="player"></div> <div id="player"></div>
<script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script> <script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script>
<script> <script type="module">
/* 注入录制的数据 */ import { getReplayConsolePlugin } from 'https://esm.sh/@rrweb/rrweb-plugin-console-replay?bundle';
const events = ${JSON.stringify(events)};
const events = JSON.parse(\`${safeEventsString}\`);
/* 初始化播放器 */
new rrwebPlayer({ new rrwebPlayer({
target: document.getElementById('player'), target: document.getElementById('player'),
props: { props: {
events: events, events: events,
width: 1024, // 可以根据需要调整 width: 1024,
height: 576, height: 576,
autoPlay: true, autoPlay: true,
showController: true, showController: true,
// --- 👇 关键修复:添加下面这行 ---
// 这会告诉 rrweb 关闭严格的 iframe 沙盒限制
// 从而消除 "Blocked script execution" 错误
UNSAFE_replayCanvas: true,
// ------------------------------
plugins: [
getReplayConsolePlugin({
level: ['info', 'log', 'warn', 'error'],
})
]
}, },
}); });
</script> </script>
</body> </body>
</html> </html>`;
`;
// 2. 创建 Blob 并下载 // 下载逻辑 (保持不变)
const blob = new Blob([htmlContent], { type: 'text/html' }); const blob = new Blob([htmlContent], { type: 'text/html' });
const url = URL.createObjectURL(blob); const reader = new FileReader();
const a = document.createElement('a'); reader.onload = () => {
a.href = url; chrome.downloads.download({
a.download = `replay-${Date.now()}.html`; // 保存为 .html 文件 url: reader.result as string,
document.body.appendChild(a); filename: `replay-${Date.now()}.html`,
a.click(); saveAs: true,
document.body.removeChild(a); });
URL.revokeObjectURL(url); };
reader.readAsDataURL(blob);
}; };
+5 -1
View File
@@ -1,12 +1,12 @@
import * as rrweb from 'rrweb'; import * as rrweb from 'rrweb';
import { listenerHandler } from '@rrweb/types'; import { listenerHandler } from '@rrweb/types';
import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record';
export const createRecorder = () => { export const createRecorder = () => {
let stopFn: listenerHandler | null = null; let stopFn: listenerHandler | null = null;
const startRecord = async (msg: string) => { const startRecord = async (msg: string) => {
try { try {
// const handler = null;
const handler = rrweb.record({ const handler = rrweb.record({
emit(event) { emit(event) {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
@@ -14,13 +14,16 @@ export const createRecorder = () => {
payload: event, payload: event,
}); });
}, },
plugins: [getRecordConsolePlugin()],
}); });
stopFn = handler || null; stopFn = handler || null;
console.log('[content] rrweb started'); console.log('[content] rrweb started');
return true;
} catch (error) { } catch (error) {
console.error('Failed to start recording:', error); console.error('Failed to start recording:', error);
return false;
} }
}; };
@@ -30,6 +33,7 @@ export const createRecorder = () => {
stopFn = null; stopFn = null;
console.log('[content] rrweb stopped'); console.log('[content] rrweb stopped');
} }
return false;
}; };
return { startRecord, stopRecord }; return { startRecord, stopRecord };
+1
View File
@@ -15,6 +15,7 @@ export default defineConfig({
'scripting', 'scripting',
'tabs', 'tabs',
'offscreen', 'offscreen',
'downloads',
], ],
host_permissions: ['<all_urls>'], host_permissions: ['<all_urls>'],
action: { action: {