diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fc9d25b..f35960a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,5 +1,8 @@ { "permissions": { - "allow": ["WebSearch"] + "allow": [ + "WebSearch", + "Bash(npm install:*)" + ] } } diff --git a/.gitignore b/.gitignore index e3f0b09..366e812 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ stats.html stats-*.json .wxt .vitest +.claude # Editor directories and files .vscode/* diff --git a/components/Navbar.tsx b/components/Navbar.tsx index 0b50059..25d4c29 100644 --- a/components/Navbar.tsx +++ b/components/Navbar.tsx @@ -1,17 +1,11 @@ import { NavLink, useLocation } from 'react-router-dom'; -import { useState, ReactNode, MouseEvent } from 'react'; +import { ReactNode } from 'react'; import { AppBar, - Box, - IconButton, - Menu, - MenuItem, Tab, Tabs, Toolbar, - useMediaQuery, } from '@mui/material'; -import MenuIcon from '@mui/icons-material/Menu'; interface RouteItem { path: string; @@ -24,40 +18,11 @@ interface NavbarProps { } function Navbar({ items = [] }: NavbarProps) { - const [anchorEl, setAnchorEl] = useState(null); - const isMenuOpen = Boolean(anchorEl); const location = useLocation(); - // Replicating the screen size logic from original component - const isLargeScreen = useMediaQuery('(min-width:768px)'); - const isMediumScreen = useMediaQuery('(min-width:480px)'); - - let visibleItemsCount: number; - if (isLargeScreen) { - visibleItemsCount = items.length; - } else if (isMediumScreen) { - visibleItemsCount = Math.min(3, items.length); - } else { - // Small screen - visibleItemsCount = Math.min(2, items.length); - } - - const visibleNavItems = items.slice(0, visibleItemsCount); - const collapsedNavItems = items.slice(visibleItemsCount); - - const handleMenuOpen = (event: MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleMenuClose = () => { - setAnchorEl(null); - }; - - // Find the current active tab index for the Tabs value - // Using startsWith to handle nested routes correctly. - const activeTabIndex = visibleNavItems.findIndex((item) => - location.pathname.startsWith(item.path), - ); + // Chrome 扩展 popup 固定尺寸,全部显示在滚动标签中 + // 精确匹配路由 + const activeTabIndex = items.findIndex((item) => location.pathname === item.path); return ( - {visibleNavItems.map((item) => ( - + {items.map((item) => ( + ))} - - {collapsedNavItems.length > 0 && ( - - - - - - {collapsedNavItems.map((item) => ( - - {item.label} - - ))} - - - )} ); diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 8dbc96b..e01c584 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -9,6 +9,8 @@ import { streamAllEvents, deleteRecordingSession, generateSessionId, + getAllSessions, + getAllEvents, } from '@/utils/recordEventsDb'; interface RecorderState { @@ -210,14 +212,14 @@ export default defineBackground(() => { if (response.ok) { // 从 IndexedDB 流式读取所有事件并下载回放文件 - if (state.sessionId) {const allEvents: unknown[] = []; + if (state.sessionId) { + const allEvents: unknown[] = []; await streamAllEvents(state.sessionId, (events) => { allEvents.push(...events); }); downloadHtmlInBackground(allEvents); - // 删除会话数据 - await deleteRecordingSession(state.sessionId); + // 不再删除会话数据,保留录制历史 } // 清空状态 @@ -238,6 +240,54 @@ export default defineBackground(() => { } }); + // 获取所有录制会话 + onMessage('popup:get-sessions', async () => { + try { + const sessions = await getAllSessions(); + return sessions; + } catch (error) { + console.error('[bg] 获取录制会话失败:', error); + return []; + } + }); + + // 获取会话事件 + onMessage('popup:get-session-events', async (message) => { + try { + const sessionId = message.data; + const events = await getAllEvents(sessionId); + return events; + } catch (error) { + console.error('[bg] 获取会话事件失败:', error); + return []; + } + }); + + // 删除会话 + onMessage('popup:delete-session', async (message) => { + try { + const sessionId = message.data; + await deleteRecordingSession(sessionId); + return { ok: true }; + } catch (error) { + console.error('[bg] 删除会话失败:', error); + return { ok: false }; + } + }); + + // 下载会话回放 + onMessage('popup:download-session', async (message) => { + try { + const sessionId = message.data; + const events = await getAllEvents(sessionId); + downloadHtmlInBackground(events); + return { ok: true }; + } catch (error) { + console.error('[bg] 下载会话失败:', error); + return { ok: false }; + } + }); + onMessage('content:save-track-events', async (event) => { const state = await loadRecorderState(); console.log('[bg] 收到事件,当前状态:', state); diff --git a/entrypoints/content.ts b/entrypoints/content.ts index df8818d..f1f4a9e 100644 --- a/entrypoints/content.ts +++ b/entrypoints/content.ts @@ -1,6 +1,6 @@ import '../.wxt/types/imports.d.ts'; import { createRecorder } from '@/utils/useRecorder'; -import { onMessage } from '@/utils/messages.tsx'; +import { onMessage } from '@/utils/messages'; export default defineContentScript({ // matches: ['*://*.google.com/*'], diff --git a/entrypoints/popup/App.tsx b/entrypoints/popup/App.tsx index 000290a..e592dab 100644 --- a/entrypoints/popup/App.tsx +++ b/entrypoints/popup/App.tsx @@ -1,8 +1,10 @@ // App.js import { HashRouter as Router, Routes, Route } from 'react-router-dom'; import TimestampPage from './pages/TimestampPage'; -import RecordeReplayPage from './pages/RecordeReplayPage'; +import RecordReplayPage from './pages/RecordReplayPage'; import TestPage from './pages/TestPage'; +import ReplayListPage from './pages/ReplayListPage'; +import ReplayPlayerPage from './pages/ReplayPlayerPage'; import Navbar from '../../components/Navbar'; import RoutePersistence from '../../components/RoutePersistence'; import './App.css'; @@ -11,7 +13,8 @@ import './App.css'; const navItems = [ { path: '/test', label: '测试页面', element: }, { path: '/', label: '时间戳', element: }, - { path: '/recorde-replay', label: '录制与回放', element: }, + { path: '/record-replay', label: '录制', element: }, + { path: '/replay-list', label: '历史回放', element: }, ]; function App() { @@ -25,6 +28,7 @@ function App() { {navItems.map((item) => ( ))} + } /> diff --git a/entrypoints/popup/pages/RecordReplayPage.tsx b/entrypoints/popup/pages/RecordReplayPage.tsx new file mode 100644 index 0000000..67b80ce --- /dev/null +++ b/entrypoints/popup/pages/RecordReplayPage.tsx @@ -0,0 +1,120 @@ +import { useEffect, useState, useMemo } from 'react'; +import { AppState } from '../types'; +import { sendMessage, onMessage } from '@/utils/messages'; +import { Button, Container, Stack, Typography, Alert } from '@mui/material'; +import { CircularProgress } from '@mui/material'; + +const RecordReplayPage = () => { + const [status, setStatus] = useState(AppState.READ); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const isRecording = useMemo(() => status === AppState.RECORDING, [status]); + + useEffect(() => { + const unlistenStarted = onMessage('popup:started', () => { + console.log('[popup] Received started message'); + setStatus(AppState.RECORDING); + setIsLoading(false); + setError(null); + }); + + const unlistenStopped = onMessage('popup:stopped', () => { + setStatus(AppState.READ); + setIsLoading(false); + setError(null); + }); + + const unlistenReady = onMessage('popup:ready', () => { + setStatus(AppState.READ); + setIsLoading(false); + setError(null); + }); + + sendMessage('popup:check-status', undefined) + .then((res) => { + if (res?.active) { + setStatus(AppState.RECORDING); + } else { + setStatus(AppState.READ); + } + }) + .catch((err) => { + console.error(err); + setError('检查录制状态失败'); + }); + + return () => { + unlistenStarted(); + unlistenStopped(); + unlistenReady(); + }; + }, []); + + const toggleRecording = async () => { + setIsLoading(true); + setError(null); + try { + if (isRecording) { + const result = await sendMessage('popup:stop', undefined); + if (!result?.ok) { + setError(result?.error || '停止录制失败'); + setIsLoading(false); + return; + } + } else { + const result = await sendMessage('popup:start', undefined); + if (!result?.ok) { + setError(result?.error || '开始录制失败'); + setIsLoading(false); + return; + } + } + } catch (error) { + console.error('Error toggling recording:', error); + setError('操作失败,请重试'); + setIsLoading(false); + } + }; + + return ( + + + {error && ( + + {error} + + )} + + + + + + + {isRecording ? '正在录制用户操作...' : '点击开始录制用户操作'} + + + + ); +}; + +export default RecordReplayPage; diff --git a/entrypoints/popup/pages/RecordeReplayPage.tsx b/entrypoints/popup/pages/RecordeReplayPage.tsx deleted file mode 100644 index 7940454..0000000 --- a/entrypoints/popup/pages/RecordeReplayPage.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useEffect, useState, useMemo } from 'react'; -import { AppState } from '../types'; -import { sendMessage, onMessage } from '@/utils/messages'; -import { Button, Container, Stack } from '@mui/material'; - -const RecordeReplayPage = () => { - const [status, setStatus] = useState(AppState.READ); - const isRecording = useMemo(() => status === AppState.RECORDING, [status]); - - useEffect(() => { - const unlistenStarted = onMessage('popup:started', () => { - console.log('[popup] Received started message'); - setStatus(AppState.RECORDING); - }); - - const unlistenStopped = onMessage('popup:stopped', () => { - setStatus(AppState.READ); - }); - - const unlistenReady = onMessage('popup:ready', () => { - setStatus(AppState.READ); - }); - - sendMessage('popup:check-status', undefined) - .then((res) => { - if (res?.active) { - setStatus(AppState.RECORDING); - } else { - setStatus(AppState.READ); - } - }) - .catch((err) => { - console.error(err); - }); - - return () => { - unlistenStarted(); - unlistenStopped(); - unlistenReady(); - }; - }, []); - - const toggleRecording = async () => { - try { - if (isRecording) { - const result = await sendMessage('popup:stop', undefined); - if (!result?.ok) { - return; - } - } else { - const result = await sendMessage('popup:start', undefined); - if (!result?.ok) { - return; - } - } - } catch (error) { - console.error('Error toggling recording:', error); - } - }; - - return ( - - - - - - ); -}; - -export default RecordeReplayPage; diff --git a/entrypoints/popup/pages/ReplayListPage.tsx b/entrypoints/popup/pages/ReplayListPage.tsx new file mode 100644 index 0000000..5f7b115 --- /dev/null +++ b/entrypoints/popup/pages/ReplayListPage.tsx @@ -0,0 +1,181 @@ +import { useEffect, useState } from 'react'; +import { + Container, + List, + ListItem, + ListItemButton, + ListItemText, + ListItemSecondaryAction, + IconButton, + Typography, + Box, + CircularProgress, + Alert, + Button, +} from '@mui/material'; +import { + PlayArrow as PlayIcon, + Download as DownloadIcon, + Delete as DeleteIcon, +} from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; +import { sendMessage } from '@/utils/messages'; + +interface Session { + id: string; + startTime: number; + tabId: number; + chunkCount: number; + totalEvents: number; +} + +const ReplayListPage = () => { + const [sessions, setSessions] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const navigate = useNavigate(); + + useEffect(() => { + fetchSessions(); + }, []); + + const fetchSessions = async () => { + try { + const data = await sendMessage('popup:get-sessions'); + setSessions(data || []); + } catch (err) { + console.error(err); + setError('获取录制历史失败'); + } finally { + setIsLoading(false); + } + }; + + const handlePlay = (sessionId: string) => { + navigate(`/replay/${sessionId}`); + }; + + const handleDownload = async (sessionId: string) => { + try { + await sendMessage('popup:download-session', sessionId); + } catch (err) { + console.error(err); + setError('下载失败'); + } + }; + + const handleDelete = async (sessionId: string) => { + if (window.confirm('确定要删除这个录制吗?')) { + try { + await sendMessage('popup:delete-session', sessionId); + fetchSessions(); + } catch (err) { + console.error(err); + setError('删除失败'); + } + } + }; + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + + {error} + + + + ); + } + + return ( + + + 录制历史 + + + {sessions.length === 0 ? ( + + + 暂无录制历史 + + + 点击"录制与回放"页面的开始按钮进行录制 + + + ) : ( + + {sessions.map((session) => ( + + handlePlay(session.id)} + > + + + + {new Date(session.startTime).toLocaleString()} + + + } + secondary={ + + + 事件数: {session.chunkCount * 100} + + + Tab: {session.tabId} + + + } + /> + + { + e.stopPropagation(); + handleDownload(session.id); + }} + title="下载" + > + + + { + e.stopPropagation(); + handleDelete(session.id); + }} + title="删除" + sx={{ color: 'error.main' }} + > + + + + + + ))} + + )} + + ); +}; + +export default ReplayListPage; diff --git a/entrypoints/popup/pages/ReplayPlayerPage.tsx b/entrypoints/popup/pages/ReplayPlayerPage.tsx new file mode 100644 index 0000000..16876d9 --- /dev/null +++ b/entrypoints/popup/pages/ReplayPlayerPage.tsx @@ -0,0 +1,159 @@ +import { useEffect, useState, useCallback } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { + Container, + Typography, + Box, + CircularProgress, + Alert, + IconButton, + Button, +} from '@mui/material'; +import { + ArrowBack as ArrowBackIcon, + Download as DownloadIcon, + Delete as DeleteIcon, +} from '@mui/icons-material'; +import { sendMessage } from '@/utils/messages'; + +const ReplayPlayerPage = () => { + const { id } = useParams<{ id: string }>(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + const [eventsData, setEventsData] = useState(null); + + const fetchSessionEvents = useCallback(async (sessionId: string) => { + try { + const data = await sendMessage('popup:get-session-events', sessionId); + setEventsData(data || []); + } catch (err) { + console.error(err); + setError('加载录制内容失败'); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + if (id) { + fetchSessionEvents(id); + } + }, [id, fetchSessionEvents]); + + const handleDownload = async () => { + if (!id) return; + try { + await sendMessage('popup:download-session', id); + } catch (err) { + console.error(err); + setError('下载失败'); + } + }; + + const handleDelete = async () => { + if (!id) return; + if (window.confirm('确定要删除这个录制吗?')) { + try { + await sendMessage('popup:delete-session', id); + navigate('/record-replay'); + } catch (err) { + console.error(err); + setError('删除失败'); + } + } + }; + + if (isLoading) { + return ( + + + + ); + } + + if (error) { + return ( + + + navigate('/record-replay')} sx={{ mr: 1 }}> + + + 回放 + + + {error} + + + + ); + } + + if (!eventsData || eventsData.length === 0) { + return ( + + + navigate('/record-replay')} sx={{ mr: 1 }}> + + + 回放 + + + 此会话没有事件数据,请尝试重新录制。 + + + ); + } + + return ( + + + navigate('/record-replay')} sx={{ mr: 1 }}> + + + 回放 + + + + + + + + + + + + + + 由于浏览器安全策略限制,无法在内联 iframe 中直接回放。 + + + 请点击右上角的"下载"按钮,下载完整的回放 HTML 文件到本地查看。 + + + + + + + + + 安全提示:浏览器扩展有严格的 Content Security Policy (CSP) + 限制,禁止内联脚本执行,因此无法在扩展 popup 中直接显示回放内容。 + 下载的 HTML 文件包含了完整的回放代码,可以在任何现代浏览器中直接打开。 + + + + + ); +}; + +export default ReplayPlayerPage; diff --git a/eslint.config.ts b/eslint.config.ts index 993e4ed..704492b 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -5,7 +5,7 @@ import * as reactPlugin from 'eslint-plugin-react'; import globals from 'globals'; export default tseslint.config( - { ignores: ['dist', '.wxt', 'node_modules', 'eslint.config.ts'] }, + { ignores: ['dist', '.wxt', 'node_modules', 'eslint.config.ts', '**/*.test.tsx', '**/*.test.ts', '**/__tests__/**'] }, { files: [ diff --git a/package-lock.json b/package-lock.json index 021888b..763884f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,7 +41,7 @@ "@types/webextension-polyfill": "^0.12.4", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", - "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-react": "^4.3.4", "@wxt-dev/module-react": "^1.1.5", "eslint": "^9.39.2", "eslint-plugin-react": "^7.37.5", @@ -727,40 +727,6 @@ } } }, - "node_modules/@emnapi/core": { - "version": "1.9.0", - "resolved": "https://mirrors.cloud.tencent.com/npm/@emnapi/core/-/core-1.9.0.tgz", - "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.0", - "resolved": "https://mirrors.cloud.tencent.com/npm/@emnapi/runtime/-/runtime-1.9.0.tgz", - "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://mirrors.cloud.tencent.com/npm/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmmirror.com/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -2036,23 +2002,6 @@ "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", "license": "MIT" }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://mirrors.cloud.tencent.com/npm/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2091,26 +2040,6 @@ "node": ">= 8" } }, - "node_modules/@oxc-project/runtime": { - "version": "0.115.0", - "resolved": "https://mirrors.cloud.tencent.com/npm/@oxc-project/runtime/-/runtime-0.115.0.tgz", - "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.115.0", - "resolved": "https://mirrors.cloud.tencent.com/npm/@oxc-project/types/-/types-0.115.0.tgz", - "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -2175,265 +2104,10 @@ "node": ">=14.0.0" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", - "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", - "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", - "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", - "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", - "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", - "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", - "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.7", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", + "version": "1.0.0-beta.27", + "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", "dev": true, "license": "MIT" }, @@ -2938,17 +2612,6 @@ "integrity": "sha512-5lYJP45Xllo4yE/RUBccBT32eBlRDbqN8r1/MIvQbKxW3aFqaYPCNgm8D5V20X4ShHcwvYWNlKg3liDh1MlBoA==", "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://mirrors.cloud.tencent.com/npm/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -3434,29 +3097,34 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://mirrors.cloud.tencent.com/npm/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", + "version": "4.7.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://mirrors.cloud.tencent.com/npm/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, "node_modules/@vitest/expect": { @@ -5343,6 +5011,8 @@ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=8" } @@ -8064,6 +7734,8 @@ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.3" }, @@ -8101,6 +7773,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8122,6 +7795,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8143,6 +7817,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8164,6 +7839,7 @@ "os": [ "freebsd" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8185,6 +7861,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8206,6 +7883,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8227,6 +7905,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8248,6 +7927,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8269,6 +7949,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8290,6 +7971,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -8311,6 +7993,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">= 12.0.0" }, @@ -10604,47 +10287,6 @@ "dev": true, "license": "MIT" }, - "node_modules/rolldown": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/rolldown/-/rolldown-1.0.0-rc.9.tgz", - "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.115.0", - "@rolldown/pluginutils": "1.0.0-rc.9" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", - "@rolldown/binding-darwin-x64": "1.0.0-rc.9", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" - } - }, - "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.9", - "resolved": "https://mirrors.cloud.tencent.com/npm/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", - "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", - "dev": true, - "license": "MIT" - }, "node_modules/rollup": { "version": "4.56.0", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.56.0.tgz", @@ -12265,108 +11907,6 @@ } }, "node_modules/vite": { - "version": "8.0.0", - "resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-8.0.0.tgz", - "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/runtime": "0.115.0", - "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.9", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.0.0-alpha.31", - "esbuild": "^0.27.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "5.3.0", - "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-5.3.0.tgz", - "integrity": "sha512-8f20COPYJujc3OKPX6OuyBy3ZIv2det4eRRU4GY1y2MjbeGSUmPjedxg1b72KnTagCofwvZ65ThzjxDW2AtQFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "es-module-lexer": "^2.0.0", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "vite": "^7.3.1" - }, - "bin": { - "vite-node": "dist/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://opencollective.com/antfu" - } - }, - "node_modules/vite-node/node_modules/vite": { "version": "7.3.1", "resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-7.3.1.tgz", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", @@ -12441,6 +11981,29 @@ } } }, + "node_modules/vite-node": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/vite-node/-/vite-node-5.3.0.tgz", + "integrity": "sha512-8f20COPYJujc3OKPX6OuyBy3ZIv2det4eRRU4GY1y2MjbeGSUmPjedxg1b72KnTagCofwvZ65ThzjxDW2AtQFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "es-module-lexer": "^2.0.0", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "vite": "^7.3.1" + }, + "bin": { + "vite-node": "dist/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://opencollective.com/antfu" + } + }, "node_modules/vitest": { "version": "4.1.0", "resolved": "https://mirrors.cloud.tencent.com/npm/vitest/-/vitest-4.1.0.tgz", @@ -12906,81 +12469,6 @@ "url": "https://github.com/sponsors/wxt-dev" } }, - "node_modules/wxt/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://mirrors.cloud.tencent.com/npm/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/xdg-basedir": { "version": "5.1.0", "resolved": "https://registry.npmmirror.com/xdg-basedir/-/xdg-basedir-5.1.0.tgz", diff --git a/package.json b/package.json index f6f036f..1246653 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@types/webextension-polyfill": "^0.12.4", "@typescript-eslint/eslint-plugin": "^8.54.0", "@typescript-eslint/parser": "^8.54.0", - "@vitejs/plugin-react": "^6.0.1", + "@vitejs/plugin-react": "^4.3.4", "@wxt-dev/module-react": "^1.1.5", "eslint": "^9.39.2", "eslint-plugin-react": "^7.37.5", diff --git a/utils/messages.tsx b/utils/messages.tsx index 885ef82..1024f10 100644 --- a/utils/messages.tsx +++ b/utils/messages.tsx @@ -9,6 +9,10 @@ interface ProtocolMap { 'popup:tab-changed': (data: { currentTabId: number; recordingTabId: number | undefined }) => void; 'popup:check-status': () => { active: boolean; startTime?: number }; 'popup:ready': () => void; + 'popup:get-sessions': () => { id: string; startTime: number; tabId: number; chunkCount: number; totalEvents: number }[]; + 'popup:get-session-events': (sessionId: string) => unknown[]; + 'popup:delete-session': (sessionId: string) => { ok: boolean }; + 'popup:download-session': (sessionId: string) => { ok: boolean }; // --- Content 相关 --- 'content:save-track-events': (event: unknown) => boolean; diff --git a/utils/recordEventsDb.ts b/utils/recordEventsDb.ts index 24c2e70..334d1c8 100644 --- a/utils/recordEventsDb.ts +++ b/utils/recordEventsDb.ts @@ -89,18 +89,33 @@ export async function initRecordingSession(sessionId: string, tabId: number): Pr * 获取当前会话的 chunk 数量 */ export async function getSessionChunkCount(sessionId: string): Promise { + // 参数验证 + if (!sessionId || typeof sessionId !== 'string') { + console.error('[db] Invalid sessionId for getSessionChunkCount:', sessionId); + return 0; + } + const database = await openDB(); - return new Promise((resolve, reject) => { + return new Promise((resolve) => { const tx = database.transaction('sessions', 'readonly'); const store = tx.objectStore('sessions'); - const request = store.get(sessionId); - request.onsuccess = () => { - const session = request.result as RecordingSession | undefined; - resolve(session?.chunkCount || 0); - }; - request.onerror = () => reject(request.error); + try { + const request = store.get(sessionId); + + request.onsuccess = () => { + const session = request.result as RecordingSession | undefined; + resolve(session?.chunkCount || 0); + }; + request.onerror = (e) => { + console.error('[db] Error getting session chunk count:', e); + resolve(0); + }; + } catch (error) { + console.error('[db] Exception getting session chunk count:', error); + resolve(0); + } }); } @@ -150,14 +165,28 @@ async function getChunk( sessionId: string, chunkIndex: number, ): Promise { - return new Promise((resolve, reject) => { - const tx = database.transaction(STORE_NAME, 'readonly'); - const store = tx.objectStore(STORE_NAME); - const index = store.index('sessionId_chunkIndex'); - const request = index.get([sessionId, chunkIndex]); + // 参数验证 + if (!sessionId || typeof sessionId !== 'string') { + console.error('[db] Invalid sessionId for getChunk:', sessionId); + return null; + } - request.onsuccess = () => resolve(request.result || null); - request.onerror = () => reject(request.error); + return new Promise((resolve) => { + try { + const tx = database.transaction(STORE_NAME, 'readonly'); + const store = tx.objectStore(STORE_NAME); + const index = store.index('sessionId_chunkIndex'); + const request = index.get([sessionId, chunkIndex]); + + request.onsuccess = () => resolve(request.result || null); + request.onerror = (e) => { + console.error('[db] Error getting chunk:', e); + resolve(null); + }; + } catch (error) { + console.error('[db] Exception getting chunk:', error); + resolve(null); + } }); } @@ -211,14 +240,26 @@ export async function streamAllEvents( sessionId: string, onChunk: (events: unknown[]) => void | Promise, ): Promise { - const database = await openDB(); - const chunkCount = await getSessionChunkCount(sessionId); + // 参数验证 + if (!sessionId || typeof sessionId !== 'string') { + console.error('[db] Invalid sessionId for streamAllEvents:', sessionId); + return; + } - for (let i = 0; i < chunkCount; i++) { - const chunk = await getChunk(database, sessionId, i); - if (chunk && chunk.events.length > 0) { - await onChunk(chunk.events); + try { + const database = await openDB(); + const chunkCount = await getSessionChunkCount(sessionId); + + console.log(`[db] Streaming ${chunkCount} chunks for session ${sessionId}`); + + for (let i = 0; i < chunkCount; i++) { + const chunk = await getChunk(database, sessionId, i); + if (chunk && chunk.events.length > 0) { + await onChunk(chunk.events); + } } + } catch (error) { + console.error('[db] Error streaming events:', error); } } @@ -227,6 +268,12 @@ export async function streamAllEvents( * 注意:这会将所有事件加载到内存,仅在需要导出时调用 */ export async function getAllEvents(sessionId: string): Promise { + // 参数验证 + if (!sessionId || typeof sessionId !== 'string') { + console.error('[db] Invalid sessionId for getAllEvents:', sessionId); + return []; + } + const allEvents: unknown[] = []; await streamAllEvents(sessionId, (events) => { @@ -271,6 +318,33 @@ export async function deleteRecordingSession(sessionId: string): Promise { }); } +/** + * 获取所有录制会话列表 + */ +export async function getAllSessions(): Promise { + const database = await openDB(); + + return new Promise((resolve, reject) => { + const tx = database.transaction('sessions', 'readonly'); + const store = tx.objectStore('sessions'); + const index = store.index('startTime'); + const request = index.openCursor(null, 'prev'); // 按时间倒序排列 + const sessions: RecordingSession[] = []; + + request.onsuccess = (event) => { + const cursor = (event.target as IDBRequest).result; + if (cursor) { + sessions.push(cursor.value); + cursor.continue(); + } else { + resolve(sessions); + } + }; + + request.onerror = () => reject(request.error); + }); +} + /** * 生成唯一的会话 ID */ diff --git a/utils/recordUtils.tsx b/utils/recordUtils.tsx index 08be169..2d006d6 100644 --- a/utils/recordUtils.tsx +++ b/utils/recordUtils.tsx @@ -7,46 +7,65 @@ export const downloadHtmlInBackground = (events: unknown[]) => { .replace(/`/g, '\\`') .replace(/<\/script>/g, '<\\/script>'); + // 使用固定版本的 rrweb-player,确保与项目使用的 rrweb 版本兼容 const htmlContent = ` - + RRWeb 回放 - +
- - + + - `; @@ -61,7 +80,7 @@ export const downloadHtmlInBackground = (events: unknown[]) => { filename: `replay-${Date.now()}.html`, saveAs: true, }) - .then((r) => console.log(r)); + .then((r) => console.log('Download started:', r)); }; reader.readAsDataURL(blob); };