// ==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 = '