chore: import broken Phase-1 extension as received

Snapshot of /home/parf/Downloads/manifest.zip as delivered, before any
GSD-driven remediation. Contains a partially-broken first attempt at the
Russian SPEC "Тз расширение фаза1.md" (Phase 1 of operator-session-recorder).

Source layout:
- manifest.json — MV3 declaration with tabCapture/activeTab/downloads/etc.
- src/background/index.ts — service worker (video buffer + archive packaging)
- src/content/index.ts — rrweb + user-event logger
- src/popup/{index.html,index.ts,style.css} — Russian popup UI
- offscreen/{index.html,index.ts} — orphaned offscreen (see audit)
- vite.config.ts — inline plugin emitting a separate live offscreen.js
- generate-icons.js, icons/ — minimal PNG icons
- "Тз расширение фаза1.md" — authoritative Russian SPEC

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-15 15:16:23 +02:00
commit 555eb0543f
21 changed files with 3935 additions and 0 deletions

358
src/content/index.ts Normal file
View File

@@ -0,0 +1,358 @@
import { record } from 'rrweb';
import { ContentLogger } from '../shared/logger';
import type { UserEvent } from '../shared/types';
interface RrwebEventsResponse {
events: EventWithTime[];
userEvents: UserEvent[];
}
type EventWithTime = any;
const logger = new ContentLogger('Main');
// Константы
const RRWEB_MAX_EVENTS = 5000;
const RRWEB_RETENTION_MS = 10 * 60 * 1000; // 10 минут
const USER_EVENT_RETENTION_MS = 10 * 60 * 1000; // 10 минут
const CLEANUP_INTERVAL_MS = 60 * 1000; // 1 минута
// Буфер rrweb событий
let rrwebEvents: EventWithTime[] = [];
// Буфер пользовательских событий
let userEvents: UserEvent[] = [];
// Очистка старых событий
function cleanupOldEvents() {
const now = Date.now();
// Очистка rrweb событий
const rrwebBeforeCount = rrwebEvents.length;
rrwebEvents = rrwebEvents.filter(event => {
return (now - event.timestamp) < RRWEB_RETENTION_MS;
});
logger.log(`rrweb cleanup: ${rrwebBeforeCount} -> ${rrwebEvents.length} events`);
// Если все равно слишком много событий, удаляем самые старые
if (rrwebEvents.length > RRWEB_MAX_EVENTS) {
const toRemove = rrwebEvents.length - RRWEB_MAX_EVENTS;
rrwebEvents = rrwebEvents.slice(toRemove);
logger.log(`rrweb limit exceeded, removed ${toRemove} oldest events`);
}
// Очистка пользовательских событий
const userBeforeCount = userEvents.length;
userEvents = userEvents.filter(event => {
return (now - event.timestamp) < USER_EVENT_RETENTION_MS;
});
logger.log(`user events cleanup: ${userBeforeCount} -> ${userEvents.length} events`);
}
// Добавление пользовательского события
function addUserEvent(event: UserEvent) {
event.timestamp = Date.now();
event.url = window.location.href;
userEvents.push(event);
logger.log(`User event: ${event.type}`, event);
}
// Логирование кликов
function setupClickLogging() {
document.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const selector = getSelector(target);
addUserEvent({
timestamp: Date.now(),
type: 'click',
target: selector,
value: target.textContent?.substring(0, 100) || '',
url: window.location.href
});
}, true);
}
// Логирование ввода (без паролей)
function setupInputLogging() {
document.addEventListener('input', (event) => {
const target = event.target as HTMLInputElement;
// Пропускаем пароли
if (target.type === 'password') {
return;
}
const selector = getSelector(target);
addUserEvent({
timestamp: Date.now(),
type: 'input',
target: selector,
value: target.value.substring(0, 200), // Ограничиваем длину
url: window.location.href
});
}, true);
}
// Логирование навигации
function setupNavigationLogging() {
const handleNavigation = () => {
addUserEvent({
timestamp: Date.now(),
type: 'navigation',
target: 'window',
value: window.location.href,
url: window.location.href,
meta: { previousUrl: history.state?.url || 'unknown' }
});
};
window.addEventListener('popstate', handleNavigation);
window.addEventListener('hashchange', handleNavigation);
// Интерсепт History API
// @ts-ignore
const originalPushState = history.pushState;
// @ts-ignore
const originalReplaceState = history.replaceState;
// @ts-ignore
history.pushState = function(...args) {
originalPushState.apply(this, args);
handleNavigation();
};
// @ts-ignore
history.replaceState = function(...args) {
originalReplaceState.apply(this, args);
handleNavigation();
};
}
// Логирование JS ошибок
function setupErrorLogging() {
window.addEventListener('error', (event) => {
addUserEvent({
timestamp: Date.now(),
type: 'js_error',
target: event.filename || 'unknown',
value: event.message,
url: window.location.href,
meta: {
line: event.lineno,
column: event.colno,
stack: event.error?.stack
}
});
});
window.addEventListener('unhandledrejection', (event) => {
addUserEvent({
timestamp: Date.now(),
type: 'js_error',
target: 'unhandledrejection',
value: event.reason?.toString() || 'Unknown rejection',
url: window.location.href,
meta: {
promise: event.promise.toString()
}
});
});
}
// Логирование сетевых ошибок
function setupNetworkLogging() {
// Перехват fetch
const originalFetch = window.fetch;
window.fetch = function(...args) {
return originalFetch.apply(this, args)
.then(response => {
if (!response.ok) {
addUserEvent({
timestamp: Date.now(),
type: 'network_error',
target: args[0]?.toString() || 'unknown',
value: `HTTP ${response.status} ${response.statusText}`,
url: window.location.href,
meta: {
status: response.status,
statusText: response.statusText,
url: response.url
}
});
}
return response;
})
.catch(error => {
addUserEvent({
timestamp: Date.now(),
type: 'network_error',
target: args[0]?.toString() || 'unknown',
value: error.message,
url: window.location.href,
meta: {
errorType: error.name
}
});
throw error;
});
};
// Перехват XMLHttpRequest
// @ts-ignore
const originalXHROpen = XMLHttpRequest.prototype.open;
// @ts-ignore
const originalXHRSend = XMLHttpRequest.prototype.send;
// @ts-ignore
XMLHttpRequest.prototype.open = function(...args) {
(this as any)._method = args[0];
(this as any)._url = args[1];
// @ts-ignore
return originalXHROpen.apply(this, args);
};
// @ts-ignore
XMLHttpRequest.prototype.send = function(...args) {
this.addEventListener('loadend', function() {
const xhr = this as XMLHttpRequest;
if (xhr.status >= 400 || xhr.status === 0) {
addUserEvent({
timestamp: Date.now(),
type: 'network_error',
target: (xhr as any)._url || 'unknown',
value: `HTTP ${xhr.status} ${xhr.statusText || 'Network Error'}`,
url: window.location.href,
meta: {
method: (xhr as any)._method,
status: xhr.status,
statusText: xhr.statusText
}
});
}
});
// @ts-ignore
return originalXHRSend.apply(this, args);
};
}
// Получение CSS селектора элемента
function getSelector(element: HTMLElement): string {
if (element.id) {
return `#${element.id}`;
}
const parts: string[] = [];
let current: HTMLElement | null = element;
while (current && current.nodeType === Node.ELEMENT_NODE) {
if (current.id) {
parts.unshift(`#${current.id}`);
break;
} else {
let tagName = current.tagName.toLowerCase();
if (current === element) {
// Для целевого элемента берем точный путь
parts.unshift(tagName);
} else {
let siblingIndex = 0;
let sibling = current.previousSibling;
while (sibling) {
if (sibling.nodeType === Node.ELEMENT_NODE &&
(sibling as HTMLElement).tagName === current.tagName) {
siblingIndex++;
}
sibling = sibling.previousSibling;
}
if (siblingIndex > 0) {
parts.unshift(`${tagName}:nth-of-type(${siblingIndex + 1})`);
} else {
parts.unshift(tagName);
}
}
current = current.parentElement;
}
if (parts.length >= 5) {
break;
}
}
return parts.join(' > ');
}
// Инициализация rrweb
function initRrweb() {
record({
emit(event) {
rrwebEvents.push(event);
},
maskInputOptions: {
color: false,
date: false,
'datetime-local': false,
email: false,
month: false,
number: false,
range: false,
search: false,
tel: false,
text: false,
time: false,
url: false,
week: false,
textarea: false,
select: false,
password: true, // Маскируем только пароли
},
maskTextSelector: '[data-sensitive="true"]',
});
logger.log('rrweb recorder initialized');
}
// Обработка сообщений от Service Worker
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
logger.log('Received message:', message);
if (message.type === 'GET_RRWEB_EVENTS') {
const response: RrwebEventsResponse = {
events: rrwebEvents,
userEvents: userEvents
};
logger.log(`Sending ${rrwebEvents.length} rrweb events and ${userEvents.length} user events`);
sendResponse(response);
return true;
}
return false;
});
// Запуск
logger.log('Content script starting');
// Ждем готовности DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
function start() {
logger.log('Starting content script on:', window.location.href);
// Инициализация rrweb
initRrweb();
// Настройка логирования
setupClickLogging();
setupInputLogging();
setupNavigationLogging();
setupErrorLogging();
setupNetworkLogging();
// Запуск очистки старых событий
setInterval(cleanupOldEvents, CLEANUP_INTERVAL_MS);
logger.log('Content script initialized');
}