feat: 添加录制功能,优化消息处理,修复拼写错误,更新下载逻辑
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
+50
-20
@@ -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 } };
|
||||||
|
|
||||||
if (msg.type === messages.content.to.startRecording) {
|
case messages.content.to.startRecording: {
|
||||||
// 开始录制的处理逻辑
|
const started = await recorder.startRecord(messages.content.from.saveTrackeEvents);
|
||||||
console.log('[content] startRecording received');
|
|
||||||
recorder.startRecord(messages.content.from.saveTrackeEvents);
|
|
||||||
sendResponse({ ok: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.type === messages.content.to.stopRecording) {
|
if (started) {
|
||||||
// 停止录制的处理逻辑
|
isRecording = true;
|
||||||
console.log('[content] stopRecording received');
|
return { ok: true };
|
||||||
recorder.stopRecord();
|
} else {
|
||||||
sendResponse({ ok: true });
|
// 如果启动失败,返回错误信息
|
||||||
}
|
return { ok: false, error: 'Failed to start recorder' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
case messages.content.to.stopRecording:
|
||||||
});
|
// 停止录制的处理逻辑
|
||||||
|
console.log('[content] stopRecording received');
|
||||||
|
recorder.stopRecord();
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,20 +11,31 @@ const RecordeReplayPage = () => {
|
|||||||
if (msg.type === messages.popup.ready) {
|
if (msg.type === messages.popup.ready) {
|
||||||
setStatus(AppState.READ);
|
setStatus(AppState.READ);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === messages.popup.to.started) {
|
if (msg.type === messages.popup.to.started) {
|
||||||
console.log('[popup] Received started message');
|
console.log('[popup] Received started message');
|
||||||
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
@@ -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',
|
||||||
|
|||||||
+37
-25
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export default defineConfig({
|
|||||||
'scripting',
|
'scripting',
|
||||||
'tabs',
|
'tabs',
|
||||||
'offscreen',
|
'offscreen',
|
||||||
|
'downloads',
|
||||||
],
|
],
|
||||||
host_permissions: ['<all_urls>'],
|
host_permissions: ['<all_urls>'],
|
||||||
action: {
|
action: {
|
||||||
|
|||||||
Reference in New Issue
Block a user