// ==UserScript== // @name 江苏开放大学自动刷课脚本【江开全自动】 // @namespace https://xuexi.jsou.cn/ // @version 1.7.0 // @description 江苏开放大学相关视频自动学习自动看视频程序【可加速及自动下一页】 // @author 一心向善 // @match http://xuexi.jsou.cn/* // @match https://xuexi.jsou.cn/* // @match http://*.jsou.cn/* // @match https://*.jsou.cn/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_addStyle // @run-at document-idle // ==/UserScript== // // v3.1 修复:goNext 用 location.href 直接跳转(绕过 SPA 拦截),等待用 title 变化检测 // // 使用:登录 → 进入任意课程活动页 → 点"▶ 开始刷课" → 全自动连续进行 (function () { 'use strict'; const CFG = { defaultSpeed: 1, videoGuardMs: 800, autoMute: true, docDwellSec: 120, watchdogMs: 1500, waitTimeoutSec: 15, skipDoneVideos: true, }; // ====================== 持久化 ====================== const K_SESSION = '__jfk_session__'; const K_STATE = '__jfk_state__'; const K_LOG = '__jfk_log__'; const K_AUTO = '__jfk_autorun__'; // ★ 是否在自动运行(跨页面持久化) const K_SPEED = '__jfk_speed__'; // ★ 倍速跨页面保留 const K_DWELL = '__jfk_dwell__'; // ★ 文档停留秒数跨页面保留 const has = typeof GM_getValue === 'function'; const get = (k, d) => has ? GM_getValue(k, d) : (localStorage.getItem(k) ?? d); const set = (k, v) => has ? GM_setValue(k, v) : localStorage.setItem(k, v); const del = (k) => has ? GM_deleteValue(k) : localStorage.removeItem(k); function loadState() { try { return JSON.parse(get(K_STATE, '{}')); } catch (e) { return {}; } } function saveState(s) { set(K_STATE, JSON.stringify(s)); } function loadLog() { try { return JSON.parse(get(K_LOG, '[]')); } catch (e) { return []; } } function pushLog(msg, level) { const l = loadLog(); l.push({ t: new Date().toLocaleTimeString('zh-CN'), level: level || 'info', msg: String(msg) }); if (l.length > 300) l.splice(0, l.length - 300); set(K_LOG, JSON.stringify(l)); if (panel) panel.renderLog(); } // ★ SESSION_ID 持久化:只第一次生成,后续页面复用同一值 // 这样 watchdog 里的 isActive() 始终返回 true let SESSION_ID = get(K_SESSION, null); if (!SESSION_ID) { SESSION_ID = String(Date.now()) + '-' + Math.random().toString(36).slice(2, 8); set(K_SESSION, SESSION_ID); } function isActive() { return get(K_SESSION, '') === SESSION_ID; } // ====================== 工具 ====================== const $ = (s, r) => (r || document).querySelector(s); const $$ = (s, r) => Array.from((r || document).querySelectorAll(s)); const sleep = ms => new Promise(r => setTimeout(r, ms)); // 安全取 URL 中的 activityId function curActivityId() { try { const m = location.search.match(/activityId=([^&]+)/); return m ? m[1] : null; } catch (e) { return null; } } function pageType() { const p = location.pathname + location.search; if (p.indexOf('/student/courseuser/myCourse') !== -1) return 'myCourse'; if (p.indexOf('/student/courseuser/courseContent') !== -1) return 'courseDir'; if (p.indexOf('/student/activity/display') !== -1) return 'activity'; return 'other'; } // 等待 title 变化或 activityId 变化(SPA 友好) function waitNav(oldAid, oldTitle, timeoutSec) { return new Promise(res => { const start = Date.now(); const to = (timeoutSec || CFG.waitTimeoutSec) * 1000; const timer = setInterval(() => { if (!isActive()) { clearInterval(timer); return res(false); } try { const newAid = curActivityId(); const newTitle = document.title; // 任一变化即认为跳转成功 if ((oldAid && newAid && newAid !== oldAid) || (oldTitle && newTitle && newTitle !== oldTitle && newTitle !== '学生端单门课程页面')) { clearInterval(timer); return res(true); } } catch (e) {} if (Date.now() - start > to) { clearInterval(timer); return res(false); } }, 300); }); } // ====================== 控制面板 ====================== let panel; GM_addStyle(` #jfk-panel { position: fixed; top: 80px; right: 16px; z-index: 2147483647; width: 340px; background: #1f2937; color: #e5e7eb; font: 13px/1.5 -apple-system, "PingFang SC", sans-serif; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,.35); overflow: hidden; user-select: none; } #jfk-panel .hdr { background: linear-gradient(135deg,#4f46e5,#06b6d4); color:#fff; padding:10px 14px; cursor: move; display:flex; justify-content:space-between; align-items:center; font-weight:600; } #jfk-panel .dot { width:8px; height:8px; border-radius:50%; background:#fbbf24; display:inline-block; margin-right:6px; } #jfk-panel .dot.run { background:#34d399; animation: jfkp 1s infinite; } @keyframes jfkp { 0%,100%{opacity:1} 50%{opacity:.3} } #jfk-panel .min { cursor:pointer; opacity:.8; } #jfk-panel .body { padding:12px 14px; max-height:480px; overflow-y:auto; } #jfk-panel .row { margin-bottom:10px; } #jfk-panel label { display:block; font-size:11px; color:#9ca3af; margin-bottom:4px; } #jfk-panel .speeds { display:flex; flex-wrap:wrap; gap:4px; } #jfk-panel .speeds button { flex:1; min-width:36px; padding:4px 0; font-size:11px; background:#374151; color:#e5e7eb; border:0; border-radius:6px; cursor:pointer; } #jfk-panel .speeds button.active { background:#06b6d4; color:#fff; font-weight:600; } #jfk-panel .acts { display:flex; gap:6px; margin-top:8px; flex-wrap:wrap; } #jfk-panel .acts button { flex:1; min-width:90px; padding:8px 0; border:0; border-radius:8px; cursor:pointer; font-weight:600; font-size:12px; } #jfk-panel .b-p { background:#4f46e5; color:#fff; } #jfk-panel .b-w { background:#f59e0b; color:#1f2937; } #jfk-panel .b-d { background:#ef4444; color:#fff; } #jfk-panel .b-g { background:#374151; color:#e5e7eb; } #jfk-panel .status { background:#111827; padding:8px 10px; border-radius:6px; font-size:11px; color:#9ca3af; font-family:monospace; word-break:break-all; min-height:40px; } #jfk-panel .status .k { color:#06b6d4; } #jfk-panel .status .v { color:#fbbf24; } #jfk-panel .logs { background:#111827; border-radius:6px; padding:6px 8px; font-size:10px; color:#9ca3af; font-family:monospace; max-height:140px; overflow-y:auto; } #jfk-panel .logs .err { color:#fca5a5; } #jfk-panel .logs .ok { color:#86efac; } #jfk-panel .logs .warn { color:#fcd34d; } #jfk-panel input[type=number] { background:#111827; color:#e5e7eb; border:1px solid #374151; border-radius:6px; padding:4px 8px; font-size:12px; width:100%; box-sizing:border-box; } #jfk-collapsed { position:fixed; top:80px; right:16px; z-index:2147483647; width:52px; height:52px; border-radius:50%; background:linear-gradient(135deg,#4f46e5,#06b6d4); color:#fff; border:0; cursor:pointer; font-size:24px; box-shadow:0 4px 16px rgba(0,0,0,.35); } #jfk-panel .jfk-foot { margin-top:12px; padding-top:10px; border-top:1px solid #374151; font-size:12px; font-weight:700; color:#ffffff; line-height:1.6; text-align:center; } `); class Panel { constructor() { this.speed = parseFloat(get(K_SPEED, null)) || CFG.defaultSpeed; this.dwell = parseFloat(get(K_DWELL, null)) || CFG.docDwellSec; CFG.docDwellSec = this.dwell; // ★ 用持久化值覆盖默认 this.dragging = false; this.build(); this.bind(); this.renderLog(); } build() { const w = document.createElement('div'); w.innerHTML = ` `; document.body.appendChild(w); this.el = document.getElementById('jfk-panel'); this.cb = document.getElementById('jfk-collapsed'); this.show(); } bind() { document.getElementById('jfk-min').onclick = () => this.hide(); this.cb.onclick = () => this.show(); const drag = document.getElementById('jfk-drag'); let sx, sy, ox, oy; drag.addEventListener('mousedown', e => { this.dragging = true; sx = e.clientX; sy = e.clientY; const r = this.el.getBoundingClientRect(); ox = r.left; oy = r.top; e.preventDefault(); }); document.addEventListener('mousemove', e => { if (!this.dragging) return; this.el.style.left = (ox + e.clientX - sx) + 'px'; this.el.style.top = (oy + e.clientY - sy) + 'px'; this.el.style.right = 'auto'; }); document.addEventListener('mouseup', () => this.dragging = false); $$('#jfk-speeds button', this.el).forEach(b => b.onclick = () => { this.speed = parseFloat(b.dataset.s); set(K_SPEED, this.speed); // ★ 持久化倍速 $$('#jfk-speeds button', this.el).forEach(x => x.classList.remove('active')); b.classList.add('active'); document.getElementById('jfk-cs').textContent = this.speed; document.getElementById('jfk-custom').value = this.speed; Engine.speed = this.speed; applySpeed(this.speed); }); document.getElementById('jfk-custom').oninput = e => { const v = parseFloat(e.target.value) || 1; this.speed = v; set(K_SPEED, v); // ★ 持久化倍速 Engine.speed = v; document.getElementById('jfk-cs').textContent = v; applySpeed(v); }; // ★ 文档停留秒数:输入框 + 保存按钮 const saveDwell = () => { const v = Math.max(5, Math.min(600, parseInt(document.getElementById('jfk-dwell').value) || 120)); this.dwell = v; CFG.docDwellSec = v; set(K_DWELL, v); document.getElementById('jfk-cd').textContent = v; pushLog(`文档停留秒数已设为 ${v}s(已持久化)`, 'ok'); }; document.getElementById('jfk-dwell-save').onclick = saveDwell; document.getElementById('jfk-dwell').addEventListener('keydown', e => { if (e.key === 'Enter') saveDwell(); }); document.getElementById('jfk-start').onclick = () => Engine.start(); document.getElementById('jfk-pause').onclick = () => { Engine.paused = true; del(K_AUTO); // 暂停=不再自动恢复 pushLog('已暂停(不会自动恢复,需手动点开始)', 'warn'); panel.setStep('idle', '已暂停'); }; document.getElementById('jfk-skip').onclick = () => { Engine.skip = true; pushLog('跳过当前', 'warn'); }; document.getElementById('jfk-apply').onclick = () => applySpeed(this.speed); document.getElementById('jfk-next').onclick = () => Engine.goNext(true); document.getElementById('jfk-reset').onclick = () => { if (confirm('确认重置刷课进度?(不清空日志)')) { del(K_STATE); Engine.reset(); this.setStep('idle'); pushLog('进度已重置', 'warn'); } }; } show() { this.el.style.display = 'block'; this.cb.style.display = 'none'; } hide() { this.el.style.display = 'none'; this.cb.style.display = 'block'; } setStatus(t) { document.getElementById('jfk-status').innerHTML = t; } setStep(s, detail) { const dot = document.getElementById('jfk-dot'); if (dot) dot.classList.toggle('run', s !== 'idle' && s !== 'paused'); const map = { idle:'空闲', paused:'已暂停', myCourse:'我的课程', courseDir:'课程目录', doc:'阅读文档', video:'播放视频', next:'切换下一项', done:'全部完成' }; this.setStatus(`步骤: ${map[s]||s}${detail?'
详情: '+detail:''}`); } renderLog() { const el = document.getElementById('jfk-logs'); if (!el) return; const l = loadLog().slice(-80).reverse(); el.innerHTML = l.map(x => `
[${x.t}] ${x.msg}
`).join('') || '
暂无日志
'; } } // ====================== 视频控制 ====================== let vGuard = null; function findVideo() { return $('video') || null; } function applySpeed(speed) { const v = findVideo(); if (!v) return; try { v.playbackRate = parseFloat(speed) || 1; if (CFG.autoMute) v.muted = true; if (v.paused) v.play().catch(() => {}); if (vGuard) clearInterval(vGuard); vGuard = setInterval(() => { if (!isActive()) { clearInterval(vGuard); return; } try { if (v.playbackRate !== speed) v.playbackRate = speed; if (v.paused && !v.ended) v.play().catch(() => {}); } catch (e) {} }, CFG.videoGuardMs); } catch (e) {} } // ====================== 核心引擎 ====================== const Engine = { paused: false, skip: false, inProgress: false, watchdog: null, state: {}, currentAid: null, handledKey: null, speed: CFG.defaultSpeed, reset() { this.paused = false; this.skip = false; this.inProgress = false; this.state = {}; this.currentAid = null; this.handledKey = null; }, start() { this.paused = false; this.skip = false; this.handledKey = null; // ★ 持久化运行状态,整页刷新后自动恢复 set(K_AUTO, '1'); // 恢复倍速 const savedSpeed = parseFloat(get(K_SPEED, null)) || CFG.defaultSpeed; this.speed = savedSpeed; if (panel) { panel.speed = savedSpeed; document.getElementById('jfk-cs').textContent = savedSpeed; } if (this.watchdog) clearInterval(this.watchdog); this.watchdog = setInterval(() => this.tick(), CFG.watchdogMs); pushLog('🚀 启动连续刷课(自动下一节)', 'ok'); this.tick(); }, stop() { del(K_AUTO); // ★ 清除自动运行标志 if (this.watchdog) { clearInterval(this.watchdog); this.watchdog = null; } }, async tick() { if (!isActive()) { this.stop(); return; } if (this.paused) return; if (this.inProgress) return; this.inProgress = true; try { await this.dispatch(); } finally { this.inProgress = false; } }, async dispatch() { const t = pageType(); if (t === 'myCourse') return this.tickMyCourse(); if (t === 'courseDir') return this.tickCourseDir(); if (t === 'activity') return this.tickActivity(); // 非目标页 → 我的课程 const myUrl = location.origin + '/jxpt-web/student/courseuser/myCourse'; if (location.href !== myUrl) { pushLog('当前非目标页,跳转我的课程', 'warn'); location.href = myUrl; } }, // ---- 我的课程页 ---- async tickMyCourse() { panel.setStep('myCourse'); const st = loadState(); const courses = $$('a').filter(a => { const h = a.getAttribute('href') || ''; return h.indexOf('courseContent') !== -1 && h.indexOf('courseVersionId=') !== -1; }).map(a => ({ href: a.getAttribute('href'), name: (a.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 40) })); const seen = new Set(); const uniq = courses.filter(c => { const m = c.href && c.href.match(/courseVersionId=([^&]+)/); const id = m ? m[1] : null; if (!id || seen.has(id)) return false; seen.add(id); return true; }); if (!uniq.length) { pushLog('未找到课程,等待...', 'warn'); return; } const idx = st.courseIdx || 0; if (idx >= uniq.length) { panel.setStep('done', '所有课程已完成🎉'); pushLog('🎉 所有课程刷完!', 'ok'); this.stop(); return; } const c = uniq[idx]; pushLog(`进入课程 ${idx + 1}/${uniq.length}: ${c.name}`, 'ok'); // 直接导航到课程目录页 const joiner = c.href.indexOf('?') !== -1 ? '&' : '?'; location.href = c.href + joiner + 'subpage=contents'; }, // ---- 课程目录页 ---- async tickCourseDir() { panel.setStep('courseDir'); if (location.search.indexOf('subpage=contents') === -1) { const joiner = location.search ? '&' : '?'; location.href = location.href + joiner + 'subpage=contents'; return; } await sleep(800); // 展开所有章节 const units = $$('div.unit'); for (const u of units) { const top = $('.unitTop', u) || u; try { ['mousedown','mouseup','click'].forEach(t => top.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, view: window }))); } catch (e) {} await sleep(250); } await sleep(400); // 找第一个 doc/video 活动,直接构造 URL 跳转 const first = $$('div.activity.doc, div.activity.video')[0]; if (!first) { pushLog('目录无活动', 'warn'); return; } const aid = first.id; const cvid = (() => { const m = location.search.match(/courseVersionId=([^&]+)/); return m ? m[1] : ''; })(); if (!cvid) { pushLog('找不到 courseVersionId', 'err'); return; } pushLog(`进入第一个活动: ${first.textContent.trim().slice(0,30)}`); // 直接构造活动页 URL 跳转 location.href = `${location.origin}/jxpt-web/student/activity/display?courseVersionId=${cvid}&activityId=${aid}`; }, // ---- 活动页(核心) ---- async tickActivity() { const aid = curActivityId(); if (!aid) return; // 新活动:初始化 if (this.currentAid !== aid) { this.currentAid = aid; this.state = { type: null, startedAt: Date.now() }; this.handledKey = null; await sleep(1500); // 等内容加载 } const v = findVideo(); if (v) { // 视频处理 if (this.state.type !== 'video') { this.state.type = 'video'; pushLog(`视频: ${document.title}`, 'ok'); applySpeed(this.speed); } await this.handleVideo(v, aid); } else { // 文档处理 if (this.state.type !== 'doc') { this.state.type = 'doc'; this.state.startedAt = Date.now(); pushLog(`文档: ${document.title},停留 ${CFG.docDwellSec}s`, 'ok'); } await this.handleDoc(aid); } }, async handleVideo(v, aid) { const key = 'v_' + aid; const dur = v.duration || 0; const cur = v.currentTime || 0; // 持续加速+自动续播 applySpeed(this.speed); if (v.paused && !v.ended) v.play().catch(() => {}); panel.setStep('video', `${document.title.slice(0,18)} | ${Math.floor(cur)}s/${Math.floor(dur)}s (${this.speed}x)`); // 已完成 → 下一项(只处理一次) if (v.ended || (dur > 0 && cur >= dur * 0.98)) { if (this.handledKey !== key) { this.handledKey = key; const st = loadState(); st.doneVideos = st.doneVideos || []; if (!st.doneVideos.includes(aid)) st.doneVideos.push(aid); saveState(st); pushLog(`✓ 视频播完: ${document.title}`, 'ok'); return this.goNext(); } } // 跳过标记已完成的 if (CFG.skipDoneVideos) { const st = loadState(); if ((st.doneVideos || []).includes(aid) && this.handledKey !== key + '_skip') { this.handledKey = key + '_skip'; pushLog(`视频已完成标记,跳过`, 'warn'); return this.goNext(); } } // 卡住超时保护 if (dur > 0) { const est = (dur / this.speed) + 45; const elapsed = (Date.now() - this.state.startedAt) / 1000; if (elapsed > est && cur < dur * 0.95 && this.handledKey !== key + '_force') { pushLog('播放卡住,跳到结尾', 'warn'); try { v.currentTime = Math.max(0, dur - 1); } catch (e) {} this.handledKey = key + '_force'; } } if (this.skip) { this.skip = false; return this.goNext(); } }, async handleDoc(aid) { const key = 'd_' + aid; const elapsed = (Date.now() - this.state.startedAt) / 1000; panel.setStep('doc', `${document.title.slice(0,18)} | ${Math.floor(elapsed)}s/${CFG.docDwellSec}s`); if (elapsed >= CFG.docDwellSec && this.handledKey !== key) { this.handledKey = key; const st = loadState(); st.doneDocs = st.doneDocs || []; if (!st.doneDocs.includes(aid)) st.doneDocs.push(aid); saveState(st); pushLog(`✓ 文档完成: ${document.title}`, 'ok'); return this.goNext(); } if (this.skip) { this.skip = false; return this.goNext(); } }, // ---- 关键:goNext 用 location.href 直接跳转,不点击 ---- async goNext(manual) { panel.setStep('next'); const oldAid = curActivityId(); const oldTitle = document.title; // 1. 从右侧目录找到下一个链接 href const links = $$('a').filter(a => { const h = a.getAttribute('href') || ''; return h.indexOf('/student/activity/display?') !== -1 && h.indexOf('activityId=') !== -1; }); let targetHref = null; if (links.length > 0 && oldAid) { // 按顺序找当前之后的那个 let idx = -1; for (let i = 0; i < links.length; i++) { const m = links[i].getAttribute('href').match(/activityId=([^&]+)/); if (m && m[1] === oldAid) { idx = i; break; } } if (idx >= 0 && idx + 1 < links.length) { targetHref = links[idx + 1].getAttribute('href'); } else if (idx < 0) { // 找不到当前位置,用第一个未完成的 const st = loadState(); const done = [...(st.doneVideos || []), ...(st.doneDocs || [])]; const undone = links.find(l => { const m = l.getAttribute('href').match(/activityId=([^&]+)/); return m && !done.includes(m[1]); }); if (undone) targetHref = undone.getAttribute('href'); } } if (targetHref) { const fullUrl = targetHref.startsWith('http') ? targetHref : location.origin + targetHref; const name = (() => { const m = targetHref.match(/activityId=([^&]+)/); if (!m) return '下一项'; const link = links.find(l => l.getAttribute('href').indexOf(m[1]) !== -1); return link ? link.textContent.trim().slice(0, 30) : '下一项'; })(); pushLog(`→ 下一项: ${name}`, 'ok'); // ★ 直接 location.href 赋值,绕过 SPA 点击拦截 location.href = fullUrl; // 等待页面切换 await waitNav(oldAid, oldTitle, 12); // 重置状态(watchdog 会因为 currentAid 不匹配重新初始化) this.currentAid = null; this.handledKey = null; this.state = {}; return; } // 目录到底 → 本课程完成,进下一门 const st = loadState(); st.courseIdx = (st.courseIdx || 0) + 1; saveState(st); pushLog('本课程目录已到末尾,进入下一门', 'ok'); location.href = location.origin + '/jxpt-web/student/courseuser/myCourse'; }, }; // ====================== 切标签恢复 ====================== document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && Engine.watchdog) { const v = findVideo(); if (v && v.paused) v.play().catch(() => {}); applySpeed(panel.speed); } }); window.addEventListener('pageshow', () => { if (Engine.watchdog && !Engine.paused) { setTimeout(() => applySpeed(panel.speed), 500); } }); // ====================== 初始化 ====================== function init() { if (window.__jfkInited) return; window.__jfkInited = true; panel = new Panel(); window.__jfkPanel = panel; // 恢复上次的倍速 const savedSpeed = parseFloat(get(K_SPEED, null)); if (savedSpeed) { panel.speed = savedSpeed; } pushLog('脚本已加载,页面: ' + pageType()); // ★ 关键:如果之前在自动运行,整页刷新后自动恢复 watchdog const autoRunning = get(K_AUTO, null); if (autoRunning === '1') { pushLog('检测到上次在自动运行,恢复 watchdog...', 'warn'); setTimeout(() => Engine.start(), 2000); panel.setStep('next', '自动恢复中...'); } else { panel.setStep('idle', '点"▶ 开始刷课"启动'); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } setTimeout(init, 2000); setTimeout(init, 5000); })();