// ==UserScript== // @name 重庆智慧教育平台 自动刷课助手 // @namespace https://www.cqooc.com/ // @version 1.2.1 // @description 在 cqooc.com 学习页自动完成课件观看(快速 / 真实时长 / 自动连播三种模式) // @author dsh // @license MIT // @match https://www.cqooc.com/* // @match https://*.cqooc.com/* // @noframes // @run-at document-idle // @grant none // ==/UserScript== (function () { 'use strict'; // ---------------- 基础工具 ---------------- const API = location.origin + '/api'; const SALT = '_2Cqooc&#(*@&('; const RESP_KEY = '0$0H_3p4$567890F0Kd0?123456789$@'; const RESP_IV = '0012345900000678'; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); // ---------------- 登录态读取 ---------------- // 站点用 store2 把 token 存在 localStorage["SQLP"](JSON 字符串),cookie 只作兜底 function normalizeToken(raw) { if (raw === null || raw === undefined || raw === '') return ''; let v = raw; try { v = JSON.parse(raw); } catch (e) { /* 非 JSON,按原文用 */ } if (typeof v === 'string') return v; return typeof raw === 'string' ? raw : ''; } function readToken() { try { const v = normalizeToken(localStorage.getItem('SQLP')); if (v) return { token: v, from: 'localStorage' }; } catch (e) { /* 隐私模式可能抛错 */ } try { const v = normalizeToken(sessionStorage.getItem('SQLP')); if (v) return { token: v, from: 'sessionStorage' }; } catch (e) { /* ignore */ } try { const m = document.cookie.match(/(?:^|;\s*)SQLP=([^;]+)/); if (m) { const v = normalizeToken(decodeURIComponent(m[1])); if (v) return { token: v, from: 'cookie' }; } } catch (e) { /* ignore */ } return { token: '', from: '' }; } function getToken() { return readToken().token; } // ---------------- MD5(浏览器内置无 md5,这里带一份实现) ---------------- const md5 = (function () { function safeAdd(x, y) { const lsw = (x & 0xffff) + (y & 0xffff); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); return (msw << 16) | (lsw & 0xffff); } function bitRotateLeft(num, cnt) { return (num << cnt) | (num >>> (32 - cnt)); } function md5cmn(q, a, b, x, s, t) { return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b); } function md5ff(a, b, c, d, x, s, t) { return md5cmn((b & c) | (~b & d), a, b, x, s, t); } function md5gg(a, b, c, d, x, s, t) { return md5cmn((b & d) | (c & ~d), a, b, x, s, t); } function md5hh(a, b, c, d, x, s, t) { return md5cmn(b ^ c ^ d, a, b, x, s, t); } function md5ii(a, b, c, d, x, s, t) { return md5cmn(c ^ (b | ~d), a, b, x, s, t); } function binlMD5(x, len) { x[len >> 5] |= 0x80 << (len % 32); x[(((len + 64) >>> 9) << 4) + 14] = len; let a = 1732584193, b = -271733879, c = -1732584194, d = 271733878; for (let i = 0; i < x.length; i += 16) { const olda = a, oldb = b, oldc = c, oldd = d; a = md5ff(a, b, c, d, x[i], 7, -680876936); d = md5ff(d, a, b, c, x[i + 1], 12, -389564586); c = md5ff(c, d, a, b, x[i + 2], 17, 606105819); b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330); a = md5ff(a, b, c, d, x[i + 4], 7, -176418897); d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426); c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341); b = md5ff(b, c, d, a, x[i + 7], 22, -45705983); a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416); d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417); c = md5ff(c, d, a, b, x[i + 10], 17, -42063); b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162); a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682); d = md5ff(d, a, b, c, x[i + 13], 12, -40341101); c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290); b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329); a = md5gg(a, b, c, d, x[i + 1], 5, -165796510); d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632); c = md5gg(c, d, a, b, x[i + 11], 14, 643717713); b = md5gg(b, c, d, a, x[i], 20, -373897302); a = md5gg(a, b, c, d, x[i + 5], 5, -701558691); d = md5gg(d, a, b, c, x[i + 10], 9, 38016083); c = md5gg(c, d, a, b, x[i + 15], 14, -660478335); b = md5gg(b, c, d, a, x[i + 4], 20, -405537848); a = md5gg(a, b, c, d, x[i + 9], 5, 568446438); d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690); c = md5gg(c, d, a, b, x[i + 3], 14, -187363961); b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501); a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467); d = md5gg(d, a, b, c, x[i + 2], 9, -51403784); c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473); b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734); a = md5hh(a, b, c, d, x[i + 5], 4, -378558); d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463); c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562); b = md5hh(b, c, d, a, x[i + 14], 23, -35309556); a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060); d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353); c = md5hh(c, d, a, b, x[i + 7], 16, -155497632); b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640); a = md5hh(a, b, c, d, x[i + 13], 4, 681279174); d = md5hh(d, a, b, c, x[i], 11, -358537222); c = md5hh(c, d, a, b, x[i + 3], 16, -722521979); b = md5hh(b, c, d, a, x[i + 6], 23, 76029189); a = md5hh(a, b, c, d, x[i + 9], 4, -640364487); d = md5hh(d, a, b, c, x[i + 12], 11, -421815835); c = md5hh(c, d, a, b, x[i + 15], 16, 530742520); b = md5hh(b, c, d, a, x[i + 2], 23, -995338651); a = md5ii(a, b, c, d, x[i], 6, -198630844); d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415); c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905); b = md5ii(b, c, d, a, x[i + 5], 21, -57434055); a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571); d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606); c = md5ii(c, d, a, b, x[i + 10], 15, -1051523); b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799); a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359); d = md5ii(d, a, b, c, x[i + 15], 10, -30611744); c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380); b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649); a = md5ii(a, b, c, d, x[i + 4], 6, -145523070); d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379); c = md5ii(c, d, a, b, x[i + 2], 15, 718787259); b = md5ii(b, c, d, a, x[i + 9], 21, -343485551); a = safeAdd(a, olda); b = safeAdd(b, oldb); c = safeAdd(c, oldc); d = safeAdd(d, oldd); } return [a, b, c, d]; } function binl2rstr(input) { let output = ''; for (let i = 0; i < input.length * 32; i += 8) output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xff); return output; } function rstr2binl(input) { const output = []; for (let i = 0; i < input.length * 8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << (i % 32); return output; } function rstrMD5(s) { return binl2rstr(binlMD5(rstr2binl(s), s.length * 8)); } function rstr2hex(input) { const hexTab = '0123456789abcdef'; let output = ''; for (let i = 0; i < input.length; i++) { const x = input.charCodeAt(i); output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f); } return output; } function str2rstrUTF8(input) { return unescape(encodeURIComponent(input)); } return function (s) { return rstr2hex(rstrMD5(str2rstrUTF8(s))); }; })(); // ---------------- AES-256-CBC 解密(WebCrypto,自包含,不依赖页面 CryptoJS) ---------------- function b64ToBytes(b64) { const bin = atob(String(b64)); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } async function aesDecrypt(b64) { const c = typeof crypto !== 'undefined' ? crypto : (typeof window !== 'undefined' ? window.crypto : null); if (!c || !c.subtle) throw new Error('当前浏览器不支持 WebCrypto'); const enc = new TextEncoder(); const key = await c.subtle.importKey('raw', enc.encode(RESP_KEY), { name: 'AES-CBC' }, false, ['decrypt']); const plain = await c.subtle.decrypt({ name: 'AES-CBC', iv: enc.encode(RESP_IV) }, key, b64ToBytes(b64)); return JSON.parse(new TextDecoder().decode(plain)); } function sign(path, ts) { const token = getToken(); const p = path.startsWith('/pub/') ? path.split('?')[0] : ''; const raw = p ? '/api' + p + '**2ooc&#(?^&(' + ts : token + SALT + ts; return md5(raw); } async function api(path, { method = 'GET', params, body } = {}) { const qs = params ? '?' + new URLSearchParams( Object.entries(params).filter(([, v]) => v !== undefined && v !== null).map(([k, v]) => [k, String(v)]) ).toString() : ''; const ts = String(Date.now()); const token = getToken(); const headers = { ts, sg: sign(path, ts), Accept: 'application/json, text/plain, */*' }; if (token) headers.SQLP = token; if (body !== undefined) headers['Content-Type'] = 'application/json;charset=UTF-8'; const res = await fetch(API + path + qs, { method, headers, credentials: 'include', body: body === undefined ? undefined : JSON.stringify(body), }); const text = await res.text(); let data; try { data = JSON.parse(text); } catch { throw new Error('非 JSON 响应: ' + text.slice(0, 120)); } if (data.encryption && typeof data.result === 'string' && (data.code === 200 || data.code === 2000)) { try { data.result = await aesDecrypt(data.result); } catch (e) { log('响应解密失败: ' + e.message); } } if (data.code === 401) throw new Error('登录已失效,请重新登录 www.cqooc.com'); return data; } // ---------------- 数据抓取 ---------------- function pickCourses(res) { const r = res && res.result; if (Array.isArray(r)) return r; for (const k of ['list', 'records', 'rows', 'data', 'content']) { if (r && Array.isArray(r[k])) return r[k]; } return []; } function walkLessons(tree) { const out = []; (tree || []).forEach((ch) => { (ch.ZLJH || []).forEach((sec) => { (sec.KJJH || []).forEach((kj) => { if (String(kj.ZT) === '4') return; out.push({ KJID: kj.ID != null ? kj.ID : kj.KJID, name: kj.KJMC || kj.NR || kj.MC || '', lx: String(kj.LX != null ? kj.LX : ''), jd: Number(kj.JD || 0), zid: sec.ZJID, section: sec.ZJMC || sec.ZJXH || '', }); }); }); }); return out; } const isCourseware = (lx) => !['2', '5', '6'].includes(lx); // 2测试 5任务 6考试 // ---------------- 刷课逻辑 ---------------- const state = { running: false, stop: false, courses: [], lessons: [], current: null }; async function loadCourses() { const res = await api('/student/my/course/list', { params: { pageNum: 1, pageSize: 100 } }); if (res.code !== 200) throw new Error('获取课程失败: ' + (res.message || res.code)); state.courses = pickCourses(res); return state.courses; } async function loadLessons(kcid) { const res = await api('/student/course/chapter/full/list', { params: { KCID: kcid } }); if (res.code !== 200) throw new Error('获取章节失败: ' + (res.message || res.code)); state.lessons = walkLessons(res.result).filter((l) => isCourseware(l.lx)); return state.lessons; } async function watchLesson(kcid, lesson, mode) { const st = await api('/student/course/lesson/start', { method: 'PUT', params: { KCID: kcid, KJID: lesson.KJID } }); if (st.code !== 200) throw new Error('start 失败: ' + (st.message || st.code)); const r = st.result || {}; const zsc = Number(r.ZSC || 0); const ybsc = Number(r.YBSC || 0); const bc = Number(r.BC || 15) || 15; if (ybsc < 0) return { skipped: true, done: true }; if (!zsc || zsc < 0) return { skipped: true }; log(` 开始 ${lesson.name} 时长=${zsc}s 已播=${Math.max(0, ybsc)}s`); let n = 0; if (mode === 'fast') { const targets = [...new Set([0.25, 0.5, 0.75, 1].map((f) => Math.min(zsc, Math.max(1, Math.round(zsc * f)))))]; for (const t of targets) { if (state.stop) return { stopped: true }; await api('/student/course/lesson/record', { method: 'PUT', params: { KCID: kcid, KJID: lesson.KJID, DQJD: t } }); n++; await sleep(300); } } else { const step = Math.max(1, Math.min(bc, 60)); let pos = Math.max(0, Math.floor(ybsc)); while (pos < zsc - 1 && !state.stop) { pos = Math.min(pos + step, zsc - 1); await api('/student/course/lesson/record', { method: 'PUT', params: { KCID: kcid, KJID: lesson.KJID, DQJD: pos } }); n++; await sleep(Math.min(step * 1000, 60000)); } } if (state.stop) return { stopped: true }; const fin = await api('/student/course/lesson/finish', { method: 'PUT', params: { KCID: kcid, KJID: lesson.KJID } }); return { n, fin }; } async function run(mode) { if (state.running) return; state.running = true; state.stop = false; try { log('扫描课程...'); const courses = await loadCourses(); log(`共 ${courses.length} 门课程`); for (const c of courses) { if (state.stop) break; const kcid = c.KCID || c.KC_ID; if (!kcid) continue; const title = c.KCMC || c.KCM || ''; log(`课程:${title} (${kcid})`); const lessons = await loadLessons(kcid); const todo = lessons.filter((l) => l.jd < 90); log(` 课件 ${lessons.length} 个,待完成 ${todo.length} 个`); for (const l of todo) { if (state.stop) break; try { const res = await watchLesson(kcid, l, mode); if (res.skipped) log(` 跳过 ${l.name}(无时长)`); else if (res.stopped) break; else log(` 完成 ${l.name} 上报${res.n}次 finish=${res.fin && res.fin.code}`); } catch (e) { log(` 失败 ${l.name}: ${e.message}`); } } } log('全部结束'); } catch (e) { log('出错: ' + e.message); } finally { state.running = false; } } // ---------------- 自动连播(真实播放器模式) ---------------- // 依据学习页 DOM:.courseware-row 为课件行,.active 为当前课件;video 为播放器 const auto = { on: false, timer: null, rate: 2, bound: new WeakSet(), noVideoSince: null, advancing: false }; function nextCoursewareRow() { const rows = Array.from(document.querySelectorAll('.courseware-row')); if (!rows.length) return null; const active = document.querySelector('.courseware-row.active'); let idx = active ? rows.indexOf(active) : -1; for (let i = idx + 1; i < rows.length; i++) { if (!rows[i].querySelector('.lock-tag')) return rows[i]; } return null; } function scheduleAdvance(reason) { if (auto.advancing) return; auto.advancing = true; setTimeout(() => { auto.advancing = false; if (!auto.on) return; const row = nextCoursewareRow(); if (!row) { log('已到最后一节,自动连播结束'); stopAuto(); return; } const name = row.querySelector('.courseware-name'); log(`→ 下一节:${name ? name.textContent.trim() : '(未命名)'}`); row.click(); auto.noVideoSince = Date.now(); }, 1800); } function autoTick() { if (!auto.on) return; if (!document.querySelector('.course-study-container')) return; // 不在学习页 const v = document.querySelector('video'); if (v) { auto.noVideoSince = null; v.muted = true; if (Math.abs(v.playbackRate - auto.rate) > 0.01) v.playbackRate = auto.rate; if (!auto.bound.has(v)) { auto.bound.add(v); v.addEventListener('ended', () => scheduleAdvance('ended')); } if (v.paused && !v.ended && v.readyState >= 2 && !v.dataset.ckResumed) { v.dataset.ckResumed = '1'; v.play().catch(() => {}); } } else if (auto.noVideoSince === null) { auto.noVideoSince = Date.now(); } // 非视频类课件(文档/任务)或加载失败:8 秒后跳过 if (!v && auto.noVideoSince && Date.now() - auto.noVideoSince > 8000) { log('当前课件无播放器,跳过'); auto.noVideoSince = null; scheduleAdvance('no-video'); } } function startAuto(rate) { if (auto.on) return; auto.on = true; auto.rate = rate; auto.noVideoSince = null; log(`自动连播已开启(${rate}x,静音)`); auto.timer = setInterval(autoTick, 1000); autoTick(); } function stopAuto() { auto.on = false; if (auto.timer) clearInterval(auto.timer); auto.timer = null; log('自动连播已停止'); } // ---------------- UI ---------------- let logBox, logLines = []; function log(msg) { const line = `[${new Date().toLocaleTimeString()}] ${msg}`; logLines.push(line); if (logLines.length > 200) logLines.shift(); if (logBox) logBox.textContent = logLines.join('\n'); console.log('[cqooc]', msg); } function buildPanel() { const wrap = document.createElement('div'); wrap.id = 'cqooc-helper'; wrap.innerHTML = `
📚 自动刷课 ×
在本页打开要刷的课件后点此

      
`; const style = document.createElement('style'); style.textContent = ` #cqooc-helper{position:fixed;right:16px;bottom:16px;width:420px;max-height:70vh;z-index:999999; background:#fff;border:1px solid #d9d9d9;border-radius:8px;box-shadow:0 6px 24px rgba(0,0,0,.18); font:12px/1.6 -apple-system,"Microsoft YaHei",sans-serif;display:flex;flex-direction:column} #cqooc-helper .ck-hd{padding:8px 12px;background:#1890ff;color:#fff;border-radius:8px 8px 0 0; display:flex;justify-content:space-between;font-weight:600} #cqooc-helper .ck-close{cursor:pointer} #cqooc-helper .ck-bd{padding:10px;display:flex;flex-direction:column;gap:8px;min-height:0} #cqooc-helper .ck-row{display:flex;gap:6px;flex-wrap:wrap;align-items:center} #cqooc-helper .ck-hint{color:#999;font-size:11px} #cqooc-helper button{padding:4px 10px;border:1px solid #d9d9d9;border-radius:4px;background:#fafafa;cursor:pointer} #cqooc-helper button:hover{color:#1890ff;border-color:#1890ff} #cqooc-helper .ck-log{flex:1;overflow:auto;max-height:40vh;margin:0;background:#f6f8fa; padding:6px;border-radius:4px;white-space:pre-wrap;word-break:break-all} `; document.head.appendChild(style); document.body.appendChild(wrap); logBox = wrap.querySelector('.ck-log'); wrap.querySelector('.ck-close').onclick = () => wrap.remove(); wrap.querySelector('[data-act="scan"]').onclick = async () => { try { const cs = await loadCourses(); log(`共 ${cs.length} 门课程:` + cs.map((c) => c.KCMC || c.KCM || c.KCID).join('、')); } catch (e) { log(e.message); } }; wrap.querySelector('[data-act="fast"]').onclick = () => run('fast'); wrap.querySelector('[data-act="real"]').onclick = () => run('real'); wrap.querySelector('[data-act="stop"]').onclick = () => { state.stop = true; log('已请求停止'); }; wrap.querySelector('[data-act="auto"]').onclick = (e) => { if (auto.on) { stopAuto(); e.target.textContent = '自动连播 ▶'; return; } const sel = wrap.querySelector('[data-act="rate"]'); startAuto(Number(sel.value) || 2); e.target.textContent = '停止连播 ■'; }; const tk = readToken(); log(`就绪,token=${tk.token ? '已检测到(' + tk.from + ')' : '未检测到(请先登录)'}`); } if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', buildPanel); else buildPanel(); // 调试 / 自动化测试入口 try { window.__CQOOC_HELPER = { api, sign, md5, aesDecrypt, getToken, loadCourses, loadLessons, watchLesson, run, state, startAuto, stopAuto, autoTick, nextCoursewareRow, auto }; } catch (e) { /* ignore */ } })();