// ==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);
})();