// ==UserScript== // @name 嘉兴继续教育自动刷课 (zy.jxkp.net) // @namespace https://zy.jxkp.net/ // @version 1.1.1 // @description 嘉兴继续教育:手动进入课程播放页后点击"开始刷课",自动顺序刷当前课程视频(1倍速)。 // @match https://zy.jxkp.net/* // @run-at document-end // @grant GM_xmlhttpRequest // @connect wachtapi.rqzb.top // @antifeature membership // @noframes // @license All Rights Reserved // ==/UserScript== (function () { 'use strict'; const CFG = { endedWait: 5000, tick: 5000, stuckTimeout: 300000, maxChapters: 3, backendUrl: 'https://wachtapi.rqzb.top/yh', }; const RUN_KEY = 'se_jxkp_brush_run'; const GROUP_NUMBER = '1063839163'; const PUBLIC_ACCOUNT = '陈风的思考日志'; const QR_FOLLOW_URL = 'http://weixin.qq.com/r/mp/9yC1rbHEbuwRrfXO93Xl'; const VERIFY_STORAGE_KEY = 'se_jxkp_verified'; let running = false; let status = '等待'; let currentChapter = ''; let lastTime = -1; let stuckSince = 0; let endedHandled = false; let boundVideo = null; let tickTimer = null; let lastEndedAt = 0; let completedChapters = 0; let limitHit = 0; let rejectCount = 0; let rejectWindowStart = 0; const $ = (sel, root) => (root || document).querySelector(sel); const $$ = (sel, root) => Array.from((root || document).querySelectorAll(sel)); const sleep = (ms) => new Promise(r => setTimeout(r, ms)); function apiRequest(method, path, body) { return new Promise((resolve, reject) => { const opts = { method: method, url: CFG.backendUrl + path, headers: { 'Content-Type': 'application/json' }, timeout: 15000, onload: (resp) => { try { if (!resp.responseText || resp.responseText.trim() === '') { reject(new Error('后端返回空响应(HTTP ' + resp.status + ')')); return; } const data = JSON.parse(resp.responseText); if (data.success) { resolve(data.data); } else { reject(new Error(data.error || 'API返回失败')); } } catch (e) { reject(new Error('响应解析失败(HTTP ' + resp.status + '): ' + e.message)); } }, onerror: (err) => reject(new Error('网络请求失败: ' + (err.error || 'unknown'))), ontimeout: () => reject(new Error('请求超时')), }; if (body) { opts.data = JSON.stringify(body); } try { GM_xmlhttpRequest(opts); } catch (e) { const fetchOpts = { method: method, headers: { 'Content-Type': 'application/json' } }; if (body) { fetchOpts.body = JSON.stringify(body); } fetch(CFG.backendUrl + path, fetchOpts) .then(r => r.json()) .then(data => { if (data.success) resolve(data.data); else reject(new Error(data.error || 'API返回失败')); }) .catch(e => reject(new Error('fetch回退也失败: ' + e.message))); } }); } async function waitUntil(condFn, timeout, interval) { const start = Date.now(); while (Date.now() - start < (timeout || 8000)) { try { if (condFn()) return true; } catch (e) {} await sleep(interval || 500); } return false; } function isLoginModal(m) { return /已在另一个浏览器|未登录/.test(m.textContent); } function closeCompletionModals() { let closed = 0; $$('.custom-modal').forEach(m => { if (isLoginModal(m)) return; const btn = m.querySelector('.custom-btn-primary') || m.querySelector('.custom-modal-close'); if (btn) { btn.click(); closed++; } else m.remove(); }); return closed; } function nextLearningChapter() { const items = $$('.chapter-item.learning'); return items.find(el => !el.classList.contains('active')) || null; } function setStatus(t) { status = t; const el = $('#jxkp-status'); if (el) el.textContent = t; const pr = $('#jxkp-progress'); if (pr) pr.textContent = t; const ms = $('#jxkp-mini-s'); if (ms) ms.innerHTML = '状态: ' + t + ''; const mp = $('#jxkp-mini-p'); if (mp) mp.textContent = t; } function getVideo() { return $('#mse video') || $('video'); } function getCourseId() { return new URLSearchParams(location.search).get('column1') || ''; } async function apiPost(path, params) { const res = await fetch('/073/' + path, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, body: new URLSearchParams(params || {}).toString(), }); return res.json(); } async function getEnrolledId(courseId) { const d = await apiPost('outService/table2Action!getTable2ByColumn2Column6.action', { column6: courseId }); return (d && d.column1) || null; } async function getChapterRows(enrolledId) { const d = await apiPost('outService/table16Action!findTable16ListByColumn3.action', { column3: enrolledId }); return (d && d.rows) || []; } async function countCompletedChapters(courseId) { const tid = await getEnrolledId(courseId); if (!tid) return 0; const rows = await getChapterRows(tid); return rows.filter(r => String(r.column7) === '1').length; } async function countTotalChapters(courseId) { const tid = await getEnrolledId(courseId); if (!tid) return 0; const rows = await getChapterRows(tid); return rows.length; } function start() { if (running) return; if (!isVerified()) { createVerifyModal(function () { start(); }); return; } running = true; endedHandled = false; lastTime = -1; stuckSince = 0; completedChapters = 0; limitHit = 0; try { sessionStorage.setItem(RUN_KEY, '1'); } catch (e) {} if (tickTimer) clearInterval(tickTimer); tickTimer = setInterval(tick, CFG.tick); tick(); setTimeout(checkCourseDoneOnLoad, 8000); } function stop(reason) { running = false; if (tickTimer) { clearInterval(tickTimer); tickTimer = null; } try { sessionStorage.removeItem(RUN_KEY); } catch (e) {} const v = getVideo(); if (v) { try { v.playbackRate = 1; } catch (e) {} } setStatus('已停止' + (reason ? ':' + reason : '')); } async function checkCourseDoneOnLoad() { if (!running) return; const cid = getCourseId(); if (!cid) return; try { const done = await countCompletedChapters(cid); const total = await countTotalChapters(cid); setStatus('已完成 ' + done + '/' + total + ' 章'); if (total > 0 && done >= total) { stop('课程全部章节已完成'); } } catch (e) {} } function bindEnded() { const v = getVideo(); if (!v || v === boundVideo) return; if (boundVideo) { try { boundVideo.removeEventListener('ended', onEnded); } catch (e) {} } boundVideo = v; v.addEventListener('ended', onEnded); endedHandled = false; } async function onEnded() { if (endedHandled || !running) return; endedHandled = true; lastEndedAt = Date.now(); setStatus('章节完成,等待上报...'); await waitUntil(() => !!$('.custom-modal') || !!nextLearningChapter(), CFG.endedWait, 500); if (!running) return; completedChapters++; if (completedChapters >= CFG.maxChapters) { limitHit++; if (limitHit >= 2) { stop('已达 ' + CFG.maxChapters + ' 章上限,请手动点击系统弹窗后重新点击开始'); return; } closeCompletionModals(); stop('已达 ' + CFG.maxChapters + ' 章上限,请重新点击开始继续'); return; } closeCompletionModals(); await sleep(800); if (!running) return; const next = nextLearningChapter(); if (next) { next.click(); return; } const cid = getCourseId(); let total = 0; try { total = await countTotalChapters(cid); } catch (e) {} if (total > 0 && completedChapters >= total) { stop('全部章节完成'); } else { location.reload(); } } function tick() { if (!running) return; const loginModal = $$('.custom-modal').find(isLoginModal); if (loginModal) { stop('检测到账号在其他浏览器登录'); return; } const rejected = $$('.custom-modal').some(function (m) { return !isLoginModal(m) && /学习进度更新失败|已存在播放视频/.test(m.textContent); }); if (rejected) { closeCompletionModals(); const now = Date.now(); if (now - rejectWindowStart > 60000) { rejectWindowStart = now; rejectCount = 0; } rejectCount++; if (rejectCount >= 3) { stop('进度更新持续被拒绝'); return; } const vv = getVideo(); if (vv) { try { vv.play(); } catch (e) {} } } closeCompletionModals(); const v = getVideo(); if (!v) { setStatus('等待视频加载...'); return; } try { if (v.playbackRate !== 1) v.playbackRate = 1; if (v.paused && !v.ended) { const p = v.play(); if (p && p.catch) p.catch(() => {}); } } catch (e) {} bindEnded(); const active = $('.chapter-item.active .chapter-item-title span'); if (active) currentChapter = active.textContent.trim(); if (v.duration > 0 && isFinite(v.duration)) { const pct = Math.floor((v.currentTime / v.duration) * 100); setStatus(currentChapter + ' ' + pct + '%'); } if (Math.abs(v.currentTime - lastTime) < 0.5) { if (stuckSince === 0) stuckSince = Date.now(); const stuckMs = Date.now() - stuckSince; if (stuckMs > CFG.stuckTimeout && !v.ended) { location.reload(); return; } } else { stuckSince = 0; } if (v.ended && endedHandled && lastEndedAt > 0 && Date.now() - lastEndedAt > 30000) { location.reload(); return; } lastTime = v.currentTime; } function isVerified() { try { const data = JSON.parse(localStorage.getItem(VERIFY_STORAGE_KEY) || '{}'); if (!data.code) return false; return /^[A-Z0-9]{8}$/.test(data.code); } catch (e) { return false; } } function setVerified(code) { try { localStorage.setItem(VERIFY_STORAGE_KEY, JSON.stringify({ code: code.toUpperCase() })); } catch (e) {} } function getQrCodeSrc() { return 'https://api.qrserver.com/v1/create-qr-code/?size=180x180&margin=8&data=' + encodeURIComponent(QR_FOLLOW_URL); } function createVerifyModal(onSuccess) { const old = document.getElementById('jxkp-verify-overlay'); if (old) old.remove(); const overlay = document.createElement('div'); overlay.id = 'jxkp-verify-overlay'; overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.92);z-index:9999999;display:flex;align-items:center;justify-content:center;font-family:"Microsoft YaHei",sans-serif;'; overlay.innerHTML = '
' + '
嘉兴继续教育刷课 授权验证
' + '
关注公众号获取推荐码后解锁使用
' + '
' + '公众号二维码' + '
' + '
公众号: ' + PUBLIC_ACCOUNT + '
' + '
扫码关注公众号 → 回复「推荐码」获取
' + '' + '
' + '' + '
' + '
' + '
官方交流群: ' + GROUP_NUMBER + '
' + '
遇到问题可加群反馈
' + '
' + '
'; document.body.appendChild(overlay); const input = document.getElementById('jxkp-verify-input'); const btn = document.getElementById('jxkp-verify-btn'); const errEl = document.getElementById('jxkp-verify-error'); async function doVerify() { const code = input.value.trim(); if (!code) { errEl.textContent = '请输入推荐码'; return; } btn.disabled = true; btn.textContent = '验证中...'; errEl.style.color = ''; errEl.textContent = ''; try { await apiRequest('POST', '/api/verify-code', { code: code }); setVerified(code); errEl.style.color = '#4CAF50'; errEl.textContent = '验证成功!正在解锁...'; setTimeout(function () { overlay.remove(); if (typeof onSuccess === 'function') onSuccess(); }, 800); } catch (e) { errEl.style.color = '#f44336'; errEl.textContent = (e && e.message) || '推荐码无效,请关注公众号获取正确推荐码'; input.style.borderColor = '#f44336'; setTimeout(function () { input.style.borderColor = 'rgba(255,255,255,0.15)'; }, 1500); } finally { btn.disabled = false; btn.textContent = '验证解锁'; } } btn.onclick = doVerify; input.onkeydown = function (e) { if (e.key === 'Enter') doVerify(); }; input.focus(); } function createPanel() { if ($('#jxkp-panel')) return; const el = document.createElement('div'); el.id = 'jxkp-panel'; el.style.cssText = 'position:fixed;top:10px;right:10px;z-index:999999;background:rgba(20,20,30,0.95);color:#fff;padding:0;border-radius:14px;font-family:"Microsoft YaHei",sans-serif;font-size:13px;width:300px;box-shadow:0 8px 32px rgba(0,0,0,0.5);overflow:hidden;border:1px solid rgba(255,255,255,0.1);'; el.innerHTML = '
' + '嘉兴继续教育刷课' + '' + '
' + '
' + '
状态: 等待
' + '
进度: --
' + '
' + '' + '' + '
' + '
' + '每轮最多刷 3 个章节,达到后请手动点击系统弹窗,再点击"开始刷课"继续
需关注公众号获取推荐码解锁使用,仅限课程播放页
' + '
' + '
' + ''; document.body.appendChild(el); $('#jxkp-start').onclick = function () { start(); }; $('#jxkp-stop').onclick = function () { stop('手动停止'); }; $('#jxkp-min').onclick = function () { const body = $('#jxkp-body'); const mini = $('#jxkp-mini'); if (body.style.display === 'none') { body.style.display = ''; mini.style.display = 'none'; this.textContent = '—'; } else { body.style.display = 'none'; mini.style.display = ''; this.textContent = '+'; } }; const header = $('#jxkp-header'); let ox = 0, oy = 0, dragging = false; header.onmousedown = function (e) { if (e.target.tagName === 'BUTTON') return; dragging = true; ox = e.clientX - el.getBoundingClientRect().left; oy = e.clientY - el.getBoundingClientRect().top; }; document.addEventListener('mousemove', function (e) { if (!dragging) return; el.style.right = 'auto'; el.style.left = (e.clientX - ox) + 'px'; el.style.top = (e.clientY - oy) + 'px'; }); document.addEventListener('mouseup', function () { dragging = false; }); } function init() { const isPlayPage = location.pathname.indexOf('index3') !== -1; if (!isPlayPage) return; createPanel(); if (!isVerified()) { const b = $('#jxkp-start'); if (b) { b.disabled = true; b.style.opacity = '0.5'; b.style.cursor = 'not-allowed'; } createVerifyModal(function () { const b2 = $('#jxkp-start'); if (b2) { b2.disabled = false; b2.style.opacity = ''; b2.style.cursor = ''; } try { if (sessionStorage.getItem(RUN_KEY) === '1') { start(); } } catch (e) {} }); return; } try { if (sessionStorage.getItem(RUN_KEY) === '1') { start(); } } catch (e) {} } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();