// ==UserScript== // @name 学习通自动刷课助手 // @namespace http://tampermonkey.net/ // @version 1.0.0 // @description 学习通(超星)自动刷课:自动播放视频、倍速播放、自动切下一节、自动答题、防挂机模拟、学习进度面板。需配合油猴(Tampermonkey)使用。 // @author you // @match *://*.chaoxing.com/* // @match *://*.xueyinonline.com/* // @match *://*.edu.cn/* // @grant none // @run-at document-start // @license MIT // ==/UserScript== (function () { 'use strict'; /* ================================================================ * 0. 激活范围:仅在“学习通”相关页面运行 * ================================================================ */ const HOST = location.hostname; const PATH = location.pathname; const ACTIVE = /chaoxing|xueyinonline/i.test(HOST) || /mooc-ans|\/knowledge\/|videoPlay|doHomeWorkNew|doWorkAnswer|exam\//i.test(PATH); if (!ACTIVE) return; const IS_TOP = (() => { try { return window === window.top; } catch (e) { return false; } })(); const SPEED_LIST = [1, 1.5, 2, 3, 4, 8, 16]; /* ================================================================ * 1. 配置(localStorage 持久化) * ================================================================ */ const LS_KEY = 'cx_shuake_cfg_v1'; const defaultCfg = { speed: 16, // 默认倍速 autoMute: true, // 自动静音 autoNext: true, // 自动切下一节 autoAnswer: true, // 自动答题 autoSubmit: false, // 答完是否直接提交(默认只保存,更安全) answerMode: 'first',// first | random | skip antiIdle: true // 防挂机模拟 }; let cfg = Object.assign({}, defaultCfg); try { Object.assign(cfg, JSON.parse(localStorage.getItem(LS_KEY) || '{}')); } catch (e) {} function saveCfg() { try { localStorage.setItem(LS_KEY, JSON.stringify(cfg)); } catch (e) {} } let running = true; // 总开关 /* ================================================================ * 2. 日志 * ================================================================ */ const logs = []; function log(msg) { const t = new Date().toLocaleTimeString('zh-CN', { hour12: false }); logs.push('[' + t + '] ' + msg); if (logs.length > 60) logs.shift(); try { const le = document.getElementById('cxsh-log'); if (le) le.textContent = logs.slice(-9).join('\n'); } catch (e) {} console.log('[学习通刷课]', msg); } /* ================================================================ * 3. 跨窗口收集(同源 iframe 全部可见) * ================================================================ */ function collectWindows() { const seen = new Set(); const queue = []; try { queue.push(window.top); } catch (e) { queue.push(window); } while (queue.length) { const w = queue.shift(); if (!w || seen.has(w)) continue; seen.add(w); try { const fs = w.document.querySelectorAll('iframe, frame'); for (const f of fs) { try { if (f.contentWindow) queue.push(f.contentWindow); } catch (e) {} } } catch (e) {} } return Array.from(seen); } function collectDocs() { const out = []; for (const w of collectWindows()) { try { out.push(w.document); } catch (e) {} } return out; } /* ================================================================ * 4. 视频:查找 + 倍速 + 自动播放 + 静音 * ================================================================ */ function findVideo() { let best = null, bestScore = -1; for (const d of collectDocs()) { let vs; try { vs = d.querySelectorAll('video'); } catch (e) { continue; } for (const v of vs) { let s = 0; if (v.duration && isFinite(v.duration) && v.duration > 0) s += 10; if (v.id === 'video_html5_api' || /video-js|vjs-tech/.test(v.className || '')) s += 5; if (v.clientWidth > 200 && v.clientHeight > 100) s += 3; try { if (v.offsetParent !== null) s += 2; } catch (e) {} if (!v.paused) s += 2; if (s > bestScore) { bestScore = s; best = { doc: d, video: v }; } } } return best; } function enforceVideo() { const ctx = findVideo(); if (!ctx) return null; const v = ctx.video; try { const spd = Math.min(16, Math.max(1, cfg.speed)); if (v.playbackRate !== spd) v.playbackRate = spd; if (cfg.autoMute && !v.muted) { v.muted = true; } if (v.paused && !v.ended && v.readyState > 0) { const p = v.play(); if (p && p.catch) p.catch(() => {}); } } catch (e) {} return ctx; } function attachVideoEvents(ctx) { const v = ctx.video; if (v.__cxWatched) return; v.__cxWatched = true; v.addEventListener('ended', () => { log('视频播放完成'); if (!cfg.autoNext) return; if (IS_TOP) setTimeout(() => clickNext('ended'), 800); else { try { window.parent.postMessage({ type: 'cx-next' }, '*'); } catch (e) {} } }); } /* ================================================================ * 5. 自动切下一节(顶部页面负责;子窗口发消息请求) * ================================================================ */ let lastNextAt = 0; function clickNext(reason) { const now = Date.now(); if (now - lastNextAt < 2000) return false; // 防重复 lastNextAt = now; // 策略1:常见按钮选择器 const sels = [ '#nextBtn', '.next_btn', '.nextBtn', '.btn-next', '.next', 'a.next', '.next-section', '.nextB', '.icon-xiayibu', '.btn-next-step' ]; for (const d of collectDocs()) { for (const sel of sels) { const els = d.querySelectorAll(sel); for (const el of els) { if (el.disabled) continue; const r = el.getBoundingClientRect(); if (r.width === 0 || r.height === 0 || r.width > 500) continue; try { el.click(); log('已点击切集按钮(' + sel + ')'); return true; } catch (e) {} } } } // 策略2:文本匹配按钮 for (const d of collectDocs()) { const btn = findBtnByText(d, ['下一节', '下一集', '下节', '下一任务点', '下一个', '继续下一']); if (btn) { try { btn.click(); log('已点击「' + (btn.textContent || '').trim().slice(0, 10) + '」'); return true; } catch (e) {} } } // 策略3:侧边栏目录,当前章节的下一项 for (const d of collectDocs()) { const cur = d.querySelector( '.ncells h4.currents, .ncells .currents, .chapter_item.cur, .chapter_item .cur, .active .chapter_item' ); if (!cur) continue; const item = cur.closest('.ncells, .chapter_item, li'); if (!item) continue; const next = item.nextElementSibling; const link = next ? next.querySelector('a, h4 a, .title, .name') : null; if (link) { try { link.click(); log('已点击目录下一节「' + (link.textContent || '').trim().slice(0, 14) + '」'); return true; } catch (e) {} } } log('未找到下一节入口(' + (reason || 'auto') + ')'); return false; } // 子窗口发来的切集请求 window.addEventListener('message', (e) => { try { const d = e.data || {}; if (d.type === 'cx-next') clickNext('frame-msg'); } catch (err) {} }); /* ================================================================ * 6. 自动答题(章节测验 / 作业 / 考试) * ================================================================ */ const QUIZ_URL = /doHomeWorkNew|doWorkAnswer|reVersionTestStartNew|test\/|exam\/|work\/|examTest|doExam/i; function isQuizPage() { if (QUIZ_URL.test(location.href)) return true; return collectDocs().some(d => !!d.querySelector('#submitTest, .qtContent, .mark_item, #submitTestContent, .workContent')); } let saveClickedFor = ''; // 记录已保存的页面 URL,避免重复点 function handleQuiz() { if (!cfg.autoAnswer || !isQuizPage()) return; for (const d of collectDocs()) { const qs = d.querySelectorAll('.questionLi, .mark_item, .TiMu, .questionItem'); if (!qs.length) continue; // 找一题未作答的客观题作答(每个周期只处理一题,避免过快) for (const q of qs) { const inputs = q.querySelectorAll('input[type=radio], input[type=checkbox]'); if (!inputs.length) continue; // 填空/主观题跳过 let hasChecked = false; for (const i of inputs) if (i.checked) { hasChecked = true; break; } if (hasChecked) continue; const arr = Array.from(inputs); const pick = cfg.answerMode === 'skip' ? null : (cfg.answerMode === 'random' ? arr[Math.floor(Math.random() * arr.length)] : arr[0]); if (pick) { const target = pick.closest('label') || pick.closest('.optionUl li') || pick.closest('.answerBg') || pick.parentElement || pick; try { target.click(); log('已作答一题(第' + (arr.indexOf(pick) + 1) + '个选项)'); } catch (e) { try { pick.click(); } catch (e2) {} } } return; } // 全部答完 → 保存 / 提交 const key = location.href.split('#')[0]; if (saveClickedFor === key) return; if (cfg.autoSubmit) { const sub = findBtnByText(d, ['提交作业', '交卷', '提交答卷', '提交']); if (sub) { sub.click(); saveClickedFor = key; log('已点击提交'); return; } } const save = findBtnByText(d, ['保存']); if (save) { save.click(); saveClickedFor = key; log('已点击保存'); } return; } } // 视频播放中途出现的题目 function handleVideoQuiz() { if (!cfg.autoAnswer) return; for (const d of collectDocs()) { const box = d.querySelector('.ans-videoquiz-opts, .videoquiz, .ans-quiz-opts'); if (!box || box.offsetParent === null) continue; const opts = box.querySelectorAll('input[type=radio]'); let done = false; for (const o of opts) { if (String(o.value).toLowerCase() === 'true') { try { o.click(); done = true; break; } catch (e) {} } } if (!done && opts.length) { try { opts[0].click(); } catch (e) {} } const sub = box.querySelector('.ans-videoquiz-submit, .submit, button[type=submit]'); if (sub) { try { sub.click(); log('已提交视频中题目'); } catch (e) {} } } } // 按文本找按钮(返回可点击元素) function findBtnByText(d, texts) { const els = d.querySelectorAll('button, a, [role="button"], .btn, .ant-btn, .layui-btn, .el-button, .btn-next, input[type=button]'); for (const b of els) { const t = (b.textContent || '').trim().replace(/\s+/g, ''); if (!t) continue; for (const tx of texts) { if (t === tx || t.indexOf(tx) === 0) { const r = b.getBoundingClientRect(); if (r.width === 0 || r.height === 0 || r.width > 500) continue; return b; } } } return null; } /* ================================================================ * 7. 拖拽验证码 / 弹窗处理 * ================================================================ */ function solveDragCaptcha() { for (const d of collectDocs()) { const slider = d.querySelector( '.drag-btn, .drag_btn, .yidun_slider, .sc-btn, .dragVerify .btn, .dragverify-btn, #dragVerify .drag-btn, .verify-slider, .nc_iconfont.btn_slide' ); if (!slider) continue; const lastTry = slider.__cxTryAt || 0; if (Date.now() - lastTry < 30000) continue; // 失败后 30s 再重试 slider.__cxTryAt = Date.now(); const track = slider.closest('.drag, .yidun_slider__track, .sc_track, .dragVerify, .yidun, .verify-slider-wrap') || slider.parentElement; if (!track) continue; const tr = track.getBoundingClientRect(); const sr = slider.getBoundingClientRect(); const sx = sr.left + sr.width / 2; const sy = sr.top + sr.height / 2; const ex = Math.max(sr.left, tr.right - 8); const fire = (type, x, y) => { const opt = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }; try { d.dispatchEvent(new MouseEvent(type, opt)); } catch (e) {} try { window.dispatchEvent(new MouseEvent(type, opt)); } catch (e) {} try { slider.dispatchEvent(new MouseEvent(type, opt)); } catch (e) {} }; fire('mousedown', sx, sy); const steps = 25; for (let i = 1; i <= steps; i++) { fire('mousemove', sx + (ex - sx) * i / steps, sy + Math.sin(i / steps * Math.PI) * 2); } fire('mouseup', ex, sy); log('已尝试拖动验证码滑块'); return; // 一次处理一个 } } const POPUP_OK = ['确定', '知道了', '好的', '关闭', '我知道了', '进入下一节', '继续学习', '开始学习', '下一步', '跳过', '略过', '不再提醒']; function closePopups() { for (const d of collectDocs()) { const dialogs = d.querySelectorAll('.layui-layer, .el-message-box, .ant-modal, .ui-dialog, .layermchild, .popup, .dialog, .modal, .layui-layer-dialog'); for (const dl of dialogs) { if (dl.offsetParent === null) continue; const btn = findBtnByText(dl, POPUP_OK); if (btn) { try { btn.click(); log('已关闭弹窗'); } catch (e) {} return; } } } } /* ================================================================ * 8. 防挂机:模拟鼠标活动 + 伪装页面可见 * ================================================================ */ let lastActivity = 0; function simulateActivity() { const now = Date.now(); if (now - lastActivity < 20000) return; lastActivity = now; const x = 100 + Math.random() * 300; const y = 100 + Math.random() * 300; try { document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: x, clientY: y })); document.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, clientX: x, clientY: y })); } catch (e) {} for (const w of collectWindows()) { try { w.document.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, clientX: x, clientY: y })); } catch (e) {} } } function overrideVisibility(win) { try { win.eval(`(function () { try { Object.defineProperty(document, 'hidden', { configurable: true, get: function () { return false; } }); Object.defineProperty(document, 'webkitHidden', { configurable: true, get: function () { return false; } }); Object.defineProperty(document, 'visibilityState', { configurable: true, get: function () { return 'visible'; } }); } catch (e) {} })()`); } catch (e) {} } /* ================================================================ * 9. 进度面板 UI(仅在顶层页面创建) * ================================================================ */ let panel = null; const PANEL_CSS = '#cxsh-panel{position:fixed;right:16px;top:120px;z-index:2147483647;width:250px;font:12px/1.5 "Microsoft YaHei",sans-serif;' + 'background:rgba(24,32,58,.96);color:#e8ecf5;border:1px solid rgba(255,255,255,.14);border-radius:10px;' + 'box-shadow:0 8px 30px rgba(0,0,0,.4);overflow:hidden;-webkit-user-select:none;user-select:none;}' + '#cxsh-panel .cxsh-h{display:flex;justify-content:space-between;align-items:center;padding:8px 12px;cursor:move;' + 'background:rgba(64,86,255,.35);font-weight:700;letter-spacing:.5px;}' + '#cxsh-panel .cxsh-min{cursor:pointer;font-weight:400;padding:0 4px;}' + '#cxsh-panel .cxsh-b{padding:10px 12px;}' + '#cxsh-panel .cxsh-row{margin:6px 0;display:flex;align-items:center;gap:6px;flex-wrap:wrap;}' + '#cxsh-panel .cxsh-tag{background:rgba(64,86,255,.5);padding:1px 8px;border-radius:4px;font-weight:700;}' + '#cxsh-panel button{background:rgba(255,255,255,.12);color:#e8ecf5;border:1px solid rgba(255,255,255,.2);' + 'border-radius:6px;padding:3px 10px;cursor:pointer;font-size:12px;}' + '#cxsh-panel button:hover{background:rgba(255,255,255,.22);}' + '#cxsh-panel button.cxsh-on{background:rgba(52,199,89,.85);border-color:transparent;color:#0b3d1e;font-weight:700;}' + '#cxsh-panel label{display:flex;align-items:center;gap:4px;margin-right:10px;cursor:pointer;}' + '#cxsh-panel .cxsh-bar{height:6px;background:rgba(255,255,255,.12);border-radius:3px;overflow:hidden;flex:1;}' + '#cxsh-panel .cxsh-bar i{display:block;height:100%;width:0;background:linear-gradient(90deg,#4f7cff,#38e08b);transition:width .4s;}' + '#cxsh-panel .cxsh-ch{margin:4px 0;color:#9fb0d4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}' + '#cxsh-panel .cxsh-log{margin:8px 0 0;padding:6px 8px;background:rgba(0,0,0,.35);border-radius:6px;' + 'max-height:130px;overflow:auto;white-space:pre-wrap;word-break:break-all;color:#9fb0d4;font-size:11px;line-height:1.5;}' + '#cxsh-panel .cxsh-danger{background:rgba(255,69,58,.85)!important;color:#fff!important;border-color:transparent!important;font-weight:700;}'; function createPanel() { if (!IS_TOP || panel) return; if (!document.body) return; const st = document.createElement('style'); st.textContent = PANEL_CSS; (document.head || document.documentElement).appendChild(st); const el = document.createElement('div'); el.id = 'cxsh-panel'; el.innerHTML = '