// ==UserScript== // @name 国开刷点击次数和时长 // @namespace https://scriptcat.org/ // @version 2.7.3 // @description 第一步展开目录|第二步自动拾取|轮流循环点击(3~18秒随机)|视频开关|2/4/6/8/10倍速|自动静音播放完再继续|苹果风格UI|ESC停止 // @author You // @match *://lms.ouchn.cn/* // @icon https://cdn.jsdelivr.net/gh/yrtyrtyrtygfr/cjtfky@2caf2337025d9bc8779c692acf1d6469fe372637/gd.png // @grant none // @run-at document-start // @license 二开请联系作者 // ==/UserScript== (function () { 'use strict'; if (window.__QCC_INSTALLED__) return; window.__QCC_INSTALLED__ = true; /* ============================================================ * 内核: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; } function getAllRoots() { 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); } } return roots; } function realClick(el) { if (!el) return false; let r; try { r = el.getBoundingClientRect(); } catch (e) { r = { left: 0, top: 0, width: 1, height: 1 }; } const x = r.left + r.width / 2; const y = r.top + r.height / 2; const base = { bubbles: true, cancelable: true, composed: true, view: window, clientX: x, clientY: y, screenX: x, screenY: y, button: 0, detail: 1 }; try { if (window.PointerEvent) { 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 }))); } el.dispatchEvent(new MouseEvent('mouseover', Object.assign({}, base, { buttons: 0 }))); el.dispatchEvent(new MouseEvent('mousedown', Object.assign({}, base, { buttons: 1 }))); if (window.PointerEvent) { el.dispatchEvent(new PointerEvent('pointerup', Object.assign({}, base, { buttons: 0, pointerId: 1, pointerType: 'mouse', isPrimary: true }))); } el.dispatchEvent(new MouseEvent('mouseup', Object.assign({}, base, { buttons: 0 }))); el.dispatchEvent(new MouseEvent('click', Object.assign({}, base, { buttons: 0 }))); } catch (e) { console.warn('[QCC] dispatch failed:', 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) { let cur = el; for (let i = 0; i < 3 && cur; i++) { const tag = (cur.tagName || '').toUpperCase(); if (tag === 'STRONG' || tag === 'B' || tag === 'H1' || tag === 'H2' || tag === 'H3' || tag === 'H4' || tag === 'H5' || tag === 'H6') return true; try { const fw = getComputedStyle(cur).fontWeight; const n = fw === 'bold' ? 700 : (fw === 'bolder' ? 800 : parseInt(fw, 10)); if (!isNaN(n) && n >= 600) return true; } catch (e) {} cur = cur.parentElement; } return false; } /* ============================================================ * 随机间隔:3 ~ 18 秒 * ============================================================ */ const RANDOM_MIN_SEC = 3; const RANDOM_MAX_SEC = 18; function getRandomDelayMs() { const sec = Math.random() * (RANDOM_MAX_SEC - RANDOM_MIN_SEC) + RANDOM_MIN_SEC; return Math.round(sec * 1000); } /* ============================================================ * 状态 * ============================================================ */ let panel, statusEl, statusTextEl, listBox, listCountEl; let selectedEls = []; let isRunning = false; let clickTimer = null; let clickIndex = 0; let clickedTotal = 0; let expanding = false; let picking = false; let processingVideo = false; 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.title = '移除'; 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 idx = selectedEls.indexOf(el); if (idx > -1) selectedEls.splice(idx, 1); renderList(); } function clearSelected() { selectedEls.forEach(el => { try { el.classList.remove('qcc-pick-selected'); } catch (e) {} }); selectedEls = []; renderList(); } /* ============================================================ * 自动拾取左侧目录 * ============================================================ */ const sleep = ms => new Promise(r => setTimeout(r, ms)); const PICK_MAX_X = 340; const PICK_MIN_W = 30; const PICK_MIN_H = 12; const PICK_MAX_H = 60; function findLeftScroller() { let best = null, bestScore = -Infinity; 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; } const oy = st.overflowY; if (oy !== 'auto' && oy !== 'scroll') continue; let r; try { r = el.getBoundingClientRect(); } catch (e) { continue; } if (r.left > PICK_MAX_X) continue; if (r.width < 80 || r.height < 150) continue; const range = el.scrollHeight - el.clientHeight; if (range < 30) continue; const score = -r.top + Math.min(range, 5000) * 0.01; if (score > bestScore) { bestScore = score; best = el; } } } return best; } 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 primaryHits = 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; let r; try { r = el.getBoundingClientRect(); } catch (e) { continue; } if (r.width < 20 || r.height < 8) continue; if (r.left < 0 || r.left > PICK_MAX_X) continue; if (r.bottom < 0 || r.top > window.innerHeight) continue; let st; try { st = getComputedStyle(el); } catch (e) { continue; } if (st.display === 'none' || st.visibility === 'hidden') continue; const txt = (el.textContent || '').trim(); if (!txt) continue; intoSet.add(resolveClickTarget(el)); primaryHits++; } } if (primaryHits === 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; let r; try { r = el.getBoundingClientRect(); } catch (e) { continue; } if (r.width < PICK_MIN_W || r.height < PICK_MIN_H || r.height > PICK_MAX_H) continue; if (r.left < 0 || r.left > PICK_MAX_X) continue; if (r.bottom < 0 || r.top > window.innerHeight) continue; let st; try { st = getComputedStyle(el); } catch (e) { continue; } if (st.cursor !== 'pointer') continue; if (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(); setStatus('🔍 正在自动拾取左侧目录…', 'running'); const scroller = findLeftScroller(); const collected = new Set(); console.log('[QCC] 左侧滚动容器:', scroller); try { if (scroller) { const origTop = scroller.scrollTop; scroller.scrollTop = 0; await sleep(220); collectOnce(collected); setStatus(`🔍 拾取中… 已收集 ${collected.size} 项`, 'running'); const step = Math.max(150, scroller.clientHeight - 60); const maxTop = scroller.scrollHeight - scroller.clientHeight; let pos = 0, pageIdx = 1; while (pos <= maxTop) { scroller.scrollTop = pos; await sleep(220); collectOnce(collected); setStatus(`🔍 拾取中… 第 ${pageIdx} 屏,已收集 ${collected.size} 项`, 'running'); pos += step; pageIdx++; } scroller.scrollTop = maxTop; await sleep(220); collectOnce(collected); scroller.scrollTop = origTop; await sleep(150); } else { 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(); if (selectedEls.length === 0) { setStatus('⚠️ 未找到悬停变色的链接,请确认左侧目录已展开', ''); } else { setStatus(`✅ 已拾取 ${selectedEls.length} 个可点击链接(已跳过加粗标题)`, ''); } } finally { picking = false; } } /* ============================================================ * 视频处理 * ============================================================ */ const VIDEO_WAIT_MS = 15000; const VIDEO_POLL_MS = 300; let autoPlayVideo = false; let videoSpeed = 2; const VIDEO_SPEED_STEPS = [2, 4, 6, 8, 10]; 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 findPlayableVideoInDoc(doc) { let videos; try { videos = doc.querySelectorAll('video'); } catch (e) { return null; } let best = null, bestArea = 0; for (const v of videos) { if (!v.isConnected) continue; let r; try { r = v.getBoundingClientRect(); } catch (e) { continue; } if (r.width < 100 || r.height < 60) continue; if (r.bottom < 0 || r.top > window.innerHeight) continue; const area = r.width * r.height; if (area > bestArea) { bestArea = area; best = v; } } return best; } function findPlayableVideo() { for (const doc of getAllDocuments()) { const v = findPlayableVideoInDoc(doc); if (v) return v; } return null; } function videoKey(v) { return (v.currentSrc || v.src || '') + '::' + (v.getAttribute('data-src') || ''); } function snapshotVideoKeys() { const set = new Set(); for (const doc of getAllDocuments()) { let videos; try { videos = doc.querySelectorAll('video'); } catch (e) { continue; } for (const v of videos) 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 videos; try { videos = doc.querySelectorAll('video'); } catch (e) { continue; } for (const v of videos) { 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', onLoaded); resolve(false); }, maxWaitMs); const onLoaded = () => { clearTimeout(timer); resolve(true); }; v.addEventListener('loadedmetadata', onLoaded, { 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 onRateChange = () => { if (video.playbackRate !== videoSpeed) forceRate(); }; video.addEventListener('ratechange', onRateChange); 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) { console.warn('[QCC] 视频播放失败:', e); video.removeEventListener('ratechange', onRateChange); clearInterval(rateTimer); return false; } await new Promise((resolve) => { let lastTime = -1; let stuckCount = 0; const startTime = Date.now(); const GRACE_MS = 15000; const check = () => { if (!video.isConnected) { resolve(); return; } if (video.ended) { resolve(); return; } const ct = video.currentTime || 0; const dur = video.duration || 0; if (dur > 0 && isFinite(dur) && ct >= dur - 0.3) { resolve(); return; } if (Date.now() - startTime > GRACE_MS) { if (ct > 0 && Math.abs(ct - lastTime) < 0.05) { stuckCount++; if (stuckCount > 30) { resolve(); return; } } else { stuckCount = 0; } } lastTime = ct; forceRate(); setTimeout(check, 500); }; check(); }); video.removeEventListener('ratechange', onRateChange); 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) return; if (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) { console.warn('[轮流点击器] 点击失败:', 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) { console.warn('[QCC] 视频处理异常:', e); } finally { processingVideo = false; } } async function clickLoop() { if (!isRunning) return; await doClick(); if (!isRunning) return; const delay = getRandomDelayMs(); clickTimer = setTimeout(clickLoop, delay); } function startClicking() { if (isRunning) return; if (selectedEls.length === 0) { setStatus('请先自动拾取左侧目录', ''); return; } isRunning = true; processingVideo = false; clickIndex = 0; clickedTotal = 0; document.getElementById('qcc-start').disabled = true; document.getElementById('qcc-stop').disabled = false; setStatus('开始执行…(无限循环,随机间隔 3~18 秒)', 'running'); clickLoop(); } function stopClicking() { if (!isRunning) return; isRunning = false; if (clickTimer) { clearTimeout(clickTimer); clickTimer = null; } const b1 = document.getElementById('qcc-start'); const b2 = document.getElementById('qcc-stop'); if (b1) b1.disabled = false; if (b2) b2.disabled = true; if (statusEl && statusEl.classList.contains('running')) { setStatus(`已停止,共点击 ${clickedTotal} 次`, ''); } } /* ============================================================ * 一键展开 * ============================================================ */ const ARROW_RE = /(arrow|triangle|caret|chevron|expand|collapse|fold|unfold|toggle|tree-?(switch|icon)|icon-(down|right|left|up|plus|minus|caret|arrow))/i; function signature(el) { let s = ''; const cls = el.className; if (typeof cls === 'string') s += ' ' + cls; else if (cls && cls.baseVal) s += ' ' + cls.baseVal; s += ' ' + (el.id || ''); if (el.getAttribute) { const attrs = ['aria-label', 'title', 'data-name', 'data-testid', 'data-type', 'alt', 'name']; for (const a of attrs) { const v = el.getAttribute(a); if (v) s += ' ' + v; } } return s; } function arrowScore(el) { let score = 0; let cur = el; for (let d = 0; d < 3 && cur; d++) { if (ARROW_RE.test(signature(cur))) score += (3 - d) * 2; if (cur.hasAttribute && cur.hasAttribute('aria-expanded')) score += 3; cur = cur.parentElement; } try { if (getComputedStyle(el).cursor === 'pointer') score += 1; } catch (e) {} const tag = (el.tagName || '').toLowerCase(); if (tag === 'svg' || tag === 'i' || tag === 'use' || tag === 'path' || tag === 'img') score += 1; return score; } async function expandAllChapters() { if (expanding) return; expanding = true; const clickedSet = new WeakSet(); function scanOnce() { const roots = getAllRoots(); const candidates = []; const seen = new Set(); for (const root of roots) { let all; try { all = root.querySelectorAll('*'); } catch (e) { continue; } for (const el of all) { if (seen.has(el)) continue; seen.add(el); if (clickedSet.has(el)) continue; if (panel && panel.contains(el)) continue; let r; try { r = el.getBoundingClientRect(); } catch (e) { continue; } if (r.width < 6 || r.height < 6) continue; if (r.width > 48 || r.height > 48) continue; if (r.left > 340) continue; if (r.top < 40 || r.bottom > window.innerHeight + 4) continue; let st; try { st = getComputedStyle(el); } catch (e) { continue; } if (st.display === 'none' || st.visibility === 'hidden' || parseFloat(st.opacity) < 0.05) continue; const score = arrowScore(el); if (score < 2) continue; const ae = el.getAttribute && el.getAttribute('aria-expanded'); if (ae === 'true') continue; candidates.push({ el, top: r.top, score }); } } candidates.sort((a, b) => a.top - b.top); const picked = []; let group = [], gTop = null; const flush = () => { if (!group.length) return; group.sort((a, b) => b.score - a.score); picked.push(group[0]); group = []; }; for (const it of candidates) { if (gTop === null || it.top - gTop > 10) { flush(); gTop = it.top; } group.push(it); } flush(); let clicked = 0; for (const p of picked) { clickedSet.add(p.el); realClick(pickClickTarget(p.el)); clicked++; } return clicked; } let totalClicked = 0, round = 0, stall = 0; const MAX_ROUNDS = 8; const ROUND_DELAY = 450; try { while (round < MAX_ROUNDS) { round++; setStatus(`正在展开目录… 第 ${round} 轮`, 'running'); const n = scanOnce(); totalClicked += n; if (n === 0) { stall++; if (stall >= 2) break; } else { stall = 0; } await sleep(ROUND_DELAY); } } finally { expanding = false; } setStatus(`展开完成,共点击 ${totalClicked} 个节点(${round} 轮)`, ''); } /* ============================================================ * 面板构建 * ============================================================ */ function buildPanel() { if (document.getElementById('quick-custom-clicker')) return; panel = document.createElement('div'); panel.id = 'quick-custom-clicker'; panel.innerHTML = `