求支持ListenForWindowEvent/PlayWindowEvent的Chrome事件录制扩展
I get exactly what you're dealing with—regular DOM event listeners and synthetic events fall flat when it comes to system-level UI components like scrollbars, native <select> dropdowns, or even some input field interactions. The issue is that these components are handled by the browser's native code, not the page's DOM, so your regular JS can't capture or replicate the low-level mouse/keyboard events that drive them.
Here's a Chrome Extension implementation that exposes the ListenForWindowEvent(), PlayWindowEvent(), and a helper StopWindowEventRecording() function directly to your page's JS. It uses Chrome's debugger API to bypass the DOM sandbox and interact with the browser's core input system.
Extension Setup
1. Manifest File (manifest.json)
This defines the extension's permissions and structure. We need the debugger permission to access low-level events, and activeTab to work on any page you activate:
{ "manifest_version": 3, "name": "Native Window Event Recorder", "version": "1.0", "permissions": ["debugger", "activeTab"], "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"] } ] }
2. Background Service Worker (background.js)
This handles the heavy lifting: connecting to the browser debugger, recording events, and playing them back. It listens for messages from the content script to control recording/playback.
let recordedEvents = []; let isRecording = false; // Listen for debugger events (low-level input actions) chrome.debugger.onEvent.addListener((source, method, params) => { if (!isRecording) return; // Only track input-related events (mouse/keyboard) if (method.startsWith('Input.')) { recordedEvents.push({ method, params, timestamp: Date.now() }); } }); // Handle messages from the content script chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { switch (message.action) { case 'startRecording': isRecording = true; recordedEvents = []; // Attach debugger to the current tab chrome.debugger.attach({ tabId: sender.tab.id }, '1.3', () => { if (chrome.runtime.lastError) { sendResponse({ success: false, error: chrome.runtime.lastError.message }); return; } // Enable input event monitoring chrome.debugger.sendCommand({ tabId: sender.tab.id }, 'Input.enable', {}, () => { sendResponse({ success: true }); }); }); return true; // Keep response channel open for async operation case 'stopRecording': isRecording = false; // Disable input monitoring and detach debugger chrome.debugger.sendCommand({ tabId: sender.tab.id }, 'Input.disable', {}, () => { chrome.debugger.detach({ tabId: sender.tab.id }); sendResponse({ success: true, events: recordedEvents }); }); return true; case 'playEvents': const events = message.events; let currentIndex = 0; const playNext = () => { if (currentIndex >= events.length) { chrome.debugger.detach({ tabId: sender.tab.id }); sendResponse({ success: true }); return; } const event = events[currentIndex]; // Calculate delay to match original timing between events const delay = currentIndex > 0 ? event.timestamp - events[currentIndex - 1].timestamp : 0; setTimeout(() => { // Dispatch the low-level event via debugger chrome.debugger.sendCommand({ tabId: sender.tab.id }, event.method, event.params, () => { currentIndex++; playNext(); }); }, delay); }; // Attach debugger and start playback chrome.debugger.attach({ tabId: sender.tab.id }, '1.3', () => { if (chrome.runtime.lastError) { sendResponse({ success: false, error: chrome.runtime.lastError.message }); return; } chrome.debugger.sendCommand({ tabId: sender.tab.id }, 'Input.enable', {}, playNext); }); return true; } });
3. Content Script (content.js)
This injects the required functions into the page's global scope, so your own JS can call them directly:
// Start recording low-level window events window.ListenForWindowEvent = async function() { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: 'startRecording' }, (response) => { response.success ? resolve('Recording initiated') : reject(new Error(response.error)); }); }); }; // Play back recorded events window.PlayWindowEvent = async function(recordedEvents) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: 'playEvents', events: recordedEvents }, (response) => { response.success ? resolve('Playback completed') : reject(new Error(response.error)); }); }); }; // Helper to stop recording and retrieve events window.StopWindowEventRecording = async function() { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ action: 'stopRecording' }, (response) => { response.success ? resolve(response.events) : reject(new Error(response.error)); }); }); };
How to Use in Your Own Code
Once the extension is loaded, you can call these functions from any script on the page:
// Example workflow async function demoRecording() { // Start recording await ListenForWindowEvent(); // Perform actions: drag scrollbars, select dropdown options, type in inputs, etc. // Stop recording and get the event array const events = await StopWindowEventRecording(); // Wait a second, then play back setTimeout(async () => { await PlayWindowEvent(events); }, 1000); } // Run the demo demoRecording();
Why This Works
- Low-Level Access: The
chrome.debuggerAPI talks directly to the browser's input system, capturing events that never reach the page's DOM (like dragging a native scrollbar or selecting a<select>option via mouse). - Accurate Playback: Instead of triggering synthetic DOM events, we use
Input.dispatchMouseEventandInput.dispatchKeyEventto replicate the exact user input, so system components behave exactly as they did during recording.
Notes
- The first time you use the extension, Chrome will prompt you to grant debugger permissions—you'll need to approve this.
- You can't use Chrome's DevTools debugger on the same tab while this extension is recording/playing back (they'll conflict).
- This covers most mouse and keyboard interactions, but if you need to capture other system events (like touch), you can extend the event filtering in
background.jsto includeInput.touch*methods.
内容的提问来源于stack exchange,提问作者M Katz

