feat: 添加内置回放界面和错误修复
This commit is contained in:
@@ -9,6 +9,10 @@ interface ProtocolMap {
|
||||
'popup:tab-changed': (data: { currentTabId: number; recordingTabId: number | undefined }) => void;
|
||||
'popup:check-status': () => { active: boolean; startTime?: number };
|
||||
'popup:ready': () => void;
|
||||
'popup:get-sessions': () => { id: string; startTime: number; tabId: number; chunkCount: number; totalEvents: number }[];
|
||||
'popup:get-session-events': (sessionId: string) => unknown[];
|
||||
'popup:delete-session': (sessionId: string) => { ok: boolean };
|
||||
'popup:download-session': (sessionId: string) => { ok: boolean };
|
||||
|
||||
// --- Content 相关 ---
|
||||
'content:save-track-events': (event: unknown) => boolean;
|
||||
|
||||
+94
-20
@@ -89,18 +89,33 @@ export async function initRecordingSession(sessionId: string, tabId: number): Pr
|
||||
* 获取当前会话的 chunk 数量
|
||||
*/
|
||||
export async function getSessionChunkCount(sessionId: string): Promise<number> {
|
||||
// 参数验证
|
||||
if (!sessionId || typeof sessionId !== 'string') {
|
||||
console.error('[db] Invalid sessionId for getSessionChunkCount:', sessionId);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const database = await openDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise((resolve) => {
|
||||
const tx = database.transaction('sessions', 'readonly');
|
||||
const store = tx.objectStore('sessions');
|
||||
const request = store.get(sessionId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const session = request.result as RecordingSession | undefined;
|
||||
resolve(session?.chunkCount || 0);
|
||||
};
|
||||
request.onerror = () => reject(request.error);
|
||||
try {
|
||||
const request = store.get(sessionId);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const session = request.result as RecordingSession | undefined;
|
||||
resolve(session?.chunkCount || 0);
|
||||
};
|
||||
request.onerror = (e) => {
|
||||
console.error('[db] Error getting session chunk count:', e);
|
||||
resolve(0);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[db] Exception getting session chunk count:', error);
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -150,14 +165,28 @@ async function getChunk(
|
||||
sessionId: string,
|
||||
chunkIndex: number,
|
||||
): Promise<EventChunk | null> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = database.transaction(STORE_NAME, 'readonly');
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const index = store.index('sessionId_chunkIndex');
|
||||
const request = index.get([sessionId, chunkIndex]);
|
||||
// 参数验证
|
||||
if (!sessionId || typeof sessionId !== 'string') {
|
||||
console.error('[db] Invalid sessionId for getChunk:', sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = () => reject(request.error);
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const tx = database.transaction(STORE_NAME, 'readonly');
|
||||
const store = tx.objectStore(STORE_NAME);
|
||||
const index = store.index('sessionId_chunkIndex');
|
||||
const request = index.get([sessionId, chunkIndex]);
|
||||
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
request.onerror = (e) => {
|
||||
console.error('[db] Error getting chunk:', e);
|
||||
resolve(null);
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[db] Exception getting chunk:', error);
|
||||
resolve(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,14 +240,26 @@ export async function streamAllEvents(
|
||||
sessionId: string,
|
||||
onChunk: (events: unknown[]) => void | Promise<void>,
|
||||
): Promise<void> {
|
||||
const database = await openDB();
|
||||
const chunkCount = await getSessionChunkCount(sessionId);
|
||||
// 参数验证
|
||||
if (!sessionId || typeof sessionId !== 'string') {
|
||||
console.error('[db] Invalid sessionId for streamAllEvents:', sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const chunk = await getChunk(database, sessionId, i);
|
||||
if (chunk && chunk.events.length > 0) {
|
||||
await onChunk(chunk.events);
|
||||
try {
|
||||
const database = await openDB();
|
||||
const chunkCount = await getSessionChunkCount(sessionId);
|
||||
|
||||
console.log(`[db] Streaming ${chunkCount} chunks for session ${sessionId}`);
|
||||
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const chunk = await getChunk(database, sessionId, i);
|
||||
if (chunk && chunk.events.length > 0) {
|
||||
await onChunk(chunk.events);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[db] Error streaming events:', error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,6 +268,12 @@ export async function streamAllEvents(
|
||||
* 注意:这会将所有事件加载到内存,仅在需要导出时调用
|
||||
*/
|
||||
export async function getAllEvents(sessionId: string): Promise<unknown[]> {
|
||||
// 参数验证
|
||||
if (!sessionId || typeof sessionId !== 'string') {
|
||||
console.error('[db] Invalid sessionId for getAllEvents:', sessionId);
|
||||
return [];
|
||||
}
|
||||
|
||||
const allEvents: unknown[] = [];
|
||||
|
||||
await streamAllEvents(sessionId, (events) => {
|
||||
@@ -271,6 +318,33 @@ export async function deleteRecordingSession(sessionId: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有录制会话列表
|
||||
*/
|
||||
export async function getAllSessions(): Promise<RecordingSession[]> {
|
||||
const database = await openDB();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const tx = database.transaction('sessions', 'readonly');
|
||||
const store = tx.objectStore('sessions');
|
||||
const index = store.index('startTime');
|
||||
const request = index.openCursor(null, 'prev'); // 按时间倒序排列
|
||||
const sessions: RecordingSession[] = [];
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;
|
||||
if (cursor) {
|
||||
sessions.push(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(sessions);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一的会话 ID
|
||||
*/
|
||||
|
||||
+46
-27
@@ -7,46 +7,65 @@ export const downloadHtmlInBackground = (events: unknown[]) => {
|
||||
.replace(/`/g, '\\`')
|
||||
.replace(/<\/script>/g, '<\\/script>');
|
||||
|
||||
// 使用固定版本的 rrweb-player,确保与项目使用的 rrweb 版本兼容
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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@1.0.0-alpha.4/dist/style.css" />
|
||||
<style>
|
||||
body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; background: #f0f2f5; }
|
||||
#player { width: 100%; max-width: 1024px; }
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
#player {
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="player"></div>
|
||||
|
||||
<!-- 使用 UMD 版本,rrwebPlayer 会作为全局变量 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/rrweb-player@latest/dist/index.js"></script>
|
||||
<!-- 使用与项目 rrweb 版本匹配的 rrweb-player -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/rrweb-player@1.0.0-alpha.4/dist/index.js"></script>
|
||||
|
||||
<script type="module">
|
||||
import { getReplayConsolePlugin } from 'https://esm.sh/@rrweb/rrweb-plugin-console-replay?bundle';
|
||||
<script>
|
||||
try {
|
||||
// 解析事件数据
|
||||
const events = JSON.parse(\`${safeEventsString}\`);
|
||||
console.log('Loaded events count:', events.length);
|
||||
|
||||
const events = JSON.parse(\`${safeEventsString}\`);
|
||||
// 创建播放器
|
||||
const player = new rrwebPlayer({
|
||||
target: document.getElementById('player'),
|
||||
props: {
|
||||
events: events,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
autoPlay: true,
|
||||
showController: true,
|
||||
UNSAFE_replayCanvas: true
|
||||
},
|
||||
});
|
||||
|
||||
// rrwebPlayer 作为全局变量可用
|
||||
new rrwebPlayer({
|
||||
target: document.getElementById('player'),
|
||||
props: {
|
||||
events: events,
|
||||
width: 1024,
|
||||
height: 576,
|
||||
autoPlay: true,
|
||||
showController: true,
|
||||
UNSAFE_replayCanvas: true,
|
||||
plugins: [
|
||||
getReplayConsolePlugin({
|
||||
level: ['info', 'log', 'warn', 'error'],
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
console.log('Player created successfully');
|
||||
} catch (error) {
|
||||
console.error('Replay error:', error);
|
||||
document.body.innerHTML = \`
|
||||
<div style="padding: 20px; text-align: center; color: #d32f2f;">
|
||||
<h2>回放失败</h2>
|
||||
<p>\${error.message}</p>
|
||||
<p>请检查控制台(F12)获取详细错误信息。</p>
|
||||
</div>
|
||||
\`;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
@@ -61,7 +80,7 @@ export const downloadHtmlInBackground = (events: unknown[]) => {
|
||||
filename: `replay-${Date.now()}.html`,
|
||||
saveAs: true,
|
||||
})
|
||||
.then((r) => console.log(r));
|
||||
.then((r) => console.log('Download started:', r));
|
||||
};
|
||||
reader.readAsDataURL(blob);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user