// ==UserScript== // @name 国开无敌自动刷课+次数和时长 // @namespace https://scriptcat.org/ // @version 4.6.0 // @description 目录展开与选择一栏|轮流循环点击|视频开关|2/4/6/8/10倍速|自动静音播放完再继续|苹果风格UI|ESC停止|后台也继续运行|自建授权服务器 // @author You // @match *://lms.ouchn.cn/* // @icon https://raw.githubusercontent.com/yrtyrtyrtygfr/cjtfky/main/gd1.png // @grant GM_xmlhttpRequest // @connect aitwo.icu // @run-at document-start // @license MPL-2.0 // ==/UserScript== (function () { 'use strict'; if (window.__QCC_INSTALLED__) return; window.__QCC_INSTALLED__ = true; /* ========== 自建授权服务器 ========== */ const AUTH_STORAGE_KEY = 'qcc_auth_code_v3'; const AUTH_EXPIRE_KEY = 'qcc_auth_expire_v3'; const AUTH_SERVER = 'https://aitwo.icu'; const HEARTBEAT_MS = 50000, HEARTBEAT_FAIL_LIMIT = 3; let authCode = '', authorized = false; /* 设备号(供后台绑定使用) */ const KEYT_DEVICE_ID = (() => { try { let id = localStorage.getItem('qcc_device_id'); if (id) return id; let hex = ''; for (let i = 0; i < 12; i++) hex += Math.floor(Math.random() * 16).toString(16).toUpperCase(); id = hex + 'MAC'; localStorage.setItem('qcc_device_id', id); return id; } catch (e) { return 'FALLBACK' + Date.now().toString(16).slice(-6).toUpperCase() + 'MAC'; } })(); let __syncAuthUI = null, heartbeatTimer = null, heartbeatFailCount = 0; /* ---- 通用 POST ---- */ function apiPost(path, data) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'POST', url: AUTH_SERVER + path, headers: { 'Content-Type': 'application/json' }, data: JSON.stringify(data || {}), timeout: 10000, onload: r => { try { resolve(JSON.parse(r.responseText || '{}')); } catch (e) { reject(new Error('bad json')); } }, onerror: e => reject(e), ontimeout: () => reject(new Error('timeout')) }); }); } /* ---- 请求验证:POST /api/verify {card, mac} -> {ok, code, msg, days} ---- */ async function requestVerify(card, mac) { try { const res = await apiPost('/api/verify', { card, mac }); return res && typeof res === 'object' ? res : null; } catch (e) { return null; } } /* ---- 验证并保存授权状态 ---- */ async function verifyAuthCode(rawCode) { const code = (rawCode || '').trim(); if (!code) return { ok: false, msg: '请输入卡密' }; try { const res = await requestVerify(code, KEYT_DEVICE_ID); if (!res) return { ok: false, msg: '❌ 网络请求失败,请检查网络后重试' }; if (res.ok) { authorized = true; authCode = code; localStorage.setItem(AUTH_STORAGE_KEY, code); const days = parseInt(res.days, 10); if (!isNaN(days) && days > 0) { localStorage.setItem(AUTH_EXPIRE_KEY, String(Date.now() + days * 86400000)); } else { localStorage.removeItem(AUTH_EXPIRE_KEY); } startHeartbeat(); if (typeof __syncAuthUI === 'function') { try { __syncAuthUI(); } catch (e) {} } const tail = (!isNaN(days) && days > 0) ? `(剩余 ${days} 天)` : '(终身有效)'; return { ok: true, msg: '✅ ' + (res.msg || '验证通过') + tail }; } return { ok: false, msg: '❌ ' + (res.msg || (res.code === 'device_mismatch' ? '设备不匹配(请在后台解绑该卡密或改用新卡密)' : '验证失败')) }; } catch (e) { return { ok: false, msg: '❌ 网络请求失败,请检查网络后重试' }; } } /* ---- 心跳 ---- */ function startHeartbeat() { stopHeartbeat(); heartbeatFailCount = 0; heartbeatTimer = setInterval(async () => { if (!authorized || !authCode) return; const res = await requestVerify(authCode, KEYT_DEVICE_ID); if (res && res.ok) { heartbeatFailCount = 0; return; } heartbeatFailCount++; if (heartbeatFailCount >= HEARTBEAT_FAIL_LIMIT) { authorized = false; stopHeartbeat(); if (typeof stopClicking === 'function') stopClicking(); localStorage.removeItem(AUTH_STORAGE_KEY); localStorage.removeItem(AUTH_EXPIRE_KEY); if (typeof __syncAuthUI === 'function') { try { __syncAuthUI(); } catch (e) {} } if (typeof setStatus === 'function') setStatus('❌ 卡密心跳失效,请重新验证', ''); } }, HEARTBEAT_MS); } function stopHeartbeat() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } heartbeatFailCount = 0; } /* ---- 本地缓存自动登录:先联网确认 ---- */ async function loadAuthFromStorage() { let saved = ''; try { saved = (localStorage.getItem(AUTH_STORAGE_KEY) || '').trim(); } catch (e) { return; } if (!saved) return; const ex = parseInt(localStorage.getItem(AUTH_EXPIRE_KEY) || '0', 10); if (ex && ex < Date.now()) { try { localStorage.removeItem(AUTH_STORAGE_KEY); localStorage.removeItem(AUTH_EXPIRE_KEY); } catch (e) {} return; } const res = await requestVerify(saved, KEYT_DEVICE_ID); if (!res || !res.ok) { try { localStorage.removeItem(AUTH_STORAGE_KEY); localStorage.removeItem(AUTH_EXPIRE_KEY); } catch (e) {} return; } authCode = saved; authorized = true; startHeartbeat(); } /* ========== Shadow DOM ========== */ const shadowRootMap = new Map(); const nativeAttachShadow = Element.prototype.attachShadow; Element.prototype.attachShadow = function (init) { const root = nativeAttachShadow.call(this, init); try { shadowRootMap.set(this, root); } catch (e) {} return root; }; function getShadowRoot(el) { if (!el || el.nodeType !== 1) return null; return el.shadowRoot || shadowRootMap.get(el) || null; } let __rootsCache = null, __rootsObserver = null, __rootsDirty = true; function ensureRootsObserver() { if (__rootsObserver || !document.documentElement) return; try { __rootsObserver = new MutationObserver(() => { __rootsDirty = true; }); __rootsObserver.observe(document.documentElement, { childList: true, subtree: true }); } catch (e) { __rootsObserver = null; } } function getAllRoots() { if (!__rootsDirty && __rootsCache) return __rootsCache; ensureRootsObserver(); const roots = [document]; for (let i = 0; i < roots.length; i++) { let all; try { all = roots[i].querySelectorAll('*'); } catch (e) { continue; } for (const el of all) { const sr = getShadowRoot(el); if (sr && !roots.includes(sr)) roots.push(sr); } } __rootsCache = roots; __rootsDirty = false; return roots; } /* ========== realClick ========== */ function realClick(el) { if (!el) return false; try { const r0 = el.getBoundingClientRect(); if (r0.top < 0 || r0.bottom > window.innerHeight || r0.left < 0 || r0.right > window.innerWidth) { try { el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' }); } catch (e) { el.scrollIntoView(); } } } catch (e) {} let r; try { r = el.getBoundingClientRect(); } catch (e) { r = { left: 0, top: 0, width: 1, height: 1 }; } const x = r.left + r.width / 2, y = r.top + r.height / 2; const base = { bubbles: true, cancelable: true, composed: true, clientX: x, clientY: y, screenX: x, screenY: y, button: 0, detail: 1 }; try { if (window.PointerEvent) { try { el.dispatchEvent(new PointerEvent('pointerover', Object.assign({}, base, { buttons: 0, pointerId: 1, pointerType: 'mouse', isPrimary: true }))); el.dispatchEvent(new PointerEvent('pointerdown', Object.assign({}, base, { buttons: 1, pointerId: 1, pointerType: 'mouse', isPrimary: true }))); } catch (e) {} } el.dispatchEvent(new MouseEvent('mouseover', Object.assign({}, base, { buttons: 0 }))); el.dispatchEvent(new MouseEvent('mousedown', Object.assign({}, base, { buttons: 1 }))); if (window.PointerEvent) { try { el.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, base, { buttons: 0, pointerId: 1, pointerType: 'mouse', isPrimary: true }))); } catch (e) {} } el.dispatchEvent(new MouseEvent('mouseup', Object.assign({}, base, { buttons: 0 }))); el.dispatchEvent(new MouseEvent('click', Object.assign({}, base, { buttons: 0 }))); } catch (e) { return false; } return true; } function pickClickTarget(el) { let cur = el; for (let i = 0; i < 4 && cur; i++) { try { if (getComputedStyle(cur).cursor === 'pointer') return cur; } catch (e) {} cur = cur.parentElement; } return el; } function isBoldText(el) { if (!el || el.nodeType !== 1) return false; const tag = (el.tagName || '').toUpperCase(); if (tag === 'STRONG' || tag === 'B' || /^H[1-6]$/.test(tag)) return true; try { const fw = getComputedStyle(el).fontWeight; const n = fw === 'bold' ? 700 : (fw === 'bolder' ? 800 : parseInt(fw, 10)); if (!isNaN(n) && n >= 600) return true; } catch (e) {} return false; } const RANDOM_MIN_SEC = 3, RANDOM_MAX_SEC = 18; function getRandomDelayMs() { const sec = Math.random() * (RANDOM_MAX_SEC - RANDOM_MIN_SEC) + RANDOM_MIN_SEC; return Math.round(sec * 1000); } const workerCode = `let timer=null;self.onmessage=function(e){if(e.data&&e.data.cmd==='start'){const ms=e.data.ms||1000;if(timer)clearInterval(timer);timer=setInterval(function(){self.postMessage({tick:Date.now()});},ms);}else if(e.data&&e.data.cmd==='stop'){if(timer){clearInterval(timer);timer=null;}}};`; let bgWorker = null; function ensureWorker() { if (bgWorker) return bgWorker; try { bgWorker = new Worker(URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' }))); } catch (e) { bgWorker = null; } return bgWorker; } let keepAliveAudio = null; function startKeepAlive() { if (keepAliveAudio) return; try { const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(), gain = ctx.createGain(); gain.gain.value = 0.0001; osc.frequency.value = 20; osc.connect(gain).connect(ctx.destination); osc.start(); keepAliveAudio = { ctx, osc }; } catch (e) {} } function stopKeepAlive() { try { if (keepAliveAudio) { keepAliveAudio.osc.stop(); keepAliveAudio.ctx.close(); } } catch (e) {} keepAliveAudio = null; } /* ========== 全局状态 ========== */ let panel, statusEl, statusTextEl, listBox, listCountEl; let __leftNavCache = null; let selectedEls = []; let isRunning = false, clickTimer = null, clickIndex = 0, clickedTotal = 0; let expanding = false, picking = false, processingVideo = false, workerTickHandler = null; function setStatus(text, cls) { if (!statusTextEl) return; statusTextEl.textContent = text; statusEl.className = 'qcc-status' + (cls ? ' ' + cls : ''); } function getElText(el) { if (!el) return '未知元素'; const txt = (el.textContent || '').trim().replace(/\s+/g, ' '); if (!txt) return `[${(el.tagName || 'el').toLowerCase()}]`; return txt.length > 30 ? txt.slice(0, 30) + '…' : txt; } function renderList() { if (!listBox || !listCountEl) return; listCountEl.textContent = selectedEls.length; if (selectedEls.length === 0) { listBox.innerHTML = '暂无元素'; return; } listBox.innerHTML = ''; selectedEls.forEach((el, i) => { const item = document.createElement('div'); item.className = 'qcc-list-item'; const txt = document.createElement('span'); txt.className = 'qcc-list-item-text'; txt.textContent = `${i + 1}. ${getElText(el)}`; const del = document.createElement('button'); del.className = 'qcc-list-item-del'; del.textContent = '×'; del.addEventListener('click', (e) => { e.stopPropagation(); removeSelected(el); }); item.appendChild(txt); item.appendChild(del); listBox.appendChild(item); }); } function removeSelected(el) { try { el.classList.remove('qcc-pick-selected'); } catch (e) {} const i = selectedEls.indexOf(el); if (i > -1) selectedEls.splice(i, 1); renderList(); } function clearSelected() { selectedEls.forEach(el => { try { el.classList.remove('qcc-pick-selected'); } catch (e) {} }); selectedEls = []; renderList(); } /* ========== 手动选择 ========== */ let manualPicking = false, manualHoverEl = null, manualOverHandler = null, manualClickHandler = null; function pickManualTarget(el) { if (!el || el.nodeType !== 1) return null; if (panel && panel.contains(el)) return null; let cur = el; for (let i = 0; i < 5 && cur; i++) { const tag = (cur.tagName || '').toUpperCase(); if (tag === 'A') return cur; if (cur.getAttribute && cur.getAttribute('href')) return cur; try { if (getComputedStyle(cur).cursor === 'pointer') return cur; } catch (e) {} cur = cur.parentElement; } return el; } function updatePickButton() { const btn = document.getElementById('qcc-auto-pick'); if (!btn) return; if (authorized) btn.innerHTML = ' 自动选择'; else if (manualPicking) btn.innerHTML = ' 退出手动'; else btn.innerHTML = ' 手动选择'; } function enterManualPick() { if (manualPicking) return; manualPicking = true; manualOverHandler = (e) => { const el = pickManualTarget(e.target); if (!el || manualHoverEl === el) return; if (manualHoverEl) { try { manualHoverEl.classList.remove('qcc-manual-hover'); } catch (err) {} } manualHoverEl = el; try { el.classList.add('qcc-manual-hover'); } catch (err) {} }; manualClickHandler = (e) => { if (panel && panel.contains(e.target)) return; e.preventDefault(); e.stopPropagation(); const el = pickManualTarget(e.target); if (!el || selectedEls.includes(el)) return; selectedEls.push(el); try { el.classList.add('qcc-pick-selected'); } catch (err) {} renderList(); setStatus(`👆 手动选择中… 已添加 ${selectedEls.length} 个元素`, 'running'); }; document.addEventListener('mouseover', manualOverHandler, true); document.addEventListener('click', manualClickHandler, true); try { document.body.style.cursor = 'crosshair'; } catch (e) {} updatePickButton(); setStatus('👆 手动选择:点击页面元素添加(再次点击按钮或 ESC 退出)', 'running'); } function exitManualPick() { if (!manualPicking) return; manualPicking = false; if (manualOverHandler) { try { document.removeEventListener('mouseover', manualOverHandler, true); } catch (e) {} manualOverHandler = null; } if (manualClickHandler) { try { document.removeEventListener('click', manualClickHandler, true); } catch (e) {} manualClickHandler = null; } if (manualHoverEl) { try { manualHoverEl.classList.remove('qcc-manual-hover'); } catch (e) {} manualHoverEl = null; } try { document.body.style.cursor = ''; } catch (e) {} updatePickButton(); setStatus(`已退出手动选择,共 ${selectedEls.length} 个元素`, ''); } /* ============================================================ * ★ 展开核心:6 秒硬限制 + 防重复点击 * ============================================================ */ const sleep = ms => new Promise(r => setTimeout(r, ms)); async function expandAllCollapsed() { if (expanding) return; expanding = true; __leftNavCache = null; const startTime = Date.now(); const HARD_LIMIT_MS = 5800; const deadline = startTime + HARD_LIMIT_MS; let totalClicked = 0; const MAX_LOOPS = 50; const clickedOnce = new WeakSet(); setStatus('🔍 正在展开目录…', 'running'); try { for (let loop = 0; loop < MAX_LOOPS; loop++) { if (Date.now() > deadline) { console.log('[QCC] 达到 6 秒时间上限,提前结束'); break; } const arrows = []; const seen = new Set(); for (const root of getAllRoots()) { let items; try { items = root.querySelectorAll('[class*="sub-menu-title"]'); } catch (e) { continue; } for (const el of items) { if (seen.has(el)) continue; seen.add(el); if (panel && panel.contains(el)) continue; if (clickedOnce.has(el)) continue; const svg = el.querySelector('svg.svg-icon'); if (!svg) continue; const style = svg.getAttribute('style') || ''; if (style.includes('rotate(180deg)')) continue; arrows.push({ svg: svg, parent: el }); } } if (arrows.length === 0) break; setStatus(`展开中… 剩 ${arrows.length} 个,已点 ${totalClicked}`, 'running'); for (const { svg, parent } of arrows) { if (Date.now() > deadline) break; if (!svg.isConnected) continue; try { realClick(svg); totalClicked++; clickedOnce.add(parent); } catch (e) {} await sleep(25); } await sleep(100); __leftNavCache = null; } const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); console.log('[QCC] 展开完成,共点击', totalClicked, '个,用时', elapsed, '秒'); const hitLimit = Date.now() - startTime >= HARD_LIMIT_MS - 50; setStatus( hitLimit ? `展开完成 ${totalClicked} 个(${elapsed}秒,达时间上限)` : `展开完成,共点击 ${totalClicked} 个(${elapsed}秒)`, '' ); } finally { expanding = false; __leftNavCache = null; } } /* ============================================================ * 拾取 * ============================================================ */ function findLeftNav() { if (__leftNavCache && __leftNavCache.isConnected) return __leftNavCache; for (const root of getAllRoots()) { let all; try { all = root.querySelectorAll('*'); } catch (e) { continue; } for (const el of all) { if (panel && panel.contains(el)) continue; let st; try { st = getComputedStyle(el); } catch (e) { continue; } if (st.overflowY !== 'auto' && st.overflowY !== 'scroll') continue; let r; try { r = el.getBoundingClientRect(); } catch (e) { continue; } if (r.width < 100 || r.width > 400) continue; if (r.left > 100) continue; if (el.scrollHeight < el.clientHeight) continue; __leftNavCache = el; return el; } } return null; } function isInLeftNav(el) { const nav = findLeftNav(); if (!nav) { try { const r = el.getBoundingClientRect(); return r.left >= 0 && r.left <= 280; } catch (e) { return false; } } return nav.contains(el); } function resolveClickTarget(el) { let cur = el; for (let i = 0; i < 4 && cur; i++) { const tag = (cur.tagName || '').toUpperCase(); if (tag === 'A') return cur; if (cur.getAttribute && cur.getAttribute('href')) return cur; try { if (getComputedStyle(cur).cursor === 'pointer') return cur; } catch (e) {} cur = cur.parentElement; } return el; } function collectOnce(intoSet) { const roots = getAllRoots(); let hits = 0; for (const root of roots) { let list; try { list = root.querySelectorAll('.text-too-long'); } catch (e) { list = []; } for (const el of list) { if (panel && panel.contains(el)) continue; if (isBoldText(el)) continue; if (!isInLeftNav(el)) continue; let r; try { r = el.getBoundingClientRect(); } catch (e) { continue; } if (r.width < 20 || r.height < 8) continue; let st; try { st = getComputedStyle(el); } catch (e) { continue; } if (st.display === 'none' || st.visibility === 'hidden') continue; if (!(el.textContent || '').trim()) continue; intoSet.add(resolveClickTarget(el)); hits++; } } if (hits === 0) { for (const root of roots) { let all; try { all = root.querySelectorAll('*'); } catch (e) { continue; } for (const el of all) { if (panel && panel.contains(el)) continue; if (isBoldText(el)) continue; if (!isInLeftNav(el)) continue; let r; try { r = el.getBoundingClientRect(); } catch (e) { continue; } if (r.width < 30 || r.height < 12 || r.height > 60) continue; let st; try { st = getComputedStyle(el); } catch (e) { continue; } if (st.cursor !== 'pointer' || st.display === 'none') continue; const txt = (el.textContent || '').trim(); if (!txt || txt.length > 80) continue; intoSet.add(el); } } } } /* ============================================================ * ★ 自动选择 * ============================================================ */ async function autoPickLeftNav() { if (picking) return; picking = true; clearSelected(); __leftNavCache = null; setStatus('🔍 正在拾取…', 'running'); try { await sleep(200); __leftNavCache = null; const collected = new Set(); collectOnce(collected); for (const el of collected) { if (selectedEls.includes(el)) continue; selectedEls.push(el); try { el.classList.add('qcc-pick-selected'); } catch (e) {} } renderList(); console.log('[QCC] 拾取完成,共', selectedEls.length, '项'); setStatus(selectedEls.length === 0 ? '⚠️ 未找到可点击链接,请先点「一键展开」' : `✅ 已拾取 ${selectedEls.length} 个可点击链接`, ''); } finally { picking = false; __leftNavCache = null; } } /* ========== 视频 ========== */ const VIDEO_WAIT_MS = 15000, VIDEO_POLL_MS = 300; let autoPlayVideo = false, videoSpeed = 2; function getAllDocuments() { const docs = [document]; for (let i = 0; i < docs.length; i++) { let iframes; try { iframes = docs[i].querySelectorAll('iframe'); } catch (e) { continue; } for (const f of iframes) { try { const d = f.contentDocument; if (d && !docs.includes(d)) docs.push(d); } catch (e) {} } } return docs; } function videoKey(v) { return (v.currentSrc || v.src || '') + '::' + (v.getAttribute('data-src') || ''); } function snapshotVideoKeys() { const set = new Set(); for (const doc of getAllDocuments()) { let vs; try { vs = doc.querySelectorAll('video'); } catch (e) { continue; } for (const v of vs) set.add(videoKey(v)); } return set; } async function waitForNewVideo(oldKeys, maxWaitMs) { const start = Date.now(); while (Date.now() - start < maxWaitMs) { for (const doc of getAllDocuments()) { let vs; try { vs = doc.querySelectorAll('video'); } catch (e) { continue; } for (const v of vs) { if (!v.isConnected) continue; const k = videoKey(v); if (k && !oldKeys.has(k)) return v; } } await sleep(VIDEO_POLL_MS); } return null; } async function waitMetadata(v, maxWaitMs = 8000) { if (v.readyState >= 1) return true; return await new Promise(resolve => { const timer = setTimeout(() => { v.removeEventListener('loadedmetadata', onL); resolve(false); }, maxWaitMs); const onL = () => { clearTimeout(timer); resolve(true); }; v.addEventListener('loadedmetadata', onL, { once: true }); }); } async function playVideoToEnd(video) { await waitMetadata(video, 8000); const forceRate = () => { try { if (video.playbackRate !== videoSpeed) video.playbackRate = videoSpeed; if (video.defaultPlaybackRate !== videoSpeed) video.defaultPlaybackRate = videoSpeed; } catch (e) {} }; const onRC = () => { if (video.playbackRate !== videoSpeed) forceRate(); }; video.addEventListener('ratechange', onRC); const rateTimer = setInterval(forceRate, 500); try { video.muted = true; forceRate(); try { video.currentTime = 0; } catch (e) {} await video.play(); forceRate(); try { video.currentTime = 0; } catch (e) {} } catch (e) { video.removeEventListener('ratechange', onRC); clearInterval(rateTimer); return false; } await new Promise(resolve => { let last = -1, stuck = 0; const st = Date.now(), GRACE = 15000; const check = () => { if (!video.isConnected || video.ended) { resolve(); return; } const ct = video.currentTime || 0, dur = video.duration || 0; if (dur > 0 && isFinite(dur) && ct >= dur - 0.3) { resolve(); return; } if (Date.now() - st > GRACE) { if (ct > 0 && Math.abs(ct - last) < 0.05) { stuck++; if (stuck > 30) { resolve(); return; } } else stuck = 0; } last = ct; forceRate(); setTimeout(check, 500); }; check(); }); video.removeEventListener('ratechange', onRC); clearInterval(rateTimer); return true; } /* ========== 轮流点击 ========== */ function highlightClick(el) { try { el.classList.add('qcc-clicking'); } catch (e) {} setTimeout(() => { try { el.classList.remove('qcc-clicking'); } catch (e) {} }, 200); } async function doClick() { if (!isRunning || processingVideo) return; if (selectedEls.length === 0) { stopClicking(); setStatus('没有可点击的元素', ''); return; } selectedEls = selectedEls.filter(el => el.isConnected); if (selectedEls.length === 0) { stopClicking(); setStatus('所有元素已从页面移除', ''); renderList(); return; } const el = selectedEls[clickIndex % selectedEls.length]; const oldKeys = snapshotVideoKeys(); try { realClick(pickClickTarget(el) || el); highlightClick(el); } catch (err) {} clickedTotal++; clickIndex++; setStatus(`执行中… 已点击 ${clickedTotal} 次 / 共 ${selectedEls.length} 个元素`, 'running'); if (!autoPlayVideo) return; const video = await waitForNewVideo(oldKeys, VIDEO_WAIT_MS); if (!video) return; processingVideo = true; setStatus(`🎬 检测到新视频,正在 ${videoSpeed} 倍速静音播放…`, 'running'); try { await playVideoToEnd(video); if (isRunning) setStatus('✅ 视频播放完成,准备下一个', ''); } catch (e) {} finally { processingVideo = false; } } async function clickLoop() { if (!isRunning) return; await doClick(); if (!isRunning) return; const delay = getRandomDelayMs(); const w = ensureWorker(); if (w) { if (workerTickHandler) { try { w.removeEventListener('message', workerTickHandler); } catch (e) {} workerTickHandler = null; } workerTickHandler = (e) => { if (!e.data || !e.data.tick) return; workerTickHandler = null; try { w.postMessage({ cmd: 'stop' }); } catch (err) {} if (isRunning) clickLoop(); }; w.addEventListener('message', workerTickHandler, { once: true }); try { w.postMessage({ cmd: 'start', ms: delay }); } catch (err) { if (workerTickHandler) { try { w.removeEventListener('message', workerTickHandler); } catch (e2) {} workerTickHandler = null; } clickTimer = setTimeout(clickLoop, delay); } } else clickTimer = setTimeout(clickLoop, delay); } function startClicking() { if (isRunning) return; if (selectedEls.length === 0) { setStatus('请先选择左侧目录元素', ''); return; } if (manualPicking) exitManualPick(); try { if (bgWorker) bgWorker.postMessage({ cmd: 'stop' }); } catch (e) {} if (bgWorker && workerTickHandler) { try { bgWorker.removeEventListener('message', workerTickHandler); } catch (e) {} workerTickHandler = null; } if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } isRunning = true; processingVideo = false; clickIndex = 0; clickedTotal = 0; const b1 = document.getElementById('qcc-start'), b2 = document.getElementById('qcc-stop'); if (b1) b1.disabled = true; if (b2) b2.disabled = false; setStatus('开始执行…(无限循环,随机间隔 3~18 秒)', 'running'); startKeepAlive(); clickLoop(); } function stopClicking() { if (!isRunning) return; isRunning = false; if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } stopKeepAlive(); try { if (bgWorker) bgWorker.postMessage({ cmd: 'stop' }); } catch (e) {} if (bgWorker && workerTickHandler) { try { bgWorker.removeEventListener('message', workerTickHandler); } catch (e) {} workerTickHandler = null; } const b1 = document.getElementById('qcc-start'), b2 = document.getElementById('qcc-stop'); if (b1) b1.disabled = false; if (b2) b2.disabled = true; if (statusEl && statusEl.classList.contains('running')) setStatus(`已停止,共点击 ${clickedTotal} 次`, ''); } /* ========== 弹窗 ========== */ function openDonateModal() { if (document.getElementById('qcc-donate-modal')) return; const modal = document.createElement('div'); modal.id = 'qcc-donate-modal'; modal.innerHTML = `