From 2c0df95104aa71af0d46bb92694910e098578253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Wed, 4 Feb 2026 23:29:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=BD=95=E5=88=B6?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96=E6=B6=88=E6=81=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86=EF=BC=8C=E4=BF=AE=E5=A4=8D=E6=8B=BC=E5=86=99?= =?UTF-8?q?=E9=94=99=E8=AF=AF=EF=BC=8C=E6=9B=B4=E6=96=B0=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- entrypoints/background.ts | 13 ++-- entrypoints/content.ts | 70 +++++++++++++------ entrypoints/popup/pages/RecordeReplayPage.tsx | 17 ++++- utils/messages.tsx | 2 +- utils/recordUtils.tsx | 62 +++++++++------- utils/useRecorder.tsx | 6 +- wxt.config.ts | 1 + 7 files changed, 117 insertions(+), 54 deletions(-) diff --git a/entrypoints/background.ts b/entrypoints/background.ts index ffb44a1..6d1fdde 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -1,5 +1,8 @@ import '../.wxt/types/imports.d.ts'; import { browser } from 'wxt/browser'; +import { downloadHtmlInBackground } from '@/utils/recordUtils.tsx'; + +const events: unknown[] = []; export default defineBackground(() => { // 监听扩展安装或更新事件 @@ -36,8 +39,8 @@ export default defineBackground(() => { if (msg.type === messages.popup.checkStatus) { console.log('[bg] checkStatus received'); sendToActiveTab({ type: messages.content.checkStatus }) - .then(() => { - sendResponse({ ok: true }); + .then((res) => { + sendResponse(res); }) .catch(() => { console.error('Failed to check status in content script'); @@ -63,7 +66,8 @@ export default defineBackground(() => { console.log('[bg] stopRecording received'); sendToActiveTab({ type: messages.content.to.stopRecording }) .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 }); }) .catch(() => { @@ -78,6 +82,7 @@ export default defineBackground(() => { chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => { if (msg.type === messages.content.from.saveTrackeEvents) { console.log('[bg] saveTrackeEvents received:', msg.payload); + events.push(msg.payload); sendResponse({ ok: true }); } }); @@ -95,7 +100,7 @@ export default defineBackground(() => { const activeTab = activeTabs[0]; const sendTo = message.activeTabId || activeTab.id; try { - await chrome.tabs.sendMessage(sendTo!, message); + return await chrome.tabs.sendMessage(sendTo!, message); } catch (error) { console.error('Error sending message to active tab:', error); throw error; diff --git a/entrypoints/content.ts b/entrypoints/content.ts index cd78896..ff6cb6e 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -1,36 +1,66 @@ import '../.wxt/types/imports.d.ts'; import { createRecorder } from '@/utils/useRecorder'; +interface IResponse { + ok: boolean; + error?: string; + data?: unknown; +} + export default defineContentScript({ // matches: ['*://*.google.com/*'], matches: [''], runAt: 'document_start', main() { const recorder = createRecorder(); + let isRecording = false; // 监听来自 background script 的消息 - chrome.runtime.onMessage.addListener((msg: { type: string }, _sender, sendResponse) => { - if (msg.type === messages.content.checkStatus) { - // 检查状态的处理逻辑 - console.log('[content] checkStatus received'); - sendResponse({ ok: true }); - } + chrome.runtime.onMessage.addListener( + (msg: { type: string }, _sender, sendResponse: (response: IResponse) => void) => { + const handleAsyncMessage = async () => { + try { + switch (msg.type) { + // 在这里处理异步消息类型 + case messages.content.checkStatus: + return { ok: true, data: { isRecording } }; - if (msg.type === messages.content.to.startRecording) { - // 开始录制的处理逻辑 - console.log('[content] startRecording received'); - recorder.startRecord(messages.content.from.saveTrackeEvents); - sendResponse({ ok: true }); - } + case messages.content.to.startRecording: { + const started = await recorder.startRecord(messages.content.from.saveTrackeEvents); - if (msg.type === messages.content.to.stopRecording) { - // 停止录制的处理逻辑 - console.log('[content] stopRecording received'); - recorder.stopRecord(); - sendResponse({ ok: true }); - } + if (started) { + isRecording = true; + return { ok: true }; + } else { + // 如果启动失败,返回错误信息 + 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; + }, + ); }, }); diff --git a/entrypoints/popup/pages/RecordeReplayPage.tsx b/entrypoints/popup/pages/RecordeReplayPage.tsx index 2f32cf2..6055090 100644 --- a/entrypoints/popup/pages/RecordeReplayPage.tsx +++ b/entrypoints/popup/pages/RecordeReplayPage.tsx @@ -11,20 +11,31 @@ const RecordeReplayPage = () => { if (msg.type === messages.popup.ready) { setStatus(AppState.READ); } - + if (msg.type === messages.popup.to.started) { console.log('[popup] Received started message'); setStatus(AppState.RECORDING); } - if (msg.type === messages.popup.to.stoped) { + if (msg.type === messages.popup.to.stopped) { setStatus(AppState.READ); } }; 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); }, []); diff --git a/utils/messages.tsx b/utils/messages.tsx index 82a5b61..94d8070 100644 --- a/utils/messages.tsx +++ b/utils/messages.tsx @@ -5,7 +5,7 @@ export const messages = { stop: 'popup:stop', }, to: { - stoped: 'popup:stoped', + stopped: 'popup:stopped', started: 'popup:started', }, checkStatus: 'popup:check-status', diff --git a/utils/recordUtils.tsx b/utils/recordUtils.tsx index 93058e1..18c7ba0 100644 --- a/utils/recordUtils.tsx +++ b/utils/recordUtils.tsx @@ -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 数据直接注入到 - - - `; +`; - // 2. 创建 Blob 并下载 + // 下载逻辑 (保持不变) const blob = new Blob([htmlContent], { type: 'text/html' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `replay-${Date.now()}.html`; // 保存为 .html 文件 - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); + const reader = new FileReader(); + reader.onload = () => { + chrome.downloads.download({ + url: reader.result as string, + filename: `replay-${Date.now()}.html`, + saveAs: true, + }); + }; + reader.readAsDataURL(blob); }; diff --git a/utils/useRecorder.tsx b/utils/useRecorder.tsx index 334e958..949d4bb 100644 --- a/utils/useRecorder.tsx +++ b/utils/useRecorder.tsx @@ -1,12 +1,12 @@ import * as rrweb from 'rrweb'; import { listenerHandler } from '@rrweb/types'; +import { getRecordConsolePlugin } from '@rrweb/rrweb-plugin-console-record'; export const createRecorder = () => { let stopFn: listenerHandler | null = null; const startRecord = async (msg: string) => { try { - // const handler = null; const handler = rrweb.record({ emit(event) { chrome.runtime.sendMessage({ @@ -14,13 +14,16 @@ export const createRecorder = () => { payload: event, }); }, + plugins: [getRecordConsolePlugin()], }); stopFn = handler || null; console.log('[content] rrweb started'); + return true; } catch (error) { console.error('Failed to start recording:', error); + return false; } }; @@ -30,6 +33,7 @@ export const createRecorder = () => { stopFn = null; console.log('[content] rrweb stopped'); } + return false; }; return { startRecord, stopRecord }; diff --git a/wxt.config.ts b/wxt.config.ts index 9e041bc..4eaff28 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -15,6 +15,7 @@ export default defineConfig({ 'scripting', 'tabs', 'offscreen', + 'downloads', ], host_permissions: [''], action: {