// ==UserScript== // @name 就创业直播平台自动刷课(开视频界面自动往下刷) // @namespace https://wsyu.wnssedu.com/ // @version 0.1.7 // @description Auto play visible course videos, advance through the course list, and click visible skip buttons in Polyv interaction prompts. // @author Codex // @match https://wsyu.wnssedu.com/* // @match http://wsyu.wnssedu.com/* // @match https://player.polyv.net/* // @match http://player.polyv.net/* // @run-at document-idle // @grant GM_addStyle // @grant unsafeWindow // ==/UserScript== (function () { "use strict"; const config = { tickMs: 1200, nextDelayMs: 1800, nextCooldownMs: 5000, playCooldownMs: 1800, skipCooldownMs: 1000, diagCooldownMs: 5000, unmuteAfterMutedAutoplayMs: -1, maxPanelLines: 12, debug: true }; const storageKeys = { logs: "wsyu-auto.logs", rate: "wsyu-auto.playbackRate" }; const speedOptions = [1, 2, 4, 8, 16, 32]; const state = { lastNextAt: 0, lastPlayAt: 0, lastSkipAt: 0, lastConfirmAt: 0, lastDiagAt: 0, hookedPlayOver: null, hookedPlayerInitOver: null, lastStatus: "运行中", logs: loadStoredLogs(), mutedAutoplayVideos: new WeakSet(), playbackRate: loadPlaybackRate(), panel: null }; const skipTextPattern = /(跳过|跳过此题|跳过题目|跳过答题|跳过互动)/; const confirmTextPattern = /(确定|确认|继续|继续播放|继续学习|我知道了|知道了|关闭)/; function readStorage(key) { try { return localStorage.getItem(key); } catch (error) { return null; } } function writeStorage(key, value) { try { localStorage.setItem(key, value); } catch (error) { // Ignore storage failures in restricted frames. } } function loadStoredLogs() { try { const parsed = JSON.parse(readStorage(storageKeys.logs) || "[]"); return Array.isArray(parsed) ? parsed.slice(0, config.maxPanelLines) : []; } catch (error) { return []; } } function loadPlaybackRate() { const value = Number(readStorage(storageKeys.rate)); return speedOptions.includes(value) ? value : 1; } function pageWindow() { return typeof unsafeWindow === "undefined" ? window : unsafeWindow; } function stringifyArg(value) { if (value instanceof Element) return describeElement(value); if (value instanceof Error) return `${value.name}: ${value.message}`; if (typeof value === "string") return value; if (value === null || value === undefined) return String(value); if (typeof value === "object") { try { return JSON.stringify(value); } catch (error) { return Object.prototype.toString.call(value); } } return String(value); } function nowTime() { return new Date().toLocaleTimeString("zh-CN", { hour12: false }); } function renderPanelLog() { if (!state.panel) return; const logBox = state.panel.querySelector("[data-log]"); if (logBox) logBox.textContent = state.logs.join("\n"); } function log(...args) { const message = args.map(stringifyArg).join(" "); if (config.debug) { console.log("[wsyu-auto]", message); } state.lastStatus = message || "运行中"; state.logs.unshift(`[${nowTime()}] ${state.lastStatus}`); state.logs = state.logs.slice(0, config.maxPanelLines); writeStorage(storageKeys.logs, JSON.stringify(state.logs)); updatePanel(state.lastStatus); renderPanelLog(); } function isCourseWatchPage() { return location.hostname === "wsyu.wnssedu.com" && location.pathname.includes("/course/newcourse/watch.htm"); } function isPolyvFrame() { return location.hostname.includes("polyv.net"); } function isVisible(element) { if (!element || element.nodeType !== 1) return false; const style = getComputedStyle(element); if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false; const rect = element.getBoundingClientRect(); return rect.width > 0 && rect.height > 0; } function textOf(element) { if (!element) return ""; if (element.tagName === "INPUT") return (element.value || "").trim(); return (element.innerText || element.textContent || "").replace(/\s+/g, " ").trim(); } function controlText(element) { if (!element) return ""; return [ textOf(element), element.getAttribute("title") || "", element.getAttribute("aria-label") || "", element.value || "" ].join(" ").replace(/\s+/g, " ").trim(); } function describeElement(element) { if (!element) return "null"; const rect = element.getBoundingClientRect(); const id = element.id ? `#${element.id}` : ""; const className = String(element.className || "").replace(/\s+/g, ".").slice(0, 80); const cls = className ? `.${className}` : ""; const attrs = ["title", "aria-label", "role"].map((name) => { const value = element.getAttribute(name); return value ? `${name}="${value.slice(0, 40)}"` : ""; }).filter(Boolean).join(" "); const text = textOf(element).slice(0, 60); return `<${element.tagName.toLowerCase()}${id}${cls}> ${attrs} text="${text}" rect=${Math.round(rect.width)}x${Math.round(rect.height)}`; } function isSkipText(text) { return Boolean(text && text.length <= 16 && skipTextPattern.test(text)); } function isConfirmText(text) { return Boolean(text && text.length <= 18 && confirmTextPattern.test(text)); } function isPlayControl(element) { const text = textOf(element); const attrs = [ element.getAttribute("class"), element.getAttribute("title"), element.getAttribute("aria-label") ].filter(Boolean).join(" "); const value = `${text} ${attrs}`; return /(播放|开始|继续|play|start|resume)/i.test(value) && !/(暂停|pause|重新|replay)/i.test(value); } function safeClick(element) { if (!element || !isVisible(element)) return false; const doc = element.ownerDocument || document; const view = doc.defaultView || window; const rect = element.getBoundingClientRect(); const clientX = Math.floor(rect.left + rect.width / 2); const clientY = Math.floor(rect.top + rect.height / 2); const eventInit = { bubbles: true, cancelable: true, view, clientX, clientY }; if (view.PointerEvent) { element.dispatchEvent(new view.PointerEvent("pointerover", eventInit)); element.dispatchEvent(new view.PointerEvent("pointerdown", eventInit)); element.dispatchEvent(new view.PointerEvent("pointerup", eventInit)); } element.dispatchEvent(new view.MouseEvent("mouseover", eventInit)); element.dispatchEvent(new view.MouseEvent("mousemove", eventInit)); element.dispatchEvent(new view.MouseEvent("mousedown", eventInit)); element.dispatchEvent(new view.MouseEvent("mouseup", eventInit)); element.dispatchEvent(new view.MouseEvent("click", eventInit)); element.click(); return true; } function clickAtElementCenter(element) { if (!element || !isVisible(element)) return false; const doc = element.ownerDocument || document; const rect = element.getBoundingClientRect(); const x = Math.floor(rect.left + rect.width / 2); const y = Math.floor(rect.top + rect.height / 2); const center = doc.elementFromPoint(x, y); if (!center || center.closest("#wsyu-auto-panel")) return false; return safeClick(center); } function uniqueElements(elements) { return elements.filter((element, index) => element && elements.indexOf(element) === index); } function clickElementDeep(element, reason) { const targets = uniqueElements([ closestActionTarget(element), element, element && element.parentElement, element && element.parentElement && element.parentElement.parentElement, element && element.parentElement && element.parentElement.parentElement && element.parentElement.parentElement.parentElement ]).filter((target) => target && !target.closest("#wsyu-auto-panel") && isVisible(target)); let clicked = false; for (const target of targets) { log(`${reason || "deep click"}:`, target); clicked = safeClick(target) || clicked; clicked = clickAtElementCenter(target) || clicked; } return clicked; } function closestActionTarget(element) { if (!element) return null; return element.closest("a,button,input[type='button'],input[type='submit'],[role='button'],[onclick]") || element; } function visibleElements(root, selector) { try { return Array.from(root.querySelectorAll(selector)).filter((element) => { return !element.closest("#wsyu-auto-panel") && isVisible(element); }); } catch (error) { return []; } } function clickSkipButton() { const now = Date.now(); if (now - state.lastSkipAt < config.skipCooldownMs) return false; let candidates = visibleElements(document, [ "button", "a", "input[type='button']", "input[type='submit']", "[role='button']", "[onclick]", ".pv-btn", ".btn", ".skip", ".Skip", "[class*='skip']", "[class*='Skip']" ].join(",")); let target = candidates.find((element) => isSkipText(controlText(element))); if (!target) { candidates = visibleElements(document, "body *").filter((element) => { const text = textOf(element); return text && text.length <= 24 && isSkipText(text); }); target = candidates[0]; } if (!target) return false; state.lastSkipAt = now; const clicked = clickElementDeep(target, "skip click"); setTimeout(() => clickConfirmButton("after skip"), 350); setTimeout(() => startCurrentVideo("after skip"), 650); return clicked; } function clickConfirmButton(reason) { const now = Date.now(); if (now - state.lastConfirmAt < 700) return false; const candidates = visibleElements(document, [ "button", "a", "input[type='button']", "input[type='submit']", "[role='button']", "[onclick]", ".pv-btn", ".btn", ".close", "[class*='close']", "[class*='confirm']", "[class*='ok']" ].join(",")); const target = candidates.find((element) => isConfirmText(controlText(element))); if (!target) return false; state.lastConfirmAt = now; return clickElementDeep(target, reason || "confirm click"); } function clickVisiblePlayButton() { const roots = [ document.querySelector("#polyv"), document.querySelector(".pv-video-player"), document.querySelector(".polyvplayer"), document ].filter(Boolean); for (const root of roots) { const candidates = Array.from(root.querySelectorAll([ "button", "a", "[role='button']", "[class*='play']", "[class*='Play']", "[title*='播放']", "[aria-label*='播放']", "[title*='Play']", "[aria-label*='Play']", "[title*='play']", "[aria-label*='play']" ].join(","))); const target = candidates.find((element) => { return !element.closest("#wsyu-auto-panel") && isVisible(element) && isPlayControl(element); }); if (target) { log("clicked play control:", target); return safeClick(target); } } logDiagnostics("no visible play control"); return false; } function tryResumePlayback() { const localPlayer = pageWindow().player; let attempted = false; try { if (localPlayer) { const playerMethods = [ "j2s_resumeVideo", "j2s_playVideo", "j2s_startVideo", "j2s_play", "j2s_start" ]; for (const method of playerMethods) { if (typeof localPlayer[method] === "function") { localPlayer[method](); log("called player method:", method); attempted = true; } } if (localPlayer.player) { for (const method of playerMethods) { if (typeof localPlayer.player[method] === "function") { localPlayer.player[method](); log("called inner player method:", method); attempted = true; } } } } else { logDiagnostics("player object missing"); } } catch (error) { log("resume player failed:", error.message); } const video = document.querySelector("video"); if (video) { applyPlaybackRate("before play"); log("video state:", videoSummary(video)); if (!video.paused) return true; video.play().then(() => { log("video.play resolved"); }).catch((error) => { log("video.play rejected:", error.name || "Error", error.message || ""); if (error && error.name === "NotAllowedError") { tryMutedAutoplay(video, "NotAllowedError fallback"); return; } clickVisiblePlayButton(); }); return true; } return clickVisiblePlayButton() || attempted; } function tryMutedAutoplay(video, reason) { if (!video || state.mutedAutoplayVideos.has(video)) return false; state.mutedAutoplayVideos.add(video); const wasMuted = video.muted; const oldVolume = video.volume; video.muted = true; applyPlaybackRate("muted autoplay"); log("muted autoplay attempt:", reason, videoSummary(video)); video.play().then(() => { log("muted autoplay resolved"); if (!wasMuted && config.unmuteAfterMutedAutoplayMs >= 0) { setTimeout(() => { try { video.volume = oldVolume; video.muted = false; log("unmute after muted autoplay:", videoSummary(video)); if (video.paused) tryMutedAutoplay(video, "unmute paused fallback"); } catch (error) { log("unmute failed:", error.message); } }, config.unmuteAfterMutedAutoplayMs); } else { log("keep muted after autoplay"); } }).catch((error) => { log("muted autoplay rejected:", error.name || "Error", error.message || ""); clickVisiblePlayButton(); }); return true; } function videoSummary(video) { if (!video) return "none"; return `paused=${video.paused} muted=${video.muted} rate=${video.playbackRate} ready=${video.readyState} time=${Math.round(video.currentTime || 0)}/${Math.round(video.duration || 0)} controls=${video.controls}`; } function applyPlaybackRate(reason) { const rate = state.playbackRate; const videos = Array.from(document.querySelectorAll("video")); let changed = 0; for (const video of videos) { try { if (video.playbackRate !== rate) { video.playbackRate = rate; changed++; } if (video.defaultPlaybackRate !== rate) video.defaultPlaybackRate = rate; } catch (error) { log("set speed failed:", error.message); } } if (changed > 0) log("speed applied:", `${rate}x`, reason || ""); return videos.length > 0; } function broadcastPlaybackRate(reason) { if (window.top !== window) return; const message = { source: "wsyu-auto", type: "set-playback-rate", rate: state.playbackRate, reason }; for (const iframe of Array.from(document.querySelectorAll("iframe"))) { try { iframe.contentWindow.postMessage(message, "*"); } catch (error) { // Cross-origin frames may refuse access; postMessage is best-effort. } } } function setPlaybackRate(rate, reason) { const parsed = Number(rate); if (!speedOptions.includes(parsed)) return; state.playbackRate = parsed; writeStorage(storageKeys.rate, String(parsed)); const select = state.panel && state.panel.querySelector("[data-speed]"); if (select) select.value = String(parsed); log("speed selected:", `${parsed}x`, reason || ""); applyPlaybackRate("selected"); broadcastPlaybackRate("selected"); } function unmuteCurrentVideos(reason) { const videos = Array.from(document.querySelectorAll("video")); if (videos.length === 0) { log("unmute: no video"); return false; } for (const video of videos) { try { video.muted = false; if (video.volume === 0) video.volume = 1; video.play().catch((error) => { log("unmute play rejected:", error.name || "Error", error.message || ""); }); } catch (error) { log("unmute failed:", error.message); } } log("unmute requested:", reason || "", videos.map(videoSummary).join(" | ")); return true; } function playerSummary() { const player = pageWindow().player; if (!player) return "missing"; const names = Object.keys(player).filter((name) => /play|resume|start|pause|seek|getCurrent/i.test(name)).slice(0, 16); const innerNames = player.player ? Object.keys(player.player).filter((name) => /play|resume|start|pause|seek|getCurrent/i.test(name)).slice(0, 16) : []; return `exists methods=[${names.join(",") || "none"}] inner=[${innerNames.join(",") || "none"}]`; } function visiblePlayControlSummary() { const controls = Array.from(document.querySelectorAll([ "button", "a", "[role='button']", "[class*='play']", "[class*='Play']", "[title*='播放']", "[aria-label*='播放']", "[title*='Play']", "[aria-label*='Play']", "[title*='play']", "[aria-label*='play']" ].join(","))).filter((element) => !element.closest("#wsyu-auto-panel") && isVisible(element)); return controls.slice(0, 4).map(describeElement).join(" | ") || "none"; } function logDiagnostics(reason, force) { const now = Date.now(); if (!force && now - state.lastDiagAt < config.diagCooldownMs) return; state.lastDiagAt = now; const videos = Array.from(document.querySelectorAll("video")).map(videoSummary).join(" | ") || "none"; const iframeCount = document.querySelectorAll("iframe").length; log("diag:", reason, `player=${playerSummary()}`, `videos=${videos}`, `iframes=${iframeCount}`, `controls=${visiblePlayControlSummary()}`); } function startCurrentVideo(reason) { const activeVideos = Array.from(document.querySelectorAll("video")); if (activeVideos.some((video) => !video.paused)) { applyPlaybackRate(reason || "playing"); return true; } const now = Date.now(); if (now - state.lastPlayAt < config.playCooldownMs) return false; state.lastPlayAt = now; logDiagnostics(`start ${reason || "tick"}`, true); applyPlaybackRate(reason || "start"); const started = tryResumePlayback() || clickVisiblePlayButton(); if (started) log("start video:", reason || "tick"); return started; } function replayIsVisible() { const replay = document.querySelector("#replay"); return Boolean(replay && isVisible(replay)); } function getCourseItems() { return Array.from(document.querySelectorAll(".chapter_list li")).filter(isVisible); } function getPlayableLink(item) { const links = Array.from(item.querySelectorAll("a[onclick*='showVideo']")); return links.find((link) => /showVideo\s*\(\s*this\s*,[^)]*,\s*0\s*\)/.test(link.getAttribute("onclick") || "")) || links[0] || null; } function parseShowVideoArgs(link) { const onclick = link && link.getAttribute("onclick"); const match = onclick && onclick.match(/showVideo\s*\(\s*this\s*,\s*([^)]*)\)/); if (!match) return null; const args = match[1].split(",").map((part) => Number(part.trim())).slice(0, 4); return args.length === 4 && args.every(Number.isFinite) ? args : null; } function callShowVideo(link) { const args = parseShowVideoArgs(link); const page = pageWindow(); if (!args || typeof page.showVideo !== "function") return false; try { page.showVideo.call(page, link, args[0], args[1], args[2], args[3]); return true; } catch (error) { log("showVideo failed:", error.message); return false; } } function callNextSection() { const page = pageWindow(); if (typeof page.nextSection !== "function") return false; try { page.nextSection.call(page); return true; } catch (error) { log("nextSection failed:", error.message); return false; } } function clickNextCourse(reason) { const now = Date.now(); if (now - state.lastNextAt < config.nextCooldownMs) return false; const items = getCourseItems(); const active = document.querySelector(".chapter_list li.active"); const currentIndex = items.indexOf(active); if (!active || currentIndex < 0) { log("no active list item yet"); return false; } const next = items[currentIndex + 1]; if (!next) { log("last course item reached"); return false; } const link = getPlayableLink(next); if (!link) { log("next item has no playable link"); return false; } const beforeText = textOf(active); state.lastNextAt = now; log("next course:", reason || "detected end", textOf(next)); const switched = callNextSection() || callShowVideo(link) || safeClick(link); setTimeout(() => startCurrentVideo("after next"), 1200); setTimeout(() => startCurrentVideo("after next retry"), 3000); setTimeout(() => { const activeAfter = document.querySelector(".chapter_list li.active"); if (textOf(activeAfter) === beforeText) { log("fallback next click"); callShowVideo(link) || safeClick(link); setTimeout(() => startCurrentVideo("after fallback next"), 1200); } }, 1500); return switched; } function hookPlayOver() { if (!isCourseWatchPage()) return; const page = pageWindow(); const current = page.s2j_onPlayOver; if (typeof current !== "function" || current === state.hookedPlayOver) return; const original = current; const hooked = function (...args) { const result = original.apply(this, args); setTimeout(() => clickNextCourse("play over callback"), config.nextDelayMs); return result; }; hooked.__wsyuAutoHook = true; state.hookedPlayOver = hooked; page.s2j_onPlayOver = hooked; log("hooked play-over callback"); } function hookPlayerInitOver() { if (!isCourseWatchPage()) return; const page = pageWindow(); const current = page.s2j_onPlayerInitOver; if (typeof current !== "function" || current === state.hookedPlayerInitOver) return; const original = current; const hooked = function (...args) { const result = original.apply(this, args); setTimeout(() => startCurrentVideo("player init"), 800); setTimeout(() => startCurrentVideo("player init retry"), 2200); return result; }; state.hookedPlayerInitOver = hooked; page.s2j_onPlayerInitOver = hooked; log("hooked player-init callback"); } function ensurePanel() { if (!isCourseWatchPage() || state.panel || window.top !== window) return; if (typeof GM_addStyle === "function") { GM_addStyle(` #wsyu-auto-panel { position: fixed; right: 16px; bottom: 16px; z-index: 2147483647; width: 380px; max-width: calc(100vw - 32px); padding: 10px 12px; border-radius: 6px; background: rgba(20, 26, 32, 0.88); color: #fff; font: 12px/1.45 Arial, sans-serif; box-shadow: 0 4px 16px rgba(0,0,0,.22); } #wsyu-auto-panel [data-status] { margin-top: 4px; word-break: break-all; } #wsyu-auto-panel [data-log] { margin-top: 8px; max-height: 180px; overflow: auto; white-space: pre-wrap; word-break: break-all; color: #cbd5e1; font: 11px/1.35 Consolas, monospace; } #wsyu-auto-panel button { margin-top: 8px; margin-right: 6px; padding: 4px 8px; border: 0; border-radius: 4px; color: #111; background: #fff; cursor: pointer; } #wsyu-auto-panel select { margin-top: 8px; margin-right: 6px; padding: 3px 6px; border: 0; border-radius: 4px; background: #fff; color: #111; } `); } const panel = document.createElement("div"); panel.id = "wsyu-auto-panel"; panel.innerHTML = `
WNSSEDU Auto
运行中
`; panel.querySelector("[data-speed]").value = String(state.playbackRate); panel.querySelector("[data-speed]").addEventListener("change", (event) => { setPlaybackRate(event.target.value, "panel"); }); panel.querySelector("[data-start]").addEventListener("click", () => { logDiagnostics("manual start", true); startCurrentVideo("manual button"); clickSkipButton(); }); panel.querySelector("[data-unmute]").addEventListener("click", () => { unmuteCurrentVideos("manual button"); }); panel.querySelector("[data-diag]").addEventListener("click", () => { logDiagnostics("manual diag", true); }); panel.querySelector("[data-copy]").addEventListener("click", () => { const text = state.logs.slice().reverse().join("\n"); navigator.clipboard.writeText(text).then(() => { log("logs copied"); }).catch((error) => { log("copy logs failed:", error.message); }); }); document.documentElement.appendChild(panel); state.panel = panel; updatePanel(state.lastStatus); renderPanelLog(); } function updatePanel(text) { if (!state.panel) return; const status = state.panel.querySelector("[data-status]"); if (status) status.textContent = text || "运行中"; } function tickCoursePage() { ensurePanel(); hookPlayOver(); hookPlayerInitOver(); clickSkipButton(); clickConfirmButton("course confirm"); applyPlaybackRate("course tick"); broadcastPlaybackRate("course tick"); if (replayIsVisible()) { setTimeout(() => clickNextCourse("replay overlay"), config.nextDelayMs); return; } startCurrentVideo("course page"); } function tickPolyvFrame() { clickSkipButton(); clickConfirmButton("polyv confirm"); applyPlaybackRate("polyv tick"); startCurrentVideo("polyv frame"); } function tick() { if (isCourseWatchPage()) { tickCoursePage(); return; } if (isPolyvFrame()) { tickPolyvFrame(); } } window.addEventListener("message", (event) => { const data = event.data; if (!data || data.source !== "wsyu-auto" || data.type !== "set-playback-rate") return; const rate = Number(data.rate); if (!speedOptions.includes(rate)) return; state.playbackRate = rate; applyPlaybackRate(data.reason || "message"); }); tick(); setInterval(tick, config.tickMs); })();