Milestone v1 (v2.0.0): Mokosh — Session Capture #1

Merged
strategy155 merged 297 commits from gsd/phase-04-harden-clean-up-optional into main 2026-05-31 15:34:17 +00:00
Showing only changes of commit 886376e789 - Show all commits

View File

@@ -10,10 +10,8 @@ import JSZip from 'jszip';
const logger = new Logger('Main'); const logger = new Logger('Main');
// Состояние // Состояние
// videoBuffer is a placeholder array on the SW side; Plan 04 wires it to // Видеобуфер живёт в offscreen-документе (D-16). SW не хранит чанки локально:
// fetch from the offscreen recorder over the 'video-keepalive' port. // при экспорте он спрашивает буфер у offscreen через long-lived port (D-17).
// Until then it stays empty (the offscreen owns the real buffer per D-16).
let videoBuffer: VideoChunk[] = [];
let isRecording = false; let isRecording = false;
let offscreenCreated = false; let offscreenCreated = false;
let lastScreenshotTime = 0; let lastScreenshotTime = 0;
@@ -21,9 +19,10 @@ let cachedScreenshot: Blob | null = null;
// userEvents хранится только в content script // userEvents хранится только в content script
// Для архивации получаем его оттуда // Для архивации получаем его оттуда
// addVideoChunkFromBlob / cleanupVideoBuffer / VIDEO_BUFFER_DURATION_MS // Ring-buffer helpers (header-pin + age-trim) and the buffer duration
// removed in plan 01-03: the ring buffer now lives in src/offscreen/recorder.ts // constant were removed in Plan 01-03 the buffer now lives in
// (D-16). Plan 05 collapses the remaining SW shell further. // src/offscreen/recorder.ts per D-16. Plan 05 completes the SW shrink:
// see deletions below.
// Создание offscreen документа // Создание offscreen документа
async function ensureOffscreen() { async function ensureOffscreen() {
@@ -38,13 +37,14 @@ async function ensureOffscreen() {
await chrome.offscreen.createDocument({ await chrome.offscreen.createDocument({
url: url, url: url,
reasons: ['USER_MEDIA'] as any, reasons: [chrome.offscreen.Reason.DISPLAY_MEDIA],
justification: 'Need to record video from tab for error reporting' justification: 'Continuous screen recording for operator session diagnostics'
}); });
offscreenCreated = true; offscreenCreated = true;
logger.log('Offscreen document created successfully'); logger.log('Offscreen document created successfully');
} catch (error) { } catch (error) {
if ((error as any).message?.includes('already exists')) { const msg = error instanceof Error ? error.message : String(error);
if (msg.includes('already exists')) {
offscreenCreated = true; offscreenCreated = true;
logger.log('Offscreen document already exists'); logger.log('Offscreen document already exists');
} else { } else {
@@ -71,22 +71,15 @@ async function startVideoCapture() {
logger.log(`Starting video capture for tab ${tab.id}: ${tab.url}`); logger.log(`Starting video capture for tab ${tab.id}: ${tab.url}`);
// Создаём offscreen документ // Создаём offscreen документ (с reason из D-02)
await ensureOffscreen(); await ensureOffscreen();
// Получаем streamId для записи вкладки (без диалога) // Просим offscreen запустить запись — getDisplayMedia вызывается там
logger.log('Getting tab media stream ID...'); // (D-01: больше нет SW-side stream-id юзаства).
const streamId = await (chrome.tabCapture as any).getMediaStreamId({
targetTabId: tab.id
});
logger.log('Got stream ID:', streamId?.substring(0, 20) + '...');
// Отправляем в offscreen через chrome.runtime.sendMessage
logger.log('Sending START_RECORDING to offscreen...'); logger.log('Sending START_RECORDING to offscreen...');
try { try {
await chrome.runtime.sendMessage({ await chrome.runtime.sendMessage({
type: 'START_RECORDING', type: 'START_RECORDING'
streamId: streamId
}); });
logger.log('START_RECORDING sent successfully'); logger.log('START_RECORDING sent successfully');
} catch (msgError) { } catch (msgError) {
@@ -104,22 +97,14 @@ async function startVideoCapture() {
} }
} }
// Keepalive для предотвращения выгрузки Service Worker // Keepalive теперь обеспечивается long-lived портом offscreen→SW (D-17/D-18).
function setupKeepalive() { // Старая alarms-based реализация удалена: alarm callbacks не сбрасывали SW idle
chrome.alarms.create('keepalive', { periodInMinutes: 0.33 }); // 20 секунд // timer (audit P1 #8), а порт сбрасывает таймер на каждое сообщение.
chrome.alarms.onAlarm.addListener((alarm) => { // Получение видеобуфера (временный синхронный стаб; Task 2 заменит его
if (alarm.name === 'keepalive') { // на асинхронный запрос к offscreen через long-lived port).
logger.log('Keepalive ping');
}
});
}
// Получение видеобуфера
function getVideoBuffer(): VideoBufferResponse { function getVideoBuffer(): VideoBufferResponse {
return { return { chunks: [] };
chunks: videoBuffer
};
} }
// Получение скриншота активной вкладки // Получение скриншота активной вкладки
@@ -291,14 +276,13 @@ async function saveArchive() {
try { try {
logger.log(`Sending GET_RRWEB_EVENTS message to tab ${tab.id}...`); logger.log(`Sending GET_RRWEB_EVENTS message to tab ${tab.id}...`);
const response = await chrome.tabs.sendMessage(tab.id, { const response: { events?: unknown[]; userEvents?: unknown[] } | undefined =
type: 'GET_RRWEB_EVENTS' await chrome.tabs.sendMessage(tab.id, { type: 'GET_RRWEB_EVENTS' });
}) as any;
logger.log(`Got response from tab ${tab.id}:`, response); logger.log(`Got response from tab ${tab.id}:`, response);
rrwebEvents = response?.events || []; rrwebEvents = response?.events ?? [];
userEvents = response?.userEvents || []; userEvents = response?.userEvents ?? [];
logger.log(`✓ Received ${rrwebEvents.length} rrweb events, ${userEvents.length} user events`); logger.log(`✓ Received ${rrwebEvents.length} rrweb events, ${userEvents.length} user events`);
@@ -337,42 +321,10 @@ async function saveArchive() {
} }
} }
// Проверка разрешений // checkPermissions / requestPermissions удалены: старая permission
async function checkPermissions(): Promise<boolean> { // больше не нужна (D-A6 — заменена на desktopCapture в manifest), а
try { // getDisplayMedia не требует runtime-разрешения — нужен только user gesture.
// Проверяем tabCapture // REQUEST_PERMISSIONS теперь просто запускает запись и возвращает granted=true.
const hasTabCapture = await chrome.permissions.contains({
permissions: ['tabCapture']
});
logger.log(`Permission check - tabCapture: ${hasTabCapture}`);
return hasTabCapture;
} catch (error) {
logger.error('Permission check failed:', error);
return false;
}
}
// Запрос разрешений
async function requestPermissions(): Promise<boolean> {
try {
const granted = await chrome.permissions.request({
permissions: ['tabCapture']
});
logger.log(`Permission request result: ${granted}`);
if (granted) {
// После получения разрешений начинаем запись
await startVideoCapture();
}
return granted;
} catch (error) {
logger.error('Permission request failed:', error);
return false;
}
}
// Обработка сообщений // Обработка сообщений
chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) => { chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) => {
@@ -380,19 +332,20 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
switch (message.type) { switch (message.type) {
case 'REQUEST_PERMISSIONS': case 'REQUEST_PERMISSIONS':
checkPermissions().then(async (hasPermissions) => { // Под getDisplayMedia (D-01) runtime-permission проверять нечего —
if (hasPermissions) { // браузер сам покажет picker по user gesture из popup. Просто
// Разрешения уже есть, запускаем запись видео // запускаем запись и подтверждаем popup-у.
(async () => {
try {
if (!isRecording) { if (!isRecording) {
await startVideoCapture(); await startVideoCapture();
} }
sendResponse({ granted: true }); sendResponse({ granted: true });
} else { } catch (error) {
requestPermissions().then(granted => { logger.error('startVideoCapture failed:', error);
sendResponse({ granted }); sendResponse({ granted: false });
});
} }
}); })();
return true; return true;
case 'GET_VIDEO_BUFFER': case 'GET_VIDEO_BUFFER':
@@ -405,13 +358,13 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
}); });
return true; return true;
// VIDEO_CHUNK and VIDEO_CHUNK_SAVED handlers removed in plan 01-03: // Legacy chunk-streaming and IndexedDB save/load handlers were removed
// in Plan 01-03:
// - the offscreen recorder now owns the buffer (D-16); // - the offscreen recorder now owns the buffer (D-16);
// - chunks no longer travel via chrome.runtime.sendMessage (D-19); // - chunks no longer travel via chrome.runtime.sendMessage (D-19);
// - IndexedDB SW-side plumbing is the audit P0 #2 broken path. // - SW-side IDB plumbing was the audit P0 #2 broken path.
// loadChunkFromIndexedDB / openIndexedDB also removed inline (they // The IDB helpers were only reachable from those deleted cases.
// were only reachable from the deleted VIDEO_CHUNK_SAVED branch). // Plan 05 finishes the SW shrink (see deletions above).
// Plan 05 collapses the remaining SW dead code further.
default: default:
logger.warn('Unknown message type:', message.type); logger.warn('Unknown message type:', message.type);
@@ -422,7 +375,6 @@ chrome.runtime.onMessage.addListener((message: Message, _sender, sendResponse) =
// Инициализация // Инициализация
function initialize() { function initialize() {
logger.log('Service Worker initializing'); logger.log('Service Worker initializing');
setupKeepalive();
logger.log('Service Worker initialized'); logger.log('Service Worker initialized');
} }