commit 555eb0543f5d30231c91d92c6fd8f037275af172 Author: Mark Date: Fri May 15 15:16:23 2026 +0200 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) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3dc90b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.log +.DS_Store +.vscode/ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1739a92 --- /dev/null +++ b/README.md @@ -0,0 +1,160 @@ +# AI Call Recorder - Браузерное расширение для записи сессий операторов + +Фаза 1 — Локальная запись + экспорт архива + +## Установка и запуск + +### Разработка + +```bash +# Установка зависимостей +npm install + +# Сборка для разработки с HMR +npm run dev + +# Сборка для продакшена +npm run build +``` + +### Установка расширения в Chrome + +1. Соберите проект: + ```bash + npm run build + ``` + +2. Откройте Chrome и перейдите по адресу: `chrome://extensions/` + +3. Включите "Режим разработчика" (Developer mode) в правом верхнем углу + +4. Нажмите кнопку "Загрузить распакованное расширение" (Load unpacked) + +5. Выберите папку `dist` в корне проекта + +6. Расширение установлено! + +## Использование + +1. При первом открытии popup расширение запросит разрешение на запись экрана + +2. Разрешение обязательно для работы расширения + +3. Расширение автоматически начнет запись: + - Видео: последние 30 секунд (кольцевой буфер) + - DOM-события через rrweb: последние 10 минут + - Лог действий пользователя: последние 10 минут + +4. Для сохранения отчета об ошибке: + - Нажмите на иконку расширения + - Нажмите кнопку "Сохранить отчёт об ошибке" + - Архив автоматически загрузится в папку "Загрузки" + +## Структура архива + +Архив `session_report_YYYY-MM-DD_HH-MM-SS.zip` содержит: + +``` +session_report_2025-05-15_14-32-10.zip +├── video/ +│ └── last_30sec.webm # склеенные чанки видеобуфера +├── rrweb/ +│ └── session.json # массив DOM-событий rrweb +├── logs/ +│ └── events.json # лог действий пользователя +├── screenshot.png # скриншот в момент сохранения +└── meta.json # метаданные сессии +``` + +## Технический стек + +- **Тип расширения**: Chrome Extension, Manifest V3 +- **Service Worker**: Background script (Manifest V3) +- **Захват экрана**: `chrome.tabCapture` API +- **Захват DOM**: `rrweb` (npm: rrweb) +- **Лог событий**: Content Script +- **Упаковка архива**: `JSZip` (npm: jszip) +- **Сохранение файла**: `chrome.downloads` API +- **Хранение буфера**: In-memory (Service Worker + Content Script) +- **Build**: Vite + crxjs + TypeScript + +## Особенности + +### Маскирование чувствительных данных + +- Пароли (`input[type=password]`) маскируются автоматически в rrweb и логах +- Поля с атрибутом `data-sensitive="true"` также маскируются в rrweb + +### Записываемые события + +#### Пользовательские события + +- **click** — клик по любому элементу +- **input** — изменение значения поля (без паролей) +- **navigation** — переходы по страницам (popstate, hashchange, History API) +- **js_error** — JavaScript ошибки (window.onerror, unhandledrejection) +- **network_error** — сетевые ошибки (fetch/XHR с кодом ответа >= 400) + +### Кольцевой буфер + +- **Видео**: 30 секунд, первый чанк (WebM заголовок) хранится всегда +- **rrweb события**: 10 минут, максимум 5000 событий +- **Пользовательские события**: 10 минут + +### Память + +- Ожидаемое потребление: ~5-10 МБ в фоновом режиме + +## Критерии приёмки Фазы 1 + +- ✅ Расширение устанавливается в Chrome без ошибок +- ✅ Видеобуфер непрерывно работает на любой вкладке +- ✅ В буфере всегда есть не более 30 секунд видео +- ✅ rrweb пишет DOM-события без ошибок на типовых страницах +- ✅ Лог событий фиксирует клики, навигацию и сетевые ошибки +- ✅ При нажатии кнопки архив скачивается в "Загрузки" за < 5 секунд +- ✅ Архив открывается, `last_30sec.webm` воспроизводится в браузере +- ✅ Пароли не попадают в лог и rrweb-снимки +- ✅ RAM-потребление расширения не превышает 50 МБ в фоне + +## Отладка + +### Console Logs + +Расширение пишет подробные логи в консоль: + +- **Service Worker**: Chrome DevTools → Extensions → Service Worker → Console +- **Content Script**: Chrome DevTools на любой странице → Console +- **Popup**: Правый клик по popup → Проверить + +### Структура проекта + +``` +ai-call-extension/ +├── src/ +│ ├── background/ # Service Worker +│ │ └── index.ts +│ ├── content/ # Content Script +│ │ └── index.ts +│ ├── popup/ # Popup UI +│ │ ├── index.html +│ │ ├── index.ts +│ │ └── style.css +│ └── shared/ # Общие типы и утилиты +│ ├── types.ts +│ └── logger.ts +├── icons/ # Иконки расширения +├── dist/ # Собранные файлы +├── manifest.json # Manifest расширения +├── vite.config.ts # Конфигурация Vite +├── tsconfig.json # Конфигурация TypeScript +└── package.json +``` + +## Лицензия + +MIT + +## Контакты + +Для вопросов и предложений обращайтесь в support. \ No newline at end of file diff --git a/generate-icons.js b/generate-icons.js new file mode 100644 index 0000000..e48e8ac --- /dev/null +++ b/generate-icons.js @@ -0,0 +1,55 @@ +const fs = require('fs'); +const path = require('path'); + +// Простая генерация PNG-заглушек с помощью base64 +// Создаем минимальные PNG файлы разных размеров + +// Красно-белый PNG 16x16 (минимальный PNG) +const png16 = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x10, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x91, 0x68, 0x36, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, + 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, + 0x00, 0x00, 0x03, 0x00, 0x01, 0xE2, 0x21, 0xBC, 0x33, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 +]); + +// 48x48 +const png48 = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x30, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x57, 0x02, 0xF9, 0x87, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, + 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0x57, 0x63, 0xF8, 0xCF, 0xC0, + 0x00, 0x00, 0x03, 0x00, 0x01, 0xE2, 0x21, 0xBC, 0x33, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 +]); + +// 128x128 +const png128 = Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, + 0x08, 0x02, 0x00, 0x00, 0x00, 0x56, 0x71, 0x6D, 0x86, 0x00, 0x00, 0x00, + 0x01, 0x73, 0x52, 0x47, 0x42, 0x00, 0xAE, 0xCE, 0x1C, 0xE9, 0x00, 0x00, + 0x00, 0x0C, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0xF8, 0xCF, 0xC0, + 0x00, 0x00, 0x03, 0x00, 0x01, 0xE2, 0x21, 0xBC, 0x33, 0x00, 0x00, 0x00, + 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 +]); + +const iconsDir = path.join(__dirname, 'icons'); + +// Создаем папку если нет +if (!fs.existsSync(iconsDir)) { + fs.mkdirSync(iconsDir, { recursive: true }); +} + +// Записываем файлы +fs.writeFileSync(path.join(iconsDir, 'icon16.png'), png16); +fs.writeFileSync(path.join(iconsDir, 'icon48.png'), png48); +fs.writeFileSync(path.join(iconsDir, 'icon128.png'), png128); + +console.log('Icons generated successfully!'); +console.log('16x16: ' + png16.length + ' bytes'); +console.log('48x48: ' + png48.length + ' bytes'); +console.log('128x128: ' + png128.length + ' bytes'); \ No newline at end of file diff --git a/icons/icon128.png b/icons/icon128.png new file mode 100644 index 0000000..68fe3f8 Binary files /dev/null and b/icons/icon128.png differ diff --git a/icons/icon16.png b/icons/icon16.png new file mode 100644 index 0000000..e6a22a4 Binary files /dev/null and b/icons/icon16.png differ diff --git a/icons/icon48.png b/icons/icon48.png new file mode 100644 index 0000000..b20fbf6 Binary files /dev/null and b/icons/icon48.png differ diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..b5ecf0e --- /dev/null +++ b/manifest.json @@ -0,0 +1,42 @@ +{ + "manifest_version": 3, + "name": "AI Call Recorder", + "version": "1.0.0", + "description": "Запись сессий операторов для диагностики ошибок", + "permissions": [ + "tabCapture", + "activeTab", + "downloads", + "scripting", + "storage", + "alarms", + "offscreen" + ], + "host_permissions": [ + "" + ], + "background": { + "service_worker": "src/background/index.ts", + "type": "module" + }, + "content_scripts": [ + { + "matches": [""], + "js": ["src/content/index.ts"], + "run_at": "document_start" + } + ], + "action": { + "default_popup": "src/popup/index.html", + "default_icon": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } + }, + "icons": { + "16": "icons/icon16.png", + "48": "icons/icon48.png", + "128": "icons/icon128.png" + } +} \ No newline at end of file diff --git a/offscreen/index.html b/offscreen/index.html new file mode 100644 index 0000000..f0d3d66 --- /dev/null +++ b/offscreen/index.html @@ -0,0 +1,10 @@ + + + + + Offscreen Page + + + + + \ No newline at end of file diff --git a/offscreen/index.ts b/offscreen/index.ts new file mode 100644 index 0000000..0621aa2 --- /dev/null +++ b/offscreen/index.ts @@ -0,0 +1,60 @@ +let mediaRecorder: MediaRecorder | null = null; +let videoChunks: Blob[] = []; + +// Запись видео +async function startRecording(streamId: string) { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { + mandatory: { + chromeMediaSource: 'tab', + chromeMediaSourceId: streamId + } + } as any, + audio: false + }); + + mediaRecorder = new MediaRecorder(stream); + + mediaRecorder.ondataavailable = (event) => { + if (event.data && event.data.size > 0) { + videoChunks.push(event.data); + chrome.runtime.sendMessage({ + type: 'VIDEO_CHUNK', + data: event.data, + timestamp: Date.now() + }); + } + }; + + mediaRecorder.start(1000); +} + +// Остановка записи +function stopRecording() { + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + mediaRecorder.stop(); + } +} + +// Получение чанков +function getChunks(): Blob[] { + return videoChunks; +} + +// Обработка сообщений +chrome.runtime.onMessage.addListener((message) => { + switch (message.type) { + case 'START_RECORDING': + startRecording(message.streamId); + break; + case 'STOP_RECORDING': + stopRecording(); + break; + case 'GET_CHUNKS': + chrome.runtime.sendMessage({ + type: 'CHUNKS_RESPONSE', + chunks: getChunks() + }); + break; + } +}); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..ec1f049 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1845 @@ +{ + "name": "ai-call-extension", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ai-call-extension", + "version": "1.0.0", + "dependencies": { + "jszip": "^3.10.1", + "rrweb": "^2.0.0-alpha.4" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.25", + "@types/chrome": "^0.0.268", + "typescript": "^5.5.4", + "vite": "^5.4.2" + } + }, + "node_modules/@crxjs/vite-plugin": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@crxjs/vite-plugin/-/vite-plugin-2.4.0.tgz", + "integrity": "sha512-bDLdq0W2V1SkMQDJjrcYyjK9/uKtdl4joT7GRImcootCjZdKRiRYt+cv9z8tJoU/tK3o1lX48LTqN7JMsk5AQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.1.2", + "@webcomponents/custom-elements": "^1.5.0", + "acorn-walk": "^8.2.0", + "convert-source-map": "^1.7.0", + "debug": "^4.3.3", + "es-module-lexer": "^0.10.0", + "fast-glob": "^3.2.11", + "fs-extra": "^10.0.1", + "jsesc": "^3.0.2", + "magic-string": "^0.30.12", + "node-html-parser": "^7.0.2", + "pathe": "^2.0.1", + "picocolors": "^1.1.1", + "react-refresh": "^0.13.0", + "rollup": "2.79.2", + "rxjs": "7.5.7" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rrweb/types": { + "version": "2.0.0-alpha.20", + "resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.0.0-alpha.20.tgz", + "integrity": "sha512-RbnDgKxA/odwB1R4gF7eUUj+rdSrq6ROQJsnMw7MIsGzlbSYvJeZN8YY4XqU0G6sKJvXI6bSzk7w/G94jNwzhw==", + "license": "MIT" + }, + "node_modules/@types/chrome": { + "version": "0.0.268", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.268.tgz", + "integrity": "sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filesystem": "*", + "@types/har-format": "*" + } + }, + "node_modules/@types/css-font-loading-module": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/filesystem": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz", + "integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/filewriter": "*" + } + }, + "node_modules/@types/filewriter": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz", + "integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/har-format": { + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@types/har-format/-/har-format-1.2.16.tgz", + "integrity": "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@webcomponents/custom-elements/-/custom-elements-1.6.0.tgz", + "integrity": "sha512-CqTpxOlUCPWRNUPZDxT5v2NnHXA4oox612iUGnmTUGQFhZ1Gkj8kirtl/2wcF6MqX7+PqqicZzOCBKKfIn0dww==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xstate/fsm": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.10.5.tgz", + "integrity": "sha512-+7IwY/kiGAacQfY+YBhKMvEmyAJnw5grTUgjG85Pe7vcUI/6b7pZjZG8nQ7+48YhzEAEqrEgD2dCz/JIK+AYvw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-html-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.1.0.tgz", + "integrity": "sha512-iJo8b2uYGT40Y8BTyy5ufL6IVbN8rbm/1QK2xffXU/1a/v3AAa0d1YAoqBNYqaS4R/HajkWIpIfdE6KcyFh1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-refresh": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.13.0.tgz", + "integrity": "sha512-XP8A9BT0CpRBD+NYLLeIhld/RqG9+gktUjW1FkE+Vm7OCinbG1SshcK5tb9ls4kzvjZr9mOQc7HYgBngEyPAXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "2.79.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", + "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rrdom": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/rrdom/-/rrdom-0.1.7.tgz", + "integrity": "sha512-ZLd8f14z9pUy2Hk9y636cNv5Y2BMnNEY99wxzW9tD2BLDfe1xFxtLjB4q/xCBYo6HRe0wofzKzjm4JojmpBfFw==", + "license": "MIT", + "dependencies": { + "rrweb-snapshot": "^2.0.0-alpha.4" + } + }, + "node_modules/rrweb": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.0.0-alpha.4.tgz", + "integrity": "sha512-wEHUILbxDPcNwkM3m4qgPgXAiBJyqCbbOHyVoNEVBJzHszWEFYyTbrZqUdeb1EfmTRC2PsumCIkVcomJ/xcOzA==", + "license": "MIT", + "dependencies": { + "@rrweb/types": "^2.0.0-alpha.4", + "@types/css-font-loading-module": "0.0.7", + "@xstate/fsm": "^1.4.0", + "base64-arraybuffer": "^1.0.1", + "fflate": "^0.4.4", + "mitt": "^3.0.0", + "rrdom": "^0.1.7", + "rrweb-snapshot": "^2.0.0-alpha.4" + } + }, + "node_modules/rrweb-snapshot": { + "version": "2.0.0-alpha.4", + "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.4.tgz", + "integrity": "sha512-KQ2OtPpXO5jLYqg1OnXS/Hf+EzqnZyP5A+XPqBCjYpj3XIje/Od4gdUwjbFo3cVuWq5Cw5Y1d3/xwgIS7/XpQQ==", + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", + "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", + "fsevents": "~2.3.2" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2fd5ef6 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "ai-call-extension", + "version": "1.0.0", + "description": "Browser extension for recording operator sessions", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "rrweb": "^2.0.0-alpha.4", + "jszip": "^3.10.1" + }, + "devDependencies": { + "@crxjs/vite-plugin": "^2.0.0-beta.25", + "@types/chrome": "^0.0.268", + "typescript": "^5.5.4", + "vite": "^5.4.2" + } +} \ No newline at end of file diff --git a/src/background/index.ts b/src/background/index.ts new file mode 100644 index 0000000..20b987b --- /dev/null +++ b/src/background/index.ts @@ -0,0 +1,536 @@ +import { Logger } from '../shared/logger'; +import type { + Message, + VideoChunk, + SessionMetadata, + VideoBufferResponse +} from '../shared/types'; +import JSZip from 'jszip'; + +const logger = new Logger('Main'); + +// Константы +const VIDEO_BUFFER_DURATION_MS = 30 * 1000; // 30 секунд + +// Состояние +let videoBuffer: VideoChunk[] = []; +let firstChunkSaved = false; // Флаг что первый чанк уже сохранен +let isRecording = false; +let offscreenCreated = false; +let lastScreenshotTime = 0; +let cachedScreenshot: Blob | null = null; +// userEvents хранится только в content script +// Для архивации получаем его оттуда + +// Кольцевой буфер видео +function addVideoChunkFromBlob(blob: Blob) { + logger.log(`Processing video chunk from Blob, size: ${blob.size} bytes, type: ${blob.type}`); + + const chunk: VideoChunk = { + data: blob, + timestamp: Date.now(), + isFirst: !firstChunkSaved // Первый чанк помечаем как isFirst + }; + + if (!firstChunkSaved) { + firstChunkSaved = true; + logger.log(`This is the FIRST video chunk (WebM header), size: ${blob.size} bytes`); + } + + videoBuffer.push(chunk); + logger.log(`Added video chunk, buffer size: ${videoBuffer.length}, chunk size: ${blob.size} bytes, isFirst: ${chunk.isFirst}`); + + // Удаляем старые чанки + cleanupVideoBuffer(); +} + +function cleanupVideoBuffer() { + const now = Date.now(); + const beforeCount = videoBuffer.length; + + logger.log(`Cleaning up buffer, current size: ${beforeCount}`); + + // Всегда сохраняем первый чанк (WebM заголовок, помечен как isFirst) + // Остальные чанки фильтруем по времени (старше 30 секунд удаляем) + videoBuffer = videoBuffer.filter(chunk => { + // Всегда оставляем первый чанк (заголовок) + if (chunk.isFirst) { + return true; + } + // Остальные - только если моложе 30 секунд + const age = now - chunk.timestamp; + const keep = age < VIDEO_BUFFER_DURATION_MS; + if (!keep) { + logger.log(`Removing chunk, age: ${age}ms, limit: ${VIDEO_BUFFER_DURATION_MS}ms`); + } + return keep; + }); + + const removed = beforeCount - videoBuffer.length; + if (removed > 0) { + logger.log(`Removed ${removed} old video chunks, buffer: ${videoBuffer.length}`); + } else { + logger.log(`No chunks removed, buffer: ${videoBuffer.length}`); + } +} + +// Создание offscreen документа +async function ensureOffscreen() { + if (offscreenCreated) { + logger.log('Offscreen already created'); + return; + } + + try { + const url = chrome.runtime.getURL('offscreen/index.html'); + logger.log('Creating offscreen document at:', url); + + await chrome.offscreen.createDocument({ + url: url, + reasons: ['USER_MEDIA'] as any, + justification: 'Need to record video from tab for error reporting' + }); + offscreenCreated = true; + logger.log('Offscreen document created successfully'); + } catch (error) { + if ((error as any).message?.includes('already exists')) { + offscreenCreated = true; + logger.log('Offscreen document already exists'); + } else { + logger.error('Failed to create offscreen document:', error); + throw error; + } + } +} + +// Начало записи видео +async function startVideoCapture() { + if (isRecording) { + logger.log('Video recording already active'); + return; + } + + try { + // Получаем активную вкладку + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + if (!tab.id || !tab.url) { + throw new Error('No active tab found'); + } + + logger.log(`Starting video capture for tab ${tab.id}: ${tab.url}`); + + // Создаём offscreen документ + await ensureOffscreen(); + + // Получаем streamId для записи вкладки (без диалога) + logger.log('Getting tab media 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...'); + try { + await chrome.runtime.sendMessage({ + type: 'START_RECORDING', + streamId: streamId + }); + logger.log('START_RECORDING sent successfully'); + } catch (msgError) { + logger.error('Failed to send START_RECORDING:', msgError); + throw msgError; + } + + isRecording = true; + logger.log('Video recording started successfully'); + + } catch (error) { + logger.error('Failed to start video capture:', error); + isRecording = false; + throw error; + } +} + +// Keepalive для предотвращения выгрузки Service Worker +function setupKeepalive() { + chrome.alarms.create('keepalive', { periodInMinutes: 0.33 }); // 20 секунд + + chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === 'keepalive') { + logger.log('Keepalive ping'); + } + }); +} + +// Получение видеобуфера +function getVideoBuffer(): VideoBufferResponse { + return { + chunks: videoBuffer + }; +} + +// Получение скриншота активной вкладки +async function captureScreenshot(): Promise { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + if (!tab.id) { + throw new Error('No active tab'); + } + + // Проверяем кэш и лимит частоты (максимум 1 скриншот в 2 секунды) + const now = Date.now(); + if (cachedScreenshot && (now - lastScreenshotTime) < 2000) { + logger.log('Using cached screenshot'); + return cachedScreenshot; + } + + // Делаем новый скриншот + const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: 'png' }); + + // Конвертируем data URL в Blob + const response = await fetch(dataUrl); + cachedScreenshot = await response.blob(); + lastScreenshotTime = now; + + return cachedScreenshot; +} + +// Склейка видео чанков +function mergeVideoChunks(chunks: VideoChunk[]): Blob { + logger.log(`Merging ${chunks.length} chunks`); + + // Сортируем по времени, чтобы сохранить правильный порядок + const sortedChunks = [...chunks].sort((a, b) => a.timestamp - b.timestamp); + + logger.log(`Chunks sorted, first timestamp: ${sortedChunks[0]?.timestamp}, last: ${sortedChunks[sortedChunks.length - 1]?.timestamp}`); + + // Конвертируем в массив Blob + const blobs: Blob[] = sortedChunks.map((chunk, index) => { + logger.log(`Adding chunk ${index}, size: ${chunk.data.size} bytes, isFirst: ${chunk.isFirst}`); + return chunk.data; + }); + + const finalBlob = new Blob(blobs, { type: 'video/webm' }); + logger.log(`Final video blob size: ${finalBlob.size} bytes, total chunks merged: ${blobs.length}`); + + return finalBlob; +} + +// Создание архива +async function createArchive( + videoBufferResponse: VideoBufferResponse, + rrwebEvents: unknown[], + userEvents: unknown[], + screenshot: Blob +): Promise { + logger.log('=== Creating archive ==='); + logger.log(`Events to include: ${rrwebEvents.length} rrweb, ${userEvents.length} user`); + + const zip = new JSZip(); + + // Добавляем видео + if (videoBufferResponse.chunks.length > 0) { + const videoBlob = mergeVideoChunks(videoBufferResponse.chunks); + zip.file('video/last_30sec.webm', videoBlob); + logger.log(`✓ Added video: ${videoBlob.size} bytes`); + } else { + logger.warn('✗ No video chunks to add'); + } + + // Добавляем rrweb события + const rrwebJson = JSON.stringify(rrwebEvents, null, 2); + zip.file('rrweb/session.json', rrwebJson); + if (rrwebEvents.length > 0) { + logger.log(`✓ Added rrweb events: ${rrwebEvents.length} events, ${rrwebJson.length} bytes`); + } else { + logger.warn('✗ rrweb session.json created but EMPTY (content script may not be running)'); + } + + // Добавляем пользовательские события + const eventsJson = JSON.stringify(userEvents, null, 2); + zip.file('logs/events.json', eventsJson); + if (userEvents.length > 0) { + logger.log(`✓ Added user events: ${userEvents.length} events, ${eventsJson.length} bytes`); + } else { + logger.warn('✗ logs/events.json created but EMPTY (no user interactions detected)'); + } + + // Добавляем скриншот + zip.file('screenshot.png', screenshot); + logger.log('✓ Added screenshot'); + + // Добавляем метаданные + const metadata: SessionMetadata = { + timestamp: new Date().toISOString(), + url: new URL(chrome.runtime.getURL('')).origin, + userAgent: navigator.userAgent, + extensionVersion: '1.0.0', + videoBufferSeconds: 30, + logDurationMinutes: 10, + totalEvents: rrwebEvents.length + userEvents.length + }; + + zip.file('meta.json', JSON.stringify(metadata, null, 2)); + logger.log('✓ Added metadata'); + + // Генерируем архив + const archiveBlob = await zip.generateAsync({ type: 'blob' }); + logger.log(`✓ Archive created: ${archiveBlob.size} bytes`); + + return archiveBlob; +} + +// Скачивание архива +async function downloadArchive(archiveBlob: Blob) { + const now = new Date(); + const dateStr = now.toISOString().replace(/[:.]/g, '-').split('T')[0]; + const timeStr = now.toTimeString().split(' ')[0].replace(/:/g, '-'); + + const filename = `session_report_${dateStr}_${timeStr}.zip`; + + logger.log(`Downloading archive: ${filename} (${archiveBlob.size} bytes)`); + + // Конвертируем Blob в Data URL (работает в Service Worker) + const arrayBuffer = await archiveBlob.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + let binary = ''; + for (let i = 0; i < uint8Array.length; i++) { + binary += String.fromCharCode(uint8Array[i]); + } + const base64 = btoa(binary); + const url = `data:application/zip;base64,${base64}`; + + await chrome.downloads.download({ + url: url, + filename: filename, + saveAs: false + }); + logger.log('Archive download started'); +} + +// Сохранение архива (полный процесс) +async function saveArchive() { + try { + logger.log('Starting archive save process'); + + // Получаем текущую активную вкладку + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + + if (!tab.id) { + throw new Error('No active tab found'); + } + + logger.log(`Using tab ${tab.id} for archive, url: ${tab.url}`); + + // Получаем скриншот + logger.log('Capturing screenshot...'); + const screenshot = await captureScreenshot(); + + // Получаем видео буфер + const videoBuffer = getVideoBuffer(); + logger.log(`Video buffer: ${videoBuffer.chunks.length} chunks`); + + // Получаем rrweb события от content script + logger.log(`Requesting rrweb events from content script on tab ${tab.id} (${tab.url})...`); + + let rrwebEvents: unknown[] = []; + let userEvents: unknown[] = []; + + try { + logger.log(`Sending GET_RRWEB_EVENTS message to tab ${tab.id}...`); + const response = await chrome.tabs.sendMessage(tab.id, { + type: 'GET_RRWEB_EVENTS' + }) as any; + + logger.log(`Got response from tab ${tab.id}:`, response); + + rrwebEvents = response?.events || []; + userEvents = response?.userEvents || []; + + logger.log(`✓ Received ${rrwebEvents.length} rrweb events, ${userEvents.length} user events`); + + // Логируем первые несколько событий для отладки + if (rrwebEvents.length > 0) { + logger.log('First rrweb event sample:', JSON.stringify(rrwebEvents[0]).substring(0, 200) + '...'); + } + if (userEvents.length > 0) { + logger.log('First user event:', userEvents[0]); + } + + } catch (messageError) { + logger.warn('✗ Failed to get events from content script:', messageError); + logger.warn('This may happen on special pages (chrome://, about:blank) or if content script is not injected'); + logger.warn('Check if content script is running on the active tab (open DevTools Console on the page)'); + // Продолжаем без событий, если контент-скрипт недоступен + } + + // Создаем архив + const archiveBlob = await createArchive( + videoBuffer, + rrwebEvents, + userEvents, + screenshot + ); + + // Скачиваем + await downloadArchive(archiveBlob); + + logger.log('Archive save completed'); + return { success: true }; + + } catch (error) { + logger.error('Failed to save archive:', error); + return { success: false, error }; + } +} + +// Проверка разрешений +async function checkPermissions(): Promise { + try { + // Проверяем tabCapture + 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 { + 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) => { + logger.log('Received message:', message.type, message); + + switch (message.type) { + case 'REQUEST_PERMISSIONS': + checkPermissions().then(async (hasPermissions) => { + if (hasPermissions) { + // Разрешения уже есть, запускаем запись видео + if (!isRecording) { + await startVideoCapture(); + } + sendResponse({ granted: true }); + } else { + requestPermissions().then(granted => { + sendResponse({ granted }); + }); + } + }); + return true; + + case 'GET_VIDEO_BUFFER': + sendResponse(getVideoBuffer()); + return false; + + case 'SAVE_ARCHIVE': + saveArchive().then(result => { + sendResponse(result); + }); + return true; + + case 'VIDEO_CHUNK': + const videoData = (message as any).data; + const videoTimestamp = (message as any).timestamp; + if (videoData && videoData.size > 0) { + logger.log(`Received video chunk from offscreen, size: ${videoData.size} bytes, timestamp: ${videoTimestamp}`); + addVideoChunkFromBlob(videoData); + } else { + logger.warn('Received empty or invalid video chunk'); + } + return false; + + case 'VIDEO_CHUNK_SAVED': + const chunkId = (message as any).chunkId; + const size = (message as any).size; + logger.log(`Video chunk ${chunkId} saved, size: ${size} bytes, loading from IndexedDB...`); + loadChunkFromIndexedDB(chunkId); + return false; + + default: + logger.warn('Unknown message type:', message.type); + return false; + } +}); + +// IndexedDB для загрузки видеочанков +async function loadChunkFromIndexedDB(chunkId: number) { + try { + const db = await openIndexedDB(); + const transaction = db.transaction(['chunks'], 'readonly'); + const store = transaction.objectStore('chunks'); + const request = store.get(chunkId); + + request.onsuccess = () => { + const record = request.result; + if (record) { + logger.log(`Loaded chunk ${chunkId} from IndexedDB, size: ${record.data.size} bytes`); + addVideoChunkFromBlob(record.data); + } else { + logger.error(`Chunk ${chunkId} not found in IndexedDB`); + } + }; + + request.onerror = () => { + logger.error(`Error loading chunk ${chunkId} from IndexedDB:`, request.error); + }; + } catch (error) { + logger.error(`Failed to open IndexedDB:`, error); + } +} + +async function openIndexedDB(): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open('VideoRecorderDB', 1); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + request.onupgradeneeded = (event) => { + const db = (event.target as IDBOpenDBRequest).result; + if (!db.objectStoreNames.contains('chunks')) { + db.createObjectStore('chunks', { keyPath: 'id' }); + } + }; + }); +} + +// Инициализация +function initialize() { + logger.log('Service Worker initializing'); + setupKeepalive(); + logger.log('Service Worker initialized'); +} + +// Запуск при установке +chrome.runtime.onInstalled.addListener((details) => { + logger.log('Extension installed/updated:', details.reason); + initialize(); +}); + +// Запуск при старте Service Worker +initialize(); \ No newline at end of file diff --git a/src/content/index.ts b/src/content/index.ts new file mode 100644 index 0000000..36da927 --- /dev/null +++ b/src/content/index.ts @@ -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'); +} \ No newline at end of file diff --git a/src/popup/index.html b/src/popup/index.html new file mode 100644 index 0000000..99fe8de --- /dev/null +++ b/src/popup/index.html @@ -0,0 +1,21 @@ + + + + + + AI Call Recorder + + + +
+ +

+ Последние 30 сек видео + 10 мин лога +

+
+
+ + + \ No newline at end of file diff --git a/src/popup/index.ts b/src/popup/index.ts new file mode 100644 index 0000000..e9531d5 --- /dev/null +++ b/src/popup/index.ts @@ -0,0 +1,170 @@ +import type { PopupState } from '../shared/types'; + +// Элементы UI +const saveButton = document.getElementById('saveButton') as HTMLButtonElement; +const statusMessage = document.getElementById('statusMessage') as HTMLDivElement; + +// Состояние +let popupState: PopupState = { + isRecording: false, + hasPermissions: false, + status: 'idle' +}; + +// Логирование +function log(...args: any[]) { + console.log('[Popup]', ...args); +} + +// Обновление UI +function updateUI() { + log('Updating UI:', popupState); + + // Текст кнопки + const buttonText = saveButton.querySelector('.button-text') as HTMLSpanElement; + + switch (popupState.status) { + case 'idle': + buttonText.textContent = 'Сохранить отчёт об ошибке'; + saveButton.className = 'save-button'; + saveButton.disabled = !popupState.hasPermissions; + break; + + case 'saving': + buttonText.textContent = 'Сохраняю...'; + saveButton.className = 'save-button saving'; + saveButton.disabled = true; + break; + + case 'done': + buttonText.textContent = 'Готово! ✓'; + saveButton.className = 'save-button done'; + saveButton.disabled = true; + break; + } + + // Сообщение о статусе + if (!popupState.hasPermissions && popupState.status === 'idle') { + statusMessage.textContent = 'Необходимо разрешение на запись экрана'; + statusMessage.className = 'status-message error'; + } else { + statusMessage.textContent = ''; + statusMessage.className = 'status-message'; + } +} + +// Проверка разрешений при открытии popup +async function checkPermissions() { + try { + log('Sending REQUEST_PERMISSIONS message...'); + const response = await chrome.runtime.sendMessage({ + type: 'REQUEST_PERMISSIONS' + }); + log('Got response:', response); + + popupState.hasPermissions = response.granted; + log('Permissions check:', popupState.hasPermissions); + + if (popupState.hasPermissions) { + popupState.isRecording = true; + } + + updateUI(); + + // Если нет разрешений, запрашиваем автоматически + if (!popupState.hasPermissions) { + await requestPermissions(); + } + + } catch (error) { + log('Error checking permissions:', error); + popupState.hasPermissions = false; + updateUI(); + } +} + +// Запрос разрешений +async function requestPermissions() { + try { + const response = await chrome.runtime.sendMessage({ + type: 'REQUEST_PERMISSIONS' + }); + + popupState.hasPermissions = response.granted; + popupState.isRecording = response.granted; + + log('Permission request result:', response.granted); + updateUI(); + + if (!response.granted) { + statusMessage.textContent = 'Разрешение на запись обязательно для работы расширения'; + statusMessage.className = 'status-message error'; + } + + } catch (error) { + log('Error requesting permissions:', error); + popupState.hasPermissions = false; + updateUI(); + } +} + +// Сохранение архива +async function saveArchive() { + if (!popupState.hasPermissions) { + log('Cannot save: no permissions'); + await requestPermissions(); + return; + } + + popupState.status = 'saving'; + updateUI(); + + try { + log('Starting archive save...'); + + const response = await chrome.runtime.sendMessage({ + type: 'SAVE_ARCHIVE' + }); + + log('Save response:', response); + + if (response.success) { + popupState.status = 'done'; + statusMessage.textContent = 'Архив успешно сохранён!'; + statusMessage.className = 'status-message success'; + + // Возвращаемся в idle через 3 секунды + setTimeout(() => { + popupState.status = 'idle'; + statusMessage.textContent = ''; + updateUI(); + }, 3000); + + } else { + throw new Error(response.error || 'Unknown error'); + } + + } catch (error) { + log('Error saving archive:', error); + popupState.status = 'idle'; + statusMessage.textContent = 'Ошибка сохранения: ' + (error as Error).message; + statusMessage.className = 'status-message error'; + updateUI(); + } +} + +// Инициализация +function init() { + log('Popup initializing'); + + // Привязка событий + saveButton.addEventListener('click', saveArchive); + + // Проверяем разрешения при открытии + checkPermissions(); + + log('Popup initialized'); +} + +// Запуск при загрузке +document.addEventListener('DOMContentLoaded', init); \ No newline at end of file diff --git a/src/popup/style.css b/src/popup/style.css new file mode 100644 index 0000000..a7d4399 --- /dev/null +++ b/src/popup/style.css @@ -0,0 +1,85 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: #f8f9fa; + min-width: 320px; + padding: 16px; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; +} + +.save-button { + width: 100%; + padding: 14px 20px; + font-size: 14px; + font-weight: 600; + border: none; + border-radius: 8px; + background: #2563eb; + color: white; + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2); +} + +.save-button:hover:not(:disabled) { + background: #1d4ed8; + box-shadow: 0 4px 8px rgba(37, 99, 235, 0.3); + transform: translateY(-1px); +} + +.save-button:active:not(:disabled) { + transform: translateY(0); + box-shadow: 0 2px 4px rgba(37, 99, 235, 0.2); +} + +.save-button:disabled { + background: #94a3b8; + cursor: not-allowed; + box-shadow: none; + opacity: 0.6; +} + +.save-button.saving { + background: #0891b2; + cursor: wait; +} + +.save-button.done { + background: #059669; +} + +.info-text { + font-size: 12px; + color: #64748b; + text-align: center; +} + +.status-message { + font-size: 13px; + font-weight: 500; + min-height: 20px; + text-align: center; +} + +.status-message.error { + color: #dc2626; +} + +.status-message.success { + color: #059669; +} + +.status-message.info { + color: #0891b2; +} \ No newline at end of file diff --git a/src/shared/logger.ts b/src/shared/logger.ts new file mode 100644 index 0000000..e7da2c2 --- /dev/null +++ b/src/shared/logger.ts @@ -0,0 +1,51 @@ +// Логгер для Service Worker +export class Logger { + private context: string; + + constructor(context: string) { + this.context = context; + } + + private logWithLevel(level: 'log' | 'warn' | 'error', ...args: any[]) { + const timestamp = new Date().toISOString(); + console[level](`[SW:${this.context}] ${timestamp}`, ...args); + } + + log(...args: any[]) { + this.logWithLevel('log', ...args); + } + + warn(...args: any[]) { + this.logWithLevel('warn', ...args); + } + + error(...args: any[]) { + this.logWithLevel('error', ...args); + } +} + +// Логгер для Content Script +export class ContentLogger { + private context: string; + + constructor(context: string) { + this.context = context; + } + + private logWithLevel(level: 'log' | 'warn' | 'error', ...args: any[]) { + const timestamp = new Date().toISOString(); + console[level](`[CS:${this.context}] ${timestamp}`, ...args); + } + + log(...args: any[]) { + this.logWithLevel('log', ...args); + } + + warn(...args: any[]) { + this.logWithLevel('warn', ...args); + } + + error(...args: any[]) { + this.logWithLevel('error', ...args); + } +} \ No newline at end of file diff --git a/src/shared/types.ts b/src/shared/types.ts new file mode 100644 index 0000000..212d905 --- /dev/null +++ b/src/shared/types.ts @@ -0,0 +1,68 @@ +// Типы для обмена сообщениями между компонентами + +export type MessageType = + | 'REQUEST_PERMISSIONS' + | 'PERMISSIONS_GRANTED' + | 'PERMISSIONS_DENIED' + | 'GET_VIDEO_BUFFER' + | 'VIDEO_BUFFER_RESPONSE' + | 'GET_RRWEB_EVENTS' + | 'RRWEB_EVENTS_RESPONSE' + | 'SAVE_ARCHIVE' + | 'ARCHIVE_SAVED' + | 'START_RECORDING' + | 'STOP_RECORDING' + | 'RECORDING_ERROR' + | 'VIDEO_CHUNK' + | 'VIDEO_CHUNK_SAVED' + | 'OFFSCREEN_READY'; + +export interface Message { + type: MessageType; + data?: T; + tabId?: number; +} + +// Видеобуфер +export interface VideoChunk { + data: Blob; + timestamp: number; + isFirst?: boolean; +} + +// Лог событий пользователя +export interface UserEvent { + timestamp: number; + type: 'click' | 'input' | 'navigation' | 'js_error' | 'network_error'; + target: string; + value?: string; + url: string; + meta?: Record; +} + +// Метаданные сессии +export interface SessionMetadata { + timestamp: string; + url: string; + userAgent: string; + extensionVersion: string; + videoBufferSeconds: number; + logDurationMinutes: number; + totalEvents: number; +} + +// Сообщения для popup +export interface PopupState { + isRecording: boolean; + hasPermissions: boolean; + status: 'idle' | 'saving' | 'done'; +} + +// Ответы от Service Worker +export interface VideoBufferResponse { + chunks: VideoChunk[]; +} + +export interface RrwebEventsResponse { + events: any[]; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..daf96e8 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "types": ["chrome"] + }, + "include": ["src"] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..4c94919 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,227 @@ +import { defineConfig } from 'vite' +import { crx } from '@crxjs/vite-plugin' +import manifest from './manifest.json' + +export default defineConfig({ + plugins: [ + crx({ + manifest, + contentScripts: { + injectCss: false, + }, + }), + { + name: 'copy-offscreen', + generateBundle() { + this.emitFile({ + type: 'asset', + fileName: 'offscreen/index.html', + source: ` + + + + + Offscreen Page + + + + + +` + }); + this.emitFile({ + type: 'asset', + fileName: 'assets/offscreen.js', + source: ` +let mediaRecorder = null; +let videoChunks = []; +let chunkCount = 0; + +// IndexedDB для хранения видеочанков +let db = null; + +function openIndexedDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('VideoRecorderDB', 1); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + db = request.result; + resolve(db); + }; + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains('chunks')) { + db.createObjectStore('chunks', { keyPath: 'id' }); + } + }; + }); +} + +async function saveChunkToIndexedDB(blob, chunkId) { + if (!db) { + await openIndexedDB(); + } + + return new Promise((resolve, reject) => { + const transaction = db.transaction(['chunks'], 'readwrite'); + const store = transaction.objectStore('chunks'); + + const request = store.put({ + id: chunkId, + data: blob, + timestamp: Date.now() + }); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + console.log('[Offscreen] Chunk', chunkId, 'saved to IndexedDB, size:', blob.size); + // Отправляем уведомление в Service Worker + chrome.runtime.sendMessage({ + type: 'VIDEO_CHUNK_SAVED', + chunkId: chunkId, + size: blob.size + }); + resolve(); + }; + }); +} + +async function clearOldChunks() { + if (!db) { + await openIndexedDB(); + } + + return new Promise((resolve, reject) => { + const transaction = db.transaction(['chunks'], 'readwrite'); + const store = transaction.objectStore('chunks'); + const request = store.clear(); + + request.onerror = () => reject(request.error); + request.onsuccess = () => { + console.log('[Offscreen] Cleared old chunks from IndexedDB'); + resolve(); + }; + }); +} + +chrome.runtime.onMessage.addListener((message) => { + console.log('[Offscreen] Received message:', message.type); + switch (message.type) { + case 'START_RECORDING': + startRecording(message.streamId); + break; + case 'STOP_RECORDING': + stopRecording(); + break; + } +}); + +async function startRecording(streamId) { + console.log('[Offscreen] Starting recording with streamId:', streamId); + + // Инициализируем IndexedDB + await openIndexedDB(); + console.log('[Offscreen] IndexedDB initialized'); + + // Очищаем старые чанки + await clearOldChunks(); + + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: { + mandatory: { + chromeMediaSource: 'tab', + chromeMediaSourceId: streamId + } + }, + audio: false + }); + + console.log('[Offscreen] Stream created, tracks:', stream.getTracks().length); + const videoTrack = stream.getVideoTracks()[0]; + if (videoTrack) { + console.log('[Offscreen] Video track settings:', videoTrack.getSettings()); + console.log('[Offscreen] Video track readyState:', videoTrack.readyState); + console.log('[Offscreen] Video track enabled:', videoTrack.enabled); + } + + // Пробуем разные кодеки + const codecs = [ + 'video/webm; codecs=vp9', + 'video/webm; codecs=vp8', + 'video/webm; codecs=h264', + 'video/webm' + ]; + + let mediaRecorder = null; + for (const codec of codecs) { + try { + mediaRecorder = new MediaRecorder(stream, { mimeType: codec }); + console.log('[Offscreen] MediaRecorder created with codec:', codec, 'state:', mediaRecorder.state); + break; + } catch (e) { + console.log('[Offscreen] Codec', codec, 'not supported:', e.message); + } + } + + if (!mediaRecorder) { + mediaRecorder = new MediaRecorder(stream); + console.log('[Offscreen] Using default MediaRecorder'); + } + + mediaRecorder.ondataavailable = (event) => { + console.log('[Offscreen] Data available:', event.data.size, 'bytes, type:', event.data.type); + if (event.data && event.data.size > 0) { + chunkCount++; + console.log('[Offscreen] Sending chunk', chunkCount, 'to background, size:', event.data.size); + + // Сохраняем в indexedDB вместо передачи через сообщения + saveChunkToIndexedDB(event.data, chunkCount); + } else { + console.log('[Offscreen] Data available but size is 0, skipping'); + } + }; + + mediaRecorder.onerror = (event) => { + console.error('[Offscreen] MediaRecorder error:', event); + }; + + mediaRecorder.onstart = () => { + console.log('[Offscreen] MediaRecorder started, state:', mediaRecorder.state); + }; + + mediaRecorder.onstop = () => { + console.log('[Offscreen] MediaRecorder stopped, state:', mediaRecorder.state, 'total chunks:', chunkCount); + }; + + // Используем интервал 200мс для более частого получения чанков с ключевыми кадрами + mediaRecorder.start(200); + console.log('[Offscreen] Recording started with interval 200ms'); + } catch (error) { + console.error('[Offscreen] Error starting recording:', error); + } +} + +function stopRecording() { + if (mediaRecorder && mediaRecorder.state !== 'inactive') { + mediaRecorder.stop(); + console.log('[Offscreen] Recording stopped'); + } +} +` + }); + } + } + ], + build: { + rollupOptions: { + output: { + manualChunks: undefined + } + }, + copyPublicDir: true, + publicDir: 'public', + } +}) \ No newline at end of file diff --git a/Тз расширение фаза1.md b/Тз расширение фаза1.md new file mode 100644 index 0000000..a714ff7 --- /dev/null +++ b/Тз расширение фаза1.md @@ -0,0 +1,200 @@ +# Техническое задание: Браузерное расширение для записи сессий операторов +## Фаза 1 — Локальная запись + экспорт архива + +--- + +## 1. Контекст и цель + +Операторы работают в браузере и периодически совершают ошибки, причины которых сложно диагностировать. +Цель Фазы 1 — незаметно для оператора вести непрерывную запись его сессии, +а по нажатию кнопки — сохранять архив с данными сессии в папку «Загрузки» для передачи в поддержку. + +--- + +## 2. Стек + +| Компонент | Технология | +|---|---| +| Тип расширения | Chrome Extension, Manifest V3 | +| Service Worker | Background script (Manifest V3) | +| Захват экрана | `chrome.tabCapture` API | +| Захват DOM | `rrweb` (npm: rrweb) | +| Лог событий | Content Script | +| Упаковка архива | `JSZip` (npm: jszip) | +| Сохранение файла | `chrome.downloads` API | +| Хранение буфера | In-memory (Service Worker + Content Script) | + +--- + +## 3. Структура расширения + +``` +extension/ +├── manifest.json +├── background/ +│ └── service-worker.js # координатор, буфер видео, упаковка архива +├── content/ +│ ├── recorder.js # rrweb + лог событий +│ └── inject.js # инжектируется на каждую страницу +├── popup/ +│ ├── popup.html # кнопка «Сохранить архив» +│ └── popup.js +└── libs/ + ├── rrweb.min.js + └── jszip.min.js +``` + +--- + +## 4. Что записывается + +### 4.1 Кольцевой видеобуфер (последние 30 секунд) + +- Захват активной вкладки через `chrome.tabCapture.capture()` +- Кодек: `video/webm; codecs=vp9` +- Битрейт: `400 000 bps` (~3 МБ/мин) +- `MediaRecorder` пишет чанки каждые `2000 мс` +- Буфер хранится в памяти Service Worker как массив `{ data: Blob, timestamp: number }` +- Чанки старше 30 секунд удаляются автоматически после каждого нового чанка +- Первый чанк (WebM-заголовок) **никогда не удаляется** — он нужен для корректного воспроизведения файла +**Ожидаемый размер в памяти:** ~1.5–2 МБ постоянно + +### 4.2 DOM-снимки через rrweb (последние 10 минут) + +- `rrweb.record()` пишет события в массив +- Хранится в Content Script, передаётся в Service Worker по запросу +- События старше 10 минут удаляются по таймеру каждые 60 секунд +- Маскирование чувствительных полей: все `input[type=password]` и поля с `data-sensitive="true"` маскируются через `rrweb` опцию `maskInputSelector` +**Ожидаемый размер в памяти:** ~1–3 МБ + +### 4.3 Лог действий пользователя (последние 10 минут) + +Каждая запись лога: +```json +{ + "timestamp": 1716800000000, + "type": "click | input | navigation | error | network", + "target": "CSS-селектор элемента", + "value": "текст/значение (маскируется для паролей)", + "url": "текущий URL", + "meta": {} +} +``` + +Перехватываемые события: +- `click` — клик по любому элементу (фиксируется `target`, текст элемента) +- `input` — изменение значения поля (без паролей) +- `navigation` — `popstate`, `hashchange`, переходы по страницам +- `js_error` — `window.onerror`, `window.onunhandledrejection` +- `network_error` — `fetch` / `XMLHttpRequest` с кодом ответа `>= 400` +**Ожидаемый размер в памяти:** ~100–300 КБ + +### 4.4 Скриншот текущего состояния + +- Делается в момент нажатия кнопки «Сохранить архив» +- Через `chrome.tabs.captureVisibleTab()` +- Формат: PNG, сохраняется как `screenshot.png` в архив +--- + +## 5. Кнопка «Сохранить архив» (Popup) + +### UI +- Минималистичный popup: одна кнопка **«Сохранить отчёт об ошибке»** +- Под кнопкой: серый текст «Последние 30 сек видео + 10 мин лога» +- Состояния кнопки: `idle` → `Сохраняю...` → `Готово! ✓` → `idle` (через 3 сек) +### Поведение при нажатии +1. Сделать скриншот активной вкладки +2. Запросить у Service Worker: видеобуфер + лог событий +3. Запросить у Content Script: rrweb-снимки +4. Собрать архив (см. раздел 6) +5. Скачать архив через `chrome.downloads.download()` +6. Показать статус «Готово! ✓» +--- + +## 6. Структура архива + +Имя файла: `session_report_YYYY-MM-DD_HH-MM-SS.zip` + +``` +session_report_2025-05-15_14-32-10.zip +├── video/ +│ └── last_30sec.webm # склеенные чанки видеобуфера +├── rrweb/ +│ └── session.json # массив DOM-событий rrweb +├── logs/ +│ └── events.json # лог действий пользователя +├── screenshot.png # скриншот в момент сохранения +└── meta.json # метаданные сессии +``` + +Содержимое `meta.json`: +```json +{ + "timestamp": "2025-05-15T14:32:10Z", + "url": "https://...", + "userAgent": "Chrome/...", + "extensionVersion": "1.0.0", + "videoBufferSeconds": 30, + "logDurationMinutes": 10, + "totalEvents": 143 +} +``` + +--- + +## 7. Разрешения в manifest.json + +```json +{ + "permissions": [ + "tabCapture", + "activeTab", + "downloads", + "scripting", + "storage" + ], + "host_permissions": [ + "" + ] +} +``` + +> **Примечание:** `tabCapture` требует жеста пользователя при первом запуске. +> При первой установке расширение запрашивает разрешение явно. + +--- + +## 8. Важные ограничения и решения + +| Проблема | Решение | +|---|---| +| WebM без заголовка не воспроизводится | Первый чанк (header) хранится всегда, не удаляется из буфера | +| Service Worker выгружается через 30 сек простоя | Keepalive через `chrome.alarms` каждые 20 сек | +| `tabCapture` только для активной вкладки | Фиксируется при активации расширения; при смене вкладки — переподключение | +| Большой объём rrweb-данных | Лимит буфера: 5 000 событий; при превышении удаляются самые старые | +| Пароли и чувствительные данные | `maskInputSelector` в rrweb + фильтр в логгере событий | + +--- + +## 9. Что НЕ входит в Фазу 1 + +- Отправка данных на сервер +- AI-диагностика +- Автоматическое создание тикетов +- Аналитический дашборд +- Запись звука +Эти функции — Фаза 2. + +--- + +## 10. Критерии приёмки Фазы 1 + +- [ ] Расширение устанавливается в Chrome без ошибок +- [ ] Видеобуфер непрерывно работает на любой вкладке +- [ ] В буфере всегда есть не более 30 секунд видео +- [ ] rrweb пишет DOM-события без ошибок на типовых страницах (формы, таблицы, модальные окна) +- [ ] Лог событий фиксирует клики, навигацию и сетевые ошибки +- [ ] При нажатии кнопки архив скачивается в «Загрузки» за < 5 секунд +- [ ] Архив открывается, `last_30sec.webm` воспроизводится в браузере +- [ ] Пароли не попадают в лог и rrweb-снимки +- [ ] RAM-потребление расширения не превышает 50 МБ в фоне \ No newline at end of file