您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Full-featured auto-clicker with rate-limiting/backoff, retry handling, spinner delays, draggable panel, and complete utility functions
// ==UserScript== // @name Material Calendar Advanced Auto-Clicker // @namespace http://tampermonkey.net/ // @version 4.15.2 // @description Full-featured auto-clicker with rate-limiting/backoff, retry handling, spinner delays, draggable panel, and complete utility functions // @match https://inpol.mazowieckie.pl/home/cases/* // @grant none // @run-at document-idle // @license CC-BY-NC-ND-4.0 // ==/UserScript== (function() { 'use strict'; // -- CONFIGURATION PARAMETERS -- const RETRY_LIMIT = 3; // Number of retry attempts on failures const INITIAL_DELAY = 1000; // Base backoff delay in ms const MAX_BACKOFF = 10000; // Maximum backoff delay in ms const RANDOM_MIN = 500; // Minimum random delay in ms const RANDOM_MAX = 1500; // Maximum random delay in ms // -- STATE VARIABLES -- let running = false; let stopRequested = false; let recordMode = false; let dayRange = []; let backoffCount = 0; // -- UTILITY FUNCTIONS -- /** Pause execution for a given duration (ms) */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } /** Generate a uniform random delay between RANDOM_MIN and RANDOM_MAX */ function randomDelay() { return Math.floor(Math.random() * (RANDOM_MAX - RANDOM_MIN + 1)) + RANDOM_MIN; } /** Compute exponential backoff delay based on backoffCount */ function getBackoffDelay() { const delay = Math.min(INITIAL_DELAY * Math.pow(2, backoffCount), MAX_BACKOFF); backoffCount = Math.min(backoffCount + 1, Math.log2(MAX_BACKOFF / INITIAL_DELAY)); return delay; } /** Reset backoff counter after a successful operation */ function resetBackoff() { backoffCount = 0; } // -- LOADING SPINNER DETECTION -- /** Wait until the Angular spinner <circle> is gone from the DOM */ async function waitForSpinnerGone(timeout = 5000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const spinner = document.querySelector('circle.ng-star-inserted'); if (!spinner) { return; } await sleep(100); } console.warn('Spinner did not disappear within timeout'); } // -- DRAGGABLE PANEL SETUP -- /** * Make a panel draggable by a handle element * @param {HTMLElement} handle The element to listen for drag events * @param {HTMLElement} panelEl The element to move */ function makeDraggable(handle, panelEl) { let isDragging = false; let startX = 0, startY = 0; let panelX = 0, panelY = 0; handle.addEventListener('mousedown', e => { isDragging = true; startX = e.clientX; startY = e.clientY; const rect = panelEl.getBoundingClientRect(); panelX = rect.left; panelY = rect.top; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!isDragging) return; const dx = e.clientX - startX; const dy = e.clientY - startY; panelEl.style.left = panelX + dx + 'px'; panelEl.style.top = panelY + dy + 'px'; }); document.addEventListener('mouseup', () => { isDragging = false; }); } // -- VISUAL FEEDBACK -- /** Flash a red overlay briefly as an error indicator */ function flashRed() { const overlay = document.createElement('div'); Object.assign(overlay.style, { position: 'fixed', top: 0, left: 0, width: '100%', height: '100%', backgroundColor: 'rgba(255,0,0,0.3)', zIndex: 99999 }); document.body.appendChild(overlay); setTimeout(() => document.body.removeChild(overlay), 300); } // -- CONTROL PANEL UI -- const panel = document.createElement('div'); Object.assign(panel.style, { position: 'fixed', top: '10px', left: '10px', width: '360px', maxHeight: '90vh', overflowY: 'auto', background: '#222', color: '#fff', padding: '10px', borderRadius: '5px', fontFamily: 'Arial, sans-serif', fontSize: '14px', zIndex: 99998 }); document.body.appendChild(panel); // Make the header draggable instead of entire panel to allow input focus // makeDraggable(panel); // removed global drag const header = document.createElement('h3'); header.textContent = 'Material Calendar Auto-Clicker v4.14'; Object.assign(header.style, { margin: '0 0 8px 0', cursor: 'move', textAlign: 'center', userSelect: 'none' }); panel.appendChild(header); makeDraggable(header, panel); // enable dragging by header // Day range input const rangeLabel = document.createElement('label'); rangeLabel.htmlFor = 'dayRangeInput'; rangeLabel.textContent = 'Enter days to click (e.g. 29,31):'; panel.appendChild(rangeLabel); const rangeInput = document.createElement('input'); rangeInput.id = 'dayRangeInput'; rangeInput.type = 'text'; Object.assign(rangeInput.style, { width: '100%', padding: '4px', margin: '4px 0' }); rangeInput.placeholder = '29,31'; rangeInput.addEventListener('change', () => { dayRange = rangeInput.value.split(',').map(s => parseInt(s.trim(), 10)).filter(n => !isNaN(n)); console.log('Day range set to:', dayRange); }); panel.appendChild(rangeInput); // Log container const logContainer = document.createElement('div'); Object.assign(logContainer.style, { width: '100%', height: '140px', overflowY: 'auto', background: '#111', padding: '6px', borderRadius: '3px', margin: '8px 0' }); panel.appendChild(logContainer); function appendLog(message, color) { const entry = document.createElement('div'); entry.textContent = message; if (color) entry.style.color = color; logContainer.appendChild(entry); logContainer.scrollTop = logContainer.scrollHeight; } ['log', 'warn', 'error'].forEach(level => { const original = console[level]; console[level] = (...args) => { original.apply(console, args); const msg = args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' '); const col = level === 'warn' ? '#f90' : level === 'error' ? '#f33' : '#fff'; appendLog(msg, col); }; }); function createButton(text, bgColor, handler) { const btn = document.createElement('button'); btn.textContent = text; Object.assign(btn.style, { width: '100%', padding: '6px', margin: '4px 0', background: bgColor, color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer' }); btn.addEventListener('click', handler); panel.appendChild(btn); return btn; } createButton('Clear Logs', '#555', () => { logContainer.innerHTML = ''; }); createButton('Export Calendar HTML', '#28a745', () => { const cal = document.querySelector('mat-calendar') || document.querySelector('div.mat-calendar'); if (!cal) return console.warn('No calendar element found'); console.log('=== CALENDAR HTML START ==='); console.log(cal.outerHTML); console.log('=== CALENDAR HTML END ==='); }); const recordBtn = createButton('Record OFF', '#cc7700', () => { recordMode = !recordMode; recordBtn.textContent = recordMode ? 'Record ON' : 'Record OFF'; recordBtn.style.background = recordMode ? '#dc3545' : '#cc7700'; console.log('Record mode', recordMode ? 'ENABLED' : 'DISABLED'); }); const toggleBtn = createButton('Start Auto-Click', '#007bff', () => { running ? stop() : start(); }); // -- FAKE SLOT INJECTION -- const injectBtn = createButton('Inject Fake Slots', '#6f42c1', () => { // choose random date cell const dates = getDates(); if (!dates.length) return console.warn('No dates to inject into'); const cell = dates[Math.floor(Math.random() * dates.length)]; // create container for fake slots const fakeContainer = document.createElement('div'); fakeContainer.setAttribute('data-fake-slot', 'true'); // sample HTML snippet fakeContainer.innerHTML = ` <div class="reservation__hours"> <div class="tiles tiles--hours"> <div class="row"> <div class="col-md-3 tiles__item"><button class="tiles__link">99:99</button></div> </div> </div> </div>`; // append under calendar body cell.parentElement.appendChild(fakeContainer); console.log('Injected fake slots under date', cell.textContent.trim()); }); // }); <--- **This closing brace+paren was extraneous; now commented out** // -- DOM INTERACTION HELPERS -- /** Get all clickable date cells matching dayRange */ function getDates() { const all = Array.from(document.querySelectorAll( 'div.mat-calendar-body-cell-content.mat-focus-indicator' )); return all.filter(cell => { const parent = cell.parentElement; const dayNum = parseInt(cell.textContent.trim(), 10); return parent && !parent.classList.contains('mat-calendar-body-disabled') && cell.offsetParent && (!dayRange.length || dayRange.includes(dayNum)); }); } /** Wait until a clicked cell appears selected */ function waitForSelection(cell, timeout = 5000) { return new Promise(resolve => { const parent = cell.parentElement; const start = Date.now(); (function check() { if (parent.classList.contains('mat-calendar-body-selected') || Date.now() - start > timeout) { resolve(); } else { setTimeout(check, 100); } })(); }); } /** Get all visible time-slot buttons */ function getTimes() { const container = document.querySelector('.tiles--hours, .reservation__hours'); if (!container) return []; return Array.from(container.querySelectorAll('button')).filter(btn => btn.offsetParent); } /** Click the Yes/Tak button in any open confirmation dialog */ function confirmDialog() { const dlg = document.querySelector('mat-dialog-container, .mat-dialog-container'); if (!dlg) return; const yesBtn = Array.from(dlg.querySelectorAll('button')).find(b => /^(Tak|Yes)$/i.test(b.textContent.trim()) ); if (yesBtn) yesBtn.click(); } // -- RETRY/ERROR HANDLING -- async function attemptWithRetry(fn, description) { for (let attempt = 1; attempt <= RETRY_LIMIT; attempt++) { try { return await fn(); } catch (err) { console.error(`${description} failed on attempt ${attempt}`, err); flashRed(); if (attempt === RETRY_LIMIT) { console.error(`Giving up on ${description} after ${RETRY_LIMIT} attempts`); throw err; } const backoff = getBackoffDelay(); console.log(`Retrying ${description} in ${backoff} ms`); await sleep(backoff); } } } // -- MAIN AUTO-CLICK LOOP -- async function autoClickLoop() { while (!stopRequested) { const dates = getDates(); if (!dates.length) { console.warn('No dates matching range, backing off'); const backoff = getBackoffDelay(); await sleep(backoff); continue; } resetBackoff(); for (const cell of dates) { if (stopRequested) break; const dayText = cell.textContent.trim(); console.log(`Processing date ${dayText}`); // 1) Click the date, wait to be selected await attemptWithRetry(async () => { cell.click(); await waitForSelection(cell); await waitForSpinnerGone(); }, `Select date ${dayText}`); // 2) Random delay after spinner disappears const postDateDelay = randomDelay(); console.log(`Waiting ${postDateDelay} ms after date select`); await sleep(postDateDelay); // 3) Fetch time slots const times = getTimes(); if (!times.length) { console.log(`No time slots for date ${dayText}, moving to next`); continue; } // 4) If recordMode off, flash and skip reservation if (!recordMode) { flashRed(); console.log('Record OFF: detected slots but not booking'); continue; } // 5) Choose a slot (earliest ≥ noon, else earliest) const entries = times.map(btn => { const hour = Number(btn.textContent.trim().split(':')[0]); return { btn, minutes: hour * 60 }; }); let candidates = entries.filter(e => e.minutes >= 750); if (!candidates.length) candidates = entries; candidates.sort((a, b) => a.minutes - b.minutes); const chosen = candidates[0].btn; const timeText = chosen.textContent.trim(); // 6) Click and confirm await attemptWithRetry(async () => { console.log(`Clicking time slot ${timeText}`); chosen.click(); await waitForSpinnerGone(); confirmDialog(); await waitForSpinnerGone(); console.log(`Confirmed appointment at ${timeText}`); }, `Book time ${timeText}`); // 7) Random delay before next date iteration const delayNext = randomDelay(); console.log(`Waiting ${delayNext} ms before next date`); await sleep(delayNext); } } } // -- START/STOP CONTROLS -- function start() { if (running) return; running = true; stopRequested = false; toggleBtn.textContent = 'Stop Auto-Click'; console.log('Auto-click started'); autoClickLoop(); } function stop() { if (!running) return; stopRequested = true; running = false; toggleBtn.textContent = 'Start Auto-Click'; console.log('Auto-click stopped'); } console.log('Material Calendar Advanced Auto-Clicker v4.15.2 loaded'); })(); /* EOF */
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址